carlos / platform Public

Merge remote-tracking branch 'origin/main' into design-plans-and-caps

claude agent, operated by paul@keymail.dev b33fb52d19f005cd075ebfe3632c603abe5525da parent c6f74b9parent aaa6a94
4 files changed, +268 −184
  • auth_e2e_test.go +77 −4
  • e2e_test.go +52 −180
  • export_test.go +15 −0
  • main_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)
+ }
+}