Luigit
repositories / bugabinga.net

bugabinga.net

personal infrastructure for bugabinga!

owned by admin

services/luci/internal/cache/cache_test.go

Raw
package cache

import (
	"os"
	"path/filepath"
	"strings"
	"testing"
	"time"

	"bugabinga.net/luci/internal/ciconfig"
)

func TestKeyRestoreAndSave(t *testing.T) {
	root := t.TempDir()
	workspace := filepath.Join(root, "work")
	if err := os.MkdirAll(filepath.Join(workspace, ".deps"), 0o755); err != nil {
		t.Fatal(err)
	}
	if err := os.WriteFile(filepath.Join(workspace, "lock"), []byte("v1"), 0o644); err != nil {
		t.Fatal(err)
	}
	if err := os.WriteFile(filepath.Join(workspace, ".deps", "module"), []byte("cached"), 0o644); err != nil {
		t.Fatal(err)
	}
	entry := ciconfig.Cache{Name: "deps", Path: ".deps", KeyFiles: []string{"lock"}}
	key, err := Key(workspace, entry)
	if err != nil || len(key) != 64 {
		t.Fatalf("key=%q err=%v", key, err)
	}
	store := Store{DataDir: filepath.Join(root, "data")}
	if err := store.Save("repo", "job", entry, key, workspace); err != nil {
		t.Fatal(err)
	}
	if err := os.RemoveAll(filepath.Join(workspace, ".deps")); err != nil {
		t.Fatal(err)
	}
	if _, err := store.Restore("repo", "job", entry, key, workspace); err != nil {
		t.Fatal(err)
	}
	data, err := os.ReadFile(filepath.Join(workspace, ".deps", "module"))
	if err != nil || string(data) != "cached" {
		t.Fatalf("data=%q err=%v", data, err)
	}
	if _, err := store.Restore("repo", "job", entry, "missing", workspace); err != nil {
		t.Fatal(err)
	}
}

func TestKeyChangesAndMarksMissingFiles(t *testing.T) {
	workspace := t.TempDir()
	entry := ciconfig.Cache{Name: "deps", Path: ".deps", KeyFiles: []string{"missing"}}
	missing, err := Key(workspace, entry)
	if err != nil {
		t.Fatal(err)
	}
	if err := os.WriteFile(filepath.Join(workspace, "missing"), []byte("present"), 0o644); err != nil {
		t.Fatal(err)
	}
	present, err := Key(workspace, entry)
	if err != nil || present == missing {
		t.Fatalf("missing=%q present=%q err=%v", missing, present, err)
	}
}

func TestRejectsUnsafePathsSymlinksAndSpecialFiles(t *testing.T) {
	root := t.TempDir()
	workspace := filepath.Join(root, "work")
	if err := os.MkdirAll(workspace, 0o755); err != nil {
		t.Fatal(err)
	}
	if _, err := Key(workspace, ciconfig.Cache{KeyFiles: []string{"../outside"}}); err == nil {
		t.Fatal("unsafe key accepted")
	}
	if err := os.Symlink(root, filepath.Join(workspace, "link")); err != nil {
		t.Fatal(err)
	}
	if _, err := Key(workspace, ciconfig.Cache{KeyFiles: []string{"link/file"}}); err == nil {
		t.Fatal("symlink key accepted")
	}
	store := Store{DataDir: filepath.Join(root, "data")}
	entry := ciconfig.Cache{Name: "deps", Path: "link/cache"}
	if err := store.Save("repo", "job", entry, "key", workspace); err == nil {
		t.Fatal("symlink cache accepted")
	}
	entry.Path = "missing"
	if err := store.Save("repo", "job", entry, "key", workspace); err != nil {
		t.Fatalf("missing cache failed: %v", err)
	}
	if _, err := store.Restore("../repo", "job", entry, "key", workspace); err == nil {
		t.Fatal("unsafe repo accepted")
	}
}

func TestSaveEvictsOldVersions(t *testing.T) {
	root := t.TempDir()
	store := Store{DataDir: root, MaxVersions: 2}
	for i, key := range []string{"aa", "bb", "cc"} {
		workspace := filepath.Join(root, "w", key)
		if err := os.MkdirAll(workspace, 0o755); err != nil {
			t.Fatal(err)
		}
		if err := os.WriteFile(filepath.Join(workspace, "value"), []byte(key), 0o644); err != nil {
			t.Fatal(err)
		}
		if err := store.Save("repo", "job", ciconfig.Cache{Name: "deps", Path: "value"}, key, workspace); err != nil {
			t.Fatal(err)
		}
		mtime := time.Date(2026, 1, 1, 0, 0, i, 0, time.UTC)
		if err := os.Chtimes(filepath.Join(root, "cache", "repo", "job", "deps", key), mtime, mtime); err != nil {
			t.Fatal(err)
		}
	}
	entries, err := os.ReadDir(filepath.Join(root, "cache", "repo", "job", "deps"))
	if err != nil {
		t.Fatal(err)
	}
	var names []string
	for _, entry := range entries {
		if !strings.HasSuffix(entry.Name(), ".lock") && !strings.HasPrefix(entry.Name(), ".") {
			names = append(names, entry.Name())
		}
	}
	if len(names) != 2 {
		t.Fatalf("remaining=%v", names)
	}
	for _, name := range names {
		if name == "aa" {
			t.Fatalf("oldest survived: %v", names)
		}
	}
}

func TestSaveEvictsToByteBudget(t *testing.T) {
	root := t.TempDir()
	store := Store{DataDir: root, MaxVersions: 5, MaxBytes: 3}
	for i, key := range []string{"aa", "bb", "cc"} {
		workspace := filepath.Join(root, "w", key)
		if err := os.MkdirAll(workspace, 0o755); err != nil {
			t.Fatal(err)
		}
		if err := os.WriteFile(filepath.Join(workspace, "value"), []byte(key), 0o644); err != nil {
			t.Fatal(err)
		}
		if err := store.Save("repo", "job", ciconfig.Cache{Name: "deps", Path: "value"}, key, workspace); err != nil {
			t.Fatal(err)
		}
		mtime := time.Date(2026, 1, 1, 0, 0, i, 0, time.UTC)
		if err := os.Chtimes(filepath.Join(root, "cache", "repo", "job", "deps", key), mtime, mtime); err != nil {
			t.Fatal(err)
		}
	}
	entries, err := os.ReadDir(filepath.Join(root, "cache", "repo", "job", "deps"))
	if err != nil {
		t.Fatal(err)
	}
	remaining := 0
	for _, entry := range entries {
		if !strings.HasSuffix(entry.Name(), ".lock") && !strings.HasPrefix(entry.Name(), ".") {
			remaining++
		}
	}
	if remaining != 1 {
		t.Fatalf("remaining=%d want 1 (newest only under 3-byte budget)", remaining)
	}
}

func TestSaveReservesAggregateBudgetBeforeStaging(t *testing.T) {
	root := t.TempDir()
	store := Store{DataDir: root, MaxVersions: 3, MaxBytes: 6}
	save := func(name, data string) error {
		workspace := filepath.Join(root, "work", name)
		if err := os.MkdirAll(workspace, 0o755); err != nil {
			t.Fatal(err)
		}
		if err := os.WriteFile(filepath.Join(workspace, "value"), []byte(data), 0o644); err != nil {
			t.Fatal(err)
		}
		return store.Save("repo", "job", ciconfig.Cache{Name: name, Path: "value"}, "key", workspace)
	}
	if err := save("first", "1234"); err != nil {
		t.Fatal(err)
	}
	if err := save("second", "5678"); err != nil {
		t.Fatal(err)
	}
	if _, err := os.Stat(filepath.Join(root, "cache", "repo", "job", "first", "key")); !os.IsNotExist(err) {
		t.Fatalf("old aggregate cache remains: %v", err)
	}
	if _, err := os.Stat(filepath.Join(root, "cache", "repo", "job", "second", "key")); err != nil {
		t.Fatalf("new aggregate cache missing: %v", err)
	}
	workspace := filepath.Join(root, "oversized")
	if err := os.MkdirAll(workspace, 0o755); err != nil {
		t.Fatal(err)
	}
	if err := os.WriteFile(filepath.Join(workspace, "value"), []byte("1234567"), 0o644); err != nil {
		t.Fatal(err)
	}
	if err := store.Save("repo", "job", ciconfig.Cache{Name: "large", Path: "value"}, "key", workspace); err == nil {
		t.Fatal("oversized cache staged")
	}
	if _, err := os.Stat(filepath.Join(root, "cache", "repo", "job", "large", "key")); !os.IsNotExist(err) {
		t.Fatalf("oversized cache was stored: %v", err)
	}
}

func TestSaveReplacesProtectedVersionWithinAggregateBudget(t *testing.T) {
	root := t.TempDir()
	workspace := filepath.Join(root, "work")
	if err := os.MkdirAll(workspace, 0o755); err != nil {
		t.Fatal(err)
	}
	entry := ciconfig.Cache{Name: "deps", Path: "value"}
	store := Store{DataDir: root, MaxBytes: 3}
	for _, value := range []string{"old", "new"} {
		if err := os.WriteFile(filepath.Join(workspace, "value"), []byte(value), 0o644); err != nil {
			t.Fatal(err)
		}
		if err := store.Save("repo", "job", entry, "key", workspace); err != nil {
			t.Fatal(err)
		}
	}
}

func TestRestoreFallsBackToNewestVersion(t *testing.T) {
	root := t.TempDir()
	workspace := filepath.Join(root, "work")
	if err := os.MkdirAll(workspace, 0o755); err != nil {
		t.Fatal(err)
	}
	store := Store{DataDir: root, MaxVersions: 3}
	for i, key := range []string{"old", "new"} {
		source := filepath.Join(root, "src", key)
		if err := os.MkdirAll(filepath.Join(source, "deps"), 0o755); err != nil {
			t.Fatal(err)
		}
		if err := os.WriteFile(filepath.Join(source, "deps", "marker"), []byte(key), 0o644); err != nil {
			t.Fatal(err)
		}
		if err := store.Save("repo", "job", ciconfig.Cache{Name: "deps", Path: "deps"}, key, source); err != nil {
			t.Fatal(err)
		}
		mtime := time.Date(2026, 1, 1, 0, i, 0, 0, time.UTC)
		if err := os.Chtimes(filepath.Join(root, "cache", "repo", "job", "deps", key), mtime, mtime); err != nil {
			t.Fatal(err)
		}
	}
	if _, err := store.Restore("repo", "job", ciconfig.Cache{Name: "deps", Path: ".deps"}, "missing", workspace); err != nil {
		t.Fatal(err)
	}
	data, err := os.ReadFile(filepath.Join(workspace, ".deps", "marker"))
	if err != nil || string(data) != "new" {
		t.Fatalf("restored=%q err=%v; want newest", data, err)
	}
}

func TestRestoreWithoutAnyVersionSucceeds(t *testing.T) {
	root := t.TempDir()
	workspace := filepath.Join(root, "work")
	if err := os.MkdirAll(workspace, 0o755); err != nil {
		t.Fatal(err)
	}
	store := Store{DataDir: root}
	if _, err := store.Restore("repo", "job", ciconfig.Cache{Name: "deps", Path: ".deps"}, "missing", workspace); err != nil {
		t.Fatalf("empty restore err=%v", err)
	}
}

func TestSaveReplacesPreviousCache(t *testing.T) {
	root := t.TempDir()
	workspace := filepath.Join(root, "work")
	path := filepath.Join(workspace, "deps")
	if err := os.MkdirAll(path, 0o755); err != nil {
		t.Fatal(err)
	}
	entry := ciconfig.Cache{Name: "deps", Path: "deps"}
	store := Store{DataDir: filepath.Join(root, "data")}
	for _, value := range []string{"old", "new"} {
		if err := os.WriteFile(filepath.Join(path, "value"), []byte(value), 0o644); err != nil {
			t.Fatal(err)
		}
		if err := store.Save("repo", "job", entry, "key", workspace); err != nil {
			t.Fatal(err)
		}
	}
	if err := os.RemoveAll(path); err != nil {
		t.Fatal(err)
	}
	if _, err := store.Restore("repo", "job", entry, "key", workspace); err != nil {
		t.Fatal(err)
	}
	data, err := os.ReadFile(filepath.Join(path, "value"))
	if err != nil || string(data) != "new" {
		t.Fatalf("data=%q err=%v", data, err)
	}
}