| // Copyright 2026 The Fuchsia Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style |
| // license that can be found in the LICENSE file. |
| |
| package project_test |
| |
| import ( |
| "os" |
| "path/filepath" |
| "testing" |
| |
| "go.fuchsia.dev/jiri/jiritest" |
| "go.fuchsia.dev/jiri/project" |
| ) |
| |
| func TestRestorePackageFromCache(t *testing.T) { |
| fake := jiritest.NewFakeJiriRoot(t) |
| |
| pkg := project.Package{ |
| Name: "test/pkg", |
| Path: "prebuilt/test", |
| Version: "v1", |
| } |
| |
| destRelPath, err := pkg.ResolvePath() |
| if err != nil { |
| t.Fatal(err) |
| } |
| destDir := filepath.Join(fake.X.Root, destRelPath) |
| |
| groups := map[string][]project.Package{ |
| destRelPath: {pkg}, |
| } |
| hash := project.InternalComputeGroupHash(groups[destRelPath]) |
| cacheDir := filepath.Join(fake.X.PackageCacheDir(), hash) |
| |
| // Case: Destination is not a symlink. |
| t.Run("NotASymlink", func(t *testing.T) { |
| if err := os.MkdirAll(destDir, 0755); err != nil { |
| t.Fatal(err) |
| } |
| defer os.RemoveAll(destDir) |
| |
| testFile := filepath.Join(destDir, "file") |
| if err := os.WriteFile(testFile, []byte("content"), 0644); err != nil { |
| t.Fatal(err) |
| } |
| |
| if err := project.InternalRestorePackageFromCache(fake.X, pkg, groups); err != nil { |
| t.Errorf("expected no error, got %v", err) |
| } |
| |
| // Verify destDir is still a directory and file exists. |
| fi, err := os.Lstat(destDir) |
| if err != nil { |
| t.Fatal(err) |
| } |
| if fi.Mode()&os.ModeSymlink != 0 { |
| t.Error("expected directory, got symlink") |
| } |
| if _, err := os.Stat(testFile); err != nil { |
| t.Errorf("expected file to exist, got %v", err) |
| } |
| }) |
| |
| // Case: Destination is a symlink, cache exists, restore successful. |
| t.Run("SymlinkAndCacheExists", func(t *testing.T) { |
| if err := os.MkdirAll(cacheDir, 0755); err != nil { |
| t.Fatal(err) |
| } |
| defer os.RemoveAll(cacheDir) |
| |
| cachedFile := filepath.Join(cacheDir, "cached_file") |
| if err := os.WriteFile(cachedFile, []byte("cached_content"), 0644); err != nil { |
| t.Fatal(err) |
| } |
| |
| if err := os.Symlink(cacheDir, destDir); err != nil { |
| t.Fatal(err) |
| } |
| defer os.RemoveAll(destDir) |
| |
| if err := project.InternalRestorePackageFromCache(fake.X, pkg, groups); err != nil { |
| t.Errorf("expected no error, got %v", err) |
| } |
| |
| // Verify destDir is now a real directory (not a symlink) and has the cached file. |
| fi, err := os.Lstat(destDir) |
| if err != nil { |
| t.Fatal(err) |
| } |
| if fi.Mode()&os.ModeSymlink != 0 { |
| t.Error("expected directory after restore, got symlink") |
| } |
| gotContent, err := os.ReadFile(filepath.Join(destDir, "cached_file")) |
| if err != nil { |
| t.Fatal(err) |
| } |
| if string(gotContent) != "cached_content" { |
| t.Errorf("expected cached_content, got %q", string(gotContent)) |
| } |
| }) |
| |
| // Case: Destination is a symlink, but cache does not exist. |
| t.Run("SymlinkAndNoCache", func(t *testing.T) { |
| // Ensure cache dir does not exist |
| if err := os.RemoveAll(cacheDir); err != nil { |
| t.Fatal(err) |
| } |
| |
| // Create destDir as a symlink to a non-existent cacheDir |
| if err := os.Symlink(cacheDir, destDir); err != nil { |
| t.Fatal(err) |
| } |
| defer os.RemoveAll(destDir) |
| |
| if err := project.InternalRestorePackageFromCache(fake.X, pkg, groups); err != nil { |
| t.Errorf("expected no error, got %v", err) |
| } |
| |
| // Verify destDir (the symlink) is removed so that CIPD can download it. |
| _, err := os.Lstat(destDir) |
| if !os.IsNotExist(err) { |
| t.Errorf("expected destDir to be removed, got err: %v", err) |
| } |
| }) |
| } |
| |
| func TestIsPackageGroupCached(t *testing.T) { |
| fake := jiritest.NewFakeJiriRoot(t) |
| |
| // Enable cache |
| fake.X.PackageCacheEnabled = true |
| |
| pkg := project.Package{ |
| Name: "test/pkg", |
| Path: "prebuilt/test", |
| Version: "v1", |
| } |
| groupPkgs := []project.Package{pkg} |
| hash := project.InternalComputeGroupHash(groupPkgs) |
| cacheDir := filepath.Join(fake.X.PackageCacheDir(), hash) |
| |
| // Case 1: Cache is cold (doesn't exist) |
| t.Run("CacheCold", func(t *testing.T) { |
| if project.InternalIsPackageGroupCached(fake.X, groupPkgs) { |
| t.Error("expected false for cold cache, got true") |
| } |
| }) |
| |
| // Case 2: Cache is hot (exists and is directory) |
| t.Run("CacheHot", func(t *testing.T) { |
| if err := os.MkdirAll(cacheDir, 0755); err != nil { |
| t.Fatal(err) |
| } |
| defer os.RemoveAll(cacheDir) |
| |
| if !project.InternalIsPackageGroupCached(fake.X, groupPkgs) { |
| t.Error("expected true for hot cache, got false") |
| } |
| }) |
| |
| // Case 3: Cache exists but is a symlink (incorrect state) |
| t.Run("CacheIsSymlink", func(t *testing.T) { |
| // Create a target for symlink |
| targetDir := filepath.Join(t.TempDir(), "target") |
| if err := os.MkdirAll(targetDir, 0755); err != nil { |
| t.Fatal(err) |
| } |
| if err := os.Symlink(targetDir, cacheDir); err != nil { |
| t.Fatal(err) |
| } |
| defer os.Remove(cacheDir) |
| |
| if project.InternalIsPackageGroupCached(fake.X, groupPkgs) { |
| t.Error("expected false when cache is a symlink, got true") |
| } |
| }) |
| } |
| |
| func TestPartitionPackages(t *testing.T) { |
| fake := jiritest.NewFakeJiriRoot(t) |
| |
| // Define 3 packages: |
| // - pkg1 and pkg2 go to same destination (same group) |
| // - pkg3 goes to a different destination (different group) |
| pkg1 := project.Package{Name: "pkg1", Path: "dir1", Version: "v1"} |
| pkg2 := project.Package{Name: "pkg2", Path: "dir1", Version: "v1"} // same dest, same group |
| pkg3 := project.Package{Name: "pkg3", Path: "dir2", Version: "v1"} // diff dest, diff group |
| |
| pkgs := project.Packages{ |
| pkg1.Key(): pkg1, |
| pkg2.Key(): pkg2, |
| pkg3.Key(): pkg3, |
| } |
| |
| // Case 1: Cache is disabled. All should be uncached. |
| t.Run("CacheDisabled", func(t *testing.T) { |
| fake.X.PackageCacheEnabled = false |
| cached, uncached, err := project.InternalPartitionPackages(fake.X, pkgs) |
| if err != nil { |
| t.Fatal(err) |
| } |
| if len(cached) != 0 { |
| t.Errorf("expected 0 cached packages, got %d", len(cached)) |
| } |
| if len(uncached) != 3 { |
| t.Errorf("expected 3 uncached packages, got %d", len(uncached)) |
| } |
| }) |
| |
| // Case 2: Cache is enabled. |
| t.Run("CacheEnabled", func(t *testing.T) { |
| fake.X.PackageCacheEnabled = true |
| cacheDir := fake.X.PackageCacheDir() |
| |
| // Cache the group for pkg1 and pkg2, but NOT pkg3 |
| group1 := []project.Package{pkg1, pkg2} |
| hash1 := project.InternalComputeGroupHash(group1) |
| pkg1And2CacheDir := filepath.Join(cacheDir, hash1) |
| if err := os.MkdirAll(pkg1And2CacheDir, 0755); err != nil { |
| t.Fatal(err) |
| } |
| defer os.RemoveAll(pkg1And2CacheDir) |
| |
| cached, uncached, err := project.InternalPartitionPackages(fake.X, pkgs) |
| if err != nil { |
| t.Fatal(err) |
| } |
| |
| // pkg1 and pkg2 should be cached |
| if len(cached) != 2 { |
| t.Errorf("expected 2 cached packages, got %d", len(cached)) |
| } |
| if _, ok := cached[pkg1.Key()]; !ok { |
| t.Errorf("expected pkg1 to be cached") |
| } |
| if _, ok := cached[pkg2.Key()]; !ok { |
| t.Errorf("expected pkg2 to be cached") |
| } |
| |
| // pkg3 should be uncached |
| if len(uncached) != 1 { |
| t.Errorf("expected 1 uncached package, got %d", len(uncached)) |
| } |
| if _, ok := uncached[pkg3.Key()]; !ok { |
| t.Errorf("expected pkg3 to be uncached") |
| } |
| }) |
| } |