root tests: the carlos binary is built once per test run, not once per test
buildCarlos and eighteen inline stanzas in e2e_test.go each ran `go build -o <tmp>/carlos .`, about 135 links of a 120 MB binary in a run (about 270 s of the root package's 359 s in CI, amadan discussion #291). sharedCarlos builds it on first use (sync.Once, so a run that never needs it never pays), in a directory TestMain makes after isolateTempRoot so it lands in the isolated temp root and is removed with it. The module root and build environment are taken before any test can t.Chdir or t.Setenv. A build failure is kept and fails every caller. The file is mode 0555: it is shared, read-only by contract. e2e_test.go is package main_test and cannot see main-package helpers, so it has a twin that finds the same file through CARLOS_TEST_BIN_DIR. The two echoapp builds and cli_test.go's cross-compiled tiny program are different programs and are untouched.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
3 files changed,
+214
−186
auth_e2e_test.go+70 −6e2e_test.go+95 −180main_test.go+49 −0
CI passed — run details
CI log
=== gate ===
cd launchctx && go vet ./... && go test ./... -count=1
ok amadan.net/carlos/platform.git/launchctx 0.012s
? amadan.net/carlos/platform.git/launchctx/controlname [no test files]
? amadan.net/carlos/platform.git/launchctx/wireid [no test files]
go vet ./...
gofmt -l .: clean
go test ./... -count=1 -timeout 30m
ok github.com/carlosframework/platform 90.044s
? github.com/carlosframework/platform/cmd/changelog [no test files]
ok github.com/carlosframework/platform/cmd/console 3.090s
ok github.com/carlosframework/platform/cmd/moveprobe 93.357s
ok github.com/carlosframework/platform/cmd/zddprobe 0.397s
ok github.com/carlosframework/platform/internal/activator 227.767s
ok github.com/carlosframework/platform/internal/agentsock 0.019s
ok github.com/carlosframework/platform/internal/alerting 0.004s
ok github.com/carlosframework/platform/internal/analytics 0.019s
ok github.com/carlosframework/platform/internal/appconfig 2.344s
ok github.com/carlosframework/platform/internal/apperr 0.092s
ok github.com/carlosframework/platform/internal/applog 0.144s
ok github.com/carlosframework/platform/internal/awsinv 0.003s
ok github.com/carlosframework/platform/internal/boxchoice 0.004s
ok github.com/carlosframework/platform/internal/boxid 0.703s
? github.com/carlosframework/platform/internal/boxid/boxidtest [no test files]
? github.com/carlosframework/platform/internal/boxid/internal/signtest [no test files]
ok github.com/carlosframework/platform/internal/boxvolumes 0.008s
ok github.com/carlosframework/platform/internal/caps 0.015s
ok github.com/carlosframework/platform/internal/certwarm 0.002s
ok github.com/carlosframework/platform/internal/cgroup 0.723s
ok github.com/carlosframework/platform/internal/changelog 0.301s
ok github.com/carlosframework/platform/internal/clamd 0.056s
? github.com/carlosframework/platform/internal/cliflag [no test files]
ok github.com/carlosframework/platform/internal/console 365.910s
? github.com/carlosframework/platform/internal/controlname [no test files]
? github.com/carlosframework/platform/internal/controlsock [no test files]
ok github.com/carlosframework/platform/internal/dbdoor 6.391s
ok github.com/carlosframework/platform/internal/dbdoor/dbdoortest 0.009s
ok github.com/carlosframework/platform/internal/dbverify 0.005s
ok github.com/carlosframework/platform/internal/dnsrec 0.003s
ok github.com/carlosframework/platform/internal/docsite 0.005s
ok github.com/carlosframework/platform/internal/domains 2.132s
ok github.com/carlosframework/platform/internal/doorkey 0.006s
ok github.com/carlosframework/platform/internal/economics 0.004s
ok github.com/carlosframework/platform/internal/edge 26.270s
? github.com/carlosframework/platform/internal/edgeproof [no test files]
ok github.com/carlosframework/platform/internal/edgesfile 0.007s
ok github.com/carlosframework/platform/internal/fleetapi 0.128s
ok github.com/carlosframework/platform/internal/gate 1.654s
ok github.com/carlosframework/platform/internal/genselect 0.015s
ok github.com/carlosframework/platform/internal/geo 0.015s
ok github.com/carlosframework/platform/internal/geoip 0.029s
ok github.com/carlosframework/platform/internal/host 98.115s
ok github.com/carlosframework/platform/internal/hostactors 0.463s
ok github.com/carlosframework/platform/internal/hostbox 2.199s
ok github.com/carlosframework/platform/internal/hostclaim 0.493s
ok github.com/carlosframework/platform/internal/hostevidence 0.006s
ok github.com/carlosframework/platform/internal/hostexec 5.000s
ok github.com/carlosframework/platform/internal/hostfence 0.029s
ok github.com/carlosframework/platform/internal/hostfiles 10.552s
ok github.com/carlosframework/platform/internal/hostfloor 0.012s
ok github.com/carlosframework/platform/internal/hostguard 1.007s
ok github.com/carlosframework/platform/internal/hostops 1.497s
? github.com/carlosframework/platform/internal/hostops/fencerelease [no test files]
ok github.com/carlosframework/platform/internal/hostops/internal/fencerelease 0.002s
ok github.com/carlosframework/platform/internal/hostquarantine 0.003s
ok github.com/carlosframework/platform/internal/hostrelocate 0.463s
ok github.com/carlosframework/platform/internal/hostresolve 0.140s
ok github.com/carlosframework/platform/internal/hostwrite 0.002s
ok github.com/carlosframework/platform/internal/infraview 0.005s
ok github.com/carlosframework/platform/internal/instancectl 1.817s
ok github.com/carlosframework/platform/internal/instanceexec 2.720s
ok github.com/carlosframework/platform/internal/instancefs 0.005s
ok github.com/carlosframework/platform/internal/instancekey 0.014s
ok github.com/carlosframework/platform/internal/instanceupdates 0.019s
ok github.com/carlosframework/platform/internal/layout 1.441s
ok github.com/carlosframework/platform/internal/lease 9.446s
ok github.com/carlosframework/platform/internal/lease/leasetest 0.005s
ok github.com/carlosframework/platform/internal/ledger 0.017s
ok github.com/carlosframework/platform/internal/mail 0.164s
ok github.com/carlosframework/platform/internal/maintenance 0.091s
ok github.com/carlosframework/platform/internal/maintenanceexec 5.546s
ok github.com/carlosframework/platform/internal/maintenanceguard 0.074s
ok github.com/carlosframework/platform/internal/metrics 0.006s
ok github.com/carlosframework/platform/internal/moves 21.152s
ok github.com/carlosframework/platform/internal/moves/archive 0.771s
ok github.com/carlosframework/platform/internal/objstore 0.165s
ok github.com/carlosframework/platform/internal/ops 0.171s
ok github.com/carlosframework/platform/internal/park 0.202s
ok github.com/carlosframework/platform/internal/privatefile 0.004s
ok github.com/carlosframework/platform/internal/provision 13.697s
ok github.com/carlosframework/platform/internal/publicrelease 0.011s
? github.com/carlosframework/platform/internal/regionname [no test files]
ok github.com/carlosframework/platform/internal/registry 4.036s
ok github.com/carlosframework/platform/internal/relay 7.242s
ok github.com/carlosframework/platform/internal/release 19.557s
ok github.com/carlosframework/platform/internal/rollout 3.770s
ok github.com/carlosframework/platform/internal/rolloutwords 0.004s
ok github.com/carlosframework/platform/internal/rootaws 0.007s
ok github.com/carlosframework/platform/internal/rootparticipation 0.007s
ok github.com/carlosframework/platform/internal/rootstate 0.027s
ok github.com/carlosframework/platform/internal/s3compat 0.005s
ok github.com/carlosframework/platform/internal/safefile 0.004s
ok github.com/carlosframework/platform/internal/schedule 2.308s
ok github.com/carlosframework/platform/internal/sim/hostlife 10.162s
ok github.com/carlosframework/platform/internal/sim/litestreamfacts 4.405s
ok github.com/carlosframework/platform/internal/status 0.041s
ok github.com/carlosframework/platform/internal/steering 0.006s
ok github.com/carlosframework/platform/internal/stopevidence 0.190s
ok github.com/carlosframework/platform/internal/svcrole 0.002s
ok github.com/carlosframework/platform/internal/telemetry 0.004s
ok github.com/carlosframework/platform/internal/termfence 0.004s
ok github.com/carlosframework/platform/internal/tunnelclient 0.004s
ok github.com/carlosframework/platform/internal/unixdial 0.002s
ok github.com/carlosframework/platform/internal/updatewords 0.004s
ok github.com/carlosframework/platform/internal/usage 0.004s
ok github.com/carlosframework/platform/internal/version 0.002s
ok github.com/carlosframework/platform/internal/vet 1.592s
=== cross ===
--- shard 0 of 5 ---
cross-compiling darwin/arm64
--- shard 1 of 5 ---
cross-compiling darwin/amd64
--- shard 2 of 5 ---
cross-compiling linux/arm64
--- shard 3 of 5 ---
cross-compiling windows/amd64
--- shard 4 of 5 ---
cross-compiling windows/arm64
=== minio-contract ===
minio contract: arm64, minio RELEASE.2025-09-07T16-13-09Z, mc RELEASE.2025-08-13T08-35-41Z
minio: downloading https://github.com/minio/minio/releases/download/RELEASE.2025-09-07T16-13-09Z/minio.linux-arm64.RELEASE.2025-09-07T16-13-09Z
minio: downloaded, digest verified
mc: downloading https://github.com/minio/mc/releases/download/RELEASE.2025-08-13T08-35-41Z/mc.linux-arm64.RELEASE.2025-08-13T08-35-41Z
mc: downloaded, digest verified
minio healthy on 127.0.0.1:20999
Added `local` successfully.
Bucket created successfully `local/contract-bucket`.
Bucket created successfully `local/denied-bucket`.
Created policy `contract-only` successfully.
Added user `scopeduser` successfully.
Attached Policies: [contract-only]
To User: scopeduser
buckets and scoped user ready
=== RUN TestDeliveredContractMinio
=== RUN TestDeliveredContractMinio/put
=== RUN TestDeliveredContractMinio/get_round-trips_the_body
=== RUN TestDeliveredContractMinio/list_shows_the_key
=== RUN TestDeliveredContractMinio/head_and_list_agree_on_the_version
=== RUN TestDeliveredContractMinio/the_app_cannot_tag_its_own_object
=== RUN TestDeliveredContractMinio/the_app_cannot_tag_its_own_object/not_by_PutObjectTagging
=== RUN TestDeliveredContractMinio/the_app_cannot_tag_its_own_object/and_not_by_x-amz-tagging_on_upload
=== RUN TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route
=== RUN TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/not_by_CreateMultipartUpload
=== RUN TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/not_by_CopyObject_with_a_REPLACE_tagging_directive
=== RUN TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/but_multipart_itself_is_allowed
=== RUN TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/and_copy_itself_is_allowed
=== RUN TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/a_POST-policy_forged_tag_with_a_matching_version_is_still_not_a_verdict
=== RUN TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/and_an_ordinary_tagged-with-something-else_upload_is_allowed
=== RUN TestDeliveredContractMinio/delete_removes_it
=== RUN TestDeliveredContractMinio/cross-bucket_denial
--- PASS: TestDeliveredContractMinio (0.02s)
--- PASS: TestDeliveredContractMinio/put (0.00s)
--- PASS: TestDeliveredContractMinio/get_round-trips_the_body (0.00s)
--- PASS: TestDeliveredContractMinio/list_shows_the_key (0.00s)
--- PASS: TestDeliveredContractMinio/head_and_list_agree_on_the_version (0.00s)
--- PASS: TestDeliveredContractMinio/the_app_cannot_tag_its_own_object (0.00s)
--- PASS: TestDeliveredContractMinio/the_app_cannot_tag_its_own_object/not_by_PutObjectTagging (0.00s)
--- PASS: TestDeliveredContractMinio/the_app_cannot_tag_its_own_object/and_not_by_x-amz-tagging_on_upload (0.00s)
--- PASS: TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route (0.01s)
--- PASS: TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/not_by_CreateMultipartUpload (0.00s)
--- PASS: TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/not_by_CopyObject_with_a_REPLACE_tagging_directive (0.00s)
--- PASS: TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/but_multipart_itself_is_allowed (0.00s)
--- PASS: TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/and_copy_itself_is_allowed (0.00s)
--- PASS: TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/a_POST-policy_forged_tag_with_a_matching_version_is_still_not_a_verdict (0.00s)
--- PASS: TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/and_an_ordinary_tagged-with-something-else_upload_is_allowed (0.00s)
--- PASS: TestDeliveredContractMinio/delete_removes_it (0.00s)
--- PASS: TestDeliveredContractMinio/cross-bucket_denial (0.00s)
PASS
ok github.com/carlosframework/platform/internal/objstore 0.023s
=== RUN TestT4StorageProbeMinio
2026/10/14 00:26:22 moves: T4: STORE: list: operation error S3: ListObjectsV2, https response error StatusCode: 403, RequestID: 18DE3DBC9E695708, HostID: 7c420053e0fbcd7f41653fdd92ffa2802c903d05303b1aa15adc2e8bb65d183f, api error SignatureDoesNotMatch: The request signature we calculated does not match the signature you provided. Check your key and signing method.
--- PASS: TestT4StorageProbeMinio (0.01s)
PASS
ok github.com/carlosframework/platform/internal/host 0.021s
minio contract: PASS
diff --git a/auth_e2e_test.go b/auth_e2e_test.go| index 38bc15301..0169e5b48 100644 |
| --- a/auth_e2e_test.go |
| +++ b/auth_e2e_test.go |
| @@ -6,6 +6,7 @@ import ( |
| "crypto/sha256" |
| "encoding/hex" |
| "encoding/json" |
| + "fmt" |
| "io" |
| "net/http" |
| "net/http/httptest" |
| @@ -14,6 +15,7 @@ import ( |
| "os/exec" |
| "path/filepath" |
| "strings" |
| + "sync" |
| "testing" |
| "time" |
| @@ -25,19 +27,81 @@ import ( |
| // buildCarlos builds the real binary, the way TestStatusCLI does, so these |
| // tests exercise the actual command line an operator types rather than the |
| // functions underneath it. |
| +// |
| +// The binary is built once per test process (sharedCarlos), not once per |
| +// test: linking it takes seconds and about 135 tests wanted it. |
| func buildCarlos(t *testing.T) string { |
| + t.Helper() |
| + return sharedCarlos(t) |
| +} |
| + |
| +// The shared binary's state, set up by TestMain. moduleRoot and |
| +// carlosBuildEnv are taken before any test can t.Chdir or t.Setenv, because |
| +// the build has to run in the module, with the real HOME and Go cache, |
| +// whichever test asks first. carlosDir is made in the isolated temp root, |
| +// after isolateTempRoot, so the 120 MB file never lands in /tmp. |
| +var ( |
| + moduleRoot string |
| + carlosBuildEnv []string |
| + carlosDir string |
| + carlosDirErr error |
| + |
| + carlosOnce sync.Once |
| + sharedCarlosPath string |
| + carlosErr error |
| +) |
| + |
| +// carlosDirEnv names carlosDir for e2e_test.go, which is package main_test |
| +// and cannot see this package's variables. Both packages run in one test |
| +// process and one binary, and each has its own sharedCarlos; the file in |
| +// that directory is how they share the build. |
| +const carlosDirEnv = "CARLOS_TEST_BIN_DIR" |
| + |
| +// sharedCarlos returns the path of the carlos binary, built from the module |
| +// on the first call and reused by every later one in this test process. The |
| +// build is lazy so a run that never needs the binary never pays for it. |
| +// |
| +// The file is read-only by contract: it is shared by every test, so a test |
| +// that writes to it, replaces it or moves it breaks the ones that run after. |
| +// It is mode 0555 to make that fail loudly. A test that needs a binary of |
| +// its own to modify or swap must copy it to its own directory first. A build |
| +// failure is kept, and fails every caller with the compiler's output. |
| +// |
| +// e2e_test.go has a twin of this function for the external test package. |
| +func sharedCarlos(t testing.TB) string { |
| t.Helper() |
| goBin, err := exec.LookPath("go") |
| if err != nil { |
| t.Skip("go toolchain not found; skipping CLI subprocess test") |
| } |
| - bin := filepath.Join(t.TempDir(), "carlos") |
| - build := exec.Command(goBin, "build", "-o", bin, ".") |
| - build.Env = os.Environ() |
| - if out, err := build.CombinedOutput(); err != nil { |
| - t.Fatalf("build carlos: %v\n%s", err, out) |
| + carlosOnce.Do(func() { |
| + if carlosDirErr != nil { |
| + carlosErr = fmt.Errorf("no directory for the shared binary: %w", carlosDirErr) |
| + return |
| + } |
| + bin := filepath.Join(carlosDir, "carlos") |
| + // Already there when the other package got to it first. |
| + if _, err := os.Stat(bin); err == nil { |
| + sharedCarlosPath = bin |
| + return |
| + } |
| + build := exec.Command(goBin, "build", "-o", bin, ".") |
| + build.Dir = moduleRoot |
| + build.Env = carlosBuildEnv |
| + if out, err := build.CombinedOutput(); err != nil { |
| + carlosErr = fmt.Errorf("%v\n%s", err, out) |
| + return |
| + } |
| + if err := os.Chmod(bin, 0o555); err != nil { |
| + carlosErr = err |
| + return |
| + } |
| + sharedCarlosPath = bin |
| + }) |
| + if carlosErr != nil { |
| + t.Fatalf("build carlos: %v", carlosErr) |
| } |
| - return bin |
| + return sharedCarlosPath |
| } |
| // TestAuthCLI drives `carlos auth` as a subprocess against a fake console: |
diff --git a/e2e_test.go b/e2e_test.go| index 977a38e5f..f5f675908 100644 |
| --- a/e2e_test.go |
| +++ b/e2e_test.go |
| @@ -29,7 +29,9 @@ import ( |
| "path/filepath" |
| "reflect" |
| "regexp" |
| + "runtime" |
| "strings" |
| + "sync" |
| "testing" |
| "time" |
| @@ -101,6 +103,81 @@ func carlosTestEnv(t *testing.T, extra ...string) []string { |
| return append(env, extra...) |
| } |
| +// The module root and environment the shared build uses, taken at package |
| +// initialisation: before TestMain, and so before any test can t.Chdir away |
| +// from the module or t.Setenv a different HOME. |
| +var ( |
| + e2eModuleRoot, _ = os.Getwd() |
| + e2eBuildEnv = os.Environ() |
| + |
| + e2eCarlosOnce sync.Once |
| + e2eCarlosPath string |
| + e2eCarlosErr error |
| +) |
| + |
| +// sharedCarlos is the external test package's twin of the helper of the same |
| +// name in auth_e2e_test.go (package main, out of reach from here, like |
| +// carlosTestEnv above). Same contract: the carlos binary is built at most |
| +// once per test process, in the directory TestMain made and named in |
| +// CARLOS_TEST_BIN_DIR, and the file is read-only and shared. If the other |
| +// package built it first, this one uses that file. |
| +func sharedCarlos(t testing.TB) string { |
| + t.Helper() |
| + goBin, err := exec.LookPath("go") |
| + if err != nil { |
| + t.Skip("go toolchain not found; skipping CLI subprocess test") |
| + } |
| + e2eCarlosOnce.Do(func() { |
| + dir := os.Getenv("CARLOS_TEST_BIN_DIR") |
| + if dir == "" { |
| + e2eCarlosErr = fmt.Errorf("TestMain made no directory for the shared binary") |
| + return |
| + } |
| + bin := filepath.Join(dir, "carlos") |
| + if _, err := os.Stat(bin); err == nil { |
| + e2eCarlosPath = bin |
| + return |
| + } |
| + build := exec.Command(goBin, "build", "-o", bin, ".") |
| + build.Dir = e2eModuleRoot |
| + build.Env = e2eBuildEnv |
| + if out, err := build.CombinedOutput(); err != nil { |
| + e2eCarlosErr = fmt.Errorf("%v\n%s", err, out) |
| + return |
| + } |
| + if err := os.Chmod(bin, 0o555); err != nil { |
| + e2eCarlosErr = err |
| + return |
| + } |
| + e2eCarlosPath = bin |
| + }) |
| + if e2eCarlosErr != nil { |
| + t.Fatalf("build carlos: %v", e2eCarlosErr) |
| + } |
| + return e2eCarlosPath |
| +} |
| + |
| +// The external package's helper returns the one path for every call, in the |
| +// directory TestMain named, and the file is executable and not writable. The |
| +// same file is what the internal package's helper hands out (TestSharedCarlos). |
| +func TestSharedCarlosFromTheExternalPackage(t *testing.T) { |
| + if testing.Short() { |
| + t.Skip("builds a binary") |
| + } |
| + t.Chdir(t.TempDir()) |
| + first, second := sharedCarlos(t), sharedCarlos(t) |
| + if first != second || first != filepath.Join(os.Getenv("CARLOS_TEST_BIN_DIR"), "carlos") { |
| + t.Fatalf("calls gave %s and %s, want one path in CARLOS_TEST_BIN_DIR", first, second) |
| + } |
| + info, err := os.Stat(first) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + if runtime.GOOS != "windows" && info.Mode().Perm() != 0o555 { |
| + t.Errorf("mode %v, want r-xr-xr-x", info.Mode().Perm()) |
| + } |
| +} |
| + |
| func e2eShipSite(t *testing.T, store release.Store, version string, files map[string]string) { |
| t.Helper() |
| dir := t.TempDir() |
| @@ -507,17 +584,8 @@ func TestEndToEndSmoke(t *testing.T) { |
| // - a bare `carlos status` (no -dry-run) must exit non-zero — RunStatus's |
| // own input validation, not the pipeline it wraps. |
| func TestStatusCLI(t *testing.T) { |
| - goBin, err := exec.LookPath("go") |
| - if err != nil { |
| - t.Skip("go toolchain not found; skipping status CLI test") |
| - } |
| tmp := t.TempDir() |
| - carlosBin := filepath.Join(tmp, "carlos") |
| - build := exec.Command(goBin, "build", "-o", carlosBin, ".") |
| - build.Env = os.Environ() |
| - if out, err := build.CombinedOutput(); err != nil { |
| - t.Fatalf("build carlos: %v\n%s", err, out) |
| - } |
| + carlosBin := sharedCarlos(t) |
| dryRunDir := filepath.Join(tmp, "dryrun") |
| if err := os.MkdirAll(dryRunDir, 0o755); err != nil { |
| @@ -722,17 +790,8 @@ func TestMultiAccountEndToEnd(t *testing.T) { |
| // the account-qualified prefix WITHOUT ever deleting the legacy ones — |
| // deletion is a separate operator step behind the verification gate. |
| func TestAccountsCLI(t *testing.T) { |
| - goBin, err := exec.LookPath("go") |
| - if err != nil { |
| - t.Skip("go toolchain not found") |
| - } |
| tmp := t.TempDir() |
| - carlosBin := filepath.Join(tmp, "carlos") |
| - build := exec.Command(goBin, "build", "-o", carlosBin, ".") |
| - build.Env = os.Environ() |
| - if out, err := build.CombinedOutput(); err != nil { |
| - t.Fatalf("build carlos: %v\n%s", err, out) |
| - } |
| + carlosBin := sharedCarlos(t) |
| storeDir := filepath.Join(tmp, "store") |
| regPath := filepath.Join(tmp, "registry.db") |
| // The subprocess must never see the operator's real ~/.carlos — a genuine |
| @@ -923,17 +982,8 @@ func TestAccountsCLI(t *testing.T) { |
| // RUNTIME does (qualified wins per key), say that it saw both, and verify |
| // afterwards that what the destination now serves is what the source served. |
| func TestAccountsMigrateTakesTheLiveObjects(t *testing.T) { |
| - goBin, err := exec.LookPath("go") |
| - if err != nil { |
| - t.Skip("go toolchain not found") |
| - } |
| tmp := t.TempDir() |
| - carlosBin := filepath.Join(tmp, "carlos") |
| - build := exec.Command(goBin, "build", "-o", carlosBin, ".") |
| - build.Env = os.Environ() |
| - if out, err := build.CombinedOutput(); err != nil { |
| - t.Fatalf("build carlos: %v\n%s", err, out) |
| - } |
| + carlosBin := sharedCarlos(t) |
| storeDir := filepath.Join(tmp, "store") |
| regPath := filepath.Join(tmp, "registry.db") |
| store := release.NewDirStore(storeDir) |
| @@ -1089,17 +1139,8 @@ func TestAccountsMigrateTakesTheLiveObjects(t *testing.T) { |
| // (verifyMigratedPointers checks channel pointers, not this). The verb skips |
| // it and says so, loudly, because the alternative failure is invisible. |
| func TestAccountsMigrateDoesNotCopyTheProductionFlag(t *testing.T) { |
| - goBin, err := exec.LookPath("go") |
| - if err != nil { |
| - t.Skip("go toolchain not found") |
| - } |
| tmp := t.TempDir() |
| - carlosBin := filepath.Join(tmp, "carlos") |
| - build := exec.Command(goBin, "build", "-o", carlosBin, ".") |
| - build.Env = os.Environ() |
| - if out, err := build.CombinedOutput(); err != nil { |
| - t.Fatalf("build carlos: %v\n%s", err, out) |
| - } |
| + carlosBin := sharedCarlos(t) |
| storeDir := filepath.Join(tmp, "store") |
| regPath := filepath.Join(tmp, "registry.db") |
| store := release.NewDirStore(storeDir) |
| @@ -1193,17 +1234,8 @@ func TestAccountsMigrateDoesNotCopyTheProductionFlag(t *testing.T) { |
| // end: the re-run would hit the create-only copy and die at "already exists |
| // with different content", so the instruction forbade the only exit. |
| func TestAccountsMigrateConvergesADivergedPointer(t *testing.T) { |
| - goBin, err := exec.LookPath("go") |
| - if err != nil { |
| - t.Skip("go toolchain not found") |
| - } |
| tmp := t.TempDir() |
| - carlosBin := filepath.Join(tmp, "carlos") |
| - build := exec.Command(goBin, "build", "-o", carlosBin, ".") |
| - build.Env = os.Environ() |
| - if out, err := build.CombinedOutput(); err != nil { |
| - t.Fatalf("build carlos: %v\n%s", err, out) |
| - } |
| + carlosBin := sharedCarlos(t) |
| storeDir := filepath.Join(tmp, "store") |
| regPath := filepath.Join(tmp, "registry.db") |
| store := release.NewDirStore(storeDir) |
| @@ -1311,17 +1343,8 @@ func TestAccountsMigrateConvergesADivergedPointer(t *testing.T) { |
| // unenforced (still the operator's call), and -db always persists onto the |
| // route when given. |
| func TestAddUnitBackingRequiresDBForHibernate(t *testing.T) { |
| - goBin, err := exec.LookPath("go") |
| - if err != nil { |
| - t.Skip("go toolchain not found") |
| - } |
| tmp := t.TempDir() |
| - carlosBin := filepath.Join(tmp, "carlos") |
| - build := exec.Command(goBin, "build", "-o", carlosBin, ".") |
| - build.Env = os.Environ() |
| - if out, err := build.CombinedOutput(); err != nil { |
| - t.Fatalf("build carlos: %v\n%s", out, err) |
| - } |
| + carlosBin := sharedCarlos(t) |
| regPath := filepath.Join(tmp, "registry.db") |
| @@ -1392,17 +1415,8 @@ func TestAddUnitBackingRequiresDBForHibernate(t *testing.T) { |
| // discoverable only by correlating 503s with child stderr in the journal |
| // (issue #70, vitogo's first deploy). |
| func TestAddRefusesRelativeDBOnExecRoutes(t *testing.T) { |
| - goBin, err := exec.LookPath("go") |
| - if err != nil { |
| - t.Skip("go toolchain not found") |
| - } |
| tmp := t.TempDir() |
| - carlosBin := filepath.Join(tmp, "carlos") |
| - build := exec.Command(goBin, "build", "-o", carlosBin, ".") |
| - build.Env = os.Environ() |
| - if out, err := build.CombinedOutput(); err != nil { |
| - t.Fatalf("build carlos: %v\n%s", err, out) |
| - } |
| + carlosBin := sharedCarlos(t) |
| regPath := filepath.Join(tmp, "registry.db") |
| run := func(args ...string) (string, error) { |
| @@ -1471,17 +1485,8 @@ func TestAddRefusesRelativeDBOnExecRoutes(t *testing.T) { |
| // (accounts_resolve_test.go); the refusal itself is log.Fatal, so only a |
| // real subprocess can observe the exit status and the absent row. |
| func TestAddAccountIDRefusesAName(t *testing.T) { |
| - goBin, err := exec.LookPath("go") |
| - if err != nil { |
| - t.Skip("go toolchain not found") |
| - } |
| tmp := t.TempDir() |
| - carlosBin := filepath.Join(tmp, "carlos") |
| - build := exec.Command(goBin, "build", "-o", carlosBin, ".") |
| - build.Env = os.Environ() |
| - if out, err := build.CombinedOutput(); err != nil { |
| - t.Fatalf("build carlos: %v\n%s", err, out) |
| - } |
| + carlosBin := sharedCarlos(t) |
| regPath := filepath.Join(tmp, "registry.db") |
| out, err := exec.Command(carlosBin, "add", "-registry", regPath, "-app", "kass", |
| @@ -1545,17 +1550,8 @@ func TestAddAccountIDRefusesAName(t *testing.T) { |
| // incident (issue #69) started with `added vito.platform.tito.io` printed |
| // over what was actually a rewrite of an existing row. |
| func TestAddOnExistingRouteSaysUpdated(t *testing.T) { |
| - goBin, err := exec.LookPath("go") |
| - if err != nil { |
| - t.Skip("go toolchain not found") |
| - } |
| tmp := t.TempDir() |
| - carlosBin := filepath.Join(tmp, "carlos") |
| - build := exec.Command(goBin, "build", "-o", carlosBin, ".") |
| - build.Env = os.Environ() |
| - if out, err := build.CombinedOutput(); err != nil { |
| - t.Fatalf("build carlos: %v\n%s", err, out) |
| - } |
| + carlosBin := sharedCarlos(t) |
| regPath := filepath.Join(tmp, "registry.db") |
| run := func(args ...string) (string, error) { |
| @@ -1606,17 +1602,8 @@ func TestAddOnExistingRouteSaysUpdated(t *testing.T) { |
| // That left every ops app stranded on legacy keys and made the read-fallback |
| // impossible to retire. |
| func TestAccountsMigrateLegacyIntoOps(t *testing.T) { |
| - goBin, err := exec.LookPath("go") |
| - if err != nil { |
| - t.Skip("go toolchain not found") |
| - } |
| tmp := t.TempDir() |
| - carlosBin := filepath.Join(tmp, "carlos") |
| - build := exec.Command(goBin, "build", "-o", carlosBin, ".") |
| - build.Env = os.Environ() |
| - if out, err := build.CombinedOutput(); err != nil { |
| - t.Fatalf("build carlos: %v\n%s", err, out) |
| - } |
| + carlosBin := sharedCarlos(t) |
| storeDir := filepath.Join(tmp, "store") |
| regPath := filepath.Join(tmp, "registry.db") |
| store := release.NewDirStore(storeDir) |
| @@ -1740,17 +1727,8 @@ func TestAccountsMigrateLegacyIntoOps(t *testing.T) { |
| } |
| func TestEnvCLI(t *testing.T) { |
| - goBin, err := exec.LookPath("go") |
| - if err != nil { |
| - t.Skip("go toolchain not found") |
| - } |
| tmp := t.TempDir() |
| - carlosBin := filepath.Join(tmp, "carlos") |
| - build := exec.Command(goBin, "build", "-o", carlosBin, ".") |
| - build.Env = os.Environ() |
| - if out, err := build.CombinedOutput(); err != nil { |
| - t.Fatalf("build carlos: %v\n%s", err, out) |
| - } |
| + carlosBin := sharedCarlos(t) |
| storeDir := filepath.Join(tmp, "store") |
| regPath := filepath.Join(tmp, "registry.db") |
| env := carlosTestEnv(t, "CARLOS_DEPLOYMENT_DIR="+storeDir) |
| @@ -1793,17 +1771,8 @@ func TestEnvCLI(t *testing.T) { |
| } |
| func TestSecretsCLI(t *testing.T) { |
| - goBin, err := exec.LookPath("go") |
| - if err != nil { |
| - t.Skip("go toolchain not found") |
| - } |
| tmp := t.TempDir() |
| - carlosBin := filepath.Join(tmp, "carlos") |
| - build := exec.Command(goBin, "build", "-o", carlosBin, ".") |
| - build.Env = os.Environ() |
| - if out, err := build.CombinedOutput(); err != nil { |
| - t.Fatalf("build carlos: %v\n%s", err, out) |
| - } |
| + carlosBin := sharedCarlos(t) |
| storeDir := filepath.Join(tmp, "store") |
| regPath := filepath.Join(tmp, "registry.db") |
| baseEnv := carlosTestEnv(t) |
| @@ -1874,17 +1843,8 @@ func TestSecretsCLI(t *testing.T) { |
| // key, created implicitly on first `set` exactly like the default bundle — |
| // and never touched by a write to the other. |
| func TestEnvironmentBundlesCLI(t *testing.T) { |
| - goBin, err := exec.LookPath("go") |
| - if err != nil { |
| - t.Skip("go toolchain not found") |
| - } |
| tmp := t.TempDir() |
| - carlosBin := filepath.Join(tmp, "carlos") |
| - build := exec.Command(goBin, "build", "-o", carlosBin, ".") |
| - build.Env = os.Environ() |
| - if out, err := build.CombinedOutput(); err != nil { |
| - t.Fatalf("build carlos: %v\n%s", err, out) |
| - } |
| + carlosBin := sharedCarlos(t) |
| storeDir := filepath.Join(tmp, "store") |
| env := carlosTestEnv(t, "CARLOS_DEPLOYMENT_DIR="+storeDir) |
| run := func(args ...string) string { |
| @@ -2026,17 +1986,8 @@ func TestEnvironmentBundlesCLI(t *testing.T) { |
| // clearing words refusing each other's ("off" vs "inherit"), following |
| // TestEnvCLI's build-a-real-binary pattern. |
| func TestExecDeliveryCLI(t *testing.T) { |
| - goBin, err := exec.LookPath("go") |
| - if err != nil { |
| - t.Skip("go toolchain not found") |
| - } |
| tmp := t.TempDir() |
| - carlosBin := filepath.Join(tmp, "carlos") |
| - build := exec.Command(goBin, "build", "-o", carlosBin, ".") |
| - build.Env = os.Environ() |
| - if out, err := build.CombinedOutput(); err != nil { |
| - t.Fatalf("build carlos: %v\n%s", err, out) |
| - } |
| + carlosBin := sharedCarlos(t) |
| storeDir := filepath.Join(tmp, "store") |
| env := carlosTestEnv(t, "CARLOS_DEPLOYMENT_DIR="+storeDir) |
| run := func(args ...string) string { |
| @@ -2173,17 +2124,8 @@ func TestExecDeliveryCLI(t *testing.T) { |
| // route bound to that environment would actually receive has the override |
| // applied and everything else inherited from default. |
| func TestEnvironmentMergeVisibleFromCLI(t *testing.T) { |
| - goBin, err := exec.LookPath("go") |
| - if err != nil { |
| - t.Skip("go toolchain not found") |
| - } |
| tmp := t.TempDir() |
| - carlosBin := filepath.Join(tmp, "carlos") |
| - build := exec.Command(goBin, "build", "-o", carlosBin, ".") |
| - build.Env = os.Environ() |
| - if out, err := build.CombinedOutput(); err != nil { |
| - t.Fatalf("build carlos: %v\n%s", err, out) |
| - } |
| + carlosBin := sharedCarlos(t) |
| storeDir := filepath.Join(tmp, "store") |
| env := carlosTestEnv(t, "CARLOS_DEPLOYMENT_DIR="+storeDir) |
| run := func(args ...string) { |
| @@ -2239,17 +2181,8 @@ func TestEnvironmentMergeVisibleFromCLI(t *testing.T) { |
| // that must never happen by accident (the keymail.dev canary/keymail -> |
| // stable move this verb was built for). |
| func TestRouteCLI(t *testing.T) { |
| - goBin, err := exec.LookPath("go") |
| - if err != nil { |
| - t.Skip("go toolchain not found") |
| - } |
| tmp := t.TempDir() |
| - carlosBin := filepath.Join(tmp, "carlos") |
| - build := exec.Command(goBin, "build", "-o", carlosBin, ".") |
| - build.Env = os.Environ() |
| - if out, err := build.CombinedOutput(); err != nil { |
| - t.Fatalf("build carlos: %v\n%s", err, out) |
| - } |
| + carlosBin := sharedCarlos(t) |
| storeDir := filepath.Join(tmp, "store") |
| regPath := filepath.Join(tmp, "registry.db") |
| store := release.NewDirStore(storeDir) |
| @@ -2550,17 +2483,8 @@ type routeTestFixture struct { |
| func newRouteTestFixture(t *testing.T) *routeTestFixture { |
| t.Helper() |
| - goBin, err := exec.LookPath("go") |
| - if err != nil { |
| - t.Skip("go toolchain not found") |
| - } |
| tmp := t.TempDir() |
| - bin := filepath.Join(tmp, "carlos") |
| - build := exec.Command(goBin, "build", "-o", bin, ".") |
| - build.Env = os.Environ() |
| - if out, err := build.CombinedOutput(); err != nil { |
| - t.Fatalf("build carlos: %v\n%s", err, out) |
| - } |
| + bin := sharedCarlos(t) |
| storeDir := filepath.Join(tmp, "store") |
| seed, pubB64, err := release.GenerateKey() |
| if err != nil { |
| @@ -3550,17 +3474,8 @@ func TestRouteNeitherChannelNorEnvironmentIsUsageError(t *testing.T) { |
| // caught only because the next adopt pass printed the downgrade. The verb |
| // now keeps a differing qualified pointer and says so. |
| func TestAccountsMigrateLegacyKeepsNewerQualifiedPointer(t *testing.T) { |
| - goBin, err := exec.LookPath("go") |
| - if err != nil { |
| - t.Skip("go toolchain not found") |
| - } |
| tmp := t.TempDir() |
| - carlosBin := filepath.Join(tmp, "carlos") |
| - build := exec.Command(goBin, "build", "-o", carlosBin, ".") |
| - build.Env = os.Environ() |
| - if out, err := build.CombinedOutput(); err != nil { |
| - t.Fatalf("build carlos: %v\n%s", err, out) |
| - } |
| + carlosBin := sharedCarlos(t) |
| storeDir := filepath.Join(tmp, "store") |
| regPath := filepath.Join(tmp, "registry.db") |
| store := release.NewDirStore(storeDir) |
diff --git a/main_test.go b/main_test.go| index c37400943..bdca63aea 100644 |
| --- a/main_test.go |
| +++ b/main_test.go |
| @@ -46,7 +46,20 @@ func TestMain(m *testing.M) { |
| if moved != "" { |
| fmt.Fprintln(os.Stderr, moved) |
| } |
| + // The shared carlos binary (sharedCarlos) is built lazily, but its home |
| + // is made here: after isolateTempRoot, so it is in the root the guard |
| + // chose, and with the module root and environment as they are before any |
| + // test can change directory or HOME. |
| + moduleRoot, _ = os.Getwd() |
| + carlosBuildEnv = os.Environ() |
| + carlosDir, carlosDirErr = os.MkdirTemp("", "carlos-bin-") |
| + if carlosDirErr == nil { |
| + os.Setenv(carlosDirEnv, carlosDir) |
| + } |
| code := m.Run() |
| + if carlosDirErr == nil { |
| + os.RemoveAll(carlosDir) |
| + } |
| cleanup() |
| os.Exit(code) |
| } |
| @@ -288,3 +301,39 @@ func TestIsolateTempRoot(t *testing.T) { |
| t.Fatalf("clear roots = %s, %s, %q, %v; want them left alone", os.TempDir(), os.Getenv("GOTMPDIR"), said, err) |
| } |
| } |
| + |
| +// The shared binary is one file, found at one path, that is executable and |
| +// that nothing can write to, and it lives inside the temp root TestMain made |
| +// for it. Asked for from a test that has left the module, it still builds. |
| +func TestSharedCarlos(t *testing.T) { |
| + if testing.Short() { |
| + t.Skip("builds a binary") |
| + } |
| + t.Chdir(t.TempDir()) |
| + first, second := sharedCarlos(t), sharedCarlos(t) |
| + if first != second { |
| + t.Fatalf("two calls gave %s and %s, want one path", first, second) |
| + } |
| + if first != filepath.Join(os.Getenv(carlosDirEnv), "carlos") { |
| + t.Errorf("%s is not the file the external package is told about (%s)", first, os.Getenv(carlosDirEnv)) |
| + } |
| + info, err := os.Stat(first) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + if runtime.GOOS != "windows" && info.Mode().Perm() != 0o555 { |
| + t.Errorf("mode %v, want r-xr-xr-x: executable and not writable", info.Mode().Perm()) |
| + } |
| + if rel, err := filepath.Rel(carlosDir, first); err != nil || strings.HasPrefix(rel, "..") { |
| + t.Errorf("%s is not under %s", first, carlosDir) |
| + } |
| + if rel, err := filepath.Rel(os.TempDir(), first); err != nil || strings.HasPrefix(rel, "..") { |
| + t.Errorf("%s is not under the temp root %s", first, os.TempDir()) |
| + } |
| + if os.Geteuid() != 0 { |
| + if f, err := os.OpenFile(first, os.O_WRONLY, 0); err == nil { |
| + f.Close() |
| + t.Error("the shared binary opened for writing") |
| + } |
| + } |
| +} |