Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
178 changes: 178 additions & 0 deletions internal/cache/git_watcher_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
package cache

import (
"os"
"os/exec"
"path/filepath"
"strings"
"testing"
)

func TestIsGitRepoNonExistent(t *testing.T) {
dir := filepath.Join(os.TempDir(), "nonexistent-git-test")
result := IsGitRepo(dir)
if result {
t.Error("expected false for non-existent directory")
}
}

func TestIsGitRepoPlainDir(t *testing.T) {
dir := t.TempDir()
result := IsGitRepo(dir)
if result {
t.Error("expected false for plain directory without .git")
}
}

func TestIsGitRepoWithDotGit(t *testing.T) {
dir := t.TempDir()
dotGit := filepath.Join(dir, ".git")
if err := os.MkdirAll(dotGit, 0o755); err != nil {
t.Fatal(err)
}
result := IsGitRepo(dir)
if !result {
t.Error("expected true for directory with .git subdirectory")
}
}

func TestNewGitWatcher(t *testing.T) {
qc, err := NewQueryCache("")
if err != nil {
t.Fatal(err)
}
w := NewGitWatcher(qc)
if w == nil {
t.Fatal("NewGitWatcher returned nil")
}
if w.repoStates == nil {
t.Error("repoStates map should be initialized")
}
if w.cache != qc {
t.Error("cache reference should match")
}
}

func TestGetChangedFilesEmpty(t *testing.T) {
dir := t.TempDir()
gitInit(t, dir)
gitCreateFile(t, dir, "initial.txt", "init")

files, err := GetChangedFiles(dir, "HEAD", "HEAD")
if err != nil {
t.Fatalf("GetChangedFiles returned error: %v", err)
}
if len(files) != 0 {
t.Errorf("expected 0 changed files between same commit, got %d", len(files))
}
}

func TestGetChangedFilesWithChanges(t *testing.T) {
dir := t.TempDir()
gitInit(t, dir)
gitCreateFile(t, dir, "initial.txt", "init")

firstCommit := gitHead(t, dir)

gitCreateFile(t, dir, "test.txt", "content")

files, err := GetChangedFiles(dir, firstCommit, "HEAD")
if err != nil {
t.Fatalf("GetChangedFiles returned error: %v", err)
}
if len(files) == 0 {
t.Error("expected changed files between commits")
}
}

func TestGetFileHashes(t *testing.T) {
dir := t.TempDir()
gitInit(t, dir)
gitCreateFile(t, dir, "foo.txt", "hello")

qc, _ := NewQueryCache("")
w := NewGitWatcher(qc)
hashes, err := w.GetFileHashes(dir, []string{"foo.txt"})
if err != nil {
t.Fatalf("GetFileHashes returned error: %v", err)
}
if _, ok := hashes[".git/HEAD"]; !ok {
t.Error("expected .git/HEAD hash")
}
}

func TestGetFileHashesNonGitDir(t *testing.T) {
dir := t.TempDir()
srcFile := filepath.Join(dir, "test.txt")
if err := os.WriteFile(srcFile, []byte("content"), 0o644); err != nil {
t.Fatal(err)
}

qc2, _ := NewQueryCache("")
w2 := NewGitWatcher(qc2)
hashes, err := w2.GetFileHashes(dir, []string{"test.txt"})
if err != nil {
t.Fatalf("GetFileHashes returned error: %v", err)
}
if len(hashes) == 0 {
t.Error("expected file hashes for non-git dir")
}
}

func TestGetFileHashesWithDirtyFile(t *testing.T) {
dir := t.TempDir()
gitInit(t, dir)
gitCreateFile(t, dir, "clean.txt", "clean content")

qc3, _ := NewQueryCache("")
w := NewGitWatcher(qc3)

dirtyFile := filepath.Join(dir, "clean.txt")
if err := os.WriteFile(dirtyFile, []byte("dirty content"), 0o644); err != nil {
t.Fatal(err)
}

hashes, err := w.GetFileHashes(dir, []string{"clean.txt"})
if err != nil {
t.Fatalf("GetFileHashes returned error: %v", err)
}
if _, ok := hashes[".git/HEAD"]; !ok {
t.Error("expected .git/HEAD hash")
}
}

func gitInit(t *testing.T, dir string) {
t.Helper()
execCommand(t, dir, "git", "init")
execCommand(t, dir, "git", "config", "user.email", "test@test.com")
execCommand(t, dir, "git", "config", "user.name", "Test")
}

func gitHead(t *testing.T, dir string) string {
t.Helper()
return execCommand(t, dir, "git", "rev-parse", "HEAD")
}

func gitCreateFile(t *testing.T, dir, name, content string) {
t.Helper()
path := filepath.Join(dir, name)
if err := os.WriteFile(path, []byte(content), 0o644); err != nil {
t.Fatal(err)
}
execCommand(t, dir, "git", "add", name)
execCommand(t, dir, "git", "commit", "-m", "add "+name)
}

func execCommand(t *testing.T, dir, cmd string, args ...string) string {
t.Helper()
c := exec.Command(cmd, args...)
c.Dir = dir
output, err := c.Output()
if err != nil {
if ee, ok := err.(*exec.ExitError); ok {
t.Fatalf("%s %v failed: %v\nstdout: %s\nstderr: %s", cmd, args, err, string(output), string(ee.Stderr))
}
t.Fatalf("%s %v failed: %v\n%s", cmd, args, err, string(output))
}
return strings.TrimSpace(string(output))
}
185 changes: 185 additions & 0 deletions internal/cache/multilevel_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
package cache

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

func TestNewMultiLevelCache(t *testing.T) {
dir := t.TempDir()
l2 := filepath.Join(dir, "l2")
mc, err := NewMultiLevelCache(l2, 10)
if err != nil {
t.Fatalf("NewMultiLevelCache returned error: %v", err)
}
if mc == nil {
t.Fatal("NewMultiLevelCache returned nil")
}
if _, err := os.Stat(l2); os.IsNotExist(err) {
t.Error("L2 directory should have been created")
}
}

func TestMultiLevelCacheSetAndGet(t *testing.T) {
dir := t.TempDir()
mc, err := NewMultiLevelCache(filepath.Join(dir, "l2"), 10)
if err != nil {
t.Fatal(err)
}

mc.Set("key1", "value1")
val, ok := mc.Get("key1")
if !ok {
t.Fatal("expected to find key1")
}
if val != "value1" {
t.Errorf("expected value1, got %q", val)
}
}

func TestMultiLevelCacheMiss(t *testing.T) {
dir := t.TempDir()
mc, err := NewMultiLevelCache(filepath.Join(dir, "l2"), 10)
if err != nil {
t.Fatal(err)
}

_, ok := mc.Get("nonexistent")
if ok {
t.Error("expected miss for nonexistent key")
}
}

func TestMultiLevelCacheOverwrite(t *testing.T) {
dir := t.TempDir()
mc, err := NewMultiLevelCache(filepath.Join(dir, "l2"), 10)
if err != nil {
t.Fatal(err)
}

mc.Set("key1", "old")
mc.Set("key1", "new")

val, ok := mc.Get("key1")
if !ok {
t.Fatal("expected to find key1")
}
if val != "new" {
t.Errorf("expected new value, got %q", val)
}
}

func TestMultiLevelCacheL1Eviction(t *testing.T) {
dir := t.TempDir()
mc, err := NewMultiLevelCache(filepath.Join(dir, "l2"), 2)
if err != nil {
t.Fatal(err)
}

mc.Set("a", "1")
mc.Set("b", "2")
mc.Set("c", "3") // L1 evicts oldest (a) to stay at maxL1=2

mc.mu.RLock()
l1Len := len(mc.l1)
mc.mu.RUnlock()

if l1Len > 2 {
t.Errorf("expected L1 size <= 2 after eviction, got %d", l1Len)
}

val, ok := mc.Get("a")
if !ok {
t.Error("expected 'a' to be retrievable from L2 after L1 eviction")
}
if val != "1" {
t.Errorf("expected '1', got %q", val)
}
}

func TestMultiLevelCacheL2Promotion(t *testing.T) {
dir := t.TempDir()
l2 := filepath.Join(dir, "l2")
mc, err := NewMultiLevelCache(l2, 10)
if err != nil {
t.Fatal(err)
}

mc.Set("key1", "value1")

mc2, err := NewMultiLevelCache(l2, 10)
if err != nil {
t.Fatal(err)
}

val, ok := mc2.Get("key1")
if !ok {
t.Fatal("expected to retrieve from L2 disk cache")
}
if val != "value1" {
t.Errorf("expected value1, got %q", val)
}
}

func TestMultiLevelCacheL2PromotionUpdatesL1(t *testing.T) {
dir := t.TempDir()
l2 := filepath.Join(dir, "l2")
mc, err := NewMultiLevelCache(l2, 10)
if err != nil {
t.Fatal(err)
}

mc.Set("key1", "value1")

mc2, err := NewMultiLevelCache(l2, 10)
if err != nil {
t.Fatal(err)
}

mc2.Get("key1")

t.Log("L1 should now have key1 from promotion")
}

func TestMultiLevelCacheSetGetLargeValue(t *testing.T) {
dir := t.TempDir()
mc, err := NewMultiLevelCache(filepath.Join(dir, "l2"), 10)
if err != nil {
t.Fatal(err)
}

largeVal := strings.Repeat("x", 10000)
mc.Set("large", largeVal)

val, ok := mc.Get("large")
if !ok {
t.Fatal("expected to find large value")
}
if val != largeVal {
t.Errorf("expected large value to match, got length %d", len(val))
}
}

func TestMultiLevelCacheConcurrent(t *testing.T) {
dir := t.TempDir()
mc, err := NewMultiLevelCache(filepath.Join(dir, "l2"), 100)
if err != nil {
t.Fatal(err)
}

done := make(chan bool)
for i := 0; i < 20; i++ {
go func(n int) {
key := string(rune('a' + n))
mc.Set(key, "val")
mc.Get(key)
done <- true
}(i)
}

for i := 0; i < 20; i++ {
<-done
}
}
Loading
Loading