| // Copyright 2026 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| package e2etests |
| |
| import ( |
| "context" |
| "errors" |
| "flag" |
| "fmt" |
| "os" |
| "path/filepath" |
| "strings" |
| "testing" |
| "time" |
| |
| "github.com/google/subcommands" |
| |
| "go.chromium.org/build/siso/auth/cred" |
| "go.chromium.org/build/siso/build/ninjabuild" |
| "go.chromium.org/build/siso/hashfs" |
| "go.chromium.org/build/siso/subcmd/fscmd" |
| ) |
| |
| const stateFileName = ".siso_fs_state" |
| |
| func setupGCTestDir(t *testing.T) (context.Context, string) { |
| t.Helper() |
| ctx := t.Context() |
| dir := tempDir(t) |
| |
| if err := os.MkdirAll(filepath.Join(dir, "build/config/siso"), 0755); err != nil { |
| t.Fatal(err) |
| } |
| if err := os.MkdirAll(filepath.Join(dir, "out/siso"), 0755); err != nil { |
| t.Fatal(err) |
| } |
| |
| setupFiles(t, dir, "TestBuild_FSGC_E2E", nil) |
| if err := os.Remove(filepath.Join(dir, "out/siso", stateFileName)); err != nil && !errors.Is(err, os.ErrNotExist) { |
| t.Fatal(err) |
| } |
| return ctx, dir |
| } |
| |
| func runNinjaWithLabel(ctx context.Context, t *testing.T, dir, label, ninjaFile string) { |
| t.Helper() |
| original := filepath.Join(dir, "out/siso", ninjaFile) |
| dest := filepath.Join(dir, "out/siso/build.ninja") |
| originalB, err := os.ReadFile(original) |
| if err != nil { |
| t.Fatal(err) |
| } |
| if err := os.WriteFile(dest, originalB, 0644); err != nil { |
| t.Fatal(err) |
| } |
| |
| opt, graph, cleanup := setupBuild(ctx, t, dir, hashfs.Option{StateFile: stateFileName, BuildLabel: label}) |
| defer cleanup() |
| |
| _, err = ninjabuild.Run(ctx, graph, opt, nil, ninjabuild.RunNinjaOpts{}) |
| if err != nil { |
| t.Fatalf("ninja err for %s, got=%v; want=<nil>", label, err) |
| } |
| } |
| |
| func runGC(ctx context.Context, t *testing.T, dir string, args ...string) error { |
| t.Helper() |
| cmd := fscmd.Cmd(cred.Options{}) |
| fs := flag.NewFlagSet("fs", flag.ContinueOnError) |
| cmd.SetFlags(fs) |
| fullArgs := append([]string{"gc", "-C", filepath.Join(dir, "out/siso"), "--fs_state", stateFileName}, args...) |
| if err := fs.Parse(fullArgs); err != nil { |
| return err |
| } |
| status := cmd.Execute(ctx, fs) |
| if status != subcommands.ExitSuccess { |
| return fmt.Errorf("fscmd gc exited with status %v", status) |
| } |
| return nil |
| } |
| |
| func TestBuild_FSGC_EvictTarget(t *testing.T) { |
| if !runInSubProcess(t) { |
| return |
| } |
| ctx, dir := setupGCTestDir(t) |
| |
| runNinjaWithLabel(ctx, t, dir, "target-1", "build.ninja.0") |
| for _, fname := range []string{"out/siso/obj/bar.o", "out/siso/obj/foo.o"} { |
| if _, err := os.Stat(filepath.Join(dir, fname)); err != nil { |
| t.Fatalf("stat(%s) err, got=%v; want=<nil>", fname, err) |
| } |
| } |
| |
| time.Sleep(50 * time.Millisecond) |
| |
| runNinjaWithLabel(ctx, t, dir, "target-2", "build.ninja.1") |
| for _, fname := range []string{"out/siso/obj/bar.o", "out/siso/obj/foo.o", "out/siso/obj/baz.o"} { |
| if _, err := os.Stat(filepath.Join(dir, fname)); err != nil { |
| t.Fatalf("stat(%s) err, got=%v; want=<nil>", fname, err) |
| } |
| } |
| |
| err := runGC(ctx, t, dir, "target-1") |
| if err != nil { |
| t.Fatalf("runGC err, got=%v; want=<nil>", err) |
| } |
| |
| // Validate obj/bar.o is physically deleted (exclusive to target-1) |
| if _, err := os.Stat(filepath.Join(dir, "out/siso/obj/bar.o")); !errors.Is(err, os.ErrNotExist) { |
| t.Errorf("stat(obj/bar.o) err, got=%v; want=os.ErrNotExist", err) |
| } |
| // Validate obj/foo.o remains intact (preventing accidental shared cache purges - used by target-2) |
| if _, err := os.Stat(filepath.Join(dir, "out/siso/obj/foo.o")); err != nil { |
| t.Errorf("stat(obj/foo.o) err, got=%v; want=<nil>", err) |
| } |
| } |
| |
| func TestBuild_FSGC_RetainLastX(t *testing.T) { |
| if !runInSubProcess(t) { |
| return |
| } |
| ctx, dir := setupGCTestDir(t) |
| |
| runNinjaWithLabel(ctx, t, dir, "target-1", "build.ninja.0") |
| time.Sleep(50 * time.Millisecond) |
| runNinjaWithLabel(ctx, t, dir, "target-2", "build.ninja.1") |
| |
| err := runGC(ctx, t, dir, "--retain_last_x=1") |
| if err != nil { |
| t.Fatalf("runGC err, got=%v; want=<nil>", err) |
| } |
| |
| // Older targets (target-1) fall out, exclusively owned artifacts are evicted. |
| if _, err := os.Stat(filepath.Join(dir, "out/siso/obj/bar.o")); !errors.Is(err, os.ErrNotExist) { |
| t.Errorf("stat(obj/bar.o) err, got=%v; want=os.ErrNotExist", err) |
| } |
| if _, err := os.Stat(filepath.Join(dir, "out/siso/obj/foo.o")); err != nil { |
| t.Errorf("stat(obj/foo.o) err, got=%v; want=<nil>", err) |
| } |
| if _, err := os.Stat(filepath.Join(dir, "out/siso/obj/baz.o")); err != nil { |
| t.Errorf("stat(obj/baz.o) err, got=%v; want=<nil>", err) |
| } |
| } |
| |
| func TestBuild_FSGC_DryRun(t *testing.T) { |
| if !runInSubProcess(t) { |
| return |
| } |
| ctx, dir := setupGCTestDir(t) |
| |
| runNinjaWithLabel(ctx, t, dir, "target-1", "build.ninja.0") |
| |
| outputFile := filepath.Join(dir, "evicted.txt") |
| err := runGC(ctx, t, dir, "--dry_run", "--output_file", outputFile, "target-1") |
| if err != nil { |
| t.Fatalf("runGC err, got=%v; want=<nil>", err) |
| } |
| |
| evictedFilesContent, err := os.ReadFile(outputFile) |
| if err != nil { |
| t.Fatalf("os.ReadFile(%s) err, got=%v; want=<nil>", outputFile, err) |
| } |
| evictedFiles := strings.Split(strings.TrimSpace(string(evictedFilesContent)), "\n") |
| if len(evictedFiles) != 7 { |
| t.Errorf("len(evictedFiles), got=%d; want=7", len(evictedFiles)) |
| } |
| |
| // Confirm zero files were dropped, obj/foo.o and obj/bar.o still exist |
| if _, err := os.Stat(filepath.Join(dir, "out/siso/obj/foo.o")); err != nil { |
| t.Errorf("stat(obj/foo.o) dry run err, got=%v; want=<nil>", err) |
| } |
| if _, err := os.Stat(filepath.Join(dir, "out/siso/obj/bar.o")); err != nil { |
| t.Errorf("stat(obj/bar.o) dry run err, got=%v; want=<nil>", err) |
| } |
| } |
| |
| func TestBuild_FSGC_ValidationErrors(t *testing.T) { |
| if !runInSubProcess(t) { |
| return |
| } |
| ctx, dir := setupGCTestDir(t) |
| |
| t.Run("NoStrategy", func(t *testing.T) { |
| err := runGC(ctx, t, dir) |
| if err == nil { |
| t.Errorf("runGC empty strategy err, got=<nil>; want=error") |
| } |
| }) |
| |
| t.Run("ConflictingStrategies", func(t *testing.T) { |
| err := runGC(ctx, t, dir, "--retain_last_x=1", "target-1") |
| if err == nil { |
| t.Errorf("runGC conflicting strategy err, got=<nil>; want=error") |
| } |
| }) |
| } |