root tests: one owner for the shared carlos build, in GOTMPDIR, made on demand
Review of 43175cbd:
- The binary's directory ignored GOTMPDIR (os.MkdirTemp("") reads TMPDIR),
so with TMPDIR on a shared RAM disk the 120 MB file went there. It is now
under GOTMPDIR when set, as t.TempDir() does.
- The external package built with an environment taken at package init,
before isolateTempRoot. There is now one build, one Once, one path and one
error for the process: the implementation is in package main's test code
and package main_test reaches it through export_test.go. CARLOS_TEST_BIN_DIR
and the file-exists check are gone.
- The directory is made by the build, not in TestMain, so a re-executed test
binary (TestQueryCleansUpWhenStdoutIsClosed's child) that never asks for the
binary leaves nothing behind.
Tests: a kept build failure, the GOTMPDIR location, and no directory when
nothing asks, the last two from a re-executed test binary with scratch roots
of its own.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
4 files changed,
+170
−119
auth_e2e_test.go+56 −47e2e_test.go+13 −56export_test.go+15 −0main_test.go+86 −16
CI passed — run details
CI log
=== gate ===
cd launchctx && go vet ./... && go test ./... -count=1
ok amadan.net/carlos/platform.git/launchctx 0.011s
? 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.151s
? github.com/carlosframework/platform/cmd/changelog [no test files]
ok github.com/carlosframework/platform/cmd/console 3.055s
ok github.com/carlosframework/platform/cmd/moveprobe 92.272s
ok github.com/carlosframework/platform/cmd/zddprobe 0.380s
ok github.com/carlosframework/platform/internal/activator 225.864s
ok github.com/carlosframework/platform/internal/agentsock 0.021s
ok github.com/carlosframework/platform/internal/alerting 0.007s
ok github.com/carlosframework/platform/internal/analytics 0.028s
ok github.com/carlosframework/platform/internal/appconfig 2.354s
ok github.com/carlosframework/platform/internal/apperr 0.097s
ok github.com/carlosframework/platform/internal/applog 0.155s
ok github.com/carlosframework/platform/internal/awsinv 0.007s
ok github.com/carlosframework/platform/internal/boxchoice 0.004s
ok github.com/carlosframework/platform/internal/boxid 0.574s
? 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.009s
ok github.com/carlosframework/platform/internal/caps 0.014s
ok github.com/carlosframework/platform/internal/certwarm 0.005s
ok github.com/carlosframework/platform/internal/cgroup 0.700s
ok github.com/carlosframework/platform/internal/changelog 0.312s
ok github.com/carlosframework/platform/internal/clamd 0.060s
? github.com/carlosframework/platform/internal/cliflag [no test files]
ok github.com/carlosframework/platform/internal/console 360.543s
? 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 5.919s
ok github.com/carlosframework/platform/internal/dbdoor/dbdoortest 0.009s
ok github.com/carlosframework/platform/internal/dbverify 0.009s
ok github.com/carlosframework/platform/internal/dnsrec 0.005s
ok github.com/carlosframework/platform/internal/docsite 0.009s
ok github.com/carlosframework/platform/internal/domains 2.185s
ok github.com/carlosframework/platform/internal/doorkey 0.009s
ok github.com/carlosframework/platform/internal/economics 0.007s
ok github.com/carlosframework/platform/internal/edge 26.018s
? github.com/carlosframework/platform/internal/edgeproof [no test files]
ok github.com/carlosframework/platform/internal/edgesfile 0.011s
ok github.com/carlosframework/platform/internal/fleetapi 0.143s
ok github.com/carlosframework/platform/internal/gate 1.655s
ok github.com/carlosframework/platform/internal/genselect 0.018s
ok github.com/carlosframework/platform/internal/geo 0.011s
ok github.com/carlosframework/platform/internal/geoip 0.040s
ok github.com/carlosframework/platform/internal/host 95.287s
ok github.com/carlosframework/platform/internal/hostactors 0.537s
ok github.com/carlosframework/platform/internal/hostbox 2.171s
ok github.com/carlosframework/platform/internal/hostclaim 0.511s
ok github.com/carlosframework/platform/internal/hostevidence 0.014s
ok github.com/carlosframework/platform/internal/hostexec 4.716s
ok github.com/carlosframework/platform/internal/hostfence 0.031s
ok github.com/carlosframework/platform/internal/hostfiles 10.356s
ok github.com/carlosframework/platform/internal/hostfloor 0.017s
ok github.com/carlosframework/platform/internal/hostguard 0.983s
ok github.com/carlosframework/platform/internal/hostops 1.482s
? github.com/carlosframework/platform/internal/hostops/fencerelease [no test files]
ok github.com/carlosframework/platform/internal/hostops/internal/fencerelease 0.001s
ok github.com/carlosframework/platform/internal/hostquarantine 0.003s
ok github.com/carlosframework/platform/internal/hostrelocate 0.458s
ok github.com/carlosframework/platform/internal/hostresolve 0.142s
ok github.com/carlosframework/platform/internal/hostwrite 0.002s
ok github.com/carlosframework/platform/internal/infraview 0.002s
ok github.com/carlosframework/platform/internal/instancectl 1.819s
ok github.com/carlosframework/platform/internal/instanceexec 2.698s
ok github.com/carlosframework/platform/internal/instancefs 0.005s
ok github.com/carlosframework/platform/internal/instancekey 0.003s
ok github.com/carlosframework/platform/internal/instanceupdates 0.007s
ok github.com/carlosframework/platform/internal/layout 1.433s
ok github.com/carlosframework/platform/internal/lease 9.431s
ok github.com/carlosframework/platform/internal/lease/leasetest 0.006s
ok github.com/carlosframework/platform/internal/ledger 0.016s
ok github.com/carlosframework/platform/internal/mail 0.154s
ok github.com/carlosframework/platform/internal/maintenance 0.081s
ok github.com/carlosframework/platform/internal/maintenanceexec 5.496s
ok github.com/carlosframework/platform/internal/maintenanceguard 0.066s
ok github.com/carlosframework/platform/internal/metrics 0.005s
ok github.com/carlosframework/platform/internal/moves 20.942s
ok github.com/carlosframework/platform/internal/moves/archive 0.771s
ok github.com/carlosframework/platform/internal/objstore 0.150s
ok github.com/carlosframework/platform/internal/ops 0.153s
ok github.com/carlosframework/platform/internal/park 0.198s
ok github.com/carlosframework/platform/internal/privatefile 0.003s
ok github.com/carlosframework/platform/internal/provision 7.278s
ok github.com/carlosframework/platform/internal/publicrelease 0.006s
? github.com/carlosframework/platform/internal/regionname [no test files]
ok github.com/carlosframework/platform/internal/registry 3.965s
ok github.com/carlosframework/platform/internal/relay 7.224s
ok github.com/carlosframework/platform/internal/release 18.728s
ok github.com/carlosframework/platform/internal/rollout 3.593s
ok github.com/carlosframework/platform/internal/rolloutwords 0.003s
ok github.com/carlosframework/platform/internal/rootaws 0.006s
ok github.com/carlosframework/platform/internal/rootparticipation 0.009s
ok github.com/carlosframework/platform/internal/rootstate 0.025s
ok github.com/carlosframework/platform/internal/s3compat 0.004s
ok github.com/carlosframework/platform/internal/safefile 0.004s
ok github.com/carlosframework/platform/internal/schedule 2.260s
ok github.com/carlosframework/platform/internal/sim/hostlife 8.196s
ok github.com/carlosframework/platform/internal/sim/litestreamfacts 4.151s
ok github.com/carlosframework/platform/internal/status 0.040s
ok github.com/carlosframework/platform/internal/steering 0.006s
ok github.com/carlosframework/platform/internal/stopevidence 0.147s
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.005s
ok github.com/carlosframework/platform/internal/unixdial 0.003s
ok github.com/carlosframework/platform/internal/updatewords 0.005s
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.528s
=== 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: cached, digest verified
mc: cached, 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.021s
=== RUN TestT4StorageProbeMinio
2026/10/14 00:41:44 moves: T4: STORE: list: operation error S3: ListObjectsV2, https response error StatusCode: 403, RequestID: 18DE3E9351EE549A, 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.00s)
PASS
ok github.com/carlosframework/platform/internal/host 0.019s
minio contract: PASS
diff --git a/auth_e2e_test.go b/auth_e2e_test.go| index 0169e5b48..d05b35d40 100644 |
| --- a/auth_e2e_test.go |
| +++ b/auth_e2e_test.go |
| @@ -16,6 +16,7 @@ import ( |
| "path/filepath" |
| "strings" |
| "sync" |
| + "sync/atomic" |
| "testing" |
| "time" |
| @@ -35,27 +36,37 @@ func buildCarlos(t *testing.T) string { |
| 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. |
| +// The shared binary's state. moduleRoot, carlosBuildEnv and carlosTmpParent |
| +// are set by TestMain, after isolateTempRoot and 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 and the isolated temp root, whichever test asks |
| +// first. carlosDir is made by the build itself, in carlosTmpParent, so a run |
| +// that never asks for the binary makes nothing (a re-executed test binary, |
| +// like the child of TestQueryCleansUpWhenStdoutIsClosed, runs TestMain too |
| +// and exits without cleanup). TestMain removes it when the tests return. |
| var ( |
| - moduleRoot string |
| - carlosBuildEnv []string |
| - carlosDir string |
| - carlosDirErr error |
| - |
| - carlosOnce sync.Once |
| - sharedCarlosPath string |
| - carlosErr error |
| + moduleRoot string |
| + carlosBuildEnv []string |
| + carlosTmpParent string |
| + carlosDir string |
| + |
| + carlosBuilds atomic.Int32 |
| + sharedCarlosState carlosShared |
| ) |
| -// 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" |
| +// carlosShared is the one build for the whole test process: one Once, one |
| +// path, one error. The external test package (package main_test) reaches it |
| +// through export_test.go, so there is no second copy to disagree with. |
| +type carlosShared struct { |
| + once sync.Once |
| + path string |
| + err error |
| +} |
| + |
| +func (s *carlosShared) get(build func() (string, error)) (string, error) { |
| + s.once.Do(func() { s.path, s.err = build() }) |
| + return s.path, s.err |
| +} |
| // 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 |
| @@ -66,42 +77,40 @@ const carlosDirEnv = "CARLOS_TEST_BIN_DIR" |
| // 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") |
| } |
| - 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) |
| + path, err := sharedCarlosState.get(func() (string, error) { return buildSharedCarlos(goBin) }) |
| + if err != nil { |
| + t.Fatalf("build carlos: %v", err) |
| + } |
| + return path |
| +} |
| + |
| +// buildSharedCarlos is the build itself, run once. The directory goes under |
| +// GOTMPDIR when that is set, as t.TempDir() does, so the 120 MB file lands |
| +// wherever the tests' other scratch does. |
| +func buildSharedCarlos(goBin string) (string, error) { |
| + carlosBuilds.Add(1) |
| + dir, err := os.MkdirTemp(carlosTmpParent, "carlos-bin-") |
| + if err != nil { |
| + return "", fmt.Errorf("no directory for the shared binary: %w", err) |
| + } |
| + carlosDir = dir |
| + bin := filepath.Join(dir, "carlos") |
| + build := exec.Command(goBin, "build", "-o", bin, ".") |
| + build.Dir = moduleRoot |
| + build.Env = carlosBuildEnv |
| + if out, err := build.CombinedOutput(); err != nil { |
| + return "", fmt.Errorf("%v\n%s", err, out) |
| + } |
| + if err := os.Chmod(bin, 0o555); err != nil { |
| + return "", err |
| } |
| - return sharedCarlosPath |
| + return bin, nil |
| } |
| // TestAuthCLI drives `carlos auth` as a subprocess against a fake console: |
diff --git a/e2e_test.go b/e2e_test.go| index f5f675908..f4ed78414 100644 |
| --- a/e2e_test.go |
| +++ b/e2e_test.go |
| @@ -31,10 +31,10 @@ import ( |
| "regexp" |
| "runtime" |
| "strings" |
| - "sync" |
| "testing" |
| "time" |
| + platform "github.com/carlosframework/platform" |
| "github.com/carlosframework/platform/internal/appconfig" |
| "github.com/carlosframework/platform/internal/domains" |
| "github.com/carlosframework/platform/internal/edge" |
| @@ -103,71 +103,28 @@ 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. |
| +// sharedCarlos is the one shared carlos build (auth_e2e_test.go), reached |
| +// through export_test.go: this file is package main_test and cannot see |
| +// package main's test helpers any other way. The binary is built at most once |
| +// per test process, whichever package asks first, and is read-only. |
| 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 |
| + return platform.SharedCarlosForTest(t) |
| } |
| -// 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). |
| +// The external package gets the very file the internal one does: one build |
| +// for the process, whoever asked first, read-only. |
| 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) |
| + if first != second { |
| + t.Fatalf("calls gave %s and %s, want one path", first, second) |
| + } |
| + if n := platform.SharedCarlosBuildsForTest(); n != 1 { |
| + t.Errorf("%d builds in this process, want 1", n) |
| } |
| info, err := os.Stat(first) |
| if err != nil { |
diff --git a/export_test.go b/export_test.go| new file mode 100644 |
| index 000000000..c8b4baa6a |
| --- /dev/null |
| +++ b/export_test.go |
| @@ -0,0 +1,15 @@ |
| +package main |
| + |
| +import "testing" |
| + |
| +// SharedCarlosForTest hands the one shared carlos build (sharedCarlos, |
| +// auth_e2e_test.go) to the external test package, package main_test, which |
| +// cannot see unexported names. Two copies of the build would be two Onces |
| +// that disagree about whether it failed. |
| +func SharedCarlosForTest(t testing.TB) string { |
| + t.Helper() |
| + return sharedCarlos(t) |
| +} |
| + |
| +// SharedCarlosBuildsForTest is how many times this process has built it. |
| +func SharedCarlosBuildsForTest() int { return int(carlosBuilds.Load()) } |
diff --git a/main_test.go b/main_test.go| index bdca63aea..cbc0ae6f6 100644 |
| --- a/main_test.go |
| +++ b/main_test.go |
| @@ -4,6 +4,7 @@ import ( |
| "errors" |
| "fmt" |
| "os" |
| + "os/exec" |
| "path/filepath" |
| "runtime" |
| "strings" |
| @@ -46,18 +47,18 @@ 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. |
| + // The shared carlos binary (sharedCarlos) is built lazily and its |
| + // directory made by the build. What is fixed here, after isolateTempRoot |
| + // and before any test can change directory or HOME: the module to build, |
| + // the environment to build in, and where the directory goes. GOTMPDIR |
| + // first, because that is where t.TempDir() puts things. |
| moduleRoot, _ = os.Getwd() |
| carlosBuildEnv = os.Environ() |
| - carlosDir, carlosDirErr = os.MkdirTemp("", "carlos-bin-") |
| - if carlosDirErr == nil { |
| - os.Setenv(carlosDirEnv, carlosDir) |
| + if carlosTmpParent = os.Getenv("GOTMPDIR"); carlosTmpParent == "" { |
| + carlosTmpParent = os.TempDir() |
| } |
| code := m.Run() |
| - if carlosDirErr == nil { |
| + if carlosDir != "" { |
| os.RemoveAll(carlosDir) |
| } |
| cleanup() |
| @@ -303,8 +304,10 @@ func TestIsolateTempRoot(t *testing.T) { |
| } |
| // 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. |
| +// that nothing can write to, and it lives inside the directory the build |
| +// made under the temp root. Asked for from a test that has left the module, |
| +// it still builds, and it is built once for the whole process, whichever |
| +// package asked (TestSharedCarlosFromTheExternalPackage asks from the other). |
| func TestSharedCarlos(t *testing.T) { |
| if testing.Short() { |
| t.Skip("builds a binary") |
| @@ -314,8 +317,8 @@ func TestSharedCarlos(t *testing.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)) |
| + if n := carlosBuilds.Load(); n != 1 { |
| + t.Errorf("%d builds in this process, want 1", n) |
| } |
| info, err := os.Stat(first) |
| if err != nil { |
| @@ -324,11 +327,11 @@ func TestSharedCarlos(t *testing.T) { |
| 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 first != filepath.Join(carlosDir, "carlos") { |
| + t.Errorf("%s is not in the directory the build made, %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 rel, err := filepath.Rel(carlosTmpParent, first); err != nil || strings.HasPrefix(rel, "..") { |
| + t.Errorf("%s is not under %s", first, carlosTmpParent) |
| } |
| if os.Geteuid() != 0 { |
| if f, err := os.OpenFile(first, os.O_WRONLY, 0); err == nil { |
| @@ -337,3 +340,70 @@ func TestSharedCarlos(t *testing.T) { |
| } |
| } |
| } |
| + |
| +// A failed build is the answer for every caller, not just the first: the |
| +// build runs once and its error is kept. |
| +func TestSharedCarlosKeepsABuildFailure(t *testing.T) { |
| + var s carlosShared |
| + builds := 0 |
| + failing := func() (string, error) { |
| + builds++ |
| + return "", errors.New("compile error in the module") |
| + } |
| + for i := 0; i < 3; i++ { |
| + if path, err := s.get(failing); path != "" || err == nil || !strings.Contains(err.Error(), "compile error") { |
| + t.Fatalf("call %d = %q, %v; want the build's error", i, path, err) |
| + } |
| + } |
| + if builds != 1 { |
| + t.Fatalf("the failing build ran %d times, want 1", builds) |
| + } |
| +} |
| + |
| +// Where the directory goes, and when it is made, read from a re-executed test |
| +// binary with scratch roots of its own. GOTMPDIR wins over TMPDIR, as it does |
| +// for t.TempDir(), so the binary is not put on a shared RAM disk that TMPDIR |
| +// names. A process that never asks for the binary makes nothing at all. |
| +// Either way the roots are empty again once the process has exited. |
| +func TestSharedCarlosLocation(t *testing.T) { |
| + if os.Getenv("CARLOS_SHARED_CHILD") != "" { |
| + fmt.Println("SHARED_CARLOS=" + sharedCarlos(t)) |
| + return |
| + } |
| + if testing.Short() { |
| + t.Skip("builds a binary") |
| + } |
| + run := func(args ...string) (tmp, gotmp, out string) { |
| + t.Helper() |
| + tmp, gotmp = t.TempDir(), t.TempDir() |
| + cmd := exec.Command(os.Args[0], args...) |
| + cmd.Dir = moduleRoot |
| + cmd.Env = append(os.Environ(), "TMPDIR="+tmp, "GOTMPDIR="+gotmp, "CARLOS_SHARED_CHILD=1") |
| + b, err := cmd.CombinedOutput() |
| + if err != nil { |
| + t.Fatalf("child %v: %v\n%s", args, err, b) |
| + } |
| + for _, root := range []string{tmp, gotmp} { |
| + if left, _ := os.ReadDir(root); len(left) != 0 { |
| + t.Errorf("the child left %v in %s", left, root) |
| + } |
| + } |
| + return tmp, gotmp, string(b) |
| + } |
| + |
| + _, _, out := run("-test.run=^$") |
| + if strings.Contains(out, "SHARED_CARLOS=") { |
| + t.Fatalf("a child that ran no test built the binary:\n%s", out) |
| + } |
| + |
| + tmp, gotmp, out := run("-test.run=^TestSharedCarlosLocation$") |
| + var path string |
| + for _, line := range strings.Split(out, "\n") { |
| + if p, ok := strings.CutPrefix(line, "SHARED_CARLOS="); ok { |
| + path = p |
| + } |
| + } |
| + if path == "" || !strings.HasPrefix(path, gotmp+string(filepath.Separator)) { |
| + t.Errorf("the binary is at %q, want it under GOTMPDIR %s (TMPDIR was %s)", path, gotmp, tmp) |
| + } |
| +} |