A failed build, or a plan closed before reaching a module, left the closure its last good build said, and a fix-forward to a newly imported package would have moved nothing. A missed merge moving only a module that packages the repository was never caught up, and an older merge read as history for it through a look that was not its own. The gate, a pull request's check, the what-if and a delivery's order now read the same view the merge handler does.
250 lines
8.6 KiB
Go
250 lines
8.6 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)
|
|
}
|
|
if got := read["route-proxy"]; len(got) != 1 || got[0].Paths != nil || got[0].Own || !got[0].Looked.After(got[0].Built) {
|
|
t.Fatalf("after a newer failed build the proxy reads as %+v", got)
|
|
}
|
|
}
|