Files
mesh-delivery/modules/mesh-delivery-view/cmd/mesh-delivery-view-tools/main_test.go
T

267 lines
11 KiB
Go

package main
import (
"encoding/json"
"fmt"
"strings"
"testing"
"mesh-delivery-view/internal/view"
)
type fake map[string]string
func (f fake) reader() view.Reader {
return view.Reader{State: view.StateRead{
Keys: func() ([]string, error) {
var k []string
for key := range f {
k = append(k, key)
}
return k, nil
},
Get: func(key string) (json.RawMessage, error) {
if v, ok := f[key]; ok {
return json.RawMessage(v), nil
}
return nil, nil
},
}}
}
func run(t *testing.T, r view.Reader, name string, args map[string]any) (any, error) {
t.Helper()
for _, tl := range Tools(r) {
if tl.Name == name {
return tl.Run(args)
}
}
t.Fatalf("no tool %s", name)
return nil, nil
}
const (
withFP2 = `{"v":3,"fp":"a1","fp2":"b2","manifest":"m1","inputs2":{"tree":"t1","bases":["x"]},"repository":"novox/r","commit":"c1","path":".","toolchains":{"go":{"version":"go1.27.1","recipe":"2"}}}`
withNone = `{"v":3,"none":"no successful trunk build recorded its inputs","none2":"builder's fingerprint not yet known","repository":"novox/r","commit":"c2","path":"modules/b"}`
)
// 1562/AC1: indexer_module answers the key's value as stored: FP, FP2, None2, Inputs2, Path and Toolchains are all there.
func TestModuleToolAnswersTheStoredValue(t *testing.T) {
r := fake{"a": withFP2, "b": withNone}.reader()
for key, want := range map[string]string{"a": withFP2, "b": withNone} {
out, err := run(t, r, view.ModuleReadTool, map[string]any{"module": key})
if err != nil {
t.Fatal(err)
}
if got := string(out.(json.RawMessage)); got != want {
t.Errorf("%s = %s, want %s", key, got, want)
}
}
out, _ := run(t, r, view.ModuleReadTool, map[string]any{"module": "a"})
var m map[string]any
if err := json.Unmarshal(out.(json.RawMessage), &m); err != nil || m["fp"] != "a1" || m["fp2"] != "b2" || m["path"] != "." || m["inputs2"] == nil || m["toolchains"] == nil {
t.Errorf("stored fields are not all there: %v %v", m, err)
}
}
// A module with no key, and a name that is no key, are said so.
func TestModuleToolRefusesWhatIsNotThere(t *testing.T) {
r := fake{"a": withFP2}.reader()
if _, err := run(t, r, view.ModuleReadTool, map[string]any{"module": "gone"}); err == nil || !strings.Contains(err.Error(), "no module gone") {
t.Errorf("a missing module: %v", err)
}
if _, err := run(t, r, view.ModuleReadTool, map[string]any{"module": ""}); err == nil {
t.Errorf("an empty name was answered")
}
}
// 1562/AC1: indexer_modules answers every key as stored, and with none2 only those that say why they have no FP2.
func TestModulesToolAnswersEveryKeyAsStored(t *testing.T) {
r := fake{"a": withFP2, "b": withNone}.reader()
out, err := run(t, r, view.ModulesReadTool, map[string]any{})
if err != nil {
t.Fatal(err)
}
all := out.(map[string]any)["modules"].(map[string]json.RawMessage)
if len(all) != 2 || string(all["a"]) != withFP2 || string(all["b"]) != withNone {
t.Errorf("modules = %v", all)
}
out, err = run(t, r, view.ModulesReadTool, map[string]any{"none2": true})
if err != nil {
t.Fatal(err)
}
only := out.(map[string]any)["modules"].(map[string]json.RawMessage)
if len(only) != 1 || string(only["b"]) != withNone {
t.Errorf("none2 modules = %v", only)
}
}
const (
fp2A = "src2:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
fp2B = "src2:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"
keyA = "netcheck.aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
keyB = "zsh.bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"
doneBuild = `{"v":1,"state":"done","by":"novox.mesh-delivery-builder","step":"built on shanks, ace","manifest":"m","artifacts":[{"name":"program","kind":"bundle","digest":"sha256:d1"}]}`
failBuild = `{"v":1,"state":"failed","by":"novox.mesh-delivery-builder","error":"refused kind: package"}`
)
func both(builds, modules fake) view.Reader {
r := modules.reader()
r.Builds = builds.reader().State
return r
}
// 1723/AC1: builder_builds lists every key as stored (module, fingerprint, state, digests, error), and narrows by module and state.
func TestBuilderBuildsListsKeysAsStored(t *testing.T) {
r := both(fake{keyA: doneBuild, keyB: failBuild, "junk": "{}"}, fake{})
out, err := run(t, r, view.BuilderBuildsReadTool, map[string]any{})
if err != nil {
t.Fatal(err)
}
rows := out.(view.BuildsAnswer).Builds
if u := out.(view.BuildsAnswer).Unreadable; len(u) != 1 || u[0] != "junk" {
t.Errorf("unreadable = %v, want [junk] (issue 1818)", u)
}
if len(rows) != 2 || rows[0].Key != keyA || rows[0].Module != "netcheck" || rows[0].Fingerprint != fp2A || string(rows[0].Build) != doneBuild || string(rows[1].Build) != failBuild {
t.Errorf("rows = %+v", rows)
}
for args, want := range map[string]string{"state": keyB, "module": keyA} {
out, _ := run(t, r, view.BuilderBuildsReadTool, map[string]any{args: map[string]string{keyB: "failed", keyA: "netcheck"}[want]})
got := out.(view.BuildsAnswer).Builds
if len(got) != 1 || got[0].Key != want {
t.Errorf("%s filter = %+v, want %s", args, got, want)
}
}
}
// 1723/AC2: builder_build answers one key by module and fp2, and by module alone through the indexer's fp2 (joined at its commit).
func TestBuilderBuildJoinsTheIndexersFingerprint(t *testing.T) {
idx := `{"v":3,"fp2":"` + fp2A + `","commit":"c1","path":"."}`
r := both(fake{keyA: doneBuild}, fake{"netcheck": idx, "zsh": `{"v":3,"none2":"not known","commit":"c9"}`})
for _, args := range []map[string]any{{"module": "netcheck", "fp2": fp2A}, {"module": "netcheck"}, {"module": "netcheck", "commit": "c1"}} {
out, err := run(t, r, view.BuilderBuildReadTool, args)
if err != nil {
t.Fatalf("%v: %v", args, err)
}
if row := out.(view.BuilderRow); row.Key != keyA || string(row.Build) != doneBuild {
t.Errorf("%v = %+v", args, row)
}
}
for args, want := range map[string]string{"netcheck/c2": "not c2", "zsh": "not known", "gone": "no module gone"} {
a := map[string]any{"module": strings.Split(args, "/")[0]}
if strings.Contains(args, "/") {
a["commit"] = "c2"
}
if _, err := run(t, r, view.BuilderBuildReadTool, a); err == nil || !strings.Contains(err.Error(), want) {
t.Errorf("%v: err = %v, want %q", a, err, want)
}
}
if _, err := run(t, r, view.BuilderBuildReadTool, map[string]any{"module": "netcheck", "fp2": fp2B}); err == nil || !strings.Contains(err.Error(), "holds no build") {
t.Errorf("an unbuilt fingerprint: %v", err)
}
}
// bigBuilds is a bucket of n keys over 100 modules that counts the values read.
type bigBuilds struct {
fake fake
reads *int
}
func newBig(n int) bigBuilds {
f := fake{}
for i := 0; i < n; i++ {
f[fmt.Sprintf("mod%02d.%064x", i%100, i)] = doneBuild
}
return bigBuilds{fake: f, reads: new(int)}
}
func (b bigBuilds) reader() view.Reader {
r := b.fake.reader()
get := r.State.Get
r.Builds = view.StateRead{Keys: r.State.Keys, Get: func(k string) (json.RawMessage, error) { *b.reads++; return get(k) },
GetEntry: func(k string) (json.RawMessage, uint64, error) {
*b.reads++
raw, err := get(k)
var rev uint64
fmt.Sscanf(k[strings.Index(k, ".")+1:], "%x", &rev) // the fake's revision is the key's number
return raw, rev + 1, err
}}
return r
}
// 1827/AC1: with 2,000 keys, a module's builds are found by key prefix, so only that module's values are read (20, not 2,000),
// newest revision first, within the limit, and the answer says it was cut.
func TestBuilderBuildsFiltersByPrefixBeforeAnyGet(t *testing.T) {
b := newBig(2000)
out, err := run(t, b.reader(), view.BuilderBuildsReadTool, map[string]any{"module": "mod07", "limit": float64(5)})
if err != nil {
t.Fatal(err)
}
a := out.(view.BuildsAnswer)
if *b.reads != 20 {
t.Errorf("read %d values for a module of 20 keys, want 20", *b.reads)
}
if len(a.Builds) != 5 || !a.Cut || !strings.Contains(a.Note, "5") {
t.Errorf("answer = %d rows, cut %v, note %q", len(a.Builds), a.Cut, a.Note)
}
for i := 1; i < len(a.Builds); i++ {
if a.Builds[i-1].Revision < a.Builds[i].Revision {
t.Errorf("not newest first: %d before %d", a.Builds[i-1].Revision, a.Builds[i].Revision)
}
}
if a.Builds[0].Revision != 1908 {
t.Errorf("newest revision = %d, want 1908", a.Builds[0].Revision)
}
}
// 1827/AC2: with no module the answer reads at most the limit's worth in key order (default 50), never the 2,000.
func TestBuilderBuildsWithoutAModuleIsBounded(t *testing.T) {
b := newBig(2000)
out, err := run(t, b.reader(), view.BuilderBuildsReadTool, map[string]any{})
if err != nil {
t.Fatal(err)
}
a := out.(view.BuildsAnswer)
if len(a.Builds) != view.DefaultBuildsLimit || !a.Cut || *b.reads > view.DefaultBuildsLimit {
t.Errorf("%d rows, cut %v, %d reads", len(a.Builds), a.Cut, *b.reads)
}
b = newBig(2000)
out, _ = run(t, b.reader(), view.BuilderBuildsReadTool, map[string]any{"state": "failed"})
if a := out.(view.BuildsAnswer); *b.reads != view.MaxBuildReads || !a.Cut || len(a.Builds) != 0 {
t.Errorf("a state matching nothing: %d reads (want the bound %d), cut %v", *b.reads, view.MaxBuildReads, a.Cut)
}
}
// 1818: fp2 together with commit is refused, naming both.
func TestBuilderBuildRefusesFP2WithCommit(t *testing.T) {
r := both(fake{keyA: doneBuild}, fake{"netcheck": `{"v":3,"fp2":"` + fp2A + `","commit":"c1","path":"."}`})
_, err := run(t, r, view.BuilderBuildReadTool, map[string]any{"module": "netcheck", "fp2": fp2A, "commit": "c1"})
if err == nil || !strings.Contains(err.Error(), "fp2") || !strings.Contains(err.Error(), "commit") || !strings.Contains(err.Error(), "not both") {
t.Errorf("err = %v", err)
}
}
// 1681: indexer_candidates and indexer_candidate answer the stored value; a pull request's fp2 per touched module shows.
func TestCandidateToolsAnswerTheStoredValue(t *testing.T) {
cand := `{"v":1,"group":"feat/x","base":3,"heads":"h","pulls":[{"modules":["zsh"],"fps":{"zsh":{"fp2":"` + fp2B + `","manifest":"m"}}}]}`
other := `{"v":1,"group":"fix","base":3,"heads":"h","pulls":[{"modules":["netcheck"]}]}`
r := fake{"feat/x": cand, "fix": other, "bad": "{"}.reader()
r.CandidatesState = r.State
out, err := run(t, r, view.CandidateReadTool, map[string]any{"group": "feat/x"})
if err != nil || string(out.(json.RawMessage)) != cand || !strings.Contains(string(out.(json.RawMessage)), fp2B) {
t.Fatalf("candidate = %v, %v", out, err)
}
if _, err := run(t, r, view.CandidateReadTool, map[string]any{"group": "gone"}); err == nil || !strings.Contains(err.Error(), "no candidate gone") {
t.Errorf("err = %v", err)
}
out, err = run(t, r, view.CandidatesReadTool, map[string]any{})
m := out.(map[string]any)
if err != nil || len(m["candidates"].(map[string]json.RawMessage)) != 2 || len(m["unreadable"].([]string)) != 1 {
t.Errorf("all = %v, %v", m, err)
}
out, _ = run(t, r, view.CandidatesReadTool, map[string]any{"module": "zsh"})
if c := out.(map[string]any)["candidates"].(map[string]json.RawMessage); len(c) != 1 || c["feat/x"] == nil {
t.Errorf("by module = %v", c)
}
}