diff --git a/merge-check.sh b/merge-check.sh new file mode 100644 index 0000000..37449c2 --- /dev/null +++ b/merge-check.sh @@ -0,0 +1,39 @@ +#!/bin/sh +# mesh-check-toolchain: go +# +# The tool runtime's own check (novox/hq ADR 0237 as amended): the second layer of a pull request's merge +# check, `mesh/repo-check`, run by the build seat in the mesh's Go toolchain — and by hand. +# +# The gate — every machine of the facts snapshot composed with this change — is the build seat's first +# layer (`mesh/merge-gate`), run because the mesh's module graph builds node-tools and mesh-tools from +# here. This is the code's own: node-tools (Go) formatted, vetted and its suite, every test on a bus of its +# own at the release the mesh runs (internal/meshtest), packages in parallel, under the race detector when +# the toolchain has a C compiler. +# +# **Said, never passed silently**: the runtime's and the console's tests launch TypeScript and Python +# bundles that import the mesh's own package (@novox/mesh-sdk), which comes from the package registry — and +# a check of code nobody has approved is given no credential for it. Where node and that package are not +# present those two packages are not tested here, and the TypeScript in node-tools/src neither. +set -eu +cd node-tools + +unformatted=$(gofmt -l cmd internal) +if [ -n "$unformatted" ]; then + echo "not gofmt'd:" + echo "$unformatted" + exit 1 +fi +CGO_ENABLED=0 go vet ./... + +packages=$(go list ./...) +if ! command -v node >/dev/null 2>&1 || [ ! -d node_modules/@novox/mesh-sdk ]; then + echo "NOT TESTED HERE: internal/runtime and internal/console launch bundles that need node and @novox/mesh-sdk" + packages=$(printf '%s\n' "$packages" | grep -v -e '/internal/runtime$' -e '/internal/console$') +fi +if command -v gcc >/dev/null 2>&1; then + CGO_ENABLED=1 go test -race -count=1 $packages +else + echo "NOT RACE-CHECKED: the toolchain holds no C compiler; the suite runs without the race detector" + CGO_ENABLED=0 go test -count=1 $packages +fi +echo "NOT TESTED HERE: node-tools/src (TypeScript) needs @novox/mesh-sdk from the package registry" diff --git a/node-tools/go.mod b/node-tools/go.mod index 06b9b50..4395904 100644 --- a/node-tools/go.mod +++ b/node-tools/go.mod @@ -8,8 +8,15 @@ require ( ) require ( + github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op // indirect + github.com/google/go-tpm v0.9.8 // indirect github.com/klauspost/compress v1.20.0 // indirect + github.com/minio/highwayhash v1.0.4 // indirect + github.com/nats-io/jwt/v2 v2.8.1 // indirect + github.com/nats-io/nats-server/v2 v2.11.17 // indirect github.com/nats-io/nkeys v0.4.16 // indirect github.com/nats-io/nuid v1.0.1 // indirect + go.uber.org/automaxprocs v1.6.0 // indirect golang.org/x/crypto v0.57.0 // indirect + golang.org/x/time v0.15.0 // indirect ) diff --git a/node-tools/go.sum b/node-tools/go.sum index 65100b8..2274dd5 100644 --- a/node-tools/go.sum +++ b/node-tools/go.sum @@ -1,12 +1,27 @@ +github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op h1:Z/MZK75wC/NSrkgqeNIa7jexam9uWzhLmFTSCPI/kn0= +github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op/go.mod h1:FQyySiasQQM8735Ddel3MRojmy4dA1IqCeyJ5jmPMbI= +github.com/google/go-tpm v0.9.8 h1:slArAR9Ft+1ybZu0lBwpSmpwhRXaa85hWtMinMyRAWo= +github.com/google/go-tpm v0.9.8/go.mod h1:h9jEsEECg7gtLis0upRBQU+GhYVH6jMjrFxI8u6bVUY= github.com/klauspost/compress v1.20.0 h1:a3C1ke2ohxFymNlb2HWAHjDeKCI90scRskErZkR0ezA= github.com/klauspost/compress v1.20.0/go.mod h1:LUdAzn7YLVvxLpc7y3V1m40wESHTgc1422pwwBSKYuI= +github.com/minio/highwayhash v1.0.4 h1:asJizugGgchQod2ja9NJlGOWq4s7KsAWr5XUc9Clgl4= +github.com/minio/highwayhash v1.0.4/go.mod h1:GGYsuwP/fPD6Y9hMiXuapVvlIUEhFhMTh0rxU3ik1LQ= +github.com/nats-io/jwt/v2 v2.8.1 h1:V0xpGuD/N8Mi+fQNDynXohVvp7ZztevW5io8CUWlPmU= +github.com/nats-io/jwt/v2 v2.8.1/go.mod h1:nWnOEEiVMiKHQpnAy4eXlizVEtSfzacZ1Q43LIRavZg= +github.com/nats-io/nats-server/v2 v2.11.17 h1:GKEghcFK6A+aFx11Yf1LjgLC3txAwvyhnYzhBIQZA8I= +github.com/nats-io/nats-server/v2 v2.11.17/go.mod h1:B1sFVz4StNosQ903ak4N1G01Fl/9f8e06mXpFIE2K24= github.com/nats-io/nats.go v1.54.0 h1:vsXoOxjHp/GmPUN+EcI7uOf/uB+iAP+kEsAFNQN0yzA= github.com/nats-io/nats.go v1.54.0/go.mod h1:y+DZoD1oBOYfZTU681eTUiUjI0vbqYGixNVFHcjHJ0k= github.com/nats-io/nkeys v0.4.16 h1:rd5oAuLOb8mnAycB0xleuEBNS1pVVnN0fv/FF34Eypg= github.com/nats-io/nkeys v0.4.16/go.mod h1:llLgWoI0o4z/Q57q2R1kHfmocyhGV6VG/U18Glg1Afs= github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw= github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c= +go.uber.org/automaxprocs v1.6.0 h1:O3y2/QNTOdbF+e/dpXNNW7Rx2hZ4sTIPyybbxyNqTUs= +go.uber.org/automaxprocs v1.6.0/go.mod h1:ifeIMSnPZuznNm6jmdzmU3/bfk01Fe2fotchwEFJ8r8= golang.org/x/crypto v0.57.0 h1:3ZVCjf8Ggz7zneR/EHRVx68Ctf+2pmIMP2UFhh9cC6M= golang.org/x/crypto v0.57.0/go.mod h1:Fdz0i5U6CoizGwLda9DttjSk6qlZo25zYNtR+ycvuZA= +golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.48.0 h1:bbX/i/6MgT9BVLM9RT1thmxL04yeTAhbEz4SyadbXoo= golang.org/x/sys v0.48.0/go.mod h1:hNLxWAXmnKAxqDtdwIYC4bM9oQPEecfsnNMuSxOs3og= +golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U= +golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno= diff --git a/node-tools/internal/meshtest/meshtest.go b/node-tools/internal/meshtest/meshtest.go index f5bc1bf..67b1bc9 100644 --- a/node-tools/internal/meshtest/meshtest.go +++ b/node-tools/internal/meshtest/meshtest.go @@ -1,9 +1,13 @@ // Package meshtest raises what the controller would, for tests against a real bus: the ASSIGNMENTS // and EVENTS streams, memberships issued by hand, and a fixture's path. // -// **The packages share one bus, so run them one at a time: `go test -p 1 ./...`.** Each test raises -// the streams afresh, and the console discovers every runtime that announces itself on the bus -// (novox/hq ADR 0197) — a runtime from another package's test is, correctly, found. +// **Every test has a bus of its own** (novox/hq ADR 0237, the controller's internal/testbus): a server +// linked in at the release go.mod pins — the release the mesh runs, held so by a test beside this one — +// started for the one test and gone after it. The packages shared one bus and had to run one at a time: +// each test raised the streams afresh, and the console discovers every runtime that announces itself on +// the bus (ADR 0197), so a runtime from another package's test was, correctly, found. Now the suite runs +// as Go runs it, in parallel and under the race detector. A person may still point a run at a bus of +// their own with MESH_TEST_NATS_EXTERNAL=1 and MESH_TEST_NATS; then the run is theirs to serialise. package meshtest import ( @@ -12,24 +16,57 @@ import ( "path/filepath" "runtime" "strings" + "sync" "testing" "time" + "github.com/nats-io/nats-server/v2/server" "github.com/nats-io/nats.go" "github.com/novox/mesh-tools/node-tools/internal/bus" ) -// URL is the test bus, or the test is skipped. +// Version is the release of the server the tests run. +const Version = server.VERSION + +// URL is the bus of this test alone: started at its first ask, the same one at every ask after — a test +// that dials twice, or hands the address to the code it tests, reaches one bus — and shut down when the +// test ends. func URL(t *testing.T) string { t.Helper() - url := os.Getenv("MESH_TEST_NATS") - if url == "" { - t.Skip("MESH_TEST_NATS unset") + if os.Getenv("MESH_TEST_NATS_EXTERNAL") == "1" { + if url := os.Getenv("MESH_TEST_NATS"); url != "" { + return url + } + t.Fatal("MESH_TEST_NATS_EXTERNAL=1 and MESH_TEST_NATS names no bus") } + if url, ok := buses.Load(t); ok { + return url.(string) + } + opts := &server.Options{Host: "127.0.0.1", Port: server.RANDOM_PORT, JetStream: true, StoreDir: t.TempDir(), + NoLog: true, NoSigs: true} + s, err := server.NewServer(opts) + if err != nil { + t.Fatalf("a bus for this test could not be made: %v", err) + } + go s.Start() + if !s.ReadyForConnections(30 * time.Second) { + s.Shutdown() + t.Fatal("a bus for this test did not come up within 30s") + } + url := s.ClientURL() + buses.Store(t, url) + t.Cleanup(func() { + buses.Delete(t) + s.Shutdown() + s.WaitForShutdown() + }) return url } +// buses are the running tests' buses, by test. +var buses sync.Map + // Mesh is the controller's job, done by hand. type Mesh struct { nc *nats.Conn diff --git a/node-tools/internal/meshtest/meshtest_test.go b/node-tools/internal/meshtest/meshtest_test.go new file mode 100644 index 0000000..220557e --- /dev/null +++ b/node-tools/internal/meshtest/meshtest_test.go @@ -0,0 +1,75 @@ +package meshtest + +import ( + "encoding/json" + "os" + "path/filepath" + "regexp" + "strings" + "testing" +) + +// **The bus the tests run is the bus the mesh runs** (novox/hq ADR 0227 rule 9, ADR 0237): the server linked +// into the tests is held to the release the catalogue's bus image is — read from beside this checkout, as +// the build seat clones it for a merge check — and, given the facts snapshot, to the release the mesh's bus +// server says it runs. A bus upgrade moves the catalogue's pin; this then fails until go.mod's moves with it. +func TestTheBusTheTestsRunIsTheBusTheMeshRuns(t *testing.T) { + beside := os.Getenv("MESH_CHECK_BESIDE") + if beside == "" { + beside = filepath.Join("..", "..", "..", "..") + } + dockerfile := filepath.Join(beside, "mesh-catalog", "modules", "nats", "Dockerfile") + raw, err := os.ReadFile(dockerfile) + switch { + case err == nil: + pinned := regexp.MustCompile(`upstream: nats ([0-9.]+)-alpine`).FindSubmatch(raw) + if pinned == nil { + t.Fatalf("%s says no release its digest is", dockerfile) + } + if string(pinned[1]) != Version { + t.Errorf("the tests run bus %s and the catalogue's bus image is %s: move go.mod's nats-server pin with the image", + Version, pinned[1]) + } + case os.IsNotExist(err): + t.Logf("the catalogue is not beside this checkout, so its bus image is not compared") + default: + t.Fatal(err) + } + path := os.Getenv("MESH_FACTS") + if path == "" { + t.Logf("no MESH_FACTS: the bus the mesh runs is compared in a merge check, which has it") + return + } + body, err := os.ReadFile(path) + if err != nil { + t.Fatal(err) + } + var f struct { + Taken string `json:"taken"` + Versions struct { + Bus string `json:"bus"` + } `json:"versions"` + } + if err := json.Unmarshal(body, &f); err != nil { + t.Fatal(err) + } + if f.Versions.Bus != "" && strings.TrimPrefix(f.Versions.Bus, "v") != Version { + t.Errorf("the tests run bus %s and the mesh's bus runs %s (facts of %s)", Version, f.Versions.Bus, f.Taken) + } +} + +// Each test is given a bus of its own, and the same one at every ask. +func TestEachTestHasABusOfItsOwn(t *testing.T) { + var first string + t.Run("one", func(t *testing.T) { + first = URL(t) + if URL(t) != first { + t.Fatal("two buses for one test") + } + }) + t.Run("two", func(t *testing.T) { + if URL(t) == first { + t.Fatal("two tests were given one bus") + } + }) +}