From f6fa75e18940059cd041d0f66c20771fae17c98c Mon Sep 17 00:00:00 2001 From: jochen Date: Mon, 2 Nov 2026 18:31:07 +0100 Subject: [PATCH] Builder's unit tests run an ask with a local runner, not mesh-sdk's MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The build runner leaves mesh-sdk for mesh-build-go (ADR 0593 §1), another repository the builder does not import; the tests answer asks as the builder needs them answered. Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL --- .../internal/build/askfake_test.go | 94 +++++++++++++++++++ .../internal/build/fixtures_test.go | 6 +- .../internal/build/run_test.go | 9 +- 3 files changed, 101 insertions(+), 8 deletions(-) create mode 100644 modules/mesh-delivery-builder/internal/build/askfake_test.go diff --git a/modules/mesh-delivery-builder/internal/build/askfake_test.go b/modules/mesh-delivery-builder/internal/build/askfake_test.go new file mode 100644 index 0000000..6894884 --- /dev/null +++ b/modules/mesh-delivery-builder/internal/build/askfake_test.go @@ -0,0 +1,94 @@ +package build + +import ( + "context" + "encoding/json" + "fmt" + "maps" + "strings" + + "git.novox.be/novox/mesh-sdk/go/delivery" +) + +// The build runner a build node runs an ask with is mesh-build-go's (novox/hq ADR 0593 §1), another repository's, +// which the builder does not import. These tests run an ask as the builder needs it run: fetch once at the job's +// fetch arguments with its package versions, build or copy each artifact, answer each one's digest and reference. They +// test the builder, not the runner. + +// stepFunc is one step of a build module, as its tool answers it. +type stepFunc func(ctx context.Context, args map[string]any) (any, error) + +// askSteps are a build module's steps an ask is run with. +type askSteps struct{ Fetch, Build, Copy stepFunc } + +// runAsk runs a build ask with steps and answers its delivery.BuildAnswer, or the error that fails the step. +func runAsk(ctx context.Context, steps askSteps, ask delivery.StepAsk) (any, error) { + if ask.Build == nil { + return nil, fmt.Errorf("the ask %s holds no build job", ask.ID) + } + job := *ask.Build + fetch := job.Fetch + if len(ask.Packages) > 0 && fetch != nil { + fetch = maps.Clone(fetch) + fetch["packages"] = maps.Clone(ask.Packages) + } + answer := delivery.BuildAnswer{} + workspace := "" + for _, a := range job.Artifacts { + var out any + var err error + if a.From != "" { + if steps.Copy == nil { + return nil, fmt.Errorf("this build module copies no artifact (%s)", a.Artifact) + } + out, err = steps.Copy(ctx, map[string]any{"module": job.Module, "artifact": a.Artifact, "from": a.From}) + } else { + if workspace == "" { + raw, err := asRaw(steps.Fetch(ctx, fetch)) + if err != nil { + return nil, err + } + var f struct { + Workspace string `json:"workspace"` + Merge string `json:"merge"` + Conflicts []string `json:"conflicts"` + } + if err := json.Unmarshal(raw, &f); err != nil || f.Workspace == "" { + return nil, fmt.Errorf("the fetch answered no workspace: %s", raw) + } + if _, candidate := fetch["base"]; candidate && (len(f.Conflicts) > 0 || f.Merge == "") { + return nil, fmt.Errorf("the head does not merge into the trunk: %s", strings.Join(f.Conflicts, ", ")) + } + workspace = f.Workspace + } + args := map[string]any{"workspace": workspace, "module": job.Path, "artifact": a.Artifact} + if job.Publish { + args["publish"] = true + } + out, err = steps.Build(ctx, args) + } + raw, err := asRaw(out, err) + if err != nil { + return nil, fmt.Errorf("%s: %w", a.Artifact, err) + } + var built struct { + Digest string `json:"digest"` + Reference string `json:"reference"` + } + if err := json.Unmarshal(raw, &built); err != nil || built.Digest == "" { + return nil, fmt.Errorf("the build of %s answered no digest: %s", a.Artifact, raw) + } + answer.Artifacts = append(answer.Artifacts, delivery.AskBuilt{Artifact: a.Artifact, Digest: built.Digest, Reference: built.Reference}) + } + return answer, nil +} + +func asRaw(v any, err error) (json.RawMessage, error) { + if err != nil { + return nil, err + } + if r, ok := v.(json.RawMessage); ok { + return r, nil + } + return json.Marshal(v) +} diff --git a/modules/mesh-delivery-builder/internal/build/fixtures_test.go b/modules/mesh-delivery-builder/internal/build/fixtures_test.go index 4ecc3e0..467f91d 100644 --- a/modules/mesh-delivery-builder/internal/build/fixtures_test.go +++ b/modules/mesh-delivery-builder/internal/build/fixtures_test.go @@ -29,7 +29,7 @@ func captured(t *testing.T, name string) json.RawMessage { return step.Result } -// The captured image steps, run as a pulled ask runs them (buildstep.RunAsk), answer what the builder reads: the trunk's +// The captured image steps, run as a pulled ask runs them (runAsk), answer what the builder reads: the trunk's // build its digest and reference, a candidate's its digest and no reference (issue 1887 A4a). func TestCapturedImageStepsDecodeWithTheBuildersOwnTypes(t *testing.T) { // review r2, mesh-catalog 2; ADR 0487 clause 1 for _, tc := range []struct { @@ -37,7 +37,7 @@ func TestCapturedImageStepsDecodeWithTheBuildersOwnTypes(t *testing.T) { // revi publish bool ref bool }{{"build-trunk.json", true, true}, {"build-candidate.json", false, false}} { - steps := buildstep.AskSteps{ + steps := askSteps{ Fetch: func(context.Context, map[string]any) (any, error) { return json.RawMessage(captured(t, "fetch.json")), nil }, @@ -47,7 +47,7 @@ func TestCapturedImageStepsDecodeWithTheBuildersOwnTypes(t *testing.T) { // revi At: time.Now(), Build: &delivery.BuildJob{Module: "m", Path: "modules/m", Publish: tc.publish, Fetch: map[string]any{"repository": "novox/mesh-catalog", "commit": strings.Repeat("a", 40)}, Artifacts: []delivery.AskArtifact{{Artifact: "server"}}}} - out, err := buildstep.RunAsk(context.Background(), steps, ask) + out, err := runAsk(context.Background(), steps, ask) if err != nil { t.Fatalf("%s: %v", tc.build, err) } diff --git a/modules/mesh-delivery-builder/internal/build/run_test.go b/modules/mesh-delivery-builder/internal/build/run_test.go index a0bfe47..181b4ee 100644 --- a/modules/mesh-delivery-builder/internal/build/run_test.go +++ b/modules/mesh-delivery-builder/internal/build/run_test.go @@ -15,7 +15,6 @@ import ( "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/buildrun" stdio "git.novox.be/novox/mesh-sdk/go" - "git.novox.be/novox/mesh-sdk/go/buildstep" "git.novox.be/novox/mesh-sdk/go/catalogue" "git.novox.be/novox/mesh-sdk/go/delivery" ) @@ -140,7 +139,7 @@ type rig struct { // toolPrefix is the start of each build module's tool names, as the fake build modules answer them. var toolPrefix = map[string]string{buildrun.GoBuildModule: "build_go_", buildrun.NodeBuildModule: "build_node_", ImageBuildModule: "build_image_"} -// answer runs an ask as the build module that pulls it does (buildstep.RunAsk), with the fake build modules' tools on the +// answer runs an ask as the build module that pulls it does (runAsk), with the fake build modules' tools on the // rig's pulling node, and answers the step that node writes. func (r *rig) answer(a delivery.StepAsk) delivery.Step { node := r.puller @@ -149,7 +148,7 @@ func (r *rig) answer(a delivery.StepAsk) delivery.Step { } module, _ := delivery.BuildModuleOf(a.Language) runner := buildrun.Runner{Caller: r.bm, Steps: r.bm.steps, Bound: time.Second} - tool := func(name string) buildstep.StepFunc { + tool := func(name string) stepFunc { return func(ctx context.Context, args map[string]any) (any, error) { raw, err := runner.Run(ctx, module, node, toolPrefix[module]+name, args) if err != nil { @@ -158,8 +157,8 @@ func (r *rig) answer(a delivery.StepAsk) delivery.Step { return raw, nil } } - steps := buildstep.AskSteps{Fetch: tool("fetch"), Build: tool("build"), Copy: tool("copy")} - out, err := buildstep.RunAsk(context.Background(), steps, a) + steps := askSteps{Fetch: tool("fetch"), Build: tool("build"), Copy: tool("copy")} + out, err := runAsk(context.Background(), steps, a) step := delivery.Step{ID: a.ID, Tool: a.Kind + " ask", Node: node, State: delivery.StepPassed} if err != nil { step.State, step.Reason = delivery.StepFailed, err.Error()