1
0
Fork 0
mirror of https://github.com/imjasonh/git-k8s synced 2026-07-13 08:57:00 +00:00
git-k8s/pkg/workspace/workspace_test.go
Claude 3b8e1bc276
Address PR review comments: fix ref counting, dedup resolveAuth, remove unused CacheStatus
- Fix Release() ref counting bug: avoid calling getRef() which increments
  count, instead look up ref directly and clean up when count reaches zero
- Remove unpopulated CacheStatus struct and fields from types, deepcopy,
  and CRD manifest since nothing writes to them
- Extract duplicated resolveAuth into shared pkg/gitauth package with
  explicit key validation for missing "username"/"password" keys
- Fix GC() race condition by skipping paths with active in-process refs
- Remove misleading WorkspaceCacheMiss metric increment for in-memory
  (non-cache) code paths
- Change push controller from shallow to full clones to avoid silent
  failures when transactions reference historical commits
- Add tests for ref count cleanup, ref leak prevention, GC active ref
  skipping, and gitauth key validation

https://claude.ai/code/session_01WJE6ozYXZMn1CD6d4vy8b6
2026-03-17 15:45:09 +00:00

514 lines
13 KiB
Go

package workspace
import (
"context"
"os"
"path/filepath"
"testing"
"github.com/go-git/go-git/v5"
"github.com/go-git/go-git/v5/plumbing"
"github.com/go-git/go-git/v5/plumbing/object"
"github.com/go-git/go-git/v5/storage/memory"
"knative.dev/pkg/logging"
logtesting "knative.dev/pkg/logging/testing"
)
// initBareRepo creates a bare git repo on disk with an initial commit.
// Returns the repo path and the initial commit hash.
func initBareRepo(t *testing.T, dir string) (string, plumbing.Hash) {
t.Helper()
// Create a non-bare repo first to make commits, then clone to bare.
workDir := filepath.Join(dir, "work")
repo, err := git.PlainInit(workDir, false)
if err != nil {
t.Fatalf("PlainInit: %v", err)
}
wt, err := repo.Worktree()
if err != nil {
t.Fatalf("Worktree: %v", err)
}
// Create a file and commit.
f, err := os.Create(filepath.Join(workDir, "README.md"))
if err != nil {
t.Fatalf("creating file: %v", err)
}
f.WriteString("# test repo")
f.Close()
if _, err := wt.Add("README.md"); err != nil {
t.Fatalf("git add: %v", err)
}
hash, err := wt.Commit("initial commit", &git.CommitOptions{
Author: &object.Signature{
Name: "test",
Email: "test@example.com",
},
})
if err != nil {
t.Fatalf("commit: %v", err)
}
// Clone to bare repo.
bareDir := filepath.Join(dir, "bare.git")
_, err = git.PlainClone(bareDir, true, &git.CloneOptions{
URL: workDir,
})
if err != nil {
t.Fatalf("PlainClone to bare: %v", err)
}
return bareDir, hash
}
func testCtx(t *testing.T) context.Context {
t.Helper()
logger := logtesting.TestLogger(t)
return logging.WithLogger(context.Background(), logger)
}
func TestNewManager_EmptyBasePath(t *testing.T) {
m := NewManager("", false)
if m.basePath != "" {
t.Errorf("basePath = %q, want empty", m.basePath)
}
}
func TestNewManager_WithBasePath(t *testing.T) {
m := NewManager("/data/repos", true)
if m.basePath != "/data/repos" {
t.Errorf("basePath = %q, want /data/repos", m.basePath)
}
if !m.shallow {
t.Error("shallow = false, want true")
}
}
func TestAcquire_InMemoryFallback_EmptyBasePath(t *testing.T) {
// With empty basePath, Acquire should always use in-memory even if
// cacheEnabled is true.
m := NewManager("", false)
ctx := testCtx(t)
// This will fail to clone because the URL is invalid, but we're testing
// the code path selection.
_, err := m.Acquire(ctx, "https://nonexistent.invalid/repo.git", nil, true)
if err == nil {
t.Fatal("expected error for invalid URL, got nil")
}
// The error should come from in-memory clone path.
if ws, err2 := m.Acquire(ctx, "https://nonexistent.invalid/repo.git", nil, false); err2 == nil {
t.Fatalf("expected error, got workspace mode=%s", ws.Mode)
}
}
func TestAcquire_InMemoryFallback_CacheDisabled(t *testing.T) {
dir := t.TempDir()
m := NewManager(dir, false)
ctx := testCtx(t)
// cacheEnabled=false should force in-memory even with a basePath.
_, err := m.Acquire(ctx, "https://nonexistent.invalid/repo.git", nil, false)
if err == nil {
t.Fatal("expected error for invalid URL")
}
}
func TestAcquire_DiskClone_NewRepo(t *testing.T) {
// Create a source repo to clone from.
sourceDir := t.TempDir()
barePath, _ := initBareRepo(t, sourceDir)
cacheDir := t.TempDir()
m := NewManager(cacheDir, false)
ctx := testCtx(t)
// First acquire: should do a full clone (cache miss).
ws, err := m.Acquire(ctx, barePath, nil, true)
if err != nil {
t.Fatalf("Acquire: %v", err)
}
if ws.Mode != "disk" {
t.Errorf("Mode = %q, want disk", ws.Mode)
}
if ws.Repo == nil {
t.Fatal("Repo is nil")
}
// Verify we can read the commit.
head, err := ws.Repo.Head()
if err != nil {
t.Fatalf("Head: %v", err)
}
if head.Hash().IsZero() {
t.Error("HEAD hash is zero")
}
m.Release(ws)
}
func TestAcquire_DiskFetch_ExistingRepo(t *testing.T) {
// Create a source repo.
sourceDir := t.TempDir()
barePath, initialHash := initBareRepo(t, sourceDir)
cacheDir := t.TempDir()
m := NewManager(cacheDir, false)
ctx := testCtx(t)
// First acquire: clone.
ws1, err := m.Acquire(ctx, barePath, nil, true)
if err != nil {
t.Fatalf("first Acquire: %v", err)
}
head1, _ := ws1.Repo.Head()
if head1.Hash() != initialHash {
t.Errorf("first HEAD = %s, want %s", head1.Hash(), initialHash)
}
m.Release(ws1)
// Add a new commit to the bare source repo directly.
srcBare, err := git.PlainOpen(barePath)
if err != nil {
t.Fatalf("opening bare source repo: %v", err)
}
// Create a new commit by modifying the bare repo directly.
srcStorer := srcBare.Storer
headRef, _ := srcBare.Head()
parentCommit, _ := srcBare.CommitObject(headRef.Hash())
newCommit := &object.Commit{
Author: object.Signature{Name: "test", Email: "test@example.com"},
Committer: object.Signature{Name: "test", Email: "test@example.com"},
Message: "second commit",
TreeHash: parentCommit.TreeHash,
ParentHashes: []plumbing.Hash{headRef.Hash()},
}
obj := srcStorer.NewEncodedObject()
newCommit.Encode(obj)
newHash, _ := srcStorer.SetEncodedObject(obj)
// Update refs/heads/main to point to the new commit.
ref := plumbing.NewHashReference("refs/heads/main", newHash)
srcStorer.SetReference(ref)
// Second acquire: should fetch and get new commit via refs/heads/*.
ws2, err := m.Acquire(ctx, barePath, nil, true)
if err != nil {
t.Fatalf("second Acquire: %v", err)
}
// Check that the main branch ref was updated.
mainRef, err := ws2.Repo.Reference("refs/heads/main", true)
if err != nil {
t.Fatalf("getting refs/heads/main: %v", err)
}
if mainRef.Hash() != newHash {
t.Errorf("refs/heads/main = %s, want %s", mainRef.Hash(), newHash)
}
m.Release(ws2)
}
func TestAcquire_ShallowClone(t *testing.T) {
sourceDir := t.TempDir()
barePath, _ := initBareRepo(t, sourceDir)
cacheDir := t.TempDir()
m := NewManager(cacheDir, true /* shallow */)
ctx := testCtx(t)
ws, err := m.Acquire(ctx, barePath, nil, true)
if err != nil {
t.Fatalf("Acquire: %v", err)
}
if ws.Mode != "disk" {
t.Errorf("Mode = %q, want disk", ws.Mode)
}
m.Release(ws)
}
func TestDeepen_NoOpForMemory(t *testing.T) {
m := NewManager("", false)
ws := &Workspace{Mode: "memory"}
ctx := testCtx(t)
if err := m.Deepen(ctx, ws, nil); err != nil {
t.Fatalf("Deepen: %v", err)
}
}
func TestRelease_NilWorkspace(t *testing.T) {
m := NewManager("", false)
// Should not panic.
m.Release(nil)
}
func TestRelease_InMemoryWorkspace(t *testing.T) {
m := NewManager("", false)
ws := &Workspace{Mode: "memory"}
// Should not panic (manager is nil for in-memory).
m.Release(ws)
}
func TestGC_EmptyBasePath(t *testing.T) {
m := NewManager("", false)
ctx := testCtx(t)
// Should be a no-op without panic.
m.GC(ctx, nil)
}
func TestGC_RemovesStaleDirs(t *testing.T) {
cacheDir := t.TempDir()
m := NewManager(cacheDir, false)
ctx := testCtx(t)
// Create some directories to simulate cached repos.
staleDir := filepath.Join(cacheDir, "stale-repo")
activeDir := filepath.Join(cacheDir, "active-repo")
os.MkdirAll(staleDir, 0o755)
os.MkdirAll(activeDir, 0o755)
// Only the active dir should survive GC.
activeRepos := map[string]bool{
activeDir: true,
}
m.GC(ctx, activeRepos)
if _, err := os.Stat(staleDir); !os.IsNotExist(err) {
t.Error("stale dir should be removed after GC")
}
if _, err := os.Stat(activeDir); os.IsNotExist(err) {
t.Error("active dir should still exist after GC")
}
}
func TestGC_IgnoresFiles(t *testing.T) {
cacheDir := t.TempDir()
m := NewManager(cacheDir, false)
ctx := testCtx(t)
// Create a regular file (not a directory).
os.WriteFile(filepath.Join(cacheDir, "some-file"), []byte("data"), 0o644)
// GC should not panic or remove files.
m.GC(ctx, map[string]bool{})
if _, err := os.Stat(filepath.Join(cacheDir, "some-file")); os.IsNotExist(err) {
t.Error("regular file should not be removed by GC")
}
}
func TestRepoPath_Deterministic(t *testing.T) {
m := NewManager("/data/repos", false)
url := "https://github.com/example/repo.git"
path1 := m.repoPath(url)
path2 := m.repoPath(url)
if path1 != path2 {
t.Errorf("repoPath not deterministic: %q != %q", path1, path2)
}
// Different URL should give different path.
path3 := m.repoPath("https://github.com/other/repo.git")
if path1 == path3 {
t.Errorf("different URLs gave same path: %q", path1)
}
}
func TestCachePath_EmptyBasePath(t *testing.T) {
m := NewManager("", false)
if p := m.CachePath("https://example.com/repo.git"); p != "" {
t.Errorf("CachePath = %q, want empty for no basePath", p)
}
}
func TestCachePath_WithBasePath(t *testing.T) {
m := NewManager("/data/repos", false)
p := m.CachePath("https://example.com/repo.git")
if p == "" {
t.Error("CachePath should be non-empty")
}
if !filepath.IsAbs(p) {
t.Errorf("CachePath should be absolute, got %q", p)
}
}
func TestBasePath(t *testing.T) {
m := NewManager("/data/repos", false)
if m.BasePath() != "/data/repos" {
t.Errorf("BasePath = %q, want /data/repos", m.BasePath())
}
}
func TestContextInjection(t *testing.T) {
m := NewManager("/test", true)
ctx := WithManager(context.Background(), m)
got := GetManager(ctx)
if got != m {
t.Error("GetManager should return the injected manager")
}
}
func TestContextInjection_Default(t *testing.T) {
// Without injection, should return a default in-memory manager.
got := GetManager(context.Background())
if got == nil {
t.Fatal("GetManager should return non-nil default")
}
if got.basePath != "" {
t.Errorf("default manager basePath = %q, want empty", got.basePath)
}
}
func TestAcquire_InMemory_ValidLocalRepo(t *testing.T) {
// Test that in-memory acquire works with a valid local repo.
sourceDir := t.TempDir()
barePath, _ := initBareRepo(t, sourceDir)
m := NewManager("", false)
ctx := testCtx(t)
ws, err := m.Acquire(ctx, barePath, nil, false)
if err != nil {
t.Fatalf("Acquire: %v", err)
}
if ws.Mode != "memory" {
t.Errorf("Mode = %q, want memory", ws.Mode)
}
if ws.Repo == nil {
t.Fatal("Repo is nil")
}
head, err := ws.Repo.Head()
if err != nil {
t.Fatalf("Head: %v", err)
}
if head.Hash().IsZero() {
t.Error("HEAD hash is zero")
}
m.Release(ws)
}
// Test that the same repo can be acquired and released multiple times.
func TestAcquire_MultipleAcquireRelease(t *testing.T) {
sourceDir := t.TempDir()
barePath, _ := initBareRepo(t, sourceDir)
cacheDir := t.TempDir()
m := NewManager(cacheDir, false)
ctx := testCtx(t)
for i := 0; i < 3; i++ {
ws, err := m.Acquire(ctx, barePath, nil, true)
if err != nil {
t.Fatalf("Acquire #%d: %v", i, err)
}
if ws.Mode != "disk" {
t.Errorf("Acquire #%d Mode = %q, want disk", i, ws.Mode)
}
m.Release(ws)
}
}
// Ensure memory.NewStorage-backed workspace works correctly (for buildMergedTree etc.)
func TestInMemoryStorerCompatibility(t *testing.T) {
storer := memory.NewStorage()
repo, err := git.Init(storer, nil)
if err != nil {
t.Fatalf("Init: %v", err)
}
if repo == nil {
t.Fatal("repo is nil")
}
}
// Test that Release properly decrements ref count and cleans up the active map.
func TestRelease_RefCountCleanup(t *testing.T) {
sourceDir := t.TempDir()
barePath, _ := initBareRepo(t, sourceDir)
cacheDir := t.TempDir()
m := NewManager(cacheDir, false)
ctx := testCtx(t)
ws, err := m.Acquire(ctx, barePath, nil, true)
if err != nil {
t.Fatalf("Acquire: %v", err)
}
// After acquire, ref should exist with count 1.
m.mu.Lock()
r, ok := m.active[ws.path]
m.mu.Unlock()
if !ok {
t.Fatal("expected active ref after acquire")
}
if r.count != 1 {
t.Errorf("ref count after acquire = %d, want 1", r.count)
}
m.Release(ws)
// After release, the ref should be cleaned up from the active map.
m.mu.Lock()
_, ok = m.active[ws.path]
m.mu.Unlock()
if ok {
t.Error("expected active ref to be cleaned up after release")
}
}
// Test that multiple acquire/release cycles don't leak refs.
func TestRelease_NoRefLeak(t *testing.T) {
sourceDir := t.TempDir()
barePath, _ := initBareRepo(t, sourceDir)
cacheDir := t.TempDir()
m := NewManager(cacheDir, false)
ctx := testCtx(t)
for i := 0; i < 5; i++ {
ws, err := m.Acquire(ctx, barePath, nil, true)
if err != nil {
t.Fatalf("Acquire #%d: %v", i, err)
}
m.Release(ws)
}
// After all cycles, active map should be empty.
m.mu.Lock()
activeCount := len(m.active)
m.mu.Unlock()
if activeCount != 0 {
t.Errorf("active map has %d entries, want 0 after all releases", activeCount)
}
}
// Test that GC skips directories with active references.
func TestGC_SkipsActiveRefs(t *testing.T) {
sourceDir := t.TempDir()
barePath, _ := initBareRepo(t, sourceDir)
cacheDir := t.TempDir()
m := NewManager(cacheDir, false)
ctx := testCtx(t)
// Acquire a workspace (creates a cached dir and holds a ref).
ws, err := m.Acquire(ctx, barePath, nil, true)
if err != nil {
t.Fatalf("Acquire: %v", err)
}
// Run GC with empty activeRepos — the cache dir should survive because
// there's an active in-process reference.
m.GC(ctx, map[string]bool{})
if _, err := os.Stat(ws.path); os.IsNotExist(err) {
t.Error("GC should not remove dir with active references")
}
m.Release(ws)
}