From 70c16e9774d1a92ecda316f29b3bdab662c6c357 Mon Sep 17 00:00:00 2001 From: Tai Groot Date: Wed, 8 Apr 2026 09:41:58 +0000 Subject: [PATCH] test(commits): add unit tests for commits and cache packages Add tests for CommitSet filtering (by year, author regex), ToYearFreq, channel-based frequency functions, cache round-trip operations, and hash determinism. Also includes goimports formatting fix in common_test.go. --- commits/cache_test.go | 99 +++++++++++++++++ commits/commits_test.go | 212 ++++++++++++++++++++++++++++++++++++ graph/common/common_test.go | 8 +- 3 files changed, 315 insertions(+), 4 deletions(-) create mode 100644 commits/cache_test.go create mode 100644 commits/commits_test.go diff --git a/commits/cache_test.go b/commits/cache_test.go new file mode 100644 index 0000000..c7df96c --- /dev/null +++ b/commits/cache_test.go @@ -0,0 +1,99 @@ +package commits + +import ( + "testing" + "time" + + "github.com/taigrr/gico/types" +) + +func TestHashSliceDeterministic(t *testing.T) { + a := hashSlice([]string{"foo", "bar", "baz"}) + b := hashSlice([]string{"baz", "foo", "bar"}) + if a != b { + t.Error("hashSlice should be order-independent") + } +} + +func TestHashSliceDifferentInputs(t *testing.T) { + a := hashSlice([]string{"foo", "bar"}) + b := hashSlice([]string{"foo", "baz"}) + if a == b { + t.Error("different inputs should produce different hashes") + } +} + +func TestCacheRepoRoundTrip(t *testing.T) { + path := "/test/cache-repo" + head := "deadbeef123" + commits := []types.Commit{ + {Hash: head, Author: types.Author{Name: "test"}, TimeStamp: time.Now()}, + } + + // Should miss before caching + _, ok := GetCachedRepo(path, head) + if ok { + t.Error("expected cache miss before storing") + } + + CacheRepo(path, commits) + + // Should hit after caching + cached, ok := GetCachedRepo(path, head) + if !ok { + t.Error("expected cache hit after storing") + } + if len(cached) != 1 || cached[0].Hash != head { + t.Error("cached data doesn't match stored data") + } + + // Different head should miss + _, ok = GetCachedRepo(path, "differenthead") + if ok { + t.Error("expected cache miss for different head") + } +} + +func TestCacheGraphRoundTrip(t *testing.T) { + year := 2025 + authors := []string{"alice@example.com"} + paths := []string{"/repo/one", "/repo/two"} + freq := types.Freq{1, 2, 3, 0, 0} + + // Should miss before caching + _, ok := GetCachedGraph(year, authors, paths) + if ok { + t.Error("expected cache miss before storing") + } + + CacheGraph(year, authors, paths, freq) + + // Should hit + cached, ok := GetCachedGraph(year, authors, paths) + if !ok { + t.Error("expected cache hit") + } + if len(cached) != len(freq) { + t.Errorf("expected freq length %d, got %d", len(freq), len(cached)) + } +} + +func TestCacheReposAuthorsRoundTrip(t *testing.T) { + paths := []string{"/repo/author-test"} + authors := []string{"alice", "bob"} + + _, ok := GetCachedReposAuthors(paths) + if ok { + t.Error("expected cache miss") + } + + CacheReposAuthors(paths, authors) + + cached, ok := GetCachedReposAuthors(paths) + if !ok { + t.Error("expected cache hit") + } + if len(cached) != 2 { + t.Errorf("expected 2 authors, got %d", len(cached)) + } +} diff --git a/commits/commits_test.go b/commits/commits_test.go new file mode 100644 index 0000000..3baace0 --- /dev/null +++ b/commits/commits_test.go @@ -0,0 +1,212 @@ +package commits + +import ( + "testing" + "time" + + "github.com/taigrr/gico/types" +) + +func makeCommit(name, email string, ts time.Time) types.Commit { + return types.Commit{ + Author: types.Author{Name: name, Email: email}, + TimeStamp: ts, + Hash: "abc123", + Repo: "/test/repo", + } +} + +func TestCommitSetFilterByYear(t *testing.T) { + cs := CommitSet{ + Commits: []types.Commit{ + makeCommit("a", "a@x.com", time.Date(2024, 3, 15, 10, 0, 0, 0, time.UTC)), + makeCommit("b", "b@x.com", time.Date(2025, 6, 1, 12, 0, 0, 0, time.UTC)), + makeCommit("c", "c@x.com", time.Date(2024, 12, 31, 23, 59, 0, 0, time.UTC)), + makeCommit("d", "d@x.com", time.Date(2023, 1, 1, 0, 0, 0, 0, time.UTC)), + }, + } + + filtered := cs.FilterByYear(2024) + if filtered.Year != 2024 { + t.Errorf("expected Year=2024, got %d", filtered.Year) + } + if len(filtered.Commits) != 2 { + t.Errorf("expected 2 commits for 2024, got %d", len(filtered.Commits)) + } + + filtered = cs.FilterByYear(2025) + if len(filtered.Commits) != 1 { + t.Errorf("expected 1 commit for 2025, got %d", len(filtered.Commits)) + } + + filtered = cs.FilterByYear(2000) + if len(filtered.Commits) != 0 { + t.Errorf("expected 0 commits for 2000, got %d", len(filtered.Commits)) + } +} + +func TestCommitSetFilterByAuthorRegex(t *testing.T) { + cs := CommitSet{ + Commits: []types.Commit{ + makeCommit("Alice", "alice@example.com", time.Now()), + makeCommit("Bob", "bob@example.com", time.Now()), + makeCommit("Charlie", "charlie@example.com", time.Now()), + }, + } + + // Filter by name regex + filtered, err := cs.FilterByAuthorRegex([]string{"^Ali"}) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if len(filtered.Commits) != 1 { + t.Errorf("expected 1 commit matching ^Ali, got %d", len(filtered.Commits)) + } + + // Filter by email regex + filtered, err = cs.FilterByAuthorRegex([]string{"bob@"}) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if len(filtered.Commits) != 1 { + t.Errorf("expected 1 commit matching bob@, got %d", len(filtered.Commits)) + } + + // Multiple patterns + filtered, err = cs.FilterByAuthorRegex([]string{"Alice", "Charlie"}) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if len(filtered.Commits) != 2 { + t.Errorf("expected 2 commits matching Alice|Charlie, got %d", len(filtered.Commits)) + } + + // Invalid regex + _, err = cs.FilterByAuthorRegex([]string{"[invalid"}) + if err == nil { + t.Error("expected error for invalid regex, got nil") + } +} + +func TestCommitSetToYearFreq(t *testing.T) { + // Regular year (2025) + cs := CommitSet{ + Year: 2025, + Commits: []types.Commit{ + makeCommit("a", "a@x.com", time.Date(2025, 1, 1, 10, 0, 0, 0, time.UTC)), + makeCommit("a", "a@x.com", time.Date(2025, 1, 1, 14, 0, 0, 0, time.UTC)), + makeCommit("b", "b@x.com", time.Date(2025, 3, 15, 12, 0, 0, 0, time.UTC)), + }, + } + + freq := cs.ToYearFreq() + if len(freq) != 365 { + t.Errorf("expected 365 days for 2025, got %d", len(freq)) + } + if freq[0] != 2 { + t.Errorf("expected 2 commits on Jan 1, got %d", freq[0]) + } + // March 15 = day 74 (31+28+15=74, index 73) + if freq[73] != 1 { + t.Errorf("expected 1 commit on Mar 15, got %d", freq[73]) + } + + // Leap year (2024) + csLeap := CommitSet{ + Year: 2024, + Commits: []types.Commit{ + makeCommit("a", "a@x.com", time.Date(2024, 12, 31, 10, 0, 0, 0, time.UTC)), + }, + } + freqLeap := csLeap.ToYearFreq() + if len(freqLeap) != 366 { + t.Errorf("expected 366 days for 2024, got %d", len(freqLeap)) + } +} + +func TestYearFreqFromChan(t *testing.T) { + cc := make(chan types.Commit, 5) + cc <- makeCommit("a", "a@x.com", time.Date(2025, 1, 1, 10, 0, 0, 0, time.UTC)) + cc <- makeCommit("a", "a@x.com", time.Date(2025, 1, 1, 14, 0, 0, 0, time.UTC)) + cc <- makeCommit("b", "b@x.com", time.Date(2025, 6, 15, 12, 0, 0, 0, time.UTC)) + close(cc) + + freq := YearFreqFromChan(cc, 2025) + if len(freq) != 365 { + t.Errorf("expected 365, got %d", len(freq)) + } + if freq[0] != 2 { + t.Errorf("expected 2 on Jan 1, got %d", freq[0]) + } +} + +func TestFreqFromChan(t *testing.T) { + cc := make(chan types.Commit, 5) + cc <- makeCommit("a", "a@x.com", time.Date(2025, 1, 1, 10, 0, 0, 0, time.UTC)) + cc <- makeCommit("b", "b@x.com", time.Date(2024, 6, 15, 12, 0, 0, 0, time.UTC)) // wrong year + cc <- makeCommit("c", "c@x.com", time.Date(2025, 12, 31, 23, 0, 0, 0, time.UTC)) + close(cc) + + freq := FreqFromChan(cc, 2025) + if len(freq) != 365 { + t.Errorf("expected 365, got %d", len(freq)) + } + if freq[0] != 1 { + t.Errorf("expected 1 on Jan 1 (skip wrong year), got %d", freq[0]) + } + if freq[364] != 1 { + t.Errorf("expected 1 on Dec 31, got %d", freq[364]) + } +} + +func TestFilterCChanByYear(t *testing.T) { + in := make(chan types.Commit, 5) + in <- makeCommit("a", "a@x.com", time.Date(2025, 3, 1, 10, 0, 0, 0, time.UTC)) + in <- makeCommit("b", "b@x.com", time.Date(2024, 3, 1, 10, 0, 0, 0, time.UTC)) + in <- makeCommit("c", "c@x.com", time.Date(2025, 7, 4, 10, 0, 0, 0, time.UTC)) + close(in) + + out := FilterCChanByYear(in, 2025) + count := 0 + for range out { + count++ + } + if count != 2 { + t.Errorf("expected 2 commits for 2025, got %d", count) + } +} + +func TestFilterCChanByAuthor(t *testing.T) { + in := make(chan types.Commit, 5) + in <- makeCommit("Alice", "alice@x.com", time.Now()) + in <- makeCommit("Bob", "bob@x.com", time.Now()) + in <- makeCommit("Charlie", "charlie@x.com", time.Now()) + close(in) + + out, err := FilterCChanByAuthor(in, []string{"Alice", "Charlie"}) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + count := 0 + for range out { + count++ + } + if count != 2 { + t.Errorf("expected 2 commits, got %d", count) + } +} + +func TestOpenRepoNonExistent(t *testing.T) { + _, err := OpenRepo("/nonexistent/path") + if err == nil { + t.Error("expected error for nonexistent path") + } +} + +func TestOpenRepoNonDirectory(t *testing.T) { + // Use a known file (not a directory) + _, err := OpenRepo("/dev/null") + if err == nil { + t.Error("expected error for non-directory path") + } +} diff --git a/graph/common/common_test.go b/graph/common/common_test.go index e793e18..f2b6c2f 100644 --- a/graph/common/common_test.go +++ b/graph/common/common_test.go @@ -8,10 +8,10 @@ import ( func TestMinMax(t *testing.T) { tests := []struct { - name string - input []int - wantMin int - wantMax int + name string + input []int + wantMin int + wantMax int }{ { name: "normal values",