A what-if named the build seat's holder as packaging the controller's source, "rebuilt without their own source moving", while it was in the plan because an open plan had not built it yet. The what-if and the merge log now say, per module, which changed files of its build source moved it, or that it is read whole and why: no build source recorded, a newer build failed, or a plan has not built it yet.
252 lines
8.7 KiB
Go
252 lines
8.7 KiB
Go
package inventory
|
|
|
|
import (
|
|
"context"
|
|
"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)
|
|
}
|
|
var stored string
|
|
if err := inv.store.Pool().QueryRow(ctx, `select built_contexts::text from build where id = 'said'`).Scan(&stored); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if 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)
|
|
}
|
|
}
|