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
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.
198 lines
7.8 KiB
Go
198 lines
7.8 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"os"
|
|
"slices"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"git.novox.be/novox/mesh-sdk/go/buildstep"
|
|
"git.novox.be/novox/mesh-sdk/go/delivery"
|
|
)
|
|
|
|
const askCommit = "0123456789abcdef0123456789abcdef01234567"
|
|
|
|
// fakeSteps are step tools that answer at once: a fetch with a workspace, a build with a digest and a reference.
|
|
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_go_fetch", map[string]any{"workspace": "ws1"}),
|
|
answer("build_go_build", map[string]any{"digest": "sha256:aa", "reference": "artifact-store://mesh-build-go/tools@sha256:aa"}),
|
|
answer("build_go_digest", map[string]any{"level": "unit", "packages": map[string]string{}}),
|
|
answer("build_go_test", map[string]any{"passed": true}),
|
|
}
|
|
}
|
|
|
|
func deliveryAsk() delivery.StepAsk {
|
|
return delivery.StepAsk{ID: buildstep.NewID(), Kind: delivery.AskDelivery, Language: delivery.AskGo, Asker: "mesh-delivery-builder",
|
|
At: time.Now(), Build: &delivery.BuildJob{Module: "mesh-delivery-view", Path: "modules/mesh-delivery-view", Publish: true,
|
|
Fetch: map[string]any{"repository": "novox/mesh-delivery", "commit": askCommit},
|
|
Artifacts: []delivery.AskArtifact{{Artifact: "program"}}}}
|
|
}
|
|
|
|
// 1887/A3go: an ask pulled runs build_go_build after build_go_fetch, and the step key <node>.<ask id> names the
|
|
// reference.
|
|
func TestAPulledAskRunsTheBuildAndItsKeyNamesTheReference(t *testing.T) {
|
|
states := newStates()
|
|
m := moduleWith(states, "go1.26.0")
|
|
m.Capacity = func() int { return 1 }
|
|
var calls []string
|
|
src := buildstep.NewFakeAsks()
|
|
a := deliveryAsk()
|
|
if err := src.PublishAsk(a); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
p := m.Puller(fakeSteps(&calls), src)
|
|
p.PullWait = 10 * time.Millisecond
|
|
ctx, stop := context.WithCancel(context.Background())
|
|
defer stop()
|
|
go p.Pull(ctx)
|
|
key, _ := delivery.AskStepKey("ace", a.ID)
|
|
var step delivery.Step
|
|
for deadline := time.Now().Add(3 * time.Second); ; time.Sleep(5 * time.Millisecond) {
|
|
states.mu.Lock()
|
|
v := states.steps[key]
|
|
states.mu.Unlock()
|
|
if len(v) > 0 && v[len(v)-1].State != delivery.StepRunning {
|
|
step = v[len(v)-1]
|
|
break
|
|
}
|
|
if time.Now().After(deadline) {
|
|
t.Fatalf("the ask's step %s never ended: %v", key, v)
|
|
}
|
|
}
|
|
if step.State != delivery.StepPassed || !strings.Contains(string(step.Result), "artifact-store://mesh-build-go/tools@sha256:aa") {
|
|
t.Fatalf("the step = %+v", step)
|
|
}
|
|
if strings.Join(calls, ",") != "build_go_fetch,build_go_build" {
|
|
t.Errorf("calls = %v", calls)
|
|
}
|
|
if acked, _, _ := src.Fates(); len(acked) != 1 || acked[0] != a.ID {
|
|
t.Errorf("acked %v", acked)
|
|
}
|
|
}
|
|
|
|
// 1887/AC-zero: at capacity 0 the node's module pulls nothing, and its tools still answer a direct call.
|
|
func TestAtCapacityZeroNothingIsPulledAndTheToolsAnswer(t *testing.T) {
|
|
states := newStates()
|
|
m := moduleWith(states, "go1.26.0")
|
|
m.Capacity = func() int { return 0 }
|
|
var calls []string
|
|
steps := fakeSteps(&calls)
|
|
src := buildstep.NewFakeAsks()
|
|
_ = src.PublishAsk(deliveryAsk())
|
|
p := m.Puller(steps, src)
|
|
p.PullWait = 10 * time.Millisecond
|
|
ctx, stop := context.WithCancel(context.Background())
|
|
defer stop()
|
|
go p.Pull(ctx)
|
|
time.Sleep(50 * time.Millisecond)
|
|
if src.Waiting(delivery.AskDelivery) != 1 || len(calls) != 0 {
|
|
t.Fatalf("a node at capacity 0 pulled: waiting %d, calls %v", src.Waiting(delivery.AskDelivery), calls)
|
|
}
|
|
for _, tool := range m.Tools(steps) {
|
|
if tool.Name == "build_go_fetch" {
|
|
out, err := tool.Run(map[string]any{"repository": "novox/x", "commit": askCommit})
|
|
if err != nil || !strings.Contains(string(mustJSON(out)), `"workspace":"ws1"`) || len(calls) != 1 {
|
|
t.Fatalf("the direct call answered %v, %v", out, err)
|
|
}
|
|
return
|
|
}
|
|
}
|
|
t.Fatal("no fetch tool")
|
|
}
|
|
|
|
func mustJSON(v any) []byte { b, _ := json.Marshal(v); return b }
|
|
|
|
// 1887/A3go, A7: the manifest claims node-build-go serving its three verbs, reads the checker's kept verdicts and takes
|
|
// the node's build capacity as a setting, with the builder's default; the seat's verbs are served as its tools.
|
|
func TestManifestClaimsTheGoBuildSeatAndReadsItsCapacity(t *testing.T) {
|
|
m := readManifest(t)
|
|
if len(m.Claims) != 1 || m.Claims[0].Name != Seat || !slices.Equal(m.Claims[0].Serves, []string{"current", "pause", "resume"}) {
|
|
t.Fatalf("claims = %+v", m.Claims)
|
|
}
|
|
if !slices.Contains(m.Reads, KeptState) {
|
|
t.Errorf("reads = %v lack %s", m.Reads, KeptState)
|
|
}
|
|
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.AskGo]) {
|
|
t.Errorf("the cores-per-step default = %s, want %g", cores, buildstep.DefaultCoresPerStep[delivery.AskGo])
|
|
}
|
|
if got := buildstep.NodeCoresPerStep(moduleDir+"/files/settings.json", 0, nil)(); got != buildstep.DefaultCoresPerStep[delivery.AskGo] {
|
|
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, want %q", capacity, buildstep.DefaultCapacities)
|
|
}
|
|
settings := resource(t, m, "settings")
|
|
if settings["path"] != "${dir:state}/settings.json" {
|
|
t.Errorf("the settings file is at %v", settings["path"])
|
|
}
|
|
served := map[string]bool{}
|
|
for _, tool := range m0().SeatTools(m0().Puller(nil, buildstep.NewFakeAsks())) {
|
|
served[tool.Name] = true
|
|
}
|
|
for _, verb := range m.Claims[0].Serves {
|
|
if !served[Seat+"."+verb] {
|
|
t.Errorf("%s.%s is not served", Seat, verb)
|
|
}
|
|
}
|
|
}
|
|
|
|
func m0() *Module { return &Module{Node: "ace", Capacity: func() int { return 0 }} }
|
|
|
|
// 1917: a check ask's digest is the digest tool's own body, which answers the packages in the call (no step key): the
|
|
// ask's level carries the digest, so the checker keys the kept verdicts by it.
|
|
func TestACheckAsksDigestIsTheToolsAnswerInTheCall(t *testing.T) {
|
|
m := &Module{Node: "ace"}
|
|
for _, s := range m.StepTools() {
|
|
if s.Name == "build_go_digest" && s.Long {
|
|
t.Fatal("build_go_digest is a long step: its answer would be a step key, not the digest")
|
|
}
|
|
}
|
|
var calls []string
|
|
steps := fakeSteps(&calls)
|
|
for i := range steps {
|
|
if steps[i].Name == "build_go_digest" {
|
|
steps[i].Run = func(ctx context.Context, args map[string]any) (any, error) {
|
|
return map[string]any{"level": "unit", "packages": map[string]string{"x/a": "d1"}}, nil
|
|
}
|
|
}
|
|
}
|
|
ask := m.askSteps(steps)
|
|
a := delivery.StepAsk{ID: buildstep.NewID(), Kind: delivery.AskCheck, Language: delivery.AskGo, Asker: "mesh-delivery-checker",
|
|
At: time.Now(), Check: &delivery.CheckJob{Fetch: map[string]any{"repository": "novox/x", "commit": askCommit}, Levels: []string{delivery.LevelUnit}}}
|
|
out, err := buildstep.RunAsk(context.Background(), ask, a)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if l := out.(delivery.CheckAnswer).Levels; len(l) != 1 || l[0].Digest.Map()["x/a"] != "d1" {
|
|
t.Fatalf("levels = %+v, want the digest's packages", l)
|
|
}
|
|
}
|