Files
mesh-sdk/go/delivery/checker_test.go
T
jschoubben 78364e318b
mesh/repo-check head 78364e318bdc merged into trunk 07a65927c362 — pass: its merge-check.sh passed, each part in its toolchain: merge-check.sh (typescript)…
mesh/delivery-group group feat/the-checker rejected: composed together: 1 problem(s) the change brings; the first: mesh-delivery-checker reads "mesh-controller…
mesh/merge-gate head 78364e318bdc merged into trunk 07a65927c362 — fail: group feat/the-checker composed: 1 problem(s) the change brings; the first: mesh-d…
mesh/delivery superseded: a newer head of the same pull request
Put the module trees of a merge into go/buildstep for both fetch steps, and let a preview name the conflicts of a head that does not merge (issues 1675, 1676, 1445)
2026-10-21 21:21:48 +02:00

593 lines
26 KiB
Go

package delivery
import (
"encoding/json"
"errors"
"strings"
"testing"
"time"
)
func passVerdict() Verdict {
return Verdict{
Group: "feat/x",
Heads: "4b5491557853",
Trunk: map[string]string{"novox/mesh-sdk": "aaaa", "novox/mesh-control": "bbbb"},
State: VerdictPass,
Levels: map[string]LevelResult{LevelUnit: {Run: 12, Kept: 301}, LevelIntegration: {Run: 3, Kept: 40}},
Took: Took{Queue: 1200, Clone: 800, Unit: 950, IntegrationSetup: 2100, IntegrationRun: 7400, Composition: 3300},
Machines: []string{"ace", "g14", "novox", "shanks"},
SDK: "v0.1.14-0.20261019091011-aa68b67389c9",
}
}
func verdictSamples() map[string]Verdict {
running := Verdict{Group: "feat/x", Heads: "4b5491557853", Trunk: map[string]string{"novox/mesh-sdk": "aaaa"}, State: VerdictRunning}
failTest := passVerdict()
failTest.State, failTest.Failed = VerdictFail, FailedFirst{Test: "novox/mesh-sdk/go/delivery TestHeadsHash"}
failTest.Levels = map[string]LevelResult{LevelUnit: {Run: 1}}
failMachine := passVerdict()
failMachine.State, failMachine.Failed = VerdictFail, FailedFirst{Machine: "ace", Words: "path /etc/x is owned by two modules"}
return map[string]Verdict{"running": running, "pass": passVerdict(), "fail by a test": failTest, "fail by a machine": failMachine}
}
func keptSample() Kept {
return Kept{State: VerdictPass, Repository: "novox/mesh-sdk", Package: "git.novox.be/novox/mesh-sdk/go/delivery", Level: LevelUnit,
Tests: 41, Took: 190, Toolchain: "1.26.0", At: time.Date(2026, 10, 20, 2, 0, 0, 0, time.UTC)}
}
// 1359/AC1: the key is <escaped group>.<heads hash>; parsing gives back the group and the hash.
func TestCheckerVerdictKey(t *testing.T) {
heads := []Head{{Repository: "novox/mesh-sdk", Number: 36, Head: "aaaa"}}
hh, _ := HeadsHash(heads)
for _, group := range []string{"feat/x", "fix/1.2", "plain", "odd name=1"} {
key, err := CheckerVerdictKey(group, hh)
if err != nil {
t.Fatal(err)
}
if want := EscapeName(group) + "." + hh; key != want {
t.Errorf("key = %q, want %q", key, want)
}
g, h, err := ParseCheckerVerdictKey(key)
if err != nil || g != group || h != hh {
t.Errorf("parse(%q) = %q, %q, %v", key, g, h, err)
}
}
if k, _ := CheckerVerdictKey("feat/x", hh); !strings.HasPrefix(k, "feat/x.") {
t.Errorf("a group with a slash is not kept readable: %q", k)
}
if _, err := CheckerVerdictKey("", hh); err == nil {
t.Error("an empty group accepted")
}
for _, bad := range []string{"", "feat/x", "feat/x.", "feat/x.abc", "feat/x." + strings.ToUpper(hh), ".4b5491557853", "a.b.4b5491557853", "feat/x.4b5491557853a"} {
if _, _, err := ParseCheckerVerdictKey(bad); err == nil || !strings.Contains(err.Error(), "\""+bad+"\"") {
t.Errorf("ParseCheckerVerdictKey(%q) = %v, want a refusal naming it", bad, err)
}
}
if _, err := CheckerVerdictKey("feat/x", "abc"); err == nil || !strings.Contains(err.Error(), "abc") {
t.Errorf("a short heads hash: %v", err)
}
}
// 1359/AC2: a verdict in each state round-trips, with times per part in milliseconds, the tests run and kept per level,
// the machines composed and the mesh-sdk version composed with.
func TestVerdictRoundTrip(t *testing.T) {
for name, v := range verdictSamples() {
data, err := EncodeVerdict(v)
if err != nil {
t.Fatalf("%s: %v", name, err)
}
if !strings.HasPrefix(string(data), `{"v":1,`) {
t.Errorf("%s at %s", name, data)
}
got, err := DecodeVerdict(data)
v.V = Version
if err != nil || !SameVerdict(got, v) {
t.Errorf("%s: %+v %v", name, got, err)
}
}
data, _ := EncodeVerdict(passVerdict())
for _, field := range []string{`"queue":1200`, `"clone":800`, `"unit":950`, `"integration-setup":2100`, `"integration-run":7400`, `"composition":3300`, `"run":12`, `"kept":301`, `"machines":["ace","g14","novox","shanks"]`, `"sdk":"v0.1.14`} {
if !strings.Contains(string(data), field) {
t.Errorf("the value lacks %s: %s", field, data)
}
}
}
// 1359/AC3: a fail that names neither a test nor a machine, and a pass whose levels are missing, are refused.
func TestVerdictChecked(t *testing.T) {
cases := map[string]struct {
edit func(*Verdict)
words []string
}{
"fail naming nothing": {func(v *Verdict) { v.State, v.Failed = VerdictFail, FailedFirst{} }, []string{"test", "machine"}},
"fail with words only": {func(v *Verdict) { v.State, v.Failed = VerdictFail, FailedFirst{Words: "x"} }, []string{"test", "machine"}},
"pass without levels": {func(v *Verdict) { v.Levels = nil }, []string{"level"}},
"pass without unit": {func(v *Verdict) { delete(v.Levels, LevelUnit) }, []string{"unit"}},
"pass without integration": {func(v *Verdict) { delete(v.Levels, LevelIntegration) }, []string{"integration"}},
"pass naming a failure": {func(v *Verdict) { v.Failed = FailedFirst{Test: "T"} }, []string{"pass"}},
"an unknown state": {func(v *Verdict) { v.State = "maybe" }, []string{"maybe"}},
"an unknown level": {func(v *Verdict) { v.Levels["e2e"] = LevelResult{} }, []string{"e2e"}},
"a negative time": {func(v *Verdict) { v.Took.Clone = -1 }, []string{"clone"}},
"a bad heads hash": {func(v *Verdict) { v.Heads = "xyz" }, []string{"xyz"}},
"no group": {func(v *Verdict) { v.Group = "" }, []string{"group"}},
"a trunk head left empty": {func(v *Verdict) { v.Trunk["novox/mesh-sdk"] = "" }, []string{"novox/mesh-sdk"}},
"a negative count": {func(v *Verdict) { v.Levels[LevelUnit] = LevelResult{Run: -1} }, []string{"unit"}},
}
for name, c := range cases {
v := passVerdict()
c.edit(&v)
_, err := EncodeVerdict(v)
if err == nil {
t.Errorf("%s accepted", name)
continue
}
for _, w := range c.words {
if !strings.Contains(err.Error(), w) {
t.Errorf("%s: error %q does not name %q", name, err, w)
}
}
}
if _, err := DecodeVerdict([]byte(`{"v":1,"group":"g","heads":"4b5491557853","state":"fail"}`)); err == nil {
t.Error("decoded a fail naming nothing")
}
}
// 1359/AC4: the kept key is <level>.<hex digest>; a level other than unit or integration, or a digest that is not hex,
// is refused, naming it.
func TestKeptKey(t *testing.T) {
digest := strings.Repeat("ab12", 16)
for _, level := range []string{LevelUnit, LevelIntegration} {
key, err := KeptKey(level, digest)
if err != nil || key != level+"."+digest {
t.Fatalf("key = %q, %v", key, err)
}
l, d, err := ParseKeptKey(key)
if err != nil || l != level || d != digest {
t.Errorf("parse = %q, %q, %v", l, d, err)
}
}
if _, err := KeptKey("e2e", digest); err == nil || !strings.Contains(err.Error(), "e2e") {
t.Errorf("a level e2e: %v", err)
}
if _, err := KeptKey("", digest); err == nil {
t.Error("an empty level accepted")
}
for _, d := range []string{"", "xyz", "ABCD", "ab 12", "sha256:abcd", "ab12.cd"} {
if _, err := KeptKey(LevelUnit, d); err == nil || !strings.Contains(err.Error(), "\""+d+"\"") {
t.Errorf("KeptKey(unit, %q) = %v, want a refusal naming it", d, err)
}
}
for _, bad := range []string{"", "unit", "unit.", "e2e." + digest, "unit.xyz", "unit." + digest + ".x", "." + digest} {
if _, _, err := ParseKeptKey(bad); err == nil || !strings.Contains(err.Error(), "\""+bad+"\"") {
t.Errorf("ParseKeptKey(%q) = %v, want a refusal naming it", bad, err)
}
}
if CheckerVerdictsBucket != "mesh-delivery-checker_verdicts" || CheckerVerdictsHistory != 5 ||
CheckerKeptBucket != "mesh-delivery-checker_kept" || CheckerKeptHistory != 1 || CheckerModule != "mesh-delivery-checker" {
t.Errorf("buckets %s %d %s %d", CheckerVerdictsBucket, CheckerVerdictsHistory, CheckerKeptBucket, CheckerKeptHistory)
}
}
// 1359/AC5: a kept value that is not a pass cannot be encoded; a failure is never kept (ADR 0444 clause 1).
func TestAFailureIsNeverKept(t *testing.T) {
data, err := EncodeKept(keptSample())
if err != nil {
t.Fatal(err)
}
got, err := DecodeKept(data)
want := keptSample()
want.V = Version
if err != nil || !SameKept(got, want) {
t.Errorf("round trip: %+v %v", got, err)
}
for _, state := range []string{VerdictFail, VerdictRunning, "", "skipped"} {
k := keptSample()
k.State = state
if _, err := EncodeKept(k); err == nil || !strings.Contains(err.Error(), "pass") {
t.Errorf("a kept value in state %q: %v", state, err)
}
}
if _, err := DecodeKept([]byte(`{"v":1,"state":"fail","repository":"novox/mesh-sdk","package":"p","level":"unit","tests":1,"took":1,"toolchain":"1.26.0","at":"2026-10-20T02:00:00Z"}`)); err == nil {
t.Error("decoded a kept failure")
}
for name, edit := range map[string]func(*Kept){
"a level e2e": func(k *Kept) { k.Level = "e2e" },
"no package": func(k *Kept) { k.Package = "" },
"a bad repository": func(k *Kept) { k.Repository = "nope" },
"no toolchain": func(k *Kept) { k.Toolchain = "" },
"no time": func(k *Kept) { k.At = time.Time{} },
"negative tests": func(k *Kept) { k.Tests = -1 },
"a negative took": func(k *Kept) { k.Took = -1 },
} {
k := keptSample()
edit(&k)
if _, err := EncodeKept(k); err == nil {
t.Errorf("%s accepted", name)
}
}
}
// 1359/AC6: a value of another version, or none, is refused with ErrUnknownVersion.
func TestCheckerVersions(t *testing.T) {
for name, dec := range map[string]func([]byte) error{
"verdict": func(b []byte) error { _, err := DecodeVerdict(b); return err },
"kept": func(b []byte) error { _, err := DecodeKept(b); return err },
} {
for _, in := range []string{`{"v":2,"state":"pass"}`, `{"state":"pass"}`} {
if err := dec([]byte(in)); !errors.Is(err, ErrUnknownVersion) {
t.Errorf("%s decode of %s = %v", name, in, err)
}
}
}
}
// oldVerdictJSON is a verdict as the SDK before issue 1672 wrote it (copied from EncodeVerdict of passVerdict's shape
// at the commit before this change): no skipped count, no composition, no over-bound names.
const oldVerdictJSON = `{"v":1,"group":"feat/x","heads":"4b5491557853","trunk":{"novox/mesh-control":"bbbb","novox/mesh-sdk":"aaaa"},"state":"pass","levels":{"integration":{"run":3,"kept":40},"unit":{"run":12,"kept":301}},"took":{"queue":1200,"clone":800,"unit":950,"integration-setup":2100,"integration-run":7400,"composition":3300},"machines":["ace","g14","novox","shanks"],"sdk":"v0.1.14-0.20261019091011-aa68b67389c9"}`
// oldVerdict is the reader of today's shape: the struct as it stood before the new fields.
type oldVerdict struct {
V int `json:"v"`
Group string `json:"group"`
Heads string `json:"heads"`
State string `json:"state"`
Levels map[string]struct{ Run, Kept int }
Machines []string
SDK string
}
// 1672/AC1: the skipped count, the composition and the over-bound names round trip and appear in the bytes.
func TestVerdictCarriesSkippedCompositionAndOverBound(t *testing.T) {
v := passVerdict()
v.Levels[LevelUnit] = LevelResult{Run: 12, Kept: 301, Skipped: 4}
v.Composition = &Composition{State: CompositionPass, SDK: v.SDK, Machines: []string{"ace", "novox"}}
v.OverBound = []string{LevelIntegration}
data, err := EncodeVerdict(v)
if err != nil {
t.Fatal(err)
}
for _, field := range []string{`"skipped":4`, `"composition":{"state":"pass"`, `"over-bound":["integration"]`} {
if !strings.Contains(string(data), field) {
t.Errorf("the value lacks %s: %s", field, data)
}
}
got, err := DecodeVerdict(data)
v.V = Version
if err != nil || !SameVerdict(got, v) || got.Levels[LevelUnit].Skipped != 4 || got.Composition == nil || got.Composition.State != CompositionPass {
t.Errorf("round trip = %+v, %v", got, err)
}
notRun := passVerdict()
notRun.Composition = &Composition{State: CompositionNotRun}
if _, err := EncodeVerdict(notRun); err != nil {
t.Errorf("a composition not yet run refused: %v", err)
}
failed := passVerdict()
failed.State, failed.Failed = VerdictFail, FailedFirst{Machine: "ace", Words: "x"}
failed.Composition = &Composition{State: CompositionFail, Words: "path /etc/x is owned by two modules", SDK: failed.SDK, Machines: []string{"ace"}}
if _, err := EncodeVerdict(failed); err != nil {
t.Errorf("a failed composition refused: %v", err)
}
}
// 1672/AC1: an invalid new value is refused, naming the field.
func TestVerdictNewFieldsChecked(t *testing.T) {
cases := map[string]struct {
edit func(*Verdict)
words []string
}{
"a negative skipped count": {func(v *Verdict) { v.Levels[LevelUnit] = LevelResult{Run: 1, Skipped: -1} }, []string{"unit", "negative"}},
"an unknown composition": {func(v *Verdict) { v.Composition = &Composition{State: "maybe"} }, []string{"composition", "maybe"}},
"a composition with no state": {func(v *Verdict) { v.Composition = &Composition{} }, []string{"composition"}},
"a failed composition without words": {func(v *Verdict) { v.Composition = &Composition{State: CompositionFail} }, []string{"composition", "words"}},
"a passed composition without the sdk": {func(v *Verdict) { v.Composition = &Composition{State: CompositionPass, Machines: []string{"ace"}} }, []string{"composition", "sdk"}},
"a not-run composition naming machines": {func(v *Verdict) { v.Composition = &Composition{State: CompositionNotRun, Machines: []string{"ace"}} }, []string{"composition", "not run"}},
"an unknown over-bound level": {func(v *Verdict) { v.OverBound = []string{"e2e"} }, []string{"e2e"}},
"an over-bound level named twice": {func(v *Verdict) { v.OverBound = []string{LevelUnit, LevelUnit} }, []string{"over-bound", "unit"}},
}
for name, c := range cases {
v := passVerdict()
c.edit(&v)
_, err := EncodeVerdict(v)
if err == nil {
t.Errorf("%s accepted", name)
continue
}
for _, w := range c.words {
if !strings.Contains(err.Error(), w) {
t.Errorf("%s: error %q does not name %q", name, err, w)
}
}
}
}
// 1672/AC2: a value written by the SDK before this change decodes with the new reader, with the new fields empty, and
// the new SDK writes the same bytes for it; a value with the new fields decodes under a reader of today's shape.
func TestVerdictOldAndNewReadersAgree(t *testing.T) {
v, err := DecodeVerdict([]byte(oldVerdictJSON))
if err != nil {
t.Fatalf("the new reader of an old value: %v", err)
}
if v.Composition != nil || v.OverBound != nil || v.Levels[LevelUnit].Skipped != 0 || v.Levels[LevelUnit].Run != 12 {
t.Errorf("an old value grew fields: %+v", v)
}
again, err := EncodeVerdict(v)
if err != nil || string(again) != oldVerdictJSON {
t.Errorf("the new SDK writes %s, %v for an old value, want today's bytes", again, err)
}
n := passVerdict()
n.Levels[LevelUnit] = LevelResult{Run: 12, Kept: 301, Skipped: 4}
n.Composition = &Composition{State: CompositionNotRun}
n.OverBound = []string{LevelUnit}
data, err := EncodeVerdict(n)
if err != nil {
t.Fatal(err)
}
var ov oldVerdict
if err := json.Unmarshal(data, &ov); err != nil || ov.Group != "feat/x" || ov.State != VerdictPass || ov.Levels[LevelUnit].Run != 12 || ov.SDK == "" {
t.Errorf("the old reader of a new value: %+v, %v", ov, err)
}
}
func sampleCheckerOwed() Owed {
return Owed{Act: OwedStatus, Context: StatusContextCheck, State: "success", Target: "https://mesh.example/delivery/check/36",
Text: "pass 4b5491557853 on aaaa: unit 12 run 301 kept, integration 3 run 40 kept; composition: not yet run"}
}
// 1673/AC1: the checker's owed bucket is the checker module plus _owed, and its key is the gate's owed key with the act
// status=mesh/check.
func TestCheckerOwedKeyAndBucket(t *testing.T) {
if CheckerOwedBucket != "mesh-delivery-checker_owed" || CheckerOwedHistory != 5 || PreviewsBucket != "mesh-delivery-checker_previews" || PreviewsHistory != 5 {
t.Errorf("buckets %s %d %s %d", CheckerOwedBucket, CheckerOwedHistory, PreviewsBucket, PreviewsHistory)
}
key, err := CheckerOwedKey("novox/mesh-sdk", 36, "aaaa")
if err != nil || key != "novox/mesh-sdk.36.aaaa.status=mesh/check" {
t.Fatalf("key = %q, %v", key, err)
}
repo, number, head, err := ParseCheckerOwedKey(key)
if err != nil || repo != "novox/mesh-sdk" || number != 36 || head != "aaaa" {
t.Errorf("parse = %q %d %q %v", repo, number, head, err)
}
other, _ := OwedKey("novox/mesh-sdk", 36, "aaaa", "status=mesh/merge-gate")
for _, bad := range []string{other, "novox/mesh-sdk.36.aaaa.view", "novox/mesh-sdk.36.aaaa.note", "novox/mesh-sdk.36.aaaa.comment.1", ""} {
if _, _, _, err := ParseCheckerOwedKey(bad); err == nil {
t.Errorf("checker owed key %q accepted", bad)
}
}
if _, err := CheckerOwedKey("nope", 36, "aaaa"); err == nil {
t.Error("a bad repository accepted")
}
}
// 1673/AC2: an owed value with the context mesh/check is accepted and an unknown context is refused, in the act and in the value.
func TestOwedAcceptsMeshCheckAndRefusesAnUnknownContext(t *testing.T) {
for _, ctx := range []string{StatusContextCheck, StatusContextMergeGate} {
if kind, got, err := ParseOwedAct("status=" + ctx); err != nil || kind != OwedStatus || got != ctx {
t.Errorf("ParseOwedAct(status=%s) = %q %q %v", ctx, kind, got, err)
}
}
for _, bad := range []string{"status=mesh/other", "status=check", "status=mesh/Check"} {
if _, err := OwedKey("novox/mesh-sdk", 36, "aaaa", bad); err == nil || !strings.Contains(err.Error(), bad[len("status="):]) {
t.Errorf("owed act %q: %v, want a refusal naming the context", bad, err)
}
}
data, err := EncodeCheckerOwed(sampleCheckerOwed())
if err != nil {
t.Fatal(err)
}
if got, err := DecodeCheckerOwed(data); err != nil || !SameOwed(got, sampleCheckerOwed()) && got.Context != StatusContextCheck {
t.Errorf("round trip = %+v, %v", got, err)
}
unknown := sampleCheckerOwed()
unknown.Context = "mesh/other"
if _, err := EncodeOwed(unknown); err == nil || !strings.Contains(err.Error(), "mesh/other") {
t.Errorf("an owed status of the context mesh/other: %v", err)
}
// The checker's bucket holds only its own status.
gate := sampleCheckerOwed()
gate.Context = StatusContextMergeGate
for name, o := range map[string]Owed{"the gate's context": gate, "a view": {Act: OwedView, Text: "x"}, "a note": {Act: OwedNote, Text: "x", Ref: "mesh-plan"}} {
if _, err := EncodeCheckerOwed(o); err == nil {
t.Errorf("the checker's bucket takes %s", name)
}
}
for _, state := range []string{"pending", "success", "failure"} {
o := sampleCheckerOwed()
o.State = state
if _, err := EncodeCheckerOwed(o); err != nil {
t.Errorf("state %s: %v", state, err)
}
}
}
func samplePreview() Preview {
return Preview{Repository: "novox/mesh-delivery", Head: "aaaa", Trunk: "bbbb", Merge: "cccc",
Modules: map[string]PreviewModule{
"modules/mesh-delivery-checker": {Tree: "t1", Manifest: strings.Repeat("1", 64), Locks: map[string]string{"go.sum": strings.Repeat("2", 64)}},
"modules/mesh-delivery-view": {Tree: "t2", Manifest: strings.Repeat("3", 64)},
}}
}
// 1673/AC3: the previews key is <repository>.<head>.<trunk> and parses back.
func TestPreviewKey(t *testing.T) {
key, err := PreviewKey("novox/mesh-delivery", "aaaa", "bbbb")
if err != nil || key != "novox/mesh-delivery.aaaa.bbbb" {
t.Fatalf("key = %q, %v", key, err)
}
repo, head, trunk, err := ParsePreviewKey(key)
if err != nil || repo != "novox/mesh-delivery" || head != "aaaa" || trunk != "bbbb" {
t.Errorf("parse = %q %q %q %v", repo, head, trunk, err)
}
for _, bad := range [][3]string{{"nope", "aaaa", "bbbb"}, {"novox/x", "", "bbbb"}, {"novox/x", "aaaa", ""}, {"novox/x", "a.b", "bbbb"}, {"novox/x", "aaaa", "b b"}} {
if _, err := PreviewKey(bad[0], bad[1], bad[2]); err == nil {
t.Errorf("PreviewKey%v accepted", bad)
}
}
for _, bad := range []string{"", "novox/x.aaaa", "novox/x.aaaa.bbbb.cccc", "nope.aaaa.bbbb", "novox/x..bbbb", "novox/x.aaaa."} {
if _, _, _, err := ParsePreviewKey(bad); err == nil || !strings.Contains(err.Error(), "\""+bad+"\"") {
t.Errorf("ParsePreviewKey(%q) = %v, want a refusal naming it", bad, err)
}
}
}
// 1673/AC4: a preview round trips, and an invalid one is refused naming the field.
func TestPreviewRoundTripAndRefusals(t *testing.T) {
data, err := EncodePreview(samplePreview())
if err != nil {
t.Fatal(err)
}
if !strings.HasPrefix(string(data), `{"v":1,`) {
t.Errorf("at %s", data)
}
got, err := DecodePreview(data)
want := samplePreview()
want.V = Version
if err != nil || !SamePreview(got, want) {
t.Errorf("round trip = %+v, %v", got, err)
}
if _, err := EncodePreview(Preview{Repository: "novox/x", Head: "aaaa", Trunk: "bbbb", Merge: "cccc"}); err != nil {
t.Errorf("a preview of a head that changes no module refused: %v", err)
}
cases := map[string]struct {
edit func(*Preview)
words []string
}{
"no repository": {func(p *Preview) { p.Repository = "" }, []string{"repository"}},
"a bad repository": {func(p *Preview) { p.Repository = "nope" }, []string{"nope"}},
"no head": {func(p *Preview) { p.Head = "" }, []string{"head"}},
"no trunk": {func(p *Preview) { p.Trunk = "" }, []string{"trunk"}},
"no merge": {func(p *Preview) { p.Merge = "" }, []string{"merge"}},
"a module with no tree": {func(p *Preview) {
p.Modules["modules/mesh-delivery-view"] = PreviewModule{Manifest: strings.Repeat("3", 64)}
}, []string{"modules/mesh-delivery-view", "tree"}},
"a bad manifest digest": {func(p *Preview) {
p.Modules["modules/mesh-delivery-view"] = PreviewModule{Tree: "t2", Manifest: "xyz"}
}, []string{"modules/mesh-delivery-view", "xyz"}},
"an empty directory": {func(p *Preview) { p.Modules[""] = PreviewModule{Tree: "t", Manifest: strings.Repeat("3", 64)} }, []string{"directory"}},
"a lock with no hash": {func(p *Preview) {
p.Modules["modules/mesh-delivery-view"] = PreviewModule{Tree: "t2", Manifest: strings.Repeat("3", 64), Locks: map[string]string{"go.sum": ""}}
}, []string{"modules/mesh-delivery-view", "go.sum"}},
}
for name, c := range cases {
p := samplePreview()
c.edit(&p)
_, err := EncodePreview(p)
if err == nil {
t.Errorf("%s accepted", name)
continue
}
for _, w := range c.words {
if !strings.Contains(err.Error(), w) {
t.Errorf("%s: error %q does not name %q", name, err, w)
}
}
}
if _, err := DecodePreview([]byte(`{"v":2,"repository":"novox/x"}`)); !errors.Is(err, ErrUnknownVersion) {
t.Errorf("a preview of version 2: %v", err)
}
if _, err := DecodePreview([]byte(`{"v":1,"repository":"novox/x","head":"a","trunk":"b"}`)); err == nil {
t.Error("decoded a preview with no merge")
}
}
// 1673/AC5: a reader of today's checker shapes ignores a field it does not know, and the checker's owed value is the
// gate's shape, so the gate's reader reads it (as indexer2_test.go does for the indexer's values).
func TestCheckerOwedIsReadableByTheGatesReaderAndAnOldOwedReaderIgnoresNewFields(t *testing.T) {
data, err := EncodeCheckerOwed(sampleCheckerOwed())
if err != nil {
t.Fatal(err)
}
var old struct {
V int `json:"v"`
Act string `json:"act"`
Context string `json:"context"`
State string `json:"state"`
}
if err := json.Unmarshal(data, &old); err != nil || old.Act != OwedStatus || old.Context != "mesh/check" {
t.Errorf("the old reader: %+v %v", old, err)
}
withNew := strings.Replace(string(data), `{"v":1,`, `{"v":1,"a-field-of-tomorrow":7,`, 1)
if _, err := DecodeOwed([]byte(withNew)); err != nil {
t.Errorf("an owed value with a field of tomorrow: %v", err)
}
p, _ := EncodePreview(samplePreview())
withNew = strings.Replace(string(p), `{"v":1,`, `{"v":1,"a-field-of-tomorrow":7,`, 1)
if _, err := DecodePreview([]byte(withNew)); err != nil {
t.Errorf("a preview with a field of tomorrow: %v", err)
}
}
// 1673/AC2 (review): the gate's recheck owes a pending status=mesh/repo-check (mesh-catalog mesh-delivery-gate owed.go,
// checks.go ctxRepo); its key and value round trip, and the checker's bucket still refuses it.
func TestTheGatesRecheckStatusRoundTripsAndTheCheckersBucketRefusesIt(t *testing.T) {
key, err := OwedKey("novox/mesh-sdk", 36, "aaaa", "status=mesh/repo-check")
if err != nil || key != "novox/mesh-sdk.36.aaaa.status=mesh/repo-check" {
t.Fatalf("key = %q, %v", key, err)
}
if _, _, _, act, err := ParseOwedKey(key); err != nil || act != "status=mesh/repo-check" {
t.Errorf("parse = %q, %v", act, err)
}
o := Owed{Act: OwedStatus, Context: StatusContextRepoCheck, State: "pending", Target: "https://mesh.example/delivery/36", Text: "checking again"}
data, err := EncodeOwed(o)
if err != nil {
t.Fatal(err)
}
if got, err := DecodeOwed(data); err != nil || !SameOwed(got, o) && got.Context != o.Context {
t.Errorf("round trip = %+v, %v", got, err)
}
if _, err := EncodeCheckerOwed(o); err == nil {
t.Error("the checker's bucket takes the gate's recheck status")
}
if _, _, _, err := ParseCheckerOwedKey(key); err == nil {
t.Error("a checker owed key of the recheck status accepted")
}
}
// 1445/AC5 (layout): a head that does not merge is a preview naming its conflicting files, with no merge commit and no
// modules; a preview with conflicts and a merge or modules is refused; a preview of today's shape (no conflicts)
// decodes with none, and a reader of today's shape reads one with conflicts.
func TestPreviewOfAHeadInConflict(t *testing.T) {
c := Preview{Repository: "novox/mesh-delivery", Head: "aaaa", Trunk: "bbbb", Conflicts: []string{"a.txt", "modules/m/module.json"}}
data, err := EncodePreview(c)
if err != nil {
t.Fatal(err)
}
if !strings.Contains(string(data), `"conflicts":["a.txt","modules/m/module.json"]`) || strings.Contains(string(data), `"merge"`) {
t.Errorf("at %s", data)
}
got, err := DecodePreview(data)
c.V = Version
if err != nil || !SamePreview(got, c) {
t.Errorf("round trip = %+v, %v", got, err)
}
withMerge := c
withMerge.Merge = "cccc"
withModules := c
withModules.Modules = samplePreview().Modules
emptyPath := c
emptyPath.Conflicts = []string{""}
for name, p := range map[string]Preview{"a merge commit": withMerge, "modules": withModules, "an empty conflicting path": emptyPath} {
if _, err := EncodePreview(p); err == nil || !strings.Contains(err.Error(), "onflict") {
t.Errorf("a preview in conflict with %s: %v", name, err)
}
}
// today's bytes: a clean preview has no conflicts and decodes with none.
clean, _ := EncodePreview(samplePreview())
if strings.Contains(string(clean), "conflicts") {
t.Errorf("a clean preview carries conflicts: %s", clean)
}
if p, err := DecodePreview(clean); err != nil || p.Conflicts != nil {
t.Errorf("a clean preview decodes with conflicts %v, %v", p.Conflicts, err)
}
var old struct {
V int `json:"v"`
Repository string `json:"repository"`
Head string `json:"head"`
Trunk string `json:"trunk"`
Merge string `json:"merge"`
}
if err := json.Unmarshal(data, &old); err != nil || old.Head != "aaaa" || old.Merge != "" {
t.Errorf("the old reader: %+v %v", old, err)
}
}