mesh/merge-gate head c79591531575 merged into trunk aa02e7bf4e78 — pass: builds build-agent, mesh-controller → ace, g14, novox, shanks; no bus step; every …
mesh/repo-check head c79591531575 merged into trunk aa02e7bf4e78 — pass: its merge-check.sh passed
mesh/delivery delivered
Milestone 5 moved 8,599 build records onto the bus under bare build ids, each carrying its module's whole manifest. Every build lookup then read the whole bucket, and a delivery plan's send did that per module per machine while it held the plans and the machine's declaration: the send of build-agent never finished, and every other act and the status waited behind it. Builds now live under <module>.build.<id> (requests under req.<id>, id.<id> names a build's module), with pointer keys newest, worked, held, commit.<sha> and source.<reuse key> that move only to a newer build and are read as hints, repaired when wrong. Manifests are kept once in the same bucket under manifest.sha256-<hex>, so no grant the running bus lacks is needed. The old keys are laid out once at start, idempotently, and left unread. The calls a plan look makes on the silent clocks while it holds the plans are bounded, since the vendored kv.Keys ignores its context.
259 lines
8.8 KiB
Go
259 lines
8.8 KiB
Go
//go:build integration
|
|
|
|
package inventory
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"reflect"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
// A build result was answered to whoever asked and kept nowhere, so "when did this last build",
|
|
// "why did it fail" and "which machine built what is running" had no answer at all.
|
|
|
|
func aBuild(id, module, failed string) Build {
|
|
b := Build{
|
|
ID: id, Repository: "https://forge.invalid/" + strings.TrimSuffix(module, "?") + ".git",
|
|
Module: module, On: "a-build-machine", Failed: failed,
|
|
}
|
|
if failed == "" {
|
|
b.Commit = "c0ffee" + id
|
|
b.Made = []Artifact{{Name: "config", Kind: "archive", Reference: "…/blobs/sha256:…"}}
|
|
}
|
|
return b
|
|
}
|
|
|
|
func TestAFailedBuildIsARowLikeAnyOther(t *testing.T) {
|
|
// One that leaves no trace is indistinguishable from a build nobody asked for, and the
|
|
// difference is the whole of whether somebody should be looking at something.
|
|
inv := fresh(t)
|
|
ctx := context.Background()
|
|
if err := inv.RecordBuild(ctx, aBuild("1", "", "cannot clone: no such repository")); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
got, err := inv.Builds(ctx, "", 10)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(got) != 1 {
|
|
t.Fatalf("got %d builds", len(got))
|
|
}
|
|
if got[0].Worked() {
|
|
t.Fatal("a failure was recorded as a success")
|
|
}
|
|
if !strings.Contains(got[0].Failed, "no such repository") {
|
|
t.Fatalf("the builder's own words were not kept: %q", got[0].Failed)
|
|
}
|
|
// And it kept what was asked for, which is the only thing a person can go and look at when
|
|
// the build never learned what it was building.
|
|
if got[0].Module != "" || got[0].Repository == "" {
|
|
t.Fatalf("got %+v", got[0])
|
|
}
|
|
}
|
|
|
|
func TestOneResultRecordedTwiceIsOneBuild(t *testing.T) {
|
|
// A result can arrive twice: as the answer to whoever asked, and on the exchange when nobody
|
|
// was. Two rows would show one build as two, and which is real is not answerable afterwards.
|
|
inv := fresh(t)
|
|
ctx := context.Background()
|
|
for i := 0; i < 2; i++ {
|
|
if err := inv.RecordBuild(ctx, aBuild("same", "shell", "")); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
got, err := inv.Builds(ctx, "", 10)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(got) != 1 {
|
|
t.Fatalf("one build was recorded %d times", len(got))
|
|
}
|
|
}
|
|
|
|
func TestBuildsComeBackNewestFirstAndCanBeAskedPerModule(t *testing.T) {
|
|
inv := fresh(t)
|
|
ctx := context.Background()
|
|
for _, b := range []Build{
|
|
aBuild("1", "shell", ""),
|
|
aBuild("2", "meshboard", ""),
|
|
aBuild("3", "shell", "the tests failed"),
|
|
} {
|
|
if err := inv.RecordBuild(ctx, b); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
|
|
all, err := inv.Builds(ctx, "", 10)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(all) != 3 || all[0].ID != "3" {
|
|
t.Fatalf("newest is not first: %v", ids(all))
|
|
}
|
|
|
|
// Per module, because "what has happened to this" is asked when deciding whether to trust it.
|
|
shell, err := inv.Builds(ctx, "shell", 10)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(shell) != 2 {
|
|
t.Fatalf("shell has %d builds: %v", len(shell), ids(shell))
|
|
}
|
|
for _, b := range shell {
|
|
if b.Module != "shell" {
|
|
t.Fatalf("asked for shell and got %q", b.Module)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestWhatWasPublishedIsKeptWithTheBuild(t *testing.T) {
|
|
// So a digest can be traced back to the build that made it, without keeping the manifest a
|
|
// second time in a place that can disagree with the first.
|
|
inv := fresh(t)
|
|
ctx := context.Background()
|
|
if err := inv.RecordBuild(ctx, aBuild("1", "shell", "")); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
got, _ := inv.Builds(ctx, "", 10)
|
|
if len(got[0].Made) != 1 || got[0].Made[0].Kind != "archive" {
|
|
t.Fatalf("what was published was not kept: %+v", got[0].Made)
|
|
}
|
|
}
|
|
|
|
func TestAskingForMoreThanThereIsIsNotAnError(t *testing.T) {
|
|
inv := fresh(t)
|
|
got, err := inv.Builds(context.Background(), "", 100)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(got) != 0 {
|
|
t.Fatalf("got %d", len(got))
|
|
}
|
|
}
|
|
|
|
func ids(builds []Build) []string {
|
|
var out []string
|
|
for _, b := range builds {
|
|
out = append(out, b.ID)
|
|
}
|
|
return out
|
|
}
|
|
|
|
// What a build read besides its module's own repository comes back for the newest build of each
|
|
// module, and only for builds that worked. Nothing recorded is absent rather than empty, which is how
|
|
// a build made before the mesh kept this is told from one that read nothing (novox/hq 04-ISSUES/131).
|
|
func TestWhatABuildReadComesBackForTheNewestBuildOfEachModule(t *testing.T) {
|
|
inv := fresh(t)
|
|
ctx := context.Background()
|
|
older := aBuild("older", "builder", "")
|
|
older.Read = []ReadRepository{{Repository: "novox/mesh-controller", Ref: "release"}}
|
|
newer := aBuild("newer", "builder", "")
|
|
newer.Read = []ReadRepository{{Repository: "novox/mesh-controller", Ref: "main"}}
|
|
plain := aBuild("plain", "gitea", "")
|
|
failed := aBuild("failed", "route-proxy", "cannot clone")
|
|
failed.Read = []ReadRepository{{Repository: "novox/mesh-controller", Ref: "main"}}
|
|
for _, b := range []Build{older, newer, plain, failed} {
|
|
if err := inv.RecordBuild(ctx, b); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
read, err := inv.ReadRepositories(ctx)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(read["builder"]) != 1 || read["builder"][0].Ref != "main" {
|
|
t.Fatalf("the newest build's reading is %+v", read["builder"])
|
|
}
|
|
if _, has := read["gitea"]; has {
|
|
t.Fatalf("a build that read nothing but its own repository reads as %+v", read["gitea"])
|
|
}
|
|
if _, has := read["route-proxy"]; has {
|
|
t.Fatal("a failed build's reading was kept as what that module reads")
|
|
}
|
|
}
|
|
|
|
// **What a build said it was made from comes back laid over what it read** (novox/hq ADR 0267): a context's
|
|
// paths on its entry, the module's own as an entry marked Own — and never in the stored `built_contexts`,
|
|
// where a controller that predates them would read an own entry as a context of its own repository.
|
|
func TestABuildsBuildSourceComesBackOverWhatItRead(t *testing.T) {
|
|
inv := fresh(t)
|
|
ctx := context.Background()
|
|
said := aBuild("said", "route-proxy", "")
|
|
said.Read = []ReadRepository{{Repository: "novox/mesh-controller", Ref: "main"}, {Repository: "novox/other"}}
|
|
said.Sources = []BuildSource{
|
|
{Paths: []string{"modules/route-proxy/Dockerfile", "modules/route-proxy/module.json"}},
|
|
{Repository: "novox/mesh-controller", Ref: "main", Paths: []string{"examples/route-proxy/", "go.mod"}},
|
|
{Repository: "novox/unread", Paths: []string{"x"}},
|
|
}
|
|
unsaid := aBuild("unsaid", "build-agent", "")
|
|
unsaid.Read = []ReadRepository{{Repository: "novox/mesh-controller", Ref: "main"}}
|
|
for _, b := range []Build{said, unsaid} {
|
|
if err := inv.RecordBuild(ctx, b); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
read, err := inv.ReadRepositories(ctx)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
want := []ReadRepository{
|
|
{Repository: "novox/mesh-controller", Ref: "main", Paths: []string{"examples/route-proxy/", "go.mod"}},
|
|
{Repository: "novox/other"},
|
|
{Paths: []string{"modules/route-proxy/Dockerfile", "modules/route-proxy/module.json"}, Own: true},
|
|
}
|
|
for k := range read["route-proxy"] {
|
|
if read["route-proxy"][k].Built.IsZero() {
|
|
t.Errorf("no build time on %+v", read["route-proxy"][k])
|
|
}
|
|
read["route-proxy"][k].Built, read["route-proxy"][k].Looked = time.Time{}, time.Time{}
|
|
}
|
|
if !reflect.DeepEqual(read["route-proxy"], want) {
|
|
t.Fatalf("read back %+v\nwanted %+v", read["route-proxy"], want)
|
|
}
|
|
if got := read["build-agent"]; len(got) != 1 || got[0].Paths != nil || got[0].Own {
|
|
t.Fatalf("a build that said no build source reads as %+v", got)
|
|
}
|
|
kept, found, err := inv.chain.Builds.Get(ctx, "route-proxy.build.said")
|
|
if err != nil || !found {
|
|
t.Fatalf("the build is not on the bus: %v %v", found, err)
|
|
}
|
|
storedRead, err := json.Marshal(kept.Value.Read)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if stored := string(storedRead); strings.Contains(stored, "paths") || strings.Contains(stored, "own") {
|
|
t.Fatalf("the build source was stored among what the build read: %s", stored)
|
|
}
|
|
}
|
|
|
|
// A build newer than the newest that worked, and failed, leaves that one's build source stale: its module is
|
|
// read whole until a build works again (novox/hq ADR 0267).
|
|
func TestAFailedNewerBuildLeavesTheBuildSourceStale(t *testing.T) {
|
|
inv := fresh(t)
|
|
ctx := context.Background()
|
|
worked := aBuild("worked", "route-proxy", "")
|
|
worked.Asked = time.Now().Add(-time.Hour)
|
|
worked.Read = []ReadRepository{{Repository: "novox/mesh-controller", Ref: "main"}}
|
|
worked.Sources = []BuildSource{{Paths: []string{"modules/route-proxy/module.json"}},
|
|
{Repository: "novox/mesh-controller", Ref: "main", Paths: []string{"examples/route-proxy/"}}}
|
|
failed := aBuild("failed", "route-proxy", "compile error")
|
|
failed.Asked = time.Now()
|
|
for _, b := range []Build{worked, failed} {
|
|
if err := inv.RecordBuild(ctx, b); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
read, err := inv.ReadRepositories(ctx)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
got := read["route-proxy"]
|
|
if len(got) != 2 || got[0].Paths != nil || got[0].Own || !got[0].Looked.After(got[0].Built) ||
|
|
!got[1].Own || got[1].Paths != nil || got[1].Whole != "a newer build of it failed" {
|
|
t.Fatalf("after a newer failed build the proxy reads as %+v", got)
|
|
}
|
|
}
|