Merge remote-tracking branch 'origin/main' into design-plans-and-caps
4 files changed,
+268
−184
auth_e2e_test.go+77 −4e2e_test.go+52 −180export_test.go+15 −0main_test.go+124 −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 109.742s
? github.com/carlosframework/platform/cmd/changelog [no test files]
ok github.com/carlosframework/platform/cmd/console 3.006s
ok github.com/carlosframework/platform/cmd/moveprobe 93.576s
ok github.com/carlosframework/platform/cmd/zddprobe 0.724s
ok github.com/carlosframework/platform/internal/activator 251.354s
ok github.com/carlosframework/platform/internal/agentsock 0.016s
ok github.com/carlosframework/platform/internal/alerting 0.004s
ok github.com/carlosframework/platform/internal/analytics 0.020s
ok github.com/carlosframework/platform/internal/appconfig 2.505s
ok github.com/carlosframework/platform/internal/apperr 0.071s
ok github.com/carlosframework/platform/internal/applog 0.131s
ok github.com/carlosframework/platform/internal/awsinv 0.004s
ok github.com/carlosframework/platform/internal/boxchoice 0.005s
ok github.com/carlosframework/platform/internal/boxid 0.727s
? 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.012s
ok github.com/carlosframework/platform/internal/certwarm 0.003s
ok github.com/carlosframework/platform/internal/cgroup 0.710s
ok github.com/carlosframework/platform/internal/changelog 0.328s
ok github.com/carlosframework/platform/internal/claimactivation 0.805s
ok github.com/carlosframework/platform/internal/clamd 0.055s
? github.com/carlosframework/platform/internal/cliflag [no test files]
ok github.com/carlosframework/platform/internal/console 448.236s
? 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.189s
ok github.com/carlosframework/platform/internal/dbdoor/dbdoortest 0.010s
ok github.com/carlosframework/platform/internal/dbverify 0.005s
ok github.com/carlosframework/platform/internal/dnsrec 0.004s
ok github.com/carlosframework/platform/internal/docsite 0.006s
ok github.com/carlosframework/platform/internal/domains 2.405s
ok github.com/carlosframework/platform/internal/doorkey 0.007s
ok github.com/carlosframework/platform/internal/economics 0.004s
ok github.com/carlosframework/platform/internal/edge 46.370s
? github.com/carlosframework/platform/internal/edgeproof [no test files]
ok github.com/carlosframework/platform/internal/edgesfile 0.009s
ok github.com/carlosframework/platform/internal/fleetapi 0.134s
ok github.com/carlosframework/platform/internal/gate 1.656s
ok github.com/carlosframework/platform/internal/genselect 0.015s
ok github.com/carlosframework/platform/internal/geo 0.016s
ok github.com/carlosframework/platform/internal/geoip 0.030s
ok github.com/carlosframework/platform/internal/historyreadback 0.156s
ok github.com/carlosframework/platform/internal/host 203.678s
ok github.com/carlosframework/platform/internal/hostactors 7.139s
ok github.com/carlosframework/platform/internal/hostactors/awsmembership 0.015s
ok github.com/carlosframework/platform/internal/hostbox 3.541s
ok github.com/carlosframework/platform/internal/hostclaim 1.051s
ok github.com/carlosframework/platform/internal/hostenv 0.008s
ok github.com/carlosframework/platform/internal/hostevidence 0.010s
ok github.com/carlosframework/platform/internal/hostexec 7.577s
ok github.com/carlosframework/platform/internal/hostfence 0.043s
ok github.com/carlosframework/platform/internal/hostfiles 11.099s
ok github.com/carlosframework/platform/internal/hostfloor 0.028s
ok github.com/carlosframework/platform/internal/hostguard 1.093s
ok github.com/carlosframework/platform/internal/hostops 2.480s
? github.com/carlosframework/platform/internal/hostops/fencerelease [no test files]
ok github.com/carlosframework/platform/internal/hostops/internal/fencerelease 0.005s
ok github.com/carlosframework/platform/internal/hostquarantine 0.003s
ok github.com/carlosframework/platform/internal/hostrelocate 0.814s
ok github.com/carlosframework/platform/internal/hostresolve 0.364s
ok github.com/carlosframework/platform/internal/hostwrite 0.007s
ok github.com/carlosframework/platform/internal/infraview 0.002s
ok github.com/carlosframework/platform/internal/instancectl 5.333s
ok github.com/carlosframework/platform/internal/instanceexec 2.820s
ok github.com/carlosframework/platform/internal/instancefs 0.011s
ok github.com/carlosframework/platform/internal/instancekey 0.006s
ok github.com/carlosframework/platform/internal/instanceupdates 0.008s
ok github.com/carlosframework/platform/internal/layout 2.134s
ok github.com/carlosframework/platform/internal/lbhealth 0.007s
ok github.com/carlosframework/platform/internal/lease 9.944s
ok github.com/carlosframework/platform/internal/lease/leasetest 0.524s
ok github.com/carlosframework/platform/internal/ledger 0.026s
ok github.com/carlosframework/platform/internal/mail 0.274s
ok github.com/carlosframework/platform/internal/maintenance 0.092s
ok github.com/carlosframework/platform/internal/maintenanceexec 5.882s
ok github.com/carlosframework/platform/internal/maintenanceguard 0.234s
ok github.com/carlosframework/platform/internal/metrics 0.006s
ok github.com/carlosframework/platform/internal/moves 22.241s
ok github.com/carlosframework/platform/internal/moves/archive 1.105s
ok github.com/carlosframework/platform/internal/objstore 0.485s
ok github.com/carlosframework/platform/internal/ops 0.207s
ok github.com/carlosframework/platform/internal/park 0.364s
ok github.com/carlosframework/platform/internal/plans 0.010s
ok github.com/carlosframework/platform/internal/privatefile 0.008s
ok github.com/carlosframework/platform/internal/provision 44.849s
ok github.com/carlosframework/platform/internal/publicrelease 0.012s
? github.com/carlosframework/platform/internal/regionname [no test files]
ok github.com/carlosframework/platform/internal/registry 6.451s
ok github.com/carlosframework/platform/internal/relay 7.327s
ok github.com/carlosframework/platform/internal/release 26.271s
ok github.com/carlosframework/platform/internal/rollout 4.432s
ok github.com/carlosframework/platform/internal/rolloutwords 0.007s
ok github.com/carlosframework/platform/internal/rootaws 0.007s
ok github.com/carlosframework/platform/internal/rootparticipation 0.061s
ok github.com/carlosframework/platform/internal/rootstate 0.030s
ok github.com/carlosframework/platform/internal/runs 0.576s
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 3.100s
ok github.com/carlosframework/platform/internal/seeds 0.011s
ok github.com/carlosframework/platform/internal/sim/hostlife 14.289s
ok github.com/carlosframework/platform/internal/sim/litestreamfacts 3.574s
ok github.com/carlosframework/platform/internal/status 0.097s
ok github.com/carlosframework/platform/internal/steering 0.011s
ok github.com/carlosframework/platform/internal/stopevidence 0.406s
ok github.com/carlosframework/platform/internal/svcrole 0.006s
ok github.com/carlosframework/platform/internal/telemetry 0.005s
ok github.com/carlosframework/platform/internal/termfence 0.004s
ok github.com/carlosframework/platform/internal/tunnelclient 0.007s
ok github.com/carlosframework/platform/internal/unixdial 0.004s
ok github.com/carlosframework/platform/internal/updatewords 0.011s
ok github.com/carlosframework/platform/internal/usage 0.006s
ok github.com/carlosframework/platform/internal/version 0.002s
ok github.com/carlosframework/platform/internal/vet 1.723s
=== 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
linux/arm64 carlos binary: 49873058 bytes (ceiling 52000000)
--- 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.023s
=== RUN TestT4StorageProbeMinio
2026/10/21 13:24:11 moves: T4: STORE: list: operation error S3: ListObjectsV2, https response error StatusCode: 403, RequestID: 18E08E3EC49C15BE, 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.022s
minio contract: PASS
diff --git a/auth_e2e_test.go b/auth_e2e_test.go| index 22ee1dfb7..9e0079637 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,8 @@ import ( |
| "os/exec" |
| "path/filepath" |
| "strings" |
| + "sync" |
| + "sync/atomic" |
| "testing" |
| "time" |
| @@ -25,19 +28,89 @@ 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. 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 |
| + carlosTmpParent string |
| + carlosDir string |
| + |
| + carlosBuilds atomic.Int32 |
| + sharedCarlosState carlosShared |
| +) |
| + |
| +// 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 |
| +// 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. |
| +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") |
| + 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.Env = os.Environ() |
| + build.Dir = moduleRoot |
| + build.Env = carlosBuildEnv |
| if out, err := build.CombinedOutput(); err != nil { |
| - t.Fatalf("build carlos: %v\n%s", err, out) |
| + return "", fmt.Errorf("%v\n%s", err, out) |
| + } |
| + if err := os.Chmod(bin, 0o555); err != nil { |
| + return "", err |
| } |
| - return bin |
| + 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 977a38e5f..f4ed78414 100644 |
| --- a/e2e_test.go |
| +++ b/e2e_test.go |
| @@ -29,10 +29,12 @@ import ( |
| "path/filepath" |
| "reflect" |
| "regexp" |
| + "runtime" |
| "strings" |
| "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" |
| @@ -101,6 +103,38 @@ func carlosTestEnv(t *testing.T, extra ...string) []string { |
| return append(env, extra...) |
| } |
| +// 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() |
| + return platform.SharedCarlosForTest(t) |
| +} |
| + |
| +// 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 { |
| + 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 { |
| + 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 +541,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 +747,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 +939,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 +1096,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 +1191,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 +1300,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 +1372,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 +1442,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 +1507,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 +1559,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 +1684,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 +1728,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 +1800,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 +1943,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 +2081,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 +2138,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 +2440,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 +3431,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/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 c37400943..1d6cc7c00 100644 |
| --- a/main_test.go |
| +++ b/main_test.go |
| @@ -4,6 +4,7 @@ import ( |
| "errors" |
| "fmt" |
| "os" |
| + "os/exec" |
| "path/filepath" |
| "runtime" |
| "strings" |
| @@ -46,7 +47,20 @@ func TestMain(m *testing.M) { |
| if moved != "" { |
| fmt.Fprintln(os.Stderr, moved) |
| } |
| + // 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() |
| + if carlosTmpParent = os.Getenv("GOTMPDIR"); carlosTmpParent == "" { |
| + carlosTmpParent = os.TempDir() |
| + } |
| code := m.Run() |
| + if carlosDir != "" { |
| + os.RemoveAll(carlosDir) |
| + } |
| cleanup() |
| os.Exit(code) |
| } |
| @@ -288,3 +302,113 @@ 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 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") |
| + } |
| + 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 n := carlosBuilds.Load(); n != 1 { |
| + t.Errorf("%d builds in this process, want 1", n) |
| + } |
| + 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 first != filepath.Join(carlosDir, "carlos") { |
| + t.Errorf("%s is not in the directory the build made, %s", first, carlosDir) |
| + } |
| + 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 { |
| + f.Close() |
| + t.Error("the shared binary opened for writing") |
| + } |
| + } |
| +} |
| + |
| +// 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") |
| + } |
| + // The child skips without a toolchain and prints no path; say so here |
| + // rather than fail on the missing line. |
| + if _, err := exec.LookPath("go"); err != nil { |
| + t.Skip("go toolchain not on PATH") |
| + } |
| + 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) |
| + } |
| +} |