The seat runs the running controller's besideRefs, which clones no mesh-sdk until this change has rolled out, so a missing clone failing the test kept this change from ever passing its own check. A follow-up makes it a failure again after the rollout.
279 lines
11 KiB
Go
279 lines
11 KiB
Go
package link
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"fmt"
|
|
"os"
|
|
"os/exec"
|
|
"path"
|
|
"path/filepath"
|
|
"regexp"
|
|
"strings"
|
|
"sync"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/novox/mesh-controller/internal/beside"
|
|
)
|
|
|
|
// The Go implementation, held to the shared fixtures (novox/hq ADR 0074, design 19).
|
|
//
|
|
// **Read from the SDK's own conformance directory, never copied by hand**: a fixture written into each
|
|
// implementation is two fixtures, and two fixtures drift, which is the failure the suite exists to prevent.
|
|
// Which SDK is read is the one this tree's go.mod pins, never the one a desktop happens to hold beside this
|
|
// checkout (novox/hq issue 449):
|
|
//
|
|
// - in a merge check, the fixture as it stands at that pin in the mesh-sdk clone the build seat places
|
|
// beside the check — read with `git show`, because the clone is checked out at the *running*
|
|
// controller's pin, and a pull request moving the SDK must be judged against the SDK it moves to, or it
|
|
// passes the check and fails everywhere after it rolls out. A pin the clone lacks fails the test; a
|
|
// missing clone reads the captured copy and says so loudly, until mesh-controller #220 has rolled out
|
|
// and the build seat clones mesh-sdk (then its follow-up makes a missing clone fail again);
|
|
// - elsewhere, the copy captured in testdata/beside at the commit testdata/beside/CAPTURED names, held to
|
|
// go.mod by TestTheCapturedSDKIsTheOneGoModPins.
|
|
//
|
|
// In a merge check the captured copy is also compared with the clone at the captured commit, byte for byte,
|
|
// so a hand-edited copy cannot pass for the SDK's.
|
|
type fixture struct {
|
|
Name string `json:"name"`
|
|
Given struct {
|
|
Module string `json:"module"`
|
|
Node string `json:"node"`
|
|
Key string `json:"key"`
|
|
Body map[string]any `json:"body"`
|
|
Headers map[string]string `json:"headers"`
|
|
} `json:"given"`
|
|
Wire struct {
|
|
Subject string `json:"subject"`
|
|
RequiredHeaders []string `json:"requiredHeaders"`
|
|
HeaderFormats map[string]string `json:"headerFormats"`
|
|
} `json:"wire"`
|
|
}
|
|
|
|
// sdkModule is the Go module of the SDK the controller is built against.
|
|
const sdkModule = "git.novox.be/novox/mesh-sdk/go"
|
|
|
|
// The tree's go.mod, and the captured copy, as this package finds them.
|
|
var (
|
|
goModFile = filepath.Join("..", "..", "go.mod")
|
|
capturedSDK = filepath.Join(beside.Captured(), "mesh-sdk")
|
|
capturedLog = filepath.Join(beside.Captured(), "CAPTURED")
|
|
)
|
|
|
|
func loadFixture(t *testing.T, name string) fixture {
|
|
t.Helper()
|
|
raw, from, err := sdkFixture(os.Getenv(beside.Env), goModFile, capturedSDK, capturedLog, name)
|
|
if err != nil {
|
|
// Failed, never skipped: a skip here passed the suite with nothing judged.
|
|
t.Fatal(err)
|
|
}
|
|
t.Logf("%s: %s", name, from)
|
|
if strings.HasPrefix(from, notJudged) {
|
|
// Said on the check's own output too, where `go test` without -v prints no log of a passing test.
|
|
saidNotJudged.Do(func() { fmt.Fprintln(os.Stderr, "internal/link conformance: "+from) })
|
|
}
|
|
var f fixture
|
|
if err := json.Unmarshal(raw, &f); err != nil {
|
|
t.Fatalf("%s: %v", name, err)
|
|
}
|
|
return f
|
|
}
|
|
|
|
// notJudged opens what a merge check without the SDK's clone says; saidNotJudged says it once on stderr.
|
|
const notJudged = "NOT JUDGED AGAINST THE CLONE"
|
|
|
|
var saidNotJudged sync.Once
|
|
|
|
// sdkFixture is one conformance fixture and where it was read: from the mesh-sdk clone in root (a merge
|
|
// check's MESH_CHECK_BESIDE) at the pin of goMod, or, with no root, from the captured copy.
|
|
func sdkFixture(root, goMod, captured, capturedLog, name string) ([]byte, string, error) {
|
|
file := path.Join("conformance", name)
|
|
readCaptured := func() ([]byte, error) {
|
|
raw, err := os.ReadFile(filepath.Join(captured, filepath.FromSlash(file)))
|
|
if err != nil {
|
|
return nil, fmt.Errorf("the captured SDK's %s: %w", file, err)
|
|
}
|
|
return raw, nil
|
|
}
|
|
if root == "" {
|
|
raw, err := readCaptured()
|
|
return raw, "the copy captured in testdata/beside (see CAPTURED); a merge check reads the SDK's clone " +
|
|
"at this tree's pin", err
|
|
}
|
|
clone := filepath.Join(root, "mesh-sdk")
|
|
if _, err := os.Stat(clone); err != nil {
|
|
// A build seat asked by a controller from before mesh-controller #220 places no mesh-sdk beside the
|
|
// check, so #220 could never pass its own check if this failed. Until #220 has rolled out, the
|
|
// captured copy is read and the log says so loudly; the follow-up of novox/hq issue 449 makes this
|
|
// a failure again.
|
|
at, cerr := capturedCommit(capturedLog)
|
|
if cerr != nil {
|
|
return nil, "", cerr
|
|
}
|
|
raw, rerr := readCaptured()
|
|
return raw, fmt.Sprintf(notJudged+": the seat placed no mesh-sdk beside this check in "+
|
|
"%s=%s (it does once mesh-controller #220 has rolled out); read the captured copy at %s", beside.Env,
|
|
root, at), rerr
|
|
}
|
|
ref, err := sdkRef(goMod)
|
|
if err != nil {
|
|
return nil, "", err
|
|
}
|
|
raw, err := gitShow(clone, ref, file)
|
|
if err != nil {
|
|
return nil, "", fmt.Errorf("the mesh-sdk clone beside this check has no %s at %s, the SDK this tree's "+
|
|
"go.mod pins: %w", file, ref, err)
|
|
}
|
|
// The captured copy is the SDK's own, never edited by hand: the clone at the captured commit says so.
|
|
at, err := capturedCommit(capturedLog)
|
|
if err != nil {
|
|
return nil, "", err
|
|
}
|
|
theirs, err := gitShow(clone, at, file)
|
|
if err != nil {
|
|
return nil, "", fmt.Errorf("the mesh-sdk clone has no %s at %s, the commit CAPTURED names: %w", file, at, err)
|
|
}
|
|
ours, err := os.ReadFile(filepath.Join(captured, filepath.FromSlash(file)))
|
|
if err != nil {
|
|
return nil, "", fmt.Errorf("the captured SDK's %s: %w", file, err)
|
|
}
|
|
if !bytes.Equal(ours, theirs) {
|
|
return nil, "", fmt.Errorf("the captured %s is not mesh-sdk's at %s, the commit CAPTURED names: it was "+
|
|
"edited or captured wrong; capture it again as CAPTURED says", file, at)
|
|
}
|
|
return raw, "mesh-sdk cloned beside this check, at " + ref + " (this tree's go.mod)", nil
|
|
}
|
|
|
|
// gitShow is one file of a repository at a ref. safe.directory, because the clone is the build seat's and
|
|
// the test may run as another user.
|
|
func gitShow(repository, ref, file string) ([]byte, error) {
|
|
cmd := exec.Command("git", "-c", "safe.directory=*", "-C", repository, "show", ref+":"+file)
|
|
var stderr bytes.Buffer
|
|
cmd.Stderr = &stderr
|
|
out, err := cmd.Output()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("git show %s:%s: %v: %s", ref, file, err, strings.TrimSpace(stderr.String()))
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
// pseudoCommit is the commit a Go pseudo-version names: v0.1.11-0.20261009143344-f047d0a4a970 → f047d0a4a970.
|
|
var pseudoCommit = regexp.MustCompile(`-([0-9a-f]{12})$`)
|
|
|
|
// sdkRef is the SDK repository's ref a go.mod pins: a pseudo-version's commit, or a release's tag (the SDK
|
|
// tags its Go module under go/).
|
|
func sdkRef(goMod string) (string, error) {
|
|
raw, err := os.ReadFile(goMod)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
version := ""
|
|
for _, line := range strings.Split(string(raw), "\n") {
|
|
fields := strings.Fields(strings.TrimPrefix(strings.TrimSpace(line), "require "))
|
|
if len(fields) >= 2 && fields[0] == sdkModule {
|
|
version = fields[1]
|
|
}
|
|
}
|
|
if m := pseudoCommit.FindStringSubmatch(version); m != nil {
|
|
return m[1], nil
|
|
}
|
|
if strings.HasPrefix(version, "v") {
|
|
return "go/" + version, nil
|
|
}
|
|
return "", fmt.Errorf("%s pins no version of %s that names a commit or a tag", goMod, sdkModule)
|
|
}
|
|
|
|
// capturedCommit is the commit testdata/beside/CAPTURED names for mesh-sdk.
|
|
func capturedCommit(capturedLog string) (string, error) {
|
|
raw, err := os.ReadFile(capturedLog)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
at := regexp.MustCompile(`(?m)^mesh-sdk\s+([0-9a-f]{40})\s`).FindSubmatch(raw)
|
|
if at == nil {
|
|
return "", fmt.Errorf("%s names no commit for mesh-sdk", capturedLog)
|
|
}
|
|
return string(at[1]), nil
|
|
}
|
|
|
|
// The captured SDK is the one this controller is built against: when go.mod moves the SDK, the copy moves
|
|
// with it, or the tests away from a merge check judge an SDK the controller no longer uses (novox/hq issue
|
|
// 449).
|
|
func TestTheCapturedSDKIsTheOneGoModPins(t *testing.T) {
|
|
pinned, err := sdkRef(goModFile)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
at, err := capturedCommit(capturedLog)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if strings.HasPrefix(pinned, "go/") || !strings.HasPrefix(at, pinned) {
|
|
t.Errorf("the SDK is captured at %s but go.mod pins %s: capture it again at the pinned commit, as "+
|
|
"testdata/beside/CAPTURED says", at, pinned)
|
|
}
|
|
}
|
|
|
|
// Every header the fixture requires is one this implementation actually sets.
|
|
func TestTheGoEmitterSetsEveryRequiredHeader(t *testing.T) {
|
|
f := loadFixture(t, "events/module-event.json")
|
|
sent := goEventHeaders(f.Given.Key, f.Given.Module, f.Given.Node)
|
|
for _, want := range f.Wire.RequiredHeaders {
|
|
if _, ok := sent[want]; !ok {
|
|
t.Errorf("the Go emitter does not set %q, which the fixture requires — an event it "+
|
|
"emits is one a conforming consumer refuses", want)
|
|
}
|
|
}
|
|
}
|
|
|
|
// And each value is in the shape the fixture pins, because a header present but differently
|
|
// formatted is the disagreement that does not announce itself.
|
|
func TestTheGoEmittersHeaderFormatsMatch(t *testing.T) {
|
|
f := loadFixture(t, "events/module-event.json")
|
|
sent := goEventHeaders(f.Given.Key, f.Given.Module, f.Given.Node)
|
|
|
|
if got := sent["content-type"]; got != f.Wire.HeaderFormats["content-type"] {
|
|
t.Errorf("content-type is %q, the fixture says %q", got, f.Wire.HeaderFormats["content-type"])
|
|
}
|
|
if _, err := time.Parse(time.RFC3339, sent["x-time"]); err != nil {
|
|
t.Errorf("x-time %q is not RFC3339, which the fixture requires: %v", sent["x-time"], err)
|
|
}
|
|
if pattern := f.Wire.HeaderFormats["x-event-id"]; pattern != "" {
|
|
if !regexp.MustCompile(pattern).MatchString(sent["x-event-id"]) {
|
|
t.Errorf("x-event-id %q does not match %q", sent["x-event-id"], pattern)
|
|
}
|
|
}
|
|
// The origin the envelope claims is the one the bus enforces by namespace. A disagreement
|
|
// here means the envelope is lying about where it came from.
|
|
if sent["x-source"] != f.Given.Module {
|
|
t.Errorf("x-source is %q for module %q", sent["x-source"], f.Given.Module)
|
|
}
|
|
}
|
|
|
|
// The subject a module's event lands on is derived, not carried — so this implementation must
|
|
// derive the same one the fixture names.
|
|
func TestTheGoSubjectMatchesTheFixture(t *testing.T) {
|
|
f := loadFixture(t, "events/module-event.json")
|
|
got := "mesh.mod." + f.Given.Module + ".event." + f.Given.Key
|
|
if got != f.Wire.Subject {
|
|
t.Errorf("this implementation would publish on %q; the fixture says %q", got, f.Wire.Subject)
|
|
}
|
|
}
|
|
|
|
// goEventHeaders is the header set EmitEvent produces, factored so conformance can see it
|
|
// without a broker. Kept beside the emitter so the two cannot drift apart silently.
|
|
func goEventHeaders(eventType, source, node string) map[string]string {
|
|
id, err := eventID()
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
return map[string]string{
|
|
"x-event-id": id,
|
|
"x-source": source,
|
|
"x-node": node,
|
|
"x-time": time.Now().UTC().Format(time.RFC3339),
|
|
"content-type": "application/json",
|
|
}
|
|
}
|