Files
mesh-catalog/modules/mesh-build-node/cmd/mesh-build-node/asks_test.go
T
jschoubben 3f8b0b4248
mesh/delivery-group group fix/capacity-follows-cpu-share rejected: composed together: the checker's verdict at the members' heads (63694fe9222d): unit: ..test
mesh/delivery closed: a newer head of the same pull request
mesh/check checking 3f8b0b42480b on 7c5c29421f02 | novox/mesh-catalog#511 in group fix/capacity-follows-cpu-share
Build modules take asks only within their node's build hours
mesh-build-go, -node and -image declare build-hours (node=HH:MM-HH:MM in the node's local time,
empty = any hour) and hand it to the SDK puller (the operator, 2026-10-26: g14 builds only from
18:00 to 06:00); their SDK pin moves to mesh-sdk d00dac1, which has it.
2026-10-26 21:22:09 +01:00

124 lines
4.8 KiB
Go

package main
import (
"context"
"encoding/json"
"fmt"
"os"
"strings"
"testing"
"time"
"git.novox.be/novox/mesh-sdk/go/buildstep"
"git.novox.be/novox/mesh-sdk/go/delivery"
)
const askCommit = "0123456789abcdef0123456789abcdef01234567"
func fakeSteps(calls *[]string) []StepTool {
answer := func(name string, out any) StepTool {
return StepTool{Name: name, Run: func(ctx context.Context, args map[string]any) (any, error) {
*calls = append(*calls, name)
return out, nil
}}
}
return []StepTool{
answer("build_node_fetch", map[string]any{"workspace": "ws1", "modules": []string{"modules/gitea"}}),
answer("build_node_build", map[string]any{"digest": "sha256:bb", "reference": "artifact-store://gitea/code@sha256:bb"}),
answer("build_node_test", map[string]any{"passed": true}),
}
}
func nodeAsk(kind string) delivery.StepAsk {
a := delivery.StepAsk{ID: buildstep.NewID(), Kind: kind, Language: delivery.AskNode, Asker: "mesh-delivery-checker", At: time.Now()}
fetch := map[string]any{"repository": "novox/mesh-catalog", "commit": askCommit}
if kind == delivery.AskDelivery {
a.Asker = "mesh-delivery-builder"
a.Build = &delivery.BuildJob{Module: "gitea", Path: "modules/gitea", Fetch: fetch, Artifacts: []delivery.AskArtifact{{Artifact: "code"}}}
} else {
a.Check = &delivery.CheckJob{Fetch: fetch, Levels: []string{delivery.LevelUnit}}
}
return a
}
// 1887/A3node: a pulled build ask runs fetch then build and its key names the reference; a check ask tests each module
// directory the fetch named.
func TestAPulledNodeAskRunsItsJob(t *testing.T) {
states := newStates()
m := moduleWith(states, "v26.10.0")
m.Capacity = func() int { return 1 }
var calls []string
src := buildstep.NewFakeAsks()
build, check := nodeAsk(delivery.AskDelivery), nodeAsk(delivery.AskCheck)
_ = src.PublishAsk(check)
_ = src.PublishAsk(build)
p := m.Puller(fakeSteps(&calls), src)
p.PullWait = 10 * time.Millisecond
ctx, stop := context.WithCancel(context.Background())
defer stop()
go p.Pull(ctx)
for deadline := time.Now().Add(3 * time.Second); ; time.Sleep(5 * time.Millisecond) {
if acked, _, _ := src.Fates(); len(acked) == 2 {
break
}
if time.Now().After(deadline) {
t.Fatalf("the asks were not both acked: calls %v", calls)
}
}
if got := strings.Join(calls, ","); got != "build_node_fetch,build_node_build,build_node_fetch,build_node_test" {
t.Errorf("calls = %s, want the delivery ask's job first, then the check's", got)
}
key, _ := delivery.AskStepKey("ace", build.ID)
states.mu.Lock()
v := states.steps[key]
states.mu.Unlock()
if len(v) == 0 || !strings.Contains(string(v[len(v)-1].Result), "artifact-store://gitea/code@sha256:bb") {
t.Fatalf("the step %s = %v", key, v)
}
}
// 1887/A3node, A7: the manifest takes the node's build capacity as a setting, with the builder's default, in a settings
// file; the seat's verbs are served.
func TestManifestTakesTheBuildCapacity(t *testing.T) {
m := readManifest(t)
raw, _ := os.ReadFile(moduleDir + "/module.json")
var doc struct {
Config map[string]struct {
Default json.RawMessage `json:"default"`
} `json:"config"`
}
_ = json.Unmarshal(raw, &doc)
// Issue 1806's follow-up: the cores a step needs is a setting of its own, in the settings file, with the SDK's default.
if cores := string(doc.Config[buildstep.CoresPerStepSetting].Default); cores != fmt.Sprint(buildstep.DefaultCoresPerStep[delivery.AskNode]) {
t.Errorf("the cores-per-step default = %s, want %g", cores, buildstep.DefaultCoresPerStep[delivery.AskNode])
}
if got := buildstep.NodeCoresPerStep(moduleDir+"/files/settings.json", 0, nil)(); got != buildstep.DefaultCoresPerStep[delivery.AskNode] {
t.Errorf("the settings template holds cores-per-step %g", got)
}
// And the build hours (the operator, 2026-10-26): a setting of their own, empty by default (any hour), in the template.
if hours := string(doc.Config[buildstep.BuildHoursSetting].Default); hours != `""` {
t.Errorf("the build-hours default = %s, want empty", hours)
}
if w, fault := buildstep.NodeBuildHours(moduleDir+"/files/settings.json", "g14", t.Errorf)(); w != nil || fault != "" {
t.Errorf("the settings template holds build hours %v (%s)", w, fault)
}
var capacity string
_ = json.Unmarshal(doc.Config[buildstep.CapacitySetting].Default, &capacity)
if capacity != buildstep.DefaultCapacities {
t.Errorf("the capacity's default = %q", capacity)
}
if resource(t, m, "settings")["path"] != "${dir:state}/settings.json" {
t.Error("no settings file")
}
m0 := &Module{Node: "ace", Capacity: func() int { return 0 }}
served := map[string]bool{}
for _, tool := range m0.SeatTools(m0.Puller(nil, buildstep.NewFakeAsks())) {
served[tool.Name] = true
}
for _, verb := range []string{"current", "pause", "resume"} {
if !served[Seat+"."+verb] {
t.Errorf("%s.%s is not served", Seat, verb)
}
}
}