Three faults in one path. A merge rebuilt every module built from the repository, so one change in a repository holding twenty-six of them meant twenty-six builds. A merge into a repository a module only *packages* source from rebuilt nothing — two modules are built from the control plane's own repository and neither had ever been rebuilt when it moved — because the manifest the mesh keeps carries no build section, so a build now says which repositories it read and the mesh keeps that beside what it stood on. And a module handed over by hand could record a repository with no directory inside it, which is a module nothing can ever rebuild (novox/hq 04-ISSUES/131, /132). A change inside no module's own directory is a change to what they share, and everything built from that repository is rebuilt: rebuilding too much is the safe direction, because the fault this whole path exists for is a mesh that believes it is current and is not.
213 lines
8.9 KiB
Go
213 lines
8.9 KiB
Go
package main
|
|
|
|
import (
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/novox/mesh-controller/internal/catalogue"
|
|
"github.com/novox/mesh-controller/internal/inventory"
|
|
"github.com/novox/mesh-controller/internal/link"
|
|
)
|
|
|
|
// entry is a module as the catalogue holds it: built, so its manifest carries no `build` any more.
|
|
func entry(module string, _ ...string) inventory.Entry {
|
|
return inventory.Entry{Manifest: catalogue.Manifest{Module: module}}
|
|
}
|
|
|
|
// stoodOn is what each module's newest build recorded it was handed.
|
|
func stoodOn(edges map[string][]string) map[string][]string {
|
|
out := map[string][]string{}
|
|
for module, bases := range edges {
|
|
for _, b := range bases {
|
|
out[module] = append(out[module], catalogue.ArtifactStoreScheme+b+"/runtime@sha256:"+strings.Repeat("0", 64))
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
// A module built before the module it stands on is built against the old one and reports success
|
|
// (novox/hq 04-ISSUES/131). So bases come first, however the set arrived.
|
|
func TestBasesAreBuiltBeforeWhatStandsOnThem(t *testing.T) {
|
|
in := []inventory.Entry{entry("app"), entry("runtime"), entry("other"), entry("base")}
|
|
edges := stoodOn(map[string][]string{"app": {"runtime"}, "runtime": {"base"}})
|
|
got := orderByBases(in, edges)
|
|
pos := map[string]int{}
|
|
for i, e := range got {
|
|
pos[e.Manifest.Module] = i
|
|
}
|
|
if !(pos["base"] < pos["runtime"] && pos["runtime"] < pos["app"]) {
|
|
t.Fatalf("bases not first: %v", pos)
|
|
}
|
|
if len(got) != 4 {
|
|
t.Fatalf("an entry was lost or doubled: %d", len(got))
|
|
}
|
|
// A base outside the set is not waited for: it is not being rebuilt.
|
|
got = orderByBases([]inventory.Entry{entry("app")}, stoodOn(map[string][]string{"app": {"elsewhere"}}))
|
|
if len(got) != 1 {
|
|
t.Fatalf("a dependency outside the set changed the set: %v", got)
|
|
}
|
|
}
|
|
|
|
// A module registered from its manifest and never built still names its bases there; once built,
|
|
// the recorded edge is what says so. Both are read, and a module never stands on itself.
|
|
func TestWhatStandsOnAModuleIsReadFromItsBuildOrItsManifest(t *testing.T) {
|
|
built := entry("gitea")
|
|
edges := stoodOn(map[string][]string{"gitea": {"mesh-tools"}})
|
|
if !standsOnModule(built, "mesh-tools", edges) {
|
|
t.Fatal("a recorded edge was not read")
|
|
}
|
|
if standsOnModule(built, "gitea", edges) || standsOnModule(built, "postgres", edges) {
|
|
t.Fatal("an edge was invented")
|
|
}
|
|
fresh := inventory.Entry{Manifest: catalogue.Manifest{Module: "plex", Build: &catalogue.Build{
|
|
On: []catalogue.BuildsOn{{Arg: "RUNTIME_BASE", Module: "mesh-tools", Artifact: "runtime"}},
|
|
}}}
|
|
if !standsOnModule(fresh, "mesh-tools", nil) {
|
|
t.Fatal("a manifest's own base was not read")
|
|
}
|
|
}
|
|
|
|
// A merge names a repository the way the forge does; a source is recorded the way a build was
|
|
// asked for. The two meet on owner/repo and branch, whichever form the record took.
|
|
func TestAMergeMatchesTheSourcesBuiltFromIt(t *testing.T) {
|
|
m := link.SourceMoved{Owner: "novox", Repo: "mesh-controller", Base: "main",
|
|
CloneURL: "http://forge.internal:20000/novox/mesh-controller.git"}
|
|
for _, s := range []inventory.Source{
|
|
{Repository: "http://forge.internal:20000/novox/mesh-controller.git", Ref: "main"},
|
|
{Repository: "novox/mesh-controller", Seat: "git", Ref: ""},
|
|
{Repository: "https://elsewhere.example/novox/mesh-controller", Ref: "main"},
|
|
} {
|
|
if !sourceIs(s, m) {
|
|
t.Errorf("%+v was not matched by the merge", s)
|
|
}
|
|
}
|
|
for _, s := range []inventory.Source{
|
|
{Repository: "novox/mesh-host", Seat: "git"},
|
|
{Repository: "http://forge.internal:20000/novox/mesh-controller.git", Ref: "release"},
|
|
} {
|
|
if sourceIs(s, m) {
|
|
t.Errorf("%+v was matched by a merge that is not its", s)
|
|
}
|
|
}
|
|
}
|
|
|
|
// A merge made before the source was last seen is history: it does not move the source, and a
|
|
// merge that says nothing about when it was made is taken as news.
|
|
func TestAMergeOlderThanTheLastLookIsHistory(t *testing.T) {
|
|
seen := time.Date(2026, 9, 28, 3, 0, 0, 0, time.UTC)
|
|
if !isHistory("2026-09-28T02:00:00Z", seen) {
|
|
t.Fatal("an older merge was taken as news")
|
|
}
|
|
if isHistory("2026-09-28T04:00:00Z", seen) {
|
|
t.Fatal("a newer merge was taken as history")
|
|
}
|
|
if isHistory("", seen) || isHistory("2026-09-28T02:00:00Z", time.Time{}) {
|
|
t.Fatal("a merge or a source with no time on it was refused")
|
|
}
|
|
}
|
|
|
|
// A module as the catalogue holds it: built from a repository, at a directory inside it.
|
|
func fromRepo(module, repository, path string) inventory.Entry {
|
|
return inventory.Entry{
|
|
Manifest: catalogue.Manifest{Module: module},
|
|
Source: inventory.Source{Repository: repository, Path: path, Ref: "main"},
|
|
}
|
|
}
|
|
|
|
// A merge rebuilds the modules whose own directories it changed, and everything when what it changed
|
|
// is shared. One repository holding many modules is the ordinary case here, and rebuilding all of
|
|
// them for a change to one is what exhausted a registry's pull limit the first night this ran.
|
|
func TestAMergeRebuildsTheModulesItChanged(t *testing.T) {
|
|
const repo = "http://forge.internal:20000/novox/mesh-catalog.git"
|
|
gitea := fromRepo("gitea", repo, "modules/gitea")
|
|
keycloak := fromRepo("keycloak", repo, "modules/keycloak")
|
|
known := []inventory.Entry{gitea, keycloak, fromRepo("plex", repo, "modules/plex")}
|
|
candidates := []inventory.Entry{gitea, keycloak}
|
|
merge := func(paths []string, truncated bool) link.SourceMoved {
|
|
return link.SourceMoved{Owner: "novox", Repo: "mesh-catalog", Base: "main",
|
|
Paths: paths, PathsTruncated: truncated}
|
|
}
|
|
named := func(entries []inventory.Entry) string {
|
|
var names []string
|
|
for _, e := range entries {
|
|
names = append(names, e.Manifest.Module)
|
|
}
|
|
return strings.Join(names, ",")
|
|
}
|
|
for _, c := range []struct {
|
|
what string
|
|
m link.SourceMoved
|
|
want string
|
|
}{
|
|
{"one module's own files", merge([]string{"modules/gitea/index.ts", "modules/gitea/client.ts"}, false), "gitea"},
|
|
{"two modules' files", merge([]string{"modules/gitea/index.ts", "modules/keycloak/module.json"}, false), "gitea,keycloak"},
|
|
{"a file they share", merge([]string{"tsconfig.json"}, false), "gitea,keycloak"},
|
|
{"a module the mesh does not hold", merge([]string{"modules/plex/index.ts"}, false), ""},
|
|
{"nothing said about the files", merge(nil, false), "gitea,keycloak"},
|
|
{"more files than were listed", merge([]string{"modules/gitea/index.ts"}, true), "gitea,keycloak"},
|
|
} {
|
|
if got := named(whatTheMergeTouched(candidates, known, c.m)); got != c.want {
|
|
t.Errorf("%s: rebuilt %q, wanted %q", c.what, got, c.want)
|
|
}
|
|
}
|
|
}
|
|
|
|
// A module whose recipe packages source from another repository is affected when that repository
|
|
// moves — the manifest the mesh keeps says nothing about it, so the record of what the build read is
|
|
// the only thing that can say so.
|
|
func TestAModuleIsAffectedByTheRepositoryItPackages(t *testing.T) {
|
|
m := link.SourceMoved{Owner: "novox", Repo: "mesh-controller", Base: "main",
|
|
CloneURL: "http://forge.internal:20000/novox/mesh-controller.git"}
|
|
for _, read := range [][]inventory.ReadRepository{
|
|
{{Repository: "http://forge.internal:20000/novox/mesh-controller.git", Ref: "main"}},
|
|
{{Repository: "novox/mesh-controller"}},
|
|
{{Repository: "https://elsewhere.example/novox/other"}, {Repository: "novox/mesh-controller.git", Ref: "main"}},
|
|
} {
|
|
if !readsFrom(read, m) {
|
|
t.Errorf("%+v was not matched by the merge", read)
|
|
}
|
|
}
|
|
for _, read := range [][]inventory.ReadRepository{
|
|
nil,
|
|
{{Repository: "novox/mesh-host", Ref: "main"}},
|
|
{{Repository: "novox/mesh-controller", Ref: "release"}},
|
|
} {
|
|
if readsFrom(read, m) {
|
|
t.Errorf("%+v was matched by a merge that is not its", read)
|
|
}
|
|
}
|
|
}
|
|
|
|
// What a module handed over by hand records about where it came from, and what is refused.
|
|
func TestWhatAHandedOverModuleRecordsAboutItsSource(t *testing.T) {
|
|
// The whole location: a repository on the mesh's own forge, the directory inside it, the branch
|
|
// and the commit the manifest was read at.
|
|
from, err := whereItComesFrom("novox/mesh-catalog", "main", "c0ffee", "modules/gitea", true)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if from.Path != "modules/gitea" || from.Seat != "git" || from.Repository != "novox/mesh-catalog" {
|
|
t.Fatalf("the source records as %+v", from)
|
|
}
|
|
// A manifest with no provenance at all is legitimate: fixing something in a hurry.
|
|
if from, err := whereItComesFrom("", "", "", "", false); err != nil || from != (inventory.Source{}) {
|
|
t.Fatalf("a manifest handed over with no provenance was refused: %+v, %v", from, err)
|
|
}
|
|
for _, c := range []struct {
|
|
what string
|
|
repository, ref, commit, path string
|
|
self bool
|
|
}{
|
|
{what: "a source with no commit", repository: "novox/mesh-catalog", commit: ""},
|
|
{what: "a commit with no source", commit: "c0ffee"},
|
|
{what: "a directory inside nothing", path: "modules/gitea"},
|
|
{what: "a forge holding nothing", self: true},
|
|
{what: "an address given as a path on the forge", repository: "http://forge.internal:20000/novox/x.git", commit: "c0ffee", self: true},
|
|
} {
|
|
if _, err := whereItComesFrom(c.repository, c.ref, c.commit, c.path, c.self); err == nil {
|
|
t.Errorf("%s was recorded as a source", c.what)
|
|
}
|
|
}
|
|
}
|