Merge pull request 'Phase 5 (3/4): every test on a bus of its own, at the release the mesh runs (hq ADR 0237)' (#97) from feat/a-suite-that-cannot-flake into main
mesh/delivery delivered
mesh/delivery delivered
This commit was merged in pull request #97.
This commit is contained in:
@@ -2,24 +2,21 @@ package broker
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/nats-io/nats.go"
|
||||
|
||||
"github.com/novox/mesh-controller/internal/testbus"
|
||||
)
|
||||
|
||||
// A consumer that reacts to announcements, made on a stream that keeps a week of them, starts from now:
|
||||
// made from the start it replays every merge and build of that week (novox/hq issue 248). And a reset
|
||||
// re-makes a stuck one from now, its configuration otherwise kept, and refuses a work queue.
|
||||
//
|
||||
// docker run -d --rm --name t -p 14231:4222 nats:2.10-alpine -js
|
||||
// MESH_TEST_NATS=nats://127.0.0.1:14231 go test ./internal/broker/ -run TestAConsumerMadeFromNow
|
||||
// go test ./internal/broker/ -run TestAConsumerMadeFromNow (each test on a bus of its own: internal/testbus)
|
||||
func TestAConsumerMadeFromNowNeverReplaysTheStreamsHistory(t *testing.T) {
|
||||
url := os.Getenv("MESH_TEST_NATS")
|
||||
if url == "" {
|
||||
t.Skip("MESH_TEST_NATS unset")
|
||||
}
|
||||
url := testbus.URL(t)
|
||||
js, err := Dial(url)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
|
||||
@@ -2,12 +2,13 @@ package broker
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/nats-io/nats.go"
|
||||
|
||||
"github.com/novox/mesh-controller/internal/testbus"
|
||||
)
|
||||
|
||||
const twoSeconds = 2 * time.Second
|
||||
@@ -17,13 +18,9 @@ const twoSeconds = 2 * time.Second
|
||||
// so bringing one to match must replace it — and must not replay what it already acknowledged
|
||||
// (novox/hq 04-ISSUES/156).
|
||||
//
|
||||
// docker run -d --rm --name t -p 14231:4222 nats:2.10-alpine -js
|
||||
// MESH_TEST_NATS=nats://127.0.0.1:14231 go test ./internal/broker/ -run TestUpgrading
|
||||
// go test ./internal/broker/ -run TestUpgrading (each test on a bus of its own: internal/testbus)
|
||||
func TestUpgradingAConsumerWhoseDeliverySubjectMoved(t *testing.T) {
|
||||
url := os.Getenv("MESH_TEST_NATS")
|
||||
if url == "" {
|
||||
t.Skip("MESH_TEST_NATS unset")
|
||||
}
|
||||
url := testbus.URL(t)
|
||||
js, err := Dial(url)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
@@ -133,10 +130,7 @@ func TestUpgradingAConsumerWhoseDeliverySubjectMoved(t *testing.T) {
|
||||
// not undo. The controller's own two consumers are in exactly this position: it asserts them before
|
||||
// it subscribes.
|
||||
func TestAConsumerNothingIsBoundToDoesMove(t *testing.T) {
|
||||
url := os.Getenv("MESH_TEST_NATS")
|
||||
if url == "" {
|
||||
t.Skip("MESH_TEST_NATS unset")
|
||||
}
|
||||
url := testbus.URL(t)
|
||||
js, err := Dial(url)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
package broker
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/novox/mesh-controller/internal/testbus"
|
||||
)
|
||||
|
||||
// Against a real server, because the questions here are all "does the server accept this" —
|
||||
@@ -10,13 +11,9 @@ import (
|
||||
//
|
||||
// Skipped unless MESH_TEST_NATS names one, so the ordinary suite stays fast and offline:
|
||||
//
|
||||
// docker run -d --rm --name t -p 14222:4222 nats:2.10-alpine -js
|
||||
// MESH_TEST_NATS=nats://127.0.0.1:14222 go test ./internal/broker/ -run TestAgainstARealServer
|
||||
// go test ./internal/broker/ -run TestAgainstARealServer (each test on a bus of its own: internal/testbus)
|
||||
func TestAgainstARealServer(t *testing.T) {
|
||||
url := os.Getenv("MESH_TEST_NATS")
|
||||
if url == "" {
|
||||
t.Skip("MESH_TEST_NATS unset")
|
||||
}
|
||||
url := testbus.URL(t)
|
||||
js, err := Dial(url)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
package broker
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/nats-io/nats.go"
|
||||
|
||||
"github.com/novox/mesh-controller/internal/testbus"
|
||||
)
|
||||
|
||||
// Raising the bus's objects against a real server.
|
||||
@@ -14,15 +15,11 @@ import (
|
||||
// could answer: that asserting twice changes nothing, and that a consumer really is bound to the one
|
||||
// subject its node is allowed to read.
|
||||
//
|
||||
// docker run -d --rm --name t -p 14227:4222 nats:2.10-alpine -js
|
||||
// MESH_TEST_NATS=nats://127.0.0.1:14227 go test ./internal/broker/ -run TestRaising
|
||||
// go test ./internal/broker/ -run TestRaising (each test on a bus of its own: internal/testbus)
|
||||
|
||||
func aLiveBus(t *testing.T) *JetStream {
|
||||
t.Helper()
|
||||
url := os.Getenv("MESH_TEST_NATS")
|
||||
if url == "" {
|
||||
t.Skip("MESH_TEST_NATS unset")
|
||||
}
|
||||
url := testbus.URL(t)
|
||||
js, err := Dial(url)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
package broker
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/nats-io/nats.go"
|
||||
|
||||
"github.com/novox/mesh-controller/internal/testbus"
|
||||
)
|
||||
|
||||
// A seat's worker that changed from push to pull delivery strands a holder built for the new shape
|
||||
@@ -14,13 +15,9 @@ import (
|
||||
// shape, type included: on a work queue it re-makes one of the wrong type, losing nothing, and a
|
||||
// pull subscription then binds and takes what was pending.
|
||||
//
|
||||
// docker run -d --rm --name t -p 14231:4222 nats:2.10-alpine -js
|
||||
// MESH_TEST_NATS=nats://127.0.0.1:14231 go test ./internal/broker/ -run TestAWorker
|
||||
// go test ./internal/broker/ -run TestAWorker (each test on a bus of its own: internal/testbus)
|
||||
func TestAWorkerOfTheWrongTypeIsRemadeOnAWorkQueueAndAPullThenBinds(t *testing.T) {
|
||||
url := os.Getenv("MESH_TEST_NATS")
|
||||
if url == "" {
|
||||
t.Skip("MESH_TEST_NATS unset")
|
||||
}
|
||||
url := testbus.URL(t)
|
||||
js, err := Dial(url)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
@@ -112,10 +109,7 @@ func TestAWorkerOfTheWrongTypeIsRemadeOnAWorkQueueAndAPullThenBinds(t *testing.T
|
||||
// re-making it from the start would replay what it acknowledged (novox/hq issue 156), and leaving it
|
||||
// for a hand re-made it exactly that way on 2026-10-03 (issue 207).
|
||||
func TestAWorkerOfTheWrongTypeOnAHistoryStreamIsRemadeFromNowOn(t *testing.T) {
|
||||
url := os.Getenv("MESH_TEST_NATS")
|
||||
if url == "" {
|
||||
t.Skip("MESH_TEST_NATS unset")
|
||||
}
|
||||
url := testbus.URL(t)
|
||||
js, err := Dial(url)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
|
||||
@@ -2,13 +2,14 @@ package conditions
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/nats-io/nats.go/jetstream"
|
||||
|
||||
"github.com/novox/mesh-controller/internal/broker"
|
||||
|
||||
"github.com/novox/mesh-controller/internal/testbus"
|
||||
)
|
||||
|
||||
// The condition store against a real server: compare-and-set, an unreadable store refused, and the
|
||||
@@ -16,10 +17,7 @@ import (
|
||||
|
||||
func busStoreForTest(t *testing.T) (*broker.JetStream, Backend, History) {
|
||||
t.Helper()
|
||||
url := os.Getenv("MESH_TEST_NATS")
|
||||
if url == "" {
|
||||
t.Skip("MESH_TEST_NATS unset")
|
||||
}
|
||||
url := testbus.URL(t)
|
||||
js, err := broker.Dial(url)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
|
||||
@@ -69,16 +69,19 @@ func TestAWaiterPinsNoConnectionAndGivesUp(t *testing.T) {
|
||||
done <- err
|
||||
}()
|
||||
}
|
||||
// While they wait, the pool lends nothing to them for longer than a look.
|
||||
pinned := 0
|
||||
for range 10 {
|
||||
time.Sleep(60 * time.Millisecond)
|
||||
if n := int(pool.Stat().AcquiredConns() - base); n > pinned {
|
||||
pinned = n
|
||||
// While they wait, the pool lends nothing to them for longer than a look: **pinned is held throughout**,
|
||||
// so at some moment of the wait fewer than all of them hold one. Asked of the least seen over many
|
||||
// looks, not of the most: a look that lands while every waiter is mid-query is a busy machine, not a
|
||||
// pinned connection, and on a loaded one (the suite's packages at once, the race detector) it did land.
|
||||
least := waiters
|
||||
for range 100 {
|
||||
time.Sleep(10 * time.Millisecond)
|
||||
if n := int(pool.Stat().AcquiredConns() - base); n < least {
|
||||
least = n
|
||||
}
|
||||
}
|
||||
if pinned >= waiters {
|
||||
t.Fatalf("%d connections were held by %d waiters", pinned, waiters)
|
||||
if least >= waiters {
|
||||
t.Fatalf("%d waiters held %d connections at every look across their wait", waiters, least)
|
||||
}
|
||||
for range waiters {
|
||||
if err := <-done; !errors.Is(err, ErrNodeBusy) || !strings.Contains(err.Error(), "anchor") {
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
package inventory
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// **The store the tests run is the store the mesh runs** (novox/hq ADR 0227 rule 9, ADR 0237). In a merge
|
||||
// check the build seat raises the store of the major release the facts snapshot says the mesh's store
|
||||
// runs; this holds the two equal there, so a test passing here is a test passing on that release. Without
|
||||
// a snapshot — on a workstation — it says what it ran against and compares nothing.
|
||||
func TestTheStoreTheTestsRunIsTheStoreTheMeshRuns(t *testing.T) {
|
||||
inv := ForTest(t)
|
||||
ran, err := inv.ServerVersionForTest(t)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
path := os.Getenv("MESH_FACTS")
|
||||
if path == "" {
|
||||
t.Logf("the tests ran the store at %s; with no MESH_FACTS, nothing to compare it with", ran)
|
||||
return
|
||||
}
|
||||
body, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var f struct {
|
||||
Versions struct {
|
||||
Store string `json:"store"`
|
||||
} `json:"versions"`
|
||||
}
|
||||
if err := json.Unmarshal(body, &f); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
major := func(v string) string { m, _, _ := strings.Cut(strings.TrimSpace(v), "."); return m }
|
||||
if f.Versions.Store != "" && major(ran) != major(f.Versions.Store) {
|
||||
t.Errorf("the tests ran the store at %s and the mesh's store runs %s", ran, f.Versions.Store)
|
||||
}
|
||||
}
|
||||
|
||||
// ServerVersionForTest is the store's own word for its release.
|
||||
func (i *Inventory) ServerVersionForTest(t *testing.T) (string, error) {
|
||||
t.Helper()
|
||||
var v string
|
||||
err := i.store.Pool().QueryRow(t.Context(), `show server_version`).Scan(&v)
|
||||
return v, err
|
||||
}
|
||||
@@ -4,13 +4,14 @@ import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/nats-io/nats.go"
|
||||
"github.com/nats-io/nats.go/jetstream"
|
||||
|
||||
"github.com/novox/mesh-controller/internal/testbus"
|
||||
)
|
||||
|
||||
// Two controllers at once, on a real bus (novox/hq to-be 45 §6, replay R1's half that lives here):
|
||||
@@ -18,7 +19,7 @@ import (
|
||||
// stops renewing is taken over once its key expires, and has stopped acting before then; a renewal
|
||||
// refused is a loss at once; and a bucket raised again from nothing never issues an epoch twice.
|
||||
//
|
||||
// MESH_TEST_NATS=nats://127.0.0.1:14222 go test ./internal/lease/
|
||||
// go test ./internal/lease/ (each test on a bus of its own: internal/testbus)
|
||||
|
||||
// The bounds in these tests are the design's divided by five, so the cases run in seconds.
|
||||
const (
|
||||
@@ -30,10 +31,7 @@ const (
|
||||
// aBucket is a fresh lease bucket on the test bus, with the test's age.
|
||||
func aBucket(t *testing.T) (jetstream.JetStream, string) {
|
||||
t.Helper()
|
||||
url := os.Getenv("MESH_TEST_NATS")
|
||||
if url == "" {
|
||||
t.Skip("MESH_TEST_NATS unset")
|
||||
}
|
||||
url := testbus.URL(t)
|
||||
conn, err := nats.Connect(url)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
@@ -55,7 +53,7 @@ func aBucket(t *testing.T) (jetstream.JetStream, string) {
|
||||
// aController is one controller instance's lease over the bucket, on a connection of its own.
|
||||
func aController(t *testing.T, bucket, name string, floor func(context.Context) (uint64, error)) *Lease {
|
||||
t.Helper()
|
||||
conn, err := nats.Connect(os.Getenv("MESH_TEST_NATS"))
|
||||
conn, err := nats.Connect(testbus.URL(t))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -275,10 +273,7 @@ func TestAnEpochIsNeverIssuedTwiceWhenTheBucketStartsOver(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestALeaseBucketWithoutAnAgeIsRefused(t *testing.T) {
|
||||
url := os.Getenv("MESH_TEST_NATS")
|
||||
if url == "" {
|
||||
t.Skip("MESH_TEST_NATS unset")
|
||||
}
|
||||
url := testbus.URL(t)
|
||||
conn, err := nats.Connect(url)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
|
||||
@@ -8,6 +8,8 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/nats-io/nats.go"
|
||||
|
||||
"github.com/novox/mesh-controller/internal/testbus"
|
||||
)
|
||||
|
||||
// **A reader's own consumer gone is not a consumer lost**: every bucket watch and stream read-back
|
||||
@@ -64,10 +66,7 @@ func TestNatsARefusedQuestionIsSaidAtOnce(t *testing.T) {
|
||||
|
||||
// The refusal reaches whoever waits on that subject, and only them.
|
||||
func TestNatsARefusalReachesItsWaiter(t *testing.T) {
|
||||
url := os.Getenv("MESH_TEST_NATS")
|
||||
if url == "" {
|
||||
t.Skip("MESH_TEST_NATS unset")
|
||||
}
|
||||
url := testbus.URL(t)
|
||||
conn, err := nats.Connect(url)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
|
||||
@@ -5,7 +5,6 @@ import (
|
||||
"encoding/json"
|
||||
"io"
|
||||
"log"
|
||||
"os"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
@@ -13,6 +12,8 @@ import (
|
||||
"github.com/nats-io/nats.go"
|
||||
|
||||
"github.com/novox/mesh-controller/internal/broker"
|
||||
|
||||
"github.com/novox/mesh-controller/internal/testbus"
|
||||
)
|
||||
|
||||
// A build, end to end, against a real server.
|
||||
@@ -22,15 +23,11 @@ import (
|
||||
// catalogue places it. On the old bus that fan-out came from a shared exchange, and it would be easy
|
||||
// to write a version where only the asker hears it and nobody notices for weeks.
|
||||
//
|
||||
// docker run -d --rm --name t -p 14230:4222 nats:2.10-alpine -js
|
||||
// MESH_TEST_NATS=nats://127.0.0.1:14230 go test ./internal/link/ -run TestNatsABuild
|
||||
// go test ./internal/link/ -run TestNatsABuild (each test on a bus of its own: internal/testbus)
|
||||
|
||||
func aBusWithTheBuildRole(t *testing.T) *broker.JetStream {
|
||||
t.Helper()
|
||||
url := os.Getenv("MESH_TEST_NATS")
|
||||
if url == "" {
|
||||
t.Skip("MESH_TEST_NATS unset")
|
||||
}
|
||||
url := testbus.URL(t)
|
||||
js, err := broker.Dial(url)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
@@ -349,7 +346,7 @@ func TestNatsAMachineOnTheRetiredBuildRoleTakesThatRolesAsks(t *testing.T) {
|
||||
})
|
||||
}()
|
||||
|
||||
asker, err := BuildsOverNATSOn(os.Getenv("MESH_TEST_NATS"), TheBuildMachineBefore)
|
||||
asker, err := BuildsOverNATSOn(testbus.URL(t), TheBuildMachineBefore)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
@@ -3,12 +3,13 @@ package link
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/nats-io/nats.go"
|
||||
|
||||
"github.com/novox/mesh-controller/internal/testbus"
|
||||
)
|
||||
|
||||
// **Both implementations, one fixture.** The point of the seam is not that the transport can be
|
||||
@@ -17,13 +18,9 @@ import (
|
||||
//
|
||||
// Against a real server, because what the fixture pins is what reaches the wire:
|
||||
//
|
||||
// docker run -d --rm --name t -p 14222:4222 nats:2.10-alpine -js
|
||||
// MESH_TEST_NATS=nats://127.0.0.1:14222 go test ./internal/link/ -run TestTheNatsBus
|
||||
// go test ./internal/link/ -run TestTheNatsBus (each test on a bus of its own: internal/testbus)
|
||||
func TestTheNatsBusEmitsTheEnvelopeTheFixturePins(t *testing.T) {
|
||||
url := os.Getenv("MESH_TEST_NATS")
|
||||
if url == "" {
|
||||
t.Skip("MESH_TEST_NATS unset")
|
||||
}
|
||||
url := testbus.URL(t)
|
||||
f := loadFixture(t, "events/module-event.json")
|
||||
|
||||
conn, err := nats.Connect(url)
|
||||
@@ -99,10 +96,7 @@ func TestAnEventsSubjectIsDerivedFromItsSource(t *testing.T) {
|
||||
// tool is slow" is the first thing a person asking wants, and waiting out the timeout to say it
|
||||
// is how a fast answer becomes a slow non-answer.
|
||||
func TestAskingAToolNobodyServesFailsAtOnce(t *testing.T) {
|
||||
url := os.Getenv("MESH_TEST_NATS")
|
||||
if url == "" {
|
||||
t.Skip("MESH_TEST_NATS unset")
|
||||
}
|
||||
url := testbus.URL(t)
|
||||
conn, err := nats.Connect(url)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
@@ -125,10 +119,7 @@ func TestAskingAToolNobodyServesFailsAtOnce(t *testing.T) {
|
||||
|
||||
// And a served tool answers, with no reply queue to declare and no correlation to check.
|
||||
func TestAskingAServedToolAnswers(t *testing.T) {
|
||||
url := os.Getenv("MESH_TEST_NATS")
|
||||
if url == "" {
|
||||
t.Skip("MESH_TEST_NATS unset")
|
||||
}
|
||||
url := testbus.URL(t)
|
||||
conn, err := nats.Connect(url)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
package link
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/nats-io/nats.go"
|
||||
|
||||
"github.com/novox/mesh-controller/internal/testbus"
|
||||
)
|
||||
|
||||
// **Verified 2026-09-27**: the caller asked for a reply to `_INBOX.LCr3M83q…` and the consumer
|
||||
@@ -18,13 +19,9 @@ import (
|
||||
// enrolment design rests on it — the reply subject travels in the payload instead — so it is
|
||||
// checked rather than believed.
|
||||
//
|
||||
// docker run -d --rm --name t -p 14222:4222 nats:2.10-alpine -js
|
||||
// MESH_TEST_NATS=nats://127.0.0.1:14222 go test ./internal/link/ -run TestAReplyAddress
|
||||
// go test ./internal/link/ -run TestAReplyAddress (each test on a bus of its own: internal/testbus)
|
||||
func TestAReplyAddressDoesNotSurviveAStream(t *testing.T) {
|
||||
url := os.Getenv("MESH_TEST_NATS")
|
||||
if url == "" {
|
||||
t.Skip("MESH_TEST_NATS unset")
|
||||
}
|
||||
url := testbus.URL(t)
|
||||
conn, err := nats.Connect(url)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
|
||||
@@ -6,7 +6,6 @@ import (
|
||||
"errors"
|
||||
"io"
|
||||
"log"
|
||||
"os"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
@@ -14,6 +13,8 @@ import (
|
||||
"github.com/nats-io/nats.go"
|
||||
|
||||
"github.com/novox/mesh-controller/internal/broker"
|
||||
|
||||
"github.com/novox/mesh-controller/internal/testbus"
|
||||
)
|
||||
|
||||
// The consume side against a real server, because what is being checked is what the server does.
|
||||
@@ -23,15 +24,11 @@ import (
|
||||
// to an address carried in the payload reaches a caller waiting on its own inbox. Each is a claim
|
||||
// about a server, so each is asked of one:
|
||||
//
|
||||
// docker run -d --rm --name t -p 14222:4222 nats:2.10-alpine -js
|
||||
// MESH_TEST_NATS=nats://127.0.0.1:14222 go test ./internal/link/ -run TestNats
|
||||
// go test ./internal/link/ -run TestNats (each test on a bus of its own: internal/testbus)
|
||||
|
||||
func aBus(t *testing.T) *broker.JetStream {
|
||||
t.Helper()
|
||||
url := os.Getenv("MESH_TEST_NATS")
|
||||
if url == "" {
|
||||
t.Skip("MESH_TEST_NATS unset")
|
||||
}
|
||||
url := testbus.URL(t)
|
||||
js, err := broker.Dial(url)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
|
||||
@@ -3,11 +3,12 @@ package link
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/novox/mesh-controller/internal/broker"
|
||||
|
||||
"github.com/novox/mesh-controller/internal/testbus"
|
||||
)
|
||||
|
||||
// novox/hq issue 213: while a machine hands its controller over, the new controller (the process)
|
||||
@@ -29,7 +30,7 @@ func TestNatsASecondControllerStandsByAndTakesOverWhenTheFirstLetsGo(t *testing.
|
||||
})
|
||||
|
||||
// The new one, on a connection of its own as the process would have.
|
||||
second, err := broker.Dial(os.Getenv("MESH_TEST_NATS"))
|
||||
second, err := broker.Dial(testbus.URL(t))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
@@ -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