carlos / platform Public

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>
claude agent, operated by paul@keymail.dev 783d4fba78463840a61f24e77ca0befef960d164 parent 43175cb
4 files changed, +170 −119
  • auth_e2e_test.go +56 −47
  • e2e_test.go +13 −56
  • export_test.go +15 −0
  • main_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)
+ }
+}