Files
mesh-delivery/modules/mesh-delivery-view/internal/view/fingerprints_test.go
T

191 lines
8.9 KiB
Go

package view
import (
"bytes"
"net/http"
"net/http/httptest"
"strings"
"testing"
"git.novox.be/novox/mesh-sdk/go/delivery"
)
const (
hA = "0123456789abcdef0123456789abcdef01234567"
hB = "89abcdef0123456789abcdef0123456789abcdef"
fF = "src1:1111111111111111111111111111111111111111111111111111111111111111"
fG = "src1:2222222222222222222222222222222222222222222222222222222222222222"
)
func candidate(t *testing.T, group string, pulls ...delivery.CandidatePull) Entry {
t.Helper()
heads := make([]delivery.Head, len(pulls))
for i, p := range pulls {
heads[i] = p.Head
}
hash, err := delivery.HeadsHash(heads)
if err != nil {
t.Fatal(err)
}
b, err := delivery.EncodeCandidate(delivery.Candidate{Group: group, HeadsHash: hash, Pulls: pulls})
if err != nil {
t.Fatal(err)
}
return Entry{Bucket: delivery.CandidatesBucket, Key: group, Value: b, At: t0}
}
func pullOf(repo string, n int, head string, modules ...string) delivery.CandidatePull {
if modules == nil {
modules = []string{}
}
return delivery.CandidatePull{Head: delivery.Head{Repository: repo, Number: n, Head: head}, Modules: modules}
}
func modEntry(t *testing.T, name string, m delivery.Module) Entry {
t.Helper()
b, err := delivery.EncodeModule(m)
if err != nil {
t.Fatal(err)
}
return Entry{Bucket: delivery.ModulesBucket, Key: name, Value: b, At: t0}
}
// trunkBuild is a build record as the controller writes it (mesh-sdk go/delivery/testdata/build-record-build-agent.json is
// the captured shape: id, state, repository, ref, module, commit, path, source-fingerprint, source-inputs, at).
func trunkBuild(module, id, ref, state, fp, at string) []Entry {
rec := `{"id":"` + id + `","state":"` + state + `","repository":"novox/mesh-delivery","ref":"` + ref + `","module":"` + module + `","commit":"7a74cee83f88e6585a5511c3cc53bdf0f5303d20","path":"modules/` + module + `","at":"` + at + `"`
if fp != "" {
rec += `,"source-fingerprint":"` + fp + `"`
}
rec += `}`
return []Entry{
{Bucket: BuildsBucket, Key: module + ".build." + id, Value: []byte(rec), At: t0},
{Bucket: BuildsBucket, Key: module + ".newest", Value: []byte(id), At: t0},
}
}
func fpRow(t *testing.T, s Snapshot, module string) FingerprintRow {
t.Helper()
for _, f := range s.Fingerprints {
if f.Module == module {
return f
}
}
t.Fatalf("no fingerprint row for %s in %+v", module, s.Fingerprints)
return FingerprintRow{}
}
func TestSnapshotListsTheCandidateGroups(t *testing.T) { // 1241/AC1
m := New()
fill(m,
candidate(t, "feat/x", pullOf("novox/mesh-delivery", 12, hA, "a", "b"), pullOf("novox/mesh-catalog", 300, hB, "c")),
candidate(t, "feat/y", pullOf("novox/mesh-delivery", 13, hB)))
g := m.Snapshot().Groups
if len(g) != 2 || g[0].Group != "feat/x" || g[1].Group != "feat/y" {
t.Fatalf("groups = %+v", g)
}
// sorted by repository: mesh-catalog before mesh-delivery
if len(g[0].Pulls) != 2 || g[0].Pulls[0].Repository != "novox/mesh-catalog" || g[0].Pulls[0].Number != 300 {
t.Fatalf("pulls = %+v", g[0].Pulls)
}
p := g[0].Pulls[1]
if p.Repository != "novox/mesh-delivery" || p.Number != 12 || p.Head != hA[:12] || strings.Join(p.Modules, ",") != "a,b" {
t.Errorf("pull = %+v, want #12 with head %s and modules a,b", p, hA[:12])
}
if len(g[1].Pulls) != 1 || len(g[1].Pulls[0].Modules) != 0 {
t.Errorf("feat/y = %+v", g[1])
}
m.Apply(Entry{Bucket: delivery.CandidatesBucket, Key: "feat/x", Deleted: true})
if g := m.Snapshot().Groups; len(g) != 1 || g[0].Group != "feat/y" {
t.Errorf("after the delete: %+v", g)
}
}
func TestFingerprintAgainstTheNewestTrunkBuild(t *testing.T) { // 1241/AC2
m := New()
fill(m, trunkBuild("a", "build-1", "main", "built", fF, "2026-10-19T10:00:00Z")...)
fill(m, trunkBuild("d", "build-2", "main", "built", fF, "2026-10-19T10:00:00Z")...)
fill(m,
modEntry(t, "a", delivery.Module{FP: fF, Repository: "novox/mesh-delivery", Commit: hA}),
modEntry(t, "d", delivery.Module{FP: fG, Repository: "novox/mesh-delivery", Commit: hA}),
modEntry(t, "b", delivery.Module{None: "an artifact compiles a Go program from a build source that git trees do not pin: no fingerprint before release 4", Repository: "novox/mesh-delivery", Commit: hA}),
modEntry(t, "e", delivery.Module{FP: fG, Repository: "novox/mesh-delivery", Commit: hA}))
s := m.Snapshot()
if r := fpRow(t, s, "a"); r.State != FPSame || r.Indexer != fF || r.Build != fF {
t.Errorf("a = %+v, want same", r)
}
if r := fpRow(t, s, "d"); r.State != FPDiffers || r.Indexer != fG || r.Build != fF {
t.Errorf("d = %+v, want differs with both", r)
}
if r := fpRow(t, s, "b"); r.State != FPNone || !strings.Contains(r.Reason, "release 4") || r.Indexer != "" {
t.Errorf("b = %+v, want none with the reason", r)
}
if r := fpRow(t, s, "e"); r.State != FPNoBuild || r.Indexer != fG || r.Build != "" {
t.Errorf("e = %+v, want no build with the indexer's alone", r)
}
}
func TestOnlyTheNewestSuccessfulTrunkBuildCounts(t *testing.T) { // 1241/AC2 (the newest key is only a hint)
m := New()
fill(m, trunkBuild("a", "build-1", "main", "built", fG, "2026-10-19T09:00:00Z")...)
fill(m, trunkBuild("a", "build-2", "main", "built", fF, "2026-10-19T10:00:00Z")...)
fill(m, trunkBuild("a", "build-3", "main", "failed", fG, "2026-10-19T11:00:00Z")...) // newer, not built
fill(m, trunkBuild("a", "build-4", "feat/x", "built", fG, "2026-10-19T12:00:00Z")...) // newer, not trunk; also the newest key
fill(m, modEntry(t, "a", delivery.Module{FP: fF, Repository: "novox/mesh-delivery", Commit: hA}))
if r := fpRow(t, m.Snapshot(), "a"); r.State != FPSame || r.Build != fF {
t.Errorf("a = %+v, want same with the 10:00 trunk build's fingerprint", r)
}
}
func TestBuildFingerprintIsReadWithTheSDKRecord(t *testing.T) { // 1241/AC3
// The record as the controller's encoder wrote it (mesh-sdk go/delivery/testdata/build-record-build-agent.json), whole,
// with its source-inputs under their Go field names: the view reads source-fingerprint from it.
const rec = `{"id":"build-1792395447088936520","state":"built","repository":"novox/mesh-controller","ref":"main","module":"build-agent","commit":"7a74cee83f88e6585a5511c3cc53bdf0f5303d20","path":"modules/build-agent","manifest-digest":"sha256:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef","source-fingerprint":"src1:e846efe73716b78e80860baaac96153de2cb76551325753838ca245d2160a3d7","source-inputs":{"Module":"build-agent","Tree":"11c0b4f90cae0d6e4ef9cc48cbfdf9f6dbe52bca","Bases":["sha256:d9e853e87e55526f6b2917df91a2115c36dd7c696a35be12163d44e6e2a4b6bc"],"Contexts":{"server":"narrow:d51ca744fd11af59d616c858f76c27b3e58e16e11fbdc0c720eb9d7d19d55fac"},"Toolchains":null,"Unpinned":""},"made":[{"name":"server","kind":"image","reference":"registry.example/build-agent@sha256:d9e853e87e55526f6b2917df91a2115c36dd7c696a35be12163d44e6e2a4b6bc"}],"at":"2026-10-19T09:38:00Z"}`
const fp = "src1:e846efe73716b78e80860baaac96153de2cb76551325753838ca245d2160a3d7"
m := New()
fill(m, Entry{Bucket: BuildsBucket, Key: delivery.BuildKey("build-agent", "build-1792395447088936520"), Value: []byte(rec), At: t0},
Entry{Bucket: BuildsBucket, Key: delivery.NewestKey("build-agent"), Value: []byte("build-1792395447088936520"), At: t0},
modEntry(t, "build-agent", delivery.Module{FP: fp, Repository: "novox/mesh-controller", Commit: hA}))
if r := fpRow(t, m.Snapshot(), "build-agent"); r.State != FPSame || r.Build != fp {
t.Errorf("build-agent = %+v, want same with %s", r, fp)
}
}
func shown(t *testing.T) *Model {
m := New()
fill(m, trunkBuild("a", "build-1", "main", "built", fF, "2026-10-19T10:00:00Z")...)
fill(m,
candidate(t, "feat/x", pullOf("novox/mesh-delivery", 12, hA, "a"), pullOf("novox/mesh-catalog", 300, hB, "c")),
modEntry(t, "a", delivery.Module{FP: fG, Repository: "novox/mesh-delivery", Commit: hA}),
modEntry(t, "b", delivery.Module{None: "no tree", Repository: "novox/mesh-delivery", Commit: hA}))
return m
}
func TestTableAndPagePrintTheGroupsAndTheComparison(t *testing.T) { // 1241/AC6
m := shown(t)
var out bytes.Buffer
Table(&out, m.Snapshot())
for _, want := range []string{"CANDIDATES", "feat/x", "novox/mesh-delivery#12", hA[:12], "novox/mesh-catalog#300", "FINGERPRINT", "differs", "indexer " + fG, "build " + fF, "no tree"} {
if !strings.Contains(out.String(), want) {
t.Errorf("the table lacks %q:\n%s", want, out.String())
}
}
rec := httptest.NewRecorder()
Web(m).ServeHTTP(rec, httptest.NewRequest(http.MethodGet, "/", nil))
for _, want := range []string{"CANDIDATES", "feat/x", "novox/mesh-delivery#12", hA[:12], "FINGERPRINTS", "differs", fG, fF, "no tree"} {
if !strings.Contains(rec.Body.String(), want) {
t.Errorf("the page lacks %q:\n%s", want, rec.Body.String())
}
}
}
func TestUnreadableIndexerValuesAreNamed(t *testing.T) { // never dropped silently
m := New()
fill(m, Entry{Bucket: delivery.CandidatesBucket, Key: "feat/x", Value: []byte("{"), At: t0},
Entry{Bucket: delivery.ModulesBucket, Key: "a", Value: []byte("{"), At: t0})
s := m.Snapshot()
if len(s.Unreadable) != 2 || len(s.Groups) != 0 || len(s.Fingerprints) != 0 {
t.Errorf("snapshot = %+v", s)
}
}