267 lines
11 KiB
Go
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)
|
|
}
|
|
}
|