Give every test a bus of its own, at the release the mesh runs (hq ADR 0237)
The live tests reached one shared bus and assert, read and remove the mesh's own objects by their fixed names, so packages run in parallel deleted what each other read and the suite passed only one package at a time; a red suite read as noise. internal/testbus starts a server per test, linked in at the nats-server release go.mod pins, and a test holds that pin to the catalogue's bus image and to the facts snapshot's bus when there is one, so the tests never run a bus the mesh does not. The waiter test read a timing (the most connections held at one look) and now reads the state it means (the fewest held across the wait). make check runs the packages in parallel under the race detector, with a timeout.
This commit is contained in:
@@ -0,0 +1,77 @@
|
||||
// Package testbus is the bus a test runs against: a server of its own, of the release the mesh runs,
|
||||
// started for the one test and gone after it.
|
||||
//
|
||||
// **Why one per test.** The live tests reached one shared bus, and on it they assert, read and remove the
|
||||
// mesh's own objects — streams and consumers with fixed names, because those names are the mesh's. Two
|
||||
// packages at once was one deleting a consumer the other was reading through, and a test left a stream
|
||||
// behind for the next to trip on: the suite passed one package at a time and failed or hung run as Go
|
||||
// runs it, which taught everybody to read a red suite as noise. A server per test makes every test start
|
||||
// from nothing and leave nothing, so the suite is the same run in any order, at any parallelism, under
|
||||
// the race detector, on a workstation or on the build seat.
|
||||
//
|
||||
// **Why this release.** The server is linked in at the version go.mod pins, and a test beside this one
|
||||
// holds that pin to the release the mesh runs — the catalogue's bus image, and the facts snapshot's when
|
||||
// a merge check has one (novox/hq ADR 0227 rule 9: a version the mesh runs is the one its tests run).
|
||||
// A bus the 2.10 line skipped messages on (issue 266) would have been caught by the tests that ran on it,
|
||||
// had they run on it.
|
||||
//
|
||||
// A person may still point a run at a bus of their own — another release, a lab's — with
|
||||
// MESH_TEST_NATS_EXTERNAL=1 and MESH_TEST_NATS; then the run is theirs to serialise.
|
||||
package testbus
|
||||
|
||||
import (
|
||||
"os"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/nats-io/nats-server/v2/server"
|
||||
)
|
||||
|
||||
// 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.TB) string {
|
||||
t.Helper()
|
||||
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)
|
||||
}
|
||||
url := Start(t).ClientURL()
|
||||
buses.Store(t, url)
|
||||
t.Cleanup(func() { buses.Delete(t) })
|
||||
return url
|
||||
}
|
||||
|
||||
// buses are the running tests' buses, by test.
|
||||
var buses sync.Map
|
||||
|
||||
// Start is a server with JetStream, for this test alone: on loopback, on a port the system chooses, its
|
||||
// store in the test's own directory.
|
||||
func Start(t testing.TB) *server.Server {
|
||||
t.Helper()
|
||||
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")
|
||||
}
|
||||
t.Cleanup(func() {
|
||||
s.Shutdown()
|
||||
s.WaitForShutdown()
|
||||
})
|
||||
return s
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
package testbus
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"encoding/json"
|
||||
|
||||
"github.com/nats-io/nats.go"
|
||||
)
|
||||
|
||||
// **The bus the tests run is the bus the mesh runs** (novox/hq ADR 0227 rule 9, to-be 45 §9). The server
|
||||
// linked into the tests is held to the release the catalogue's bus image is — read from beside this
|
||||
// checkout — and, in a merge check that has 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 the tests' pin moves with it,
|
||||
// so the controller is never tested against a bus the mesh no longer runs, or not yet.
|
||||
func TestTheBusTheTestsRunIsTheBusTheMeshRuns(t *testing.T) {
|
||||
dockerfile := filepath.Join("..", "..", "..", "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 "+
|
||||
"(and `go mod vendor`) 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)
|
||||
}
|
||||
// Read as little of the snapshot as this needs (internal/facts holds the whole shape).
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
// A test's bus is its own: what one test makes, the next never sees.
|
||||
func TestEveryTestHasABusOfItsOwn(t *testing.T) {
|
||||
seen := map[string]bool{}
|
||||
for i := 0; i < 2; i++ {
|
||||
t.Run("one", func(t *testing.T) {
|
||||
url := URL(t)
|
||||
if seen[url] {
|
||||
t.Fatalf("two tests were handed %s", url)
|
||||
}
|
||||
seen[url] = true
|
||||
conn, err := nats.Connect(url)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer conn.Close()
|
||||
js, err := conn.JetStream()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := js.StreamInfo("EVENTS"); err == nil {
|
||||
t.Fatal("a stream an earlier test made is here")
|
||||
}
|
||||
if _, err := js.AddStream(&nats.StreamConfig{Name: "EVENTS", Subjects: []string{"mesh.>"}}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
})
|
||||
}
|
||||
// Asked twice, one test reaches one bus.
|
||||
if a, b := URL(t), URL(t); a != b {
|
||||
t.Errorf("one test was handed two buses: %s and %s", a, b)
|
||||
}
|
||||
if v := strings.Count(Version, "."); v != 2 {
|
||||
t.Errorf("the server's release reads %q", Version)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user