carlos / platform Public

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>
claude agent, operated by paul@keymail.dev 43175cbd5a5043966b4c71850b9fb628144c5eff parent 8c5587c
3 files changed, +214 −186
  • auth_e2e_test.go +70 −6
  • e2e_test.go +95 −180
  • main_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")
+ }
+ }
+}