diff --git a/cmd/wfctl/fixture_build_artifacts_contract_test.go b/cmd/wfctl/fixture_build_artifacts_contract_test.go new file mode 100644 index 00000000..d25c04b9 --- /dev/null +++ b/cmd/wfctl/fixture_build_artifacts_contract_test.go @@ -0,0 +1,644 @@ +//go:build linux || darwin + +package main + +import ( + "context" + "errors" + "fmt" + "os" + "os/exec" + "path/filepath" + "slices" + "strings" + "sync" + "sync/atomic" + "testing" +) + +func fixtureControlSource(t *testing.T) string { + t.Helper() + root := t.TempDir() + for name, data := range map[string]string{ + "go.mod": "module fixture.control\n\ngo 1.26.5\n", + "main.go": "package main\nimport (\"embed\"; \"fmt\")\nvar _ embed.FS\n//go:embed asset.txt\nvar asset string\nfunc main(){fmt.Print(asset)}\n", + "asset.txt": "one", + } { + fixtureControlWrite(t, filepath.Join(root, name), data) + } + return root +} + +func fixtureControlWrite(t *testing.T, path, data string) { + t.Helper() + if err := os.MkdirAll(filepath.Dir(path), 0700); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(path, []byte(data), 0600); err != nil { + t.Fatal(err) + } +} + +func fixtureControlCommand(ctx context.Context, root, output string, extra ...string) *exec.Cmd { + args := append([]string{"build", "-o", output}, extra...) + cmd := exec.CommandContext(ctx, "go", append(args, ".")...) + cmd.Dir = root + for _, item := range os.Environ() { + name, _, _ := strings.Cut(item, "=") + if !slices.Contains(fixtureNativeOverrideNames, name) { + cmd.Env = append(cmd.Env, item) + } + } + cmd.Env = append(cmd.Env, "GOWORK=off", "GOENV=off", "GOFLAGS=", "GOTOOLCHAIN=go1.27.1") + return cmd +} + +// Sentinels exercise custody/publication only, never claim host/plugin execution. +func fixtureControlSentinel(cmd *exec.Cmd) ([]byte, error) { + for i, arg := range cmd.Args { + if arg == "-o" { + return nil, os.WriteFile(cmd.Args[i+1], []byte("unit-artifact-sentinel"), 0700) + } + } + return nil, errors.New("control command has no output") +} + +func fixtureControlRequest(t *testing.T, c *fixtureBuildArtifacts, root string, extra ...string) string { + t.Helper() + output := filepath.Join(t.TempDir(), "executable") + cmd := fixtureControlCommand(t.Context(), root, output, extra...) + if out, err := c.build(t.Context(), cmd, root, output, fixtureControlSentinel); err != nil { + t.Fatalf("control build: %v\n%s", err, out) + } + return output +} + +func fixtureControlCounts(t *testing.T, c *fixtureBuildArtifacts, builds, hits, bypasses int) { + t.Helper() + got := c.stats() + t.Logf("artifact controls builds=%d hits=%d bypasses=%d", got.builds, got.hits, got.bypasses) + if got != (fixtureBuildCounts{builds, hits, bypasses}) { + t.Fatalf("repeated-build contract: got builds=%d hits=%d bypasses=%d; want %d/%d/%d", got.builds, got.hits, got.bypasses, builds, hits, bypasses) + } +} + +func TestFixtureBuildArtifactsIdenticalPrivateCopies(t *testing.T) { + var c fixtureBuildArtifacts + t.Cleanup(func() { + if err := c.close(); err != nil { + t.Error(err) + } + }) + if c.root != "" { + t.Fatal("cache must start cold and lazy") + } + root := fixtureControlSource(t) + a, b := fixtureControlRequest(t, &c, root), fixtureControlRequest(t, &c, root) + fixtureControlCounts(t, &c, 1, 1, 0) + if a == b || strings.HasPrefix(a, c.root+string(os.PathSeparator)) { + t.Fatal("consumer received cache path") + } + for _, path := range []string{a, b, c.root} { + info, err := os.Lstat(path) + if err != nil || info.Mode().Perm() != 0700 { + t.Fatalf("private mode: %s: %v %v", path, info, err) + } + } + entries, err := os.ReadDir(c.root) + if err != nil || len(entries) == 0 { + t.Fatalf("missing private cache artifacts: %v", err) + } + for _, e := range entries { + if e.IsDir() { + continue + } + info, err := e.Info() + if err != nil || !info.Mode().IsRegular() || info.Mode().Perm() != 0400 { + t.Fatalf("cache artifact not read-only regular file: %v %v", info, err) + } + } + fixtureControlWrite(t, a, "mutated sibling") + if err := os.Remove(b); err != nil { + t.Fatal(err) + } + d := fixtureControlRequest(t, &c, root) + data, err := os.ReadFile(d) + if err != nil || string(data) != "unit-artifact-sentinel" { + t.Fatalf("sibling mutation poisoned reuse: %q %v", data, err) + } + fixtureControlCounts(t, &c, 1, 2, 0) + cacheRoot := c.root + if err := c.close(); err != nil { + t.Fatal(err) + } + if _, err := os.Stat(cacheRoot); !errors.Is(err, os.ErrNotExist) { + t.Fatalf("owned root survived close: %v", err) + } + if _, err := os.Stat(d); err != nil { + t.Fatalf("close removed consumer: %v", err) + } + var next fixtureBuildArtifacts + t.Cleanup(func() { _ = next.close() }) + fixtureControlRequest(t, &next, root) + fixtureControlCounts(t, &next, 1, 0, 0) +} + +func TestFixtureBuildArtifactsInputSeparation(t *testing.T) { + var c fixtureBuildArtifacts + t.Cleanup(func() { _ = c.close() }) + root := fixtureControlSource(t) + fixtureControlRequest(t, &c, root) + fixtureControlRequest(t, &c, root) + fixtureControlCounts(t, &c, 1, 1, 0) + for _, name := range []string{"ui/node_modules/dep/index.js", ".git/control"} { + fixtureControlWrite(t, filepath.Join(root, name), "excluded non-input") + fixtureControlRequest(t, &c, root) + } + fixtureControlCounts(t, &c, 1, 3, 0) + for i, name := range []string{"asset.txt", "templates/template.txt", "plugin.json", "db/schema.sql", "main.go", "go.mod"} { + path := filepath.Join(root, name) + old, _ := os.ReadFile(path) + fixtureControlWrite(t, path, string(old)+"\n// changed input") + fixtureControlRequest(t, &c, root) + fixtureControlCounts(t, &c, i+2, 3, 0) + } + if err := os.Chmod(filepath.Join(root, "asset.txt"), 0640); err != nil { + t.Fatal(err) + } + fixtureControlRequest(t, &c, root) + fixtureControlCounts(t, &c, 8, 3, 0) + fixtureControlRequest(t, &c, fixtureControlSource(t)) + fixtureControlCounts(t, &c, 9, 3, 0) +} + +func TestFixtureBuildArtifactsCommandEnvironmentVariants(t *testing.T) { + for _, variant := range []string{"race", "ldflags", "api", "environment", "compiler", "launcher", "effective-config", "readonly", "argv0", "output-position"} { + t.Run(variant, func(t *testing.T) { + var c fixtureBuildArtifacts + t.Cleanup(func() { _ = c.close() }) + root := fixtureControlSource(t) + fixtureControlRequest(t, &c, root) + output := filepath.Join(t.TempDir(), "executable") + cmd := fixtureControlCommand(t.Context(), root, output) + switch variant { + case "race": + cmd = fixtureControlCommand(t.Context(), root, output, "-race") + case "ldflags": + cmd = fixtureControlCommand(t.Context(), root, output, "-ldflags", "-s -w") + case "api": + cmd = fixtureControlCommand(t.Context(), root, output, "-ldflags", "-X main.gitHubAPIBaseURL=http://fixture.invalid") + case "environment": + cmd.Env = append(cmd.Env, "FIXTURE_CONTROL_VARIANT=changed") + case "compiler": + cmd.Env = append(cmd.Env, "GOTOOLCHAIN=go1.26.5") + case "launcher": + path := filepath.Join(t.TempDir(), "go-launcher") + fixtureControlWrite(t, path, "#!/bin/sh\nexec "+cmd.Path+" \"$@\"\n") + if err := os.Chmod(path, 0700); err != nil { + t.Fatal(err) + } + cmd.Path = path + case "effective-config": + path := filepath.Join(t.TempDir(), "goenv") + fixtureControlWrite(t, path, "GOPRIVATE=fixture.control\n") + cmd.Env = append(cmd.Env, "GOENV="+path) + case "readonly": + cmd.Env = append(cmd.Env, "GOFLAGS=-mod=readonly") + case "argv0": + cmd.Args[0] = "distinct-launcher-argv0" + case "output-position": + cmd.Args = []string{cmd.Args[0], "build", ".", "-o", output} + } + if out, err := c.build(t.Context(), cmd, root, output, fixtureControlSentinel); err != nil { + t.Fatalf("variant build: %v\n%s", err, out) + } + fixtureControlCounts(t, &c, 2, 0, 0) + }) + } +} + +func TestFixtureBuildArtifactsExternalInputsBypass(t *testing.T) { + for _, flag := range []string{"-overlay", "-modfile", "-ldflags=-s", "-gcflags=all=-N", "-tags=unknown", "native", "goenv-native", "goenv-overlay"} { + t.Run(flag, func(t *testing.T) { + var c fixtureBuildArtifacts + t.Cleanup(func() { _ = c.close() }) + root := fixtureControlSource(t) + external := filepath.Join(t.TempDir(), "external") + for i := 0; i < 2; i++ { + fixtureControlWrite(t, external, fmt.Sprintf("changed %d", i)) + output := filepath.Join(t.TempDir(), "executable") + cmd := fixtureControlCommand(t.Context(), root, output) + value := flag + if flag == "-overlay" || flag == "-modfile" { + value += "=" + external + } + switch flag { + case "native": + cmd.Env = append(cmd.Env, "CGO_CFLAGS=-DFIXTURE_EXTERNAL") + case "goenv-native", "goenv-overlay": + config := filepath.Join(filepath.Dir(external), "goenv") + setting := "CGO_CFLAGS=-DFIXTURE_EXTERNAL\n" + if flag == "goenv-overlay" { + setting = "GOFLAGS=-overlay=" + external + "\n" + } + fixtureControlWrite(t, config, setting) + cmd.Env = append(cmd.Env, "GOENV="+config) + // Remove inherited overrides before checking the GOENV settings. + var env []string + for _, item := range cmd.Env { + if !strings.HasPrefix(item, "GOFLAGS=") && !strings.HasPrefix(item, "CGO_CFLAGS=") { + env = append(env, item) + } + } + cmd.Env = env + default: + cmd.Env = append(cmd.Env, "GOFLAGS="+value) + } + wantArgs, wantEnv := strings.Join(cmd.Args, "\x00"), strings.Join(cmd.Env, "\x00") + run := func(got *exec.Cmd) ([]byte, error) { + if strings.Join(got.Args, "\x00") != wantArgs || strings.Join(got.Env, "\x00") != wantEnv { + return nil, errors.New("bypass rewrote original command") + } + return fixtureControlSentinel(got) + } + if out, err := c.build(t.Context(), cmd, root, output, run); err != nil { + t.Fatalf("bypass: %v\n%s", err, out) + } + } + fixtureControlCounts(t, &c, 2, 0, 2) + if c.root != "" { + t.Fatal("bypass allocated cache") + } + }) + } +} + +var fixtureControlNativeEnvironmentNames = []string{ + "CC", "CXX", "AR", "PKG_CONFIG", + "CGO_CFLAGS", "CGO_CPPFLAGS", "CGO_CXXFLAGS", "CGO_FFLAGS", "CGO_LDFLAGS", + "CPATH", "C_INCLUDE_PATH", "CPLUS_INCLUDE_PATH", "OBJC_INCLUDE_PATH", + "LIBRARY_PATH", "COMPILER_PATH", "GCC_EXEC_PREFIX", + "SDKROOT", "DEVELOPER_DIR", "TOOLCHAINS", + "PKG_CONFIG_PATH", "PKG_CONFIG_LIBDIR", "PKG_CONFIG_SYSROOT_DIR", "PKG_CONFIG_TOP_BUILD_DIR", + "LD_LIBRARY_PATH", "LD_PRELOAD", "LD_AUDIT", "LD_RUN_PATH", + "DYLD_LIBRARY_PATH", "DYLD_FRAMEWORK_PATH", "DYLD_FALLBACK_LIBRARY_PATH", + "DYLD_FALLBACK_FRAMEWORK_PATH", "DYLD_INSERT_LIBRARIES", "DYLD_ROOT_PATH", +} + +// These callbacks read external bytes directly to control publication/bypass. +// They do not prove cgo compilation or invalidation of Go's own native cache. +func TestFixtureBuildArtifactsNativeExternalPathMutationBypass(t *testing.T) { + for _, name := range fixtureControlNativeEnvironmentNames { + t.Run(name, func(t *testing.T) { + var c fixtureBuildArtifacts + t.Cleanup(func() { _ = c.close() }) + root := fixtureControlSource(t) + externalRoot := t.TempDir() + header := filepath.Join(externalRoot, "external.h") + calls := 0 + for _, want := range []string{"external-header-A", "external-header-B"} { + fixtureControlWrite(t, header, want) + output := filepath.Join(t.TempDir(), "executable") + cmd := fixtureControlCommand(t.Context(), root, output) + var env []string + for _, item := range cmd.Env { + key, _, _ := strings.Cut(item, "=") + native := false + for _, known := range fixtureControlNativeEnvironmentNames { + if key == known { + native = true + break + } + } + if !native { + env = append(env, item) + } + } + cmd.Env = append(env, name+"="+externalRoot) + wantArgs, wantEnv := strings.Join(cmd.Args, "\x00"), strings.Join(cmd.Env, "\x00") + run := func(got *exec.Cmd) ([]byte, error) { + calls++ + if strings.Join(got.Args, "\x00") != wantArgs || strings.Join(got.Env, "\x00") != wantEnv { + t.Errorf("native path bypass rewrote original argv/environment for %s", name) + } + data, err := os.ReadFile(header) + if err != nil { + return nil, err + } + for i, arg := range got.Args { + if arg == "-o" { + return nil, os.WriteFile(got.Args[i+1], data, 0700) + } + } + return nil, errors.New("native path control has no output") + } + if out, err := c.build(t.Context(), cmd, root, output, run); err != nil { + t.Fatalf("native path control: %v\n%s", err, out) + } + got, err := os.ReadFile(output) + if err != nil || string(got) != want { + t.Errorf("same-path external bytes became stale for %s: got %q, want %q: %v", name, got, want, err) + } + } + if calls != 2 { + t.Errorf("native external path callback ran %d times; want 2", calls) + } + if c.root != "" { + t.Error("native external path bypass allocated a cache root") + } + fixtureControlCounts(t, &c, 2, 0, 2) + }) + } +} + +func TestFixtureBuildArtifactsControlCommandHermeticNativeEnvironment(t *testing.T) { + root := fixtureControlSource(t) + customPath := t.TempDir() + for _, name := range fixtureControlNativeEnvironmentNames { + value := customPath + switch name { + case "CC": + value = "clang" + case "CXX": + value = "clang++" + case "AR": + value = "ar" + case "PKG_CONFIG": + value = "pkg-config" + case "CGO_CFLAGS", "CGO_CPPFLAGS", "CGO_CXXFLAGS", "CGO_FFLAGS", "CGO_LDFLAGS": + value = "-DFIXTURE_INHERITED_NATIVE" + } + t.Setenv(name, value) + } + t.Setenv("WFCTL_FIXTURE_CONTROL_ENV_SENTINEL", "unit-must-retain") + parentEnv := strings.Join(os.Environ(), "\x00") + var c fixtureBuildArtifacts + t.Cleanup(func() { _ = c.close() }) + for i := 0; i < 2; i++ { + output := filepath.Join(t.TempDir(), "executable") + cmd := fixtureControlCommand(t.Context(), root, output) + sentinel := false + for _, item := range cmd.Env { + key, value, _ := strings.Cut(item, "=") + for _, native := range fixtureControlNativeEnvironmentNames { + if key == native && value != "" { + t.Errorf("unit control retained inherited native override %s", key) + } + } + if item == "WFCTL_FIXTURE_CONTROL_ENV_SENTINEL=unit-must-retain" { + sentinel = true + } + } + if !sentinel { + t.Error("unit control discarded an unrelated environment variable") + } + if strings.Join(os.Environ(), "\x00") != parentEnv { + t.Error("unit command normalization rewrote the process environment") + } + if out, err := c.build(t.Context(), cmd, root, output, fixtureControlSentinel); err != nil { + t.Fatalf("hermetic native environment control: %v\n%s", err, out) + } + } + fixtureControlCounts(t, &c, 1, 1, 0) +} + +func TestFixtureBuildArtifactsRealOverlayMutation(t *testing.T) { + var c fixtureBuildArtifacts + t.Cleanup(func() { _ = c.close() }) + root := fixtureControlSource(t) + external := t.TempDir() + asset, overlay := filepath.Join(external, "asset.txt"), filepath.Join(external, "overlay.json") + fixtureControlWrite(t, overlay, fmt.Sprintf(`{"Replace":{%q:%q}}`, filepath.Join(root, "asset.txt"), asset)) + for _, want := range []string{"first-overlay", "second-overlay"} { + fixtureControlWrite(t, asset, want) + output := filepath.Join(t.TempDir(), "executable") + cmd := fixtureControlCommand(t.Context(), root, output) + cmd.Env = append(cmd.Env, "GOFLAGS=-overlay="+overlay) + if out, err := c.build(t.Context(), cmd, root, output, (*exec.Cmd).CombinedOutput); err != nil { + t.Fatalf("real overlay build: %v\n%s", err, out) + } + got, err := exec.CommandContext(t.Context(), output).CombinedOutput() + if err != nil || string(got) != want { + t.Fatalf("real overlay output=%q: %v", got, err) + } + } + fixtureControlCounts(t, &c, 2, 0, 2) +} + +func TestFixtureBuildArtifactsFailureRetry(t *testing.T) { + for _, failure := range []string{"failed-partial", "cancelled-partial", "symlink", "directory", "special"} { + t.Run(failure, func(t *testing.T) { + var c fixtureBuildArtifacts + t.Cleanup(func() { _ = c.close() }) + root := fixtureControlSource(t) + ctx, cancel := context.WithCancel(t.Context()) + defer cancel() + output := filepath.Join(t.TempDir(), "executable") + cmd := fixtureControlCommand(ctx, root, output) + partial := "" + run := func(cmd *exec.Cmd) ([]byte, error) { + for i, arg := range cmd.Args { + if arg == "-o" { + partial = cmd.Args[i+1] + } + } + switch failure { + case "failed-partial": + _, _ = fixtureControlSentinel(cmd) + return nil, errors.New("expected build failure") + case "cancelled-partial": + _, _ = fixtureControlSentinel(cmd) + cancel() + return nil, nil + case "symlink": + return nil, os.Symlink(filepath.Join(root, "asset.txt"), partial) + case "directory": + return nil, os.Mkdir(partial, 0700) + case "special": + return exec.Command("mkfifo", partial).CombinedOutput() + } + return nil, nil + } + if _, err := c.build(ctx, cmd, root, output, run); err == nil { + t.Error("invalid/failed/cancelled artifact was published") + } + if _, err := os.Lstat(partial); !errors.Is(err, os.ErrNotExist) { + t.Errorf("partial artifact survived failed publication: %v", err) + } + fixtureControlRequest(t, &c, root) + fixtureControlRequest(t, &c, root) + fixtureControlCounts(t, &c, 2, 1, 0) + }) + } +} + +func TestFixtureBuildArtifactsConcurrentCancellation(t *testing.T) { + var c fixtureBuildArtifacts + t.Cleanup(func() { _ = c.close() }) + root := fixtureControlSource(t) + started, release := make(chan struct{}), make(chan struct{}) + var attempts atomic.Int32 + run := func(cmd *exec.Cmd) ([]byte, error) { + if attempts.Add(1) == 1 { + close(started) + <-release + } + return fixtureControlSentinel(cmd) + } + owner := filepath.Join(t.TempDir(), "owner") + done := make(chan error, 1) + go func() { + _, err := c.build(t.Context(), fixtureControlCommand(t.Context(), root, owner), root, owner, run) + done <- err + }() + <-started + ctx, cancel := context.WithCancel(t.Context()) + waitOutput := filepath.Join(t.TempDir(), "cancelled-waiter") + waitCmd := fixtureControlCommand(ctx, root, waitOutput) + key, outputIndex, err := fixtureBuildKey(t.Context(), waitCmd, root) + if err != nil { + close(release) + t.Fatal(err) + } + waitDone := make(chan error, 1) + waiting := make(chan struct{}) + waitContext := &fixtureControlWaitContext{Context: ctx, waiting: waiting} + go func() { + _, err := c.buildKey(waitContext, waitCmd, key, outputIndex, waitOutput, run) + waitDone <- err + }() + <-waiting + cancel() + if err := <-waitDone; !errors.Is(err, context.Canceled) { + t.Errorf("cancelled waiter consumed/build artifact: %v", err) + } + if _, err := os.Stat(waitOutput); !errors.Is(err, os.ErrNotExist) { + t.Errorf("cancelled waiter created executable: %v", err) + } + const callers = 6 + outputs := make([]string, callers) + for i := range outputs { + outputs[i] = filepath.Join(t.TempDir(), "waiter") + } + var wg sync.WaitGroup + errs := make(chan error, callers) + for _, output := range outputs { + wg.Add(1) + go func() { + defer wg.Done() + _, err := c.build(t.Context(), fixtureControlCommand(t.Context(), root, output), root, output, run) + errs <- err + }() + } + close(release) + if err := <-done; err != nil { + t.Error(err) + } + wg.Wait() + close(errs) + for err := range errs { + if err != nil { + t.Error(err) + } + } + if got := attempts.Load(); got != 1 { + t.Errorf("repeated-build concurrency contract: actual builds=%d; want 1", got) + } + fixtureControlCounts(t, &c, 1, callers, 0) +} + +type fixtureControlWaitContext struct { + context.Context + waiting chan struct{} + once sync.Once +} + +func (c *fixtureControlWaitContext) Done() <-chan struct{} { + c.once.Do(func() { close(c.waiting) }) + return c.Context.Done() +} + +func TestFixtureBuildArtifactsUnknownArgumentsBypass(t *testing.T) { + for _, args := range [][]string{{"-mod=mod"}, {"-overlay=outside.json"}, {"-modfile=outside.mod"}, {"-gcflags=all=-N"}, {"-ldflags", "-extldflags=-L/outside"}, {"-ldflags", "-importcfg /outside/config"}} { + t.Run(strings.Join(args, " "), func(t *testing.T) { + var c fixtureBuildArtifacts + t.Cleanup(func() { _ = c.close() }) + root := fixtureControlSource(t) + for i := 0; i < 2; i++ { + output := filepath.Join(t.TempDir(), "executable") + cmd := fixtureControlCommand(t.Context(), root, output, args...) + want := strings.Join(cmd.Args, "\x00") + run := func(got *exec.Cmd) ([]byte, error) { + if strings.Join(got.Args, "\x00") != want { + return nil, errors.New("unknown-argument bypass rewrote build") + } + return fixtureControlSentinel(got) + } + if out, err := c.build(t.Context(), cmd, root, output, run); err != nil { + t.Fatalf("argument bypass: %v\n%s", err, out) + } + } + fixtureControlCounts(t, &c, 2, 0, 2) + }) + } +} + +func TestFixtureBuildArtifactsExclusiveCopy(t *testing.T) { + var c fixtureBuildArtifacts + t.Cleanup(func() { _ = c.close() }) + root := fixtureControlSource(t) + output := fixtureControlRequest(t, &c, root) + cmd := fixtureControlCommand(t.Context(), root, output) + if _, err := c.build(t.Context(), cmd, root, output, fixtureControlSentinel); err == nil { + t.Fatal("existing consumer executable was overwritten instead of exclusive copy failure") + } + fixtureControlCounts(t, &c, 1, 1, 0) +} + +func TestFixtureBuildArtifactsEffectiveConfigMutation(t *testing.T) { + var c fixtureBuildArtifacts + t.Cleanup(func() { _ = c.close() }) + root := fixtureControlSource(t) + config := filepath.Join(t.TempDir(), "goenv") + for i := 0; i < 2; i++ { + fixtureControlWrite(t, config, fmt.Sprintf("GOPRIVATE=fixture%d.control\n", i)) + output := filepath.Join(t.TempDir(), "executable") + cmd := fixtureControlCommand(t.Context(), root, output) + cmd.Env = append(cmd.Env, "GOENV="+config) + if out, err := c.build(t.Context(), cmd, root, output, fixtureControlSentinel); err != nil { + t.Fatalf("effective config mutation: %v\n%s", err, out) + } + } + fixtureControlCounts(t, &c, 2, 0, 0) +} + +func TestFixtureBuildArtifactsOrdinaryFailureTeardown(t *testing.T) { + if path := os.Getenv("WFCTL_FIXTURE_TEARDOWN_CONTROL"); path != "" { + root := fixtureControlSource(t) + fixtureControlRequest(t, &invocationFixtureBuilds, root) + if invocationFixtureBuilds.root == "" { + t.Fatal("lazy invocation cache was not allocated") + } + fixtureControlWrite(t, path, invocationFixtureBuilds.root) + t.Error("expected ordinary failure teardown control") + return + } + self, err := os.Executable() + if err != nil { + t.Fatal(err) + } + path := filepath.Join(t.TempDir(), "owned-root") + cmd := exec.CommandContext(t.Context(), self, "-test.run=^TestFixtureBuildArtifactsOrdinaryFailureTeardown$", "-test.count=1") + cmd.Env = append(os.Environ(), "WFCTL_FIXTURE_TEARDOWN_CONTROL="+path) + out, err := cmd.CombinedOutput() + if err == nil || !strings.Contains(string(out), "expected ordinary failure teardown control") { + t.Fatalf("not an ordinary assertion failure: %v\n%s", err, out) + } + data, err := os.ReadFile(path) + if err != nil || len(data) == 0 { + t.Fatalf("no teardown root evidence: %v", err) + } + if _, err := os.Lstat(string(data)); !errors.Is(err, os.ErrNotExist) { + t.Fatalf("invocation root survived ordinary test failure: %v", err) + } +} diff --git a/cmd/wfctl/fixture_build_artifacts_test.go b/cmd/wfctl/fixture_build_artifacts_test.go new file mode 100644 index 00000000..747cb662 --- /dev/null +++ b/cmd/wfctl/fixture_build_artifacts_test.go @@ -0,0 +1,425 @@ +package main + +import ( + "context" + "crypto/sha256" + "encoding/hex" + "encoding/json" + "errors" + "fmt" + "io" + "io/fs" + "os" + "os/exec" + "path/filepath" + "sort" + "strings" + "sync" + "testing" +) + +type fixtureBuildCounts struct{ builds, hits, bypasses int } + +type fixtureBuildArtifacts struct { + mu sync.Mutex + root string + entries map[string]*fixtureBuildEntry + counts fixtureBuildCounts +} + +type fixtureBuildEntry struct { + ready chan struct{} + path string + data []byte +} + +var invocationFixtureBuilds fixtureBuildArtifacts + +// External native inputs are not fingerprinted, so recognized overrides bypass. +var fixtureNativeOverrideNames = []string{ + "CC", "CXX", "AR", "PKG_CONFIG", + "CGO_CFLAGS", "CGO_CPPFLAGS", "CGO_CXXFLAGS", "CGO_FFLAGS", "CGO_LDFLAGS", + "CPATH", "C_INCLUDE_PATH", "CPLUS_INCLUDE_PATH", "OBJC_INCLUDE_PATH", + "LIBRARY_PATH", "COMPILER_PATH", "GCC_EXEC_PREFIX", + "SDKROOT", "DEVELOPER_DIR", "TOOLCHAINS", + "PKG_CONFIG_PATH", "PKG_CONFIG_LIBDIR", "PKG_CONFIG_SYSROOT_DIR", "PKG_CONFIG_TOP_BUILD_DIR", + "LD_LIBRARY_PATH", "LD_PRELOAD", "LD_AUDIT", "LD_RUN_PATH", + "DYLD_LIBRARY_PATH", "DYLD_FRAMEWORK_PATH", "DYLD_FALLBACK_LIBRARY_PATH", + "DYLD_FALLBACK_FRAMEWORK_PATH", "DYLD_INSERT_LIBRARIES", "DYLD_ROOT_PATH", +} + +// Only the fixed committed fixture callers opt in. Modules, native tools and +// VCS state must remain immutable for this invocation; the suite driver checks +// them before/after. Unclosed inputs take the original command, unchanged. +func (c *fixtureBuildArtifacts) build(ctx context.Context, cmd *exec.Cmd, sourceRoot, output string, run func(*exec.Cmd) ([]byte, error)) ([]byte, error) { + if err := ctx.Err(); err != nil { + return nil, err + } + key, outputIndex, err := fixtureBuildKey(ctx, cmd, sourceRoot) + if err != nil { + if err := ctx.Err(); err != nil { + return nil, err + } + c.mu.Lock() + c.counts.builds++ + c.counts.bypasses++ + c.mu.Unlock() + return run(cmd) + } + return c.buildKey(ctx, cmd, key, outputIndex, output, run) +} + +func (c *fixtureBuildArtifacts) buildKey(ctx context.Context, cmd *exec.Cmd, key string, outputIndex int, output string, run func(*exec.Cmd) ([]byte, error)) ([]byte, error) { + for { + if err := ctx.Err(); err != nil { + return nil, err + } + c.mu.Lock() + entry := c.entries[key] + if entry != nil { + c.mu.Unlock() + select { + case <-ctx.Done(): + return nil, ctx.Err() + case <-entry.ready: + } + if err := ctx.Err(); err != nil { + return nil, err + } + if entry.path == "" { + continue + } + c.mu.Lock() + c.counts.hits++ + c.mu.Unlock() + return entry.data, copyFixtureBuildBinary(ctx, entry.path, output) + } + if c.root == "" { + root, err := os.MkdirTemp("", "wfctl-fixture-builds-") + if err != nil { + c.mu.Unlock() + return nil, err + } + c.root = root + c.entries = make(map[string]*fixtureBuildEntry) + } + entry = &fixtureBuildEntry{ready: make(chan struct{})} + c.entries[key] = entry + root := c.root + c.mu.Unlock() + + data, buildErr := c.publish(ctx, cmd, outputIndex, root, key, run) + c.mu.Lock() + if buildErr == nil { + entry.path, entry.data = filepath.Join(root, key), data + } else { + delete(c.entries, key) + } + close(entry.ready) + c.mu.Unlock() + if buildErr != nil { + return data, buildErr + } + return data, copyFixtureBuildBinary(ctx, entry.path, output) + } +} + +func (c *fixtureBuildArtifacts) publish(ctx context.Context, cmd *exec.Cmd, outputIndex int, root, key string, run func(*exec.Cmd) ([]byte, error)) ([]byte, error) { + stage, err := os.MkdirTemp(root, "build-") + if err != nil { + return nil, err + } + defer os.RemoveAll(stage) + artifact := filepath.Join(stage, "artifact") + args := cmd.Args + cmd.Args = append([]string(nil), args...) + cmd.Args[outputIndex] = artifact + defer func() { cmd.Args = args }() + if err := ctx.Err(); err != nil { + return nil, err + } + c.mu.Lock() + c.counts.builds++ + c.mu.Unlock() + data, err := run(cmd) + if err != nil { + return data, err + } + if err := ctx.Err(); err != nil { + return data, err + } + info, err := os.Lstat(artifact) + if err != nil { + return data, err + } + if !info.Mode().IsRegular() { + return data, errors.New("fixture build output is not a regular file") + } + if err := os.Chmod(artifact, 0400); err != nil { + return data, err + } + path := filepath.Join(root, key) + if err := os.Rename(artifact, path); err != nil { + return data, err + } + if err := ctx.Err(); err != nil { + _ = os.Remove(path) + return data, err + } + return data, nil +} + +func (c *fixtureBuildArtifacts) stats() fixtureBuildCounts { + c.mu.Lock() + defer c.mu.Unlock() + return c.counts +} + +func (c *fixtureBuildArtifacts) close() error { + c.mu.Lock() + defer c.mu.Unlock() + if c.root == "" { + return nil + } + err := os.RemoveAll(c.root) + c.root, c.entries = "", nil + return err +} + +func buildFixtureArtifact(t *testing.T, ctx context.Context, cmd *exec.Cmd, sourceRoot, output string) ([]byte, error) { + t.Helper() + data, err := invocationFixtureBuilds.build(ctx, cmd, sourceRoot, output, (*exec.Cmd).CombinedOutput) + counts := invocationFixtureBuilds.stats() + t.Logf("fixture artifacts: builds=%d hits=%d bypasses=%d", counts.builds, counts.hits, counts.bypasses) + return data, err +} + +// Share the existing pipeline host O_EXCL/0700 copy mechanism, also for cache +// consumers. Wrapping the reader charges copying to the caller's context. +func copyFixtureBuildBinary(ctx context.Context, source, destination string) error { + if err := ctx.Err(); err != nil { + return err + } + input, err := os.Open(source) + if err != nil { + return err + } + defer input.Close() + output, err := os.OpenFile(destination, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0700) + if err != nil { + return err + } + _, copyErr := io.Copy(output, fixtureContextReader{ctx, input}) + err = errors.Join(copyErr, output.Chmod(0700), output.Close(), ctx.Err()) + if err != nil { + _ = os.Remove(destination) + } + return err +} + +type fixtureContextReader struct { + ctx context.Context + reader io.Reader +} + +func (r fixtureContextReader) Read(p []byte) (int, error) { + if err := r.ctx.Err(); err != nil { + return 0, err + } + return r.reader.Read(p) +} + +func fixtureBuildKey(ctx context.Context, cmd *exec.Cmd, sourceRoot string) (string, int, error) { + root, err := filepath.Abs(sourceRoot) + if err != nil { + return "", 0, err + } + cwd := cmd.Dir + if cwd == "" { + cwd, err = os.Getwd() + if err != nil { + return "", 0, err + } + } + cwd, err = filepath.Abs(cwd) + if err != nil { + return "", 0, err + } + if len(cmd.Args) < 2 || cmd.Args[1] != "build" { + return "", 0, errors.New("not a fixture build") + } + args := []string{cmd.Args[0], "build"} + outputIndex := 0 + for i := 2; i < len(cmd.Args); i++ { + arg := cmd.Args[i] + switch { + case arg == "-o" && outputIndex == 0 && i+1 < len(cmd.Args): + args = append(args, arg) + i++ + outputIndex = i + case arg == "-race" || arg == "-p=2": + args = append(args, arg) + case arg == "-ldflags" && i+1 < len(cmd.Args): + i++ + flags := cmd.Args[i] + fields := strings.Fields(flags) + for j := 0; j < len(fields); j++ { + switch fields[j] { + case "-s", "-w": + case "-X": + j++ + if j >= len(fields) || !strings.HasPrefix(fields[j], "main.gitHubAPIBaseURL=") { + return "", 0, errors.New("unclosed linker inputs") + } + default: + return "", 0, errors.New("unclosed linker inputs") + } + } + args = append(args, arg, flags) + case !strings.HasPrefix(arg, "-"): + args = append(args, arg) + default: + return "", 0, errors.New("unclosed build flags") + } + } + if outputIndex == 0 { + return "", 0, errors.New("missing fixture output") + } + env := cmd.Env + if env == nil { + env = os.Environ() + } + values := make(map[string]string) + for _, item := range env { + name, value, ok := strings.Cut(item, "=") + if ok { + values[name] = value + } + } + for _, name := range fixtureNativeOverrideNames { + if values[name] != "" { + return "", 0, errors.New("external native override") + } + } + probe := exec.CommandContext(ctx, cmd.Path, "env", "-json") + probe.Dir, probe.Env, probe.WaitDelay = cmd.Dir, cmd.Env, cmd.WaitDelay + data, err := probe.Output() + if err != nil { + return "", 0, err + } + var effective map[string]string + if err := json.Unmarshal(data, &effective); err != nil { + return "", 0, err + } + if flags := effective["GOFLAGS"]; flags != "" && flags != "-mod=readonly" { + return "", 0, errors.New("unclosed effective Go flags") + } + if work := effective["GOWORK"]; work != "" && work != "off" { + return "", 0, errors.New("external workspace") + } + for name, want := range map[string]string{"AR": "ar", "PKG_CONFIG": "pkg-config", "CGO_CFLAGS": "-O2 -g", "CGO_CPPFLAGS": "", "CGO_CXXFLAGS": "-O2 -g", "CGO_FFLAGS": "-O2 -g", "CGO_LDFLAGS": "-O2 -g"} { + if effective[name] != want { + return "", 0, errors.New("external effective native override") + } + } + if cc := effective["CC"]; cc != "cc" && cc != "gcc" && cc != "clang" { + return "", 0, errors.New("external effective compiler") + } + if cxx := effective["CXX"]; cxx != "c++" && cxx != "g++" && cxx != "clang++" { + return "", 0, errors.New("external effective compiler") + } + // GOGCCFLAGS contains a fresh go-build temp path, not an independent input. + delete(effective, "GOGCCFLAGS") + config := []byte(nil) + if path := effective["GOENV"]; path != "" && path != "off" { + config, err = os.ReadFile(path) + if err != nil && !errors.Is(err, os.ErrNotExist) { + return "", 0, err + } + } + normalizedEnv := make([]string, 0, len(values)) + for name, value := range values { + normalizedEnv = append(normalizedEnv, name+"="+value) + } + sort.Strings(normalizedEnv) + hash := sha256.New() + encoder := json.NewEncoder(hash) + if err := encoder.Encode(struct { + Root, Cwd string + Args, Env []string + Effective map[string]string + Config []byte + }{root, cwd, args, normalizedEnv, effective, config}); err != nil { + return "", 0, err + } + launcher, err := filepath.Abs(cmd.Path) + if err != nil { + return "", 0, err + } + for _, path := range []string{launcher, filepath.Join(effective["GOROOT"], "bin", "go"), filepath.Join(effective["GOTOOLDIR"], "compile"), filepath.Join(effective["GOTOOLDIR"], "link")} { + resolved, err := filepath.EvalSymlinks(path) + if err != nil { + return "", 0, err + } + if err := fixtureHashFile(ctx, encoder, hash, resolved, resolved); err != nil { + return "", 0, err + } + } + err = filepath.WalkDir(root, func(path string, entry fs.DirEntry, err error) error { + if err != nil { + return err + } + if err := ctx.Err(); err != nil { + return err + } + rel, err := filepath.Rel(root, path) + if err != nil { + return err + } + if rel == ".git" || rel == filepath.Join("ui", "node_modules") { + if entry.IsDir() { + return filepath.SkipDir + } + return nil + } + if entry.IsDir() { + return nil + } + return fixtureHashFile(ctx, encoder, hash, path, rel) + }) + if err != nil { + return "", 0, err + } + return hex.EncodeToString(hash.Sum(nil)), outputIndex, ctx.Err() +} + +func fixtureHashFile(ctx context.Context, encoder *json.Encoder, hash io.Writer, path, name string) error { + info, err := os.Lstat(path) + if err != nil { + return err + } + if !info.Mode().IsRegular() { + return errors.New("unclosed nonregular source input") + } + if err := encoder.Encode(struct { + Name string + Mode fs.FileMode + Size int64 + }{name, info.Mode(), info.Size()}); err != nil { + return err + } + file, err := os.Open(path) + if err != nil { + return err + } + _, copyErr := io.Copy(hash, fixtureContextReader{ctx, file}) + return errors.Join(copyErr, file.Close(), ctx.Err()) +} + +func closeInvocationFixtureBuilds() error { + counts := invocationFixtureBuilds.stats() + if counts != (fixtureBuildCounts{}) { + fmt.Printf("fixture artifacts final: builds=%d hits=%d bypasses=%d\n", counts.builds, counts.hits, counts.bypasses) + } + return invocationFixtureBuilds.close() +} diff --git a/cmd/wfctl/infra_resource_discovery_native_test.go b/cmd/wfctl/infra_resource_discovery_native_test.go index 6f9c49f1..2302172b 100644 --- a/cmd/wfctl/infra_resource_discovery_native_test.go +++ b/cmd/wfctl/infra_resource_discovery_native_test.go @@ -48,7 +48,14 @@ func discoveryBuild(t *testing.T, cwd, output, source string) { defer cancel() cmd := exec.CommandContext(ctx, "go", "build", "-race", "-o", output, source) cmd.Dir, cmd.Env, cmd.WaitDelay = cwd, discoveryBuildEnv(), 5*time.Second - if out, err := cmd.CombinedOutput(); err != nil { + var out []byte + var err error + if source == "./cmd/wfctl" { + out, err = buildFixtureArtifact(t, ctx, cmd, cwd, output) + } else { + out, err = cmd.CombinedOutput() + } + if err != nil { t.Fatalf("build actual artifact: %v\n%s", err, out) } t.Logf("go build -race -o %s %s: exit 0", output, source) diff --git a/cmd/wfctl/integration/linked_version_test.go b/cmd/wfctl/integration/linked_version_test.go new file mode 100644 index 00000000..cef0e1c3 --- /dev/null +++ b/cmd/wfctl/integration/linked_version_test.go @@ -0,0 +1,46 @@ +package integration_test + +import ( + "bytes" + "os" + "os/exec" + "path/filepath" + "runtime" + "strings" + "testing" +) + +func TestLinkedVersionOverridesBuildInfo(t *testing.T) { + t.Setenv("WFCTL_DIFFCACHE", "disabled") + exeName := "wfctl" + if runtime.GOOS == "windows" { + exeName += ".exe" + } + exe := filepath.Join(t.TempDir(), exeName) + build := exec.Command("go", "build", "-o", exe, "-ldflags", "-X main.version=v9.9.9", ".") + build.Dir = ".." + build.Env = append(os.Environ(), "GOWORK=off") + if out, err := build.CombinedOutput(); err != nil { + t.Fatalf("go build wfctl: %v\n%s", err, out) + } + + run := exec.Command(exe, "--version") + run.Dir = ".." + run.Env = append(os.Environ(), "WFCTL_NO_UPDATE_CHECK=1", "CI=true") + var stderr bytes.Buffer + run.Stderr = &stderr + out, err := run.Output() + if err != nil { + t.Fatalf("wfctl --version: %v\nstdout: %s\nstderr: %s", err, out, stderr.String()) + } + if len(out) != 0 { + t.Fatalf("wfctl --version unexpectedly wrote stdout: %s", out) + } + lines := strings.Split(strings.TrimSpace(stderr.String()), "\n") + if len(lines) > 1 { + t.Logf("wfctl --version diagnostics: %s", strings.Join(lines[:len(lines)-1], "\n")) + } + if got := lines[len(lines)-1]; got != "v9.9.9" { + t.Fatalf("linked version = %q, want v9.9.9", got) + } +} diff --git a/cmd/wfctl/main_test.go b/cmd/wfctl/main_test.go index ae981fb6..347bcab6 100644 --- a/cmd/wfctl/main_test.go +++ b/cmd/wfctl/main_test.go @@ -36,7 +36,12 @@ func TestMain(m *testing.M) { if err := os.Setenv("WFCTL_DIFFCACHE", "disabled"); err != nil { panic("setenv WFCTL_DIFFCACHE: " + err.Error()) } - os.Exit(m.Run()) + code := m.Run() + if err := closeInvocationFixtureBuilds(); err != nil { + fmt.Fprintf(os.Stderr, "fixture artifact teardown: %v\n", err) + code = 1 + } + os.Exit(code) } func TestHelpFlagDoesNotLeakEngineError(t *testing.T) { @@ -136,29 +141,6 @@ func TestBuildVersionStripsDirtyMarker(t *testing.T) { } } -func TestLinkedVersionOverridesBuildInfo(t *testing.T) { - exeName := "wfctl" - if runtime.GOOS == "windows" { - exeName += ".exe" - } - exe := filepath.Join(t.TempDir(), exeName) - build := exec.Command("go", "build", "-o", exe, "-ldflags", "-X main.version=v9.9.9", ".") - build.Env = append(os.Environ(), "GOWORK=off") - if out, err := build.CombinedOutput(); err != nil { - t.Fatalf("go build wfctl: %v\n%s", err, out) - } - - run := exec.Command(exe, "--version") - run.Env = append(os.Environ(), "WFCTL_NO_UPDATE_CHECK=1", "CI=true") - out, err := run.CombinedOutput() - if err != nil { - t.Fatalf("wfctl --version: %v\n%s", err, out) - } - if got := strings.TrimSpace(string(out)); got != "v9.9.9" { - t.Fatalf("linked version = %q, want v9.9.9", got) - } -} - // TestSecretsSetupNonTTYDoesNotHang is the binary-level regression guard for // the two findings in PR2 code review: // diff --git a/cmd/wfctl/pipeline_record_host_test.go b/cmd/wfctl/pipeline_record_host_test.go index 3c46f1a0..2df62981 100644 --- a/cmd/wfctl/pipeline_record_host_test.go +++ b/cmd/wfctl/pipeline_record_host_test.go @@ -56,7 +56,15 @@ func buildPipelineRecordHostGo(t *testing.T, output, target string, race bool) { args = append(args, target) cmd := exec.CommandContext(ctx, "go", args...) cmd.Env = append(os.Environ(), "GOWORK=off") - if data, err := cmd.CombinedOutput(); err != nil { + var data []byte + var err error + switch target { + case ".", "./testdata/pipeline-record/plugin", "./testdata/pipeline-record/docker": + data, err = buildFixtureArtifact(t, ctx, cmd, "../..", output) + default: + data, err = cmd.CombinedOutput() + } + if err != nil { t.Fatalf("build actual host/SDK fixture %s: %v\n%s", target, err, data) } } @@ -433,17 +441,7 @@ func pipelineRecordHostCompositeExpectations(t *testing.T, name, poolName, marke func copyPipelineRecordHostBinary(t *testing.T, source, destination string) { t.Helper() - input, err := os.Open(source) - if err != nil { - t.Fatal(err) - } - defer input.Close() - output, err := os.OpenFile(destination, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0700) - if err != nil { - t.Fatal(err) - } - _, copyErr := io.Copy(output, input) - if err := errors.Join(copyErr, output.Chmod(0700), output.Close()); err != nil { + if err := copyFixtureBuildBinary(t.Context(), source, destination); err != nil { t.Fatal(err) } } diff --git a/cmd/wfctl/plugin_registry_sync_host_test.go b/cmd/wfctl/plugin_registry_sync_host_test.go index 40d30a0e..98a51139 100644 --- a/cmd/wfctl/plugin_registry_sync_host_test.go +++ b/cmd/wfctl/plugin_registry_sync_host_test.go @@ -50,7 +50,7 @@ func registrySyncActualCLIBinary(t *testing.T, host, apiURL string) string { defer cancel() build := exec.CommandContext(buildCtx, "go", registrySyncHostBuildArguments(host, binary, apiURL)...) build.Env = registrySyncHostEnvironment(os.Environ()) - if out, err := build.CombinedOutput(); err != nil { + if out, err := buildFixtureArtifact(t, buildCtx, build, "../..", binary); err != nil { t.Fatalf("build actual CLI: %v\n%s", err, out) } return binary