diff --git a/modules/mesh-delivery-indexer/cmd/mesh-judge/main.go b/modules/mesh-delivery-indexer/cmd/mesh-judge/main.go index 3d42826..8cd3038 100644 --- a/modules/mesh-delivery-indexer/cmd/mesh-judge/main.go +++ b/modules/mesh-delivery-indexer/cmd/mesh-judge/main.go @@ -12,8 +12,6 @@ import ( "fmt" "io" "os" - "sort" - "strings" "git.novox.be/novox/mesh-sdk/go/composer" @@ -23,9 +21,6 @@ import ( // Package is this command's import path in the indexer's Go module (the directory delivery.ComposeJob's Dir names). const Package = "mesh-delivery-indexer/cmd/mesh-judge" -// Between names, in a case's verdict, the problems between manifests that name no module of the case. -const Between = "(between manifests)" - func main() { os.Exit(run(os.Args[1:], os.Stdin, os.Stdout, os.Stderr)) } func run(args []string, stdin io.Reader, stdout, stderr io.Writer) int { @@ -51,7 +46,7 @@ func run(args []string, stdin io.Reader, stdout, stderr io.Writer) int { fmt.Fprintln(stderr, "mesh-judge:", err) return 1 } - v, err := Judge(r) + v, err := judge.Request(r) if err != nil { fmt.Fprintln(stderr, "mesh-judge:", err) return 1 @@ -64,39 +59,3 @@ func run(args []string, stdin io.Reader, stdout, stderr io.Writer) int { fmt.Fprintln(stdout, string(out)) return 0 } - -// Judge judges every case of r: each case's manifests as one catalogue (judge.Shelf), every manifest accepted or -// refused with its problems; problems between manifests that name none of them under Between. The verdict's SDK -// names this judge (judge.Identity). A manifest left as a digest is refused as a request it cannot run. -func Judge(r composer.Request) (composer.Verdict, error) { - v := composer.Verdict{V: composer.Version, SDK: judge.Identity()} - seen := map[string]bool{} - for _, c := range r.Cases { - if seen[c.Name] { - return composer.Verdict{}, fmt.Errorf("the case %q is named twice", c.Name) - } - seen[c.Name] = true - raws := map[string][]byte{} - for name, m := range c.Manifests { - if len(m.Raw) == 0 { - return composer.Verdict{}, fmt.Errorf("%w: %s (digest %s)", composer.ErrUnfilled, name, m.Digest) - } - raws[name] = m.Raw - } - refused, unnamed := judge.Shelf(raws, r.Predefined, 0) - cv := composer.CaseVerdict{Name: c.Name, Manifests: map[string]composer.ManifestRead{}} - for name := range raws { - if problems := refused[name]; len(problems) > 0 { - cv.Manifests[name] = composer.ManifestRead{Refused: strings.Join(problems, "\n")} - continue - } - cv.Manifests[name] = composer.ManifestRead{Accepted: true} - } - if len(unnamed) > 0 { - sort.Strings(unnamed) - cv.Manifests[Between] = composer.ManifestRead{Refused: strings.Join(unnamed, "\n")} - } - v.Cases = append(v.Cases, cv) - } - return v, nil -} diff --git a/modules/mesh-delivery-indexer/cmd/mesh-judge/main_test.go b/modules/mesh-delivery-indexer/cmd/mesh-judge/main_test.go deleted file mode 100644 index 0edf390..0000000 --- a/modules/mesh-delivery-indexer/cmd/mesh-judge/main_test.go +++ /dev/null @@ -1,40 +0,0 @@ -package main - -import ( - "bytes" - "encoding/json" - "strings" - "testing" - - "git.novox.be/novox/mesh-sdk/go/composer" -) - -// The command the checker runs (novox/hq ADR 0578 §2): a composer request in, one line of a composer verdict out, each -// manifest accepted or refused with its problems; a request it cannot run exits non-zero. -func TestTheCommandJudgesARequest(t *testing.T) { - r := composer.Request{V: composer.Version, Cases: []composer.Case{{Name: "change", Manifests: map[string]composer.Manifest{ - "a": {Raw: json.RawMessage(`{"module":"a","version":"1"}`)}, - "b": {Raw: json.RawMessage(`{"module":"b","version":"1","requires":["amqp"]}`)}, - }}}} - raw, _ := json.Marshal(r) - var out, errs bytes.Buffer - if code := run(nil, bytes.NewReader(raw), &out, &errs); code != 0 { - t.Fatalf("exit %d: %s", code, errs.String()) - } - var v composer.Verdict - if err := json.Unmarshal(out.Bytes(), &v); err != nil || v.V != composer.Version || len(v.Cases) != 1 || v.SDK == "" { - t.Fatalf("verdict %s: %v", out.String(), err) - } - c := v.Cases[0] - if !c.Manifests["a"].Accepted || c.Manifests["b"].Accepted || !strings.Contains(c.Manifests["b"].Refused, `requires "amqp"`) { - t.Fatalf("case: %+v", c) - } - r.Cases[0].Manifests["c"] = composer.Manifest{Digest: strings.Repeat("a", 64)} - raw, _ = json.Marshal(r) - if code := run(nil, bytes.NewReader(raw), &out, &errs); code == 0 { - t.Fatal("a manifest left as a digest was judged") - } - if code := run(nil, strings.NewReader("{"), &out, &errs); code == 0 { - t.Fatal("a request that is no JSON was judged") - } -} diff --git a/modules/mesh-delivery-indexer/internal/index/inputs.go b/modules/mesh-delivery-indexer/internal/index/inputs.go index d2e54a2..6623603 100644 --- a/modules/mesh-delivery-indexer/internal/index/inputs.go +++ b/modules/mesh-delivery-indexer/internal/index/inputs.go @@ -9,6 +9,8 @@ import ( "git.novox.be/novox/mesh-sdk/go/catalogue" "git.novox.be/novox/mesh-sdk/go/delivery" + + "mesh-delivery-indexer/judge" ) // The reasons a module has no fingerprint of the new form while the indexer writes none for any module (novox/hq @@ -67,7 +69,7 @@ func inputsOf(module, raw, tree string, locks map[string]string, base baseFP) mo // gives the merge's value (novox/hq ADR 0499 clause 2); a message that does not carry the object of each such path gives it // none, naming the paths (clause 3). Every other module keeps its directory's tree, whatever objects are given. func inputsOfNamed(module, raw, tree string, locks, objects map[string]string, base baseFP) moduleInputs { - m, err := catalogue.ParseManifestForInputs([]byte(raw)) + m, err := judge.ForInputs([]byte(raw)) if err != nil { return moduleInputs{None2: None2Unreadable + err.Error()} } diff --git a/modules/mesh-delivery-indexer/internal/index/modules.go b/modules/mesh-delivery-indexer/internal/index/modules.go index 199f125..1b0166e 100644 --- a/modules/mesh-delivery-indexer/internal/index/modules.go +++ b/modules/mesh-delivery-indexer/internal/index/modules.go @@ -7,8 +7,9 @@ import ( "slices" "strings" - "git.novox.be/novox/mesh-sdk/go/catalogue" "git.novox.be/novox/mesh-sdk/go/delivery" + + "mesh-delivery-indexer/judge" ) // applyMerged writes, for each module whose manifest the merged message carries (each directory of the merge commit that @@ -153,7 +154,7 @@ func (ix *Indexer) namedObjects(ctx context.Context, f delivery.Forge) (map[stri objects := map[string]map[string]string{} need := map[string][]string{} for dir, raw := range f.Manifests { - m, err := catalogue.ParseManifestForInputs([]byte(raw)) + m, err := judge.ForInputs([]byte(raw)) if err != nil { continue // inputsOfNamed says why } diff --git a/modules/mesh-delivery-indexer/internal/index/predefined_test.go b/modules/mesh-delivery-indexer/internal/index/predefined_test.go index d83f31f..a08ad11 100644 --- a/modules/mesh-delivery-indexer/internal/index/predefined_test.go +++ b/modules/mesh-delivery-indexer/internal/index/predefined_test.go @@ -19,9 +19,7 @@ func TestAManifestFallingBackToAPredefinedKeyIsRead(t *testing.T) { // 1751 t.Errorf("%s: inputs = %+v, want the manifest read and its inputs kept", module, in) } } -} - -func TestAPredefinedKeyOfAnotherTypeThanItsValueIsStillRefused(t *testing.T) { // 1751: the read judges what it can + // A predefined key of another type than its value is still refused: the read judges what it can. raw := strings.Replace(fixture(t, "catalogue-835bf22b-xterm"), `"predefined": "font-size"`, `"predefined": "font-size", "trusted": "yes"`, 1) if in := inputsOf("xterm", raw, "newtree", nil, func(string) (string, string) { return "", "none" }); in.Kept != nil { t.Errorf("inputs = %+v, want a manifest the catalogue refuses still refused", in) diff --git a/modules/mesh-delivery-indexer/internal/index/toolchains.go b/modules/mesh-delivery-indexer/internal/index/toolchains.go index 21e022f..a59e504 100644 --- a/modules/mesh-delivery-indexer/internal/index/toolchains.go +++ b/modules/mesh-delivery-indexer/internal/index/toolchains.go @@ -10,6 +10,8 @@ import ( "git.novox.be/novox/mesh-sdk/go/catalogue" "git.novox.be/novox/mesh-sdk/go/delivery" + + "mesh-delivery-indexer/judge" ) // The new-form fingerprint of a module is written, and written again with no merge, when what it is of changes @@ -385,7 +387,7 @@ func (ix *Indexer) manifestRaw(ctx context.Context, digest string) (string, cata if err != nil { return "", catalogue.Manifest{}, err } - m, err := catalogue.ParseManifestForInputs([]byte(kept.JSON)) + m, err := judge.ForInputs([]byte(kept.JSON)) return kept.JSON, m, err } diff --git a/modules/mesh-delivery-indexer/judge/judge.go b/modules/mesh-delivery-indexer/judge/judge.go index bab6eab..b14c448 100644 --- a/modules/mesh-delivery-indexer/judge/judge.go +++ b/modules/mesh-delivery-indexer/judge/judge.go @@ -24,6 +24,7 @@ import ( "sync" "git.novox.be/novox/mesh-sdk/go/catalogue" + "git.novox.be/novox/mesh-sdk/go/composer" ) // Manifest judges one manifest alone: decoded strictly, and every rule a manifest is held to by itself. The list of @@ -43,6 +44,12 @@ func Manifest(raw []byte, predefined catalogue.PredefinedList) (m catalogue.Mani return m, nil } +// ForInputs is Manifest worded as the strict parser words a refusal: the indexer's read of a manifest for the inputs of its +// fingerprint, which keeps a refused manifest's reason in its value (None2) as it always has. +func ForInputs(raw []byte) (catalogue.Manifest, error) { + return catalogue.ParseManifestForInputs(raw) +} + // listed is how the strict parser words a refusal of several problems, one per line after its header. const listed = "this manifest cannot be used:\n - " @@ -160,6 +167,45 @@ func Named(problem string, shelf catalogue.Shelf) []string { return out } +// Unnamed names, in a request's case verdict, the problems between manifests that no one module's removal ends. +const Unnamed = "(between manifests)" + +// Request judges every case of a composer request (mesh-sdk composer.Request), what cmd/mesh-judge answers the checker: +// each case's manifests as one catalogue (Shelf), every manifest accepted or refused with its problems, one per line; the +// problems no module's removal ends under Between. The verdict's SDK names this judge (Identity). A manifest left as a +// digest, or a case named twice, is a request it cannot run. +func Request(r composer.Request) (composer.Verdict, error) { + v := composer.Verdict{V: composer.Version, SDK: Identity()} + seen := map[string]bool{} + for _, c := range r.Cases { + if seen[c.Name] { + return composer.Verdict{}, fmt.Errorf("the case %q is named twice", c.Name) + } + seen[c.Name] = true + raws := map[string][]byte{} + for name, m := range c.Manifests { + if len(m.Raw) == 0 { + return composer.Verdict{}, fmt.Errorf("%w: %s (digest %s)", composer.ErrUnfilled, name, m.Digest) + } + raws[name] = m.Raw + } + refused, unnamed := Shelf(raws, r.Predefined, 0) + cv := composer.CaseVerdict{Name: c.Name, Manifests: map[string]composer.ManifestRead{}} + for name := range raws { + if problems := refused[name]; len(problems) > 0 { + cv.Manifests[name] = composer.ManifestRead{Refused: strings.Join(problems, "\n")} + continue + } + cv.Manifests[name] = composer.ManifestRead{Accepted: true} + } + if len(unnamed) > 0 { + cv.Manifests[Unnamed] = composer.ManifestRead{Refused: strings.Join(unnamed, "\n")} + } + v.Cases = append(v.Cases, cv) + } + return v, nil +} + var ( identityOnce sync.Once identity string diff --git a/modules/mesh-delivery-indexer/judge/judge_test.go b/modules/mesh-delivery-indexer/judge/judge_test.go index 7f0ac40..81967ba 100644 --- a/modules/mesh-delivery-indexer/judge/judge_test.go +++ b/modules/mesh-delivery-indexer/judge/judge_test.go @@ -1,11 +1,13 @@ package judge import ( + "encoding/json" "reflect" "strings" "testing" "git.novox.be/novox/mesh-sdk/go/catalogue" + "git.novox.be/novox/mesh-sdk/go/composer" ) // The judge (novox/hq ADR 0578 §1): a manifest alone is accepted, or refused with each problem; an unknown field is @@ -64,4 +66,19 @@ func TestTheJudge(t *testing.T) { if id := Identity(); !strings.HasPrefix(id, "exe:") || id != Identity() { t.Errorf("identity %q", id) } + // What cmd/mesh-judge answers the checker (ADR 0578 §2): a composer request in, a verdict out, each manifest accepted + // or refused; a manifest left as a digest is a request it cannot run. + r := composer.Request{V: composer.Version, Cases: []composer.Case{{Name: "change", Manifests: map[string]composer.Manifest{ + "a": {Raw: json.RawMessage(`{"module":"a","version":"1"}`)}, + "b": {Raw: json.RawMessage(`{"module":"b","version":"1","requires":["amqp"]}`)}, + }}}} + v, err := Request(r) + if err != nil || v.V != composer.Version || len(v.Cases) != 1 || v.SDK != Identity() || !v.Cases[0].Manifests["a"].Accepted || + v.Cases[0].Manifests["b"].Accepted || !strings.Contains(v.Cases[0].Manifests["b"].Refused, `requires "amqp"`) { + t.Fatalf("request: %+v %v", v, err) + } + r.Cases[0].Manifests["c"] = composer.Manifest{Digest: strings.Repeat("a", 64)} + if _, err := Request(r); err == nil { + t.Fatal("a manifest left as a digest was judged") + } }