135 lines
6.0 KiB
Go
135 lines
6.0 KiB
Go
package buildstep
|
|
|
|
import (
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
// 1887/A7: the node's build capacity is the build module's setting, read at every pull: the control-node at 0 pulls
|
|
// nothing, a change takes effect at the next read, and a setting that cannot be read pulls nothing.
|
|
// ADR 0553 §4: a step's threads are the composer's share over the module's own capacity; without the word, none. And
|
|
// issue 1806's follow-up: the capacity judged by is the configured one bounded by the build slice's cores over the cores
|
|
// a step needs (the three build nodes as they stand on 2026-10-26), and cores-per-step is read from the settings file.
|
|
func TestNodeCapacityAndLimits(t *testing.T) {
|
|
if got := NodeCapacity("", DefaultCapacities, "novox", nil)(); got != 0 {
|
|
t.Errorf("novox = %d by default, want 0", got)
|
|
}
|
|
if got := NodeCapacity("", DefaultCapacities, "shanks", nil)(); got != 3 {
|
|
t.Errorf("shanks = %d by default, want 3", got)
|
|
}
|
|
file := filepath.Join(t.TempDir(), "settings.json")
|
|
os.WriteFile(file, []byte(`{"capacity":"shanks=1,ace=2"}`), 0o600)
|
|
capacity := NodeCapacity(file, DefaultCapacities, "shanks", nil)
|
|
if got := capacity(); got != 1 {
|
|
t.Errorf("shanks = %d from the file, want 1", got)
|
|
}
|
|
os.WriteFile(file, []byte(`{"capacity":"shanks=4"}`), 0o600)
|
|
if got := capacity(); got != 4 {
|
|
t.Errorf("shanks = %d after the change, want 4", got)
|
|
}
|
|
if got := NodeCapacity(file, "", "pi", nil)(); got != 1 {
|
|
t.Errorf("a node not named = %d, want 1", got)
|
|
}
|
|
os.WriteFile(file, []byte(`{"capacity":"shanks=lots"}`), 0o600)
|
|
if got := capacity(); got != 0 {
|
|
t.Errorf("an unreadable setting = %d, want 0", got)
|
|
}
|
|
if got := NodeCapacity(filepath.Join(t.TempDir(), "none"), DefaultCapacities, "shanks", nil)(); got != 0 {
|
|
t.Errorf("a missing file = %d, want 0", got)
|
|
}
|
|
t.Setenv("MESH_BUILD_CPU_SHARE", "")
|
|
if l := NodeLimits(3); l.Threads != 0 || l.Slots != 3 {
|
|
t.Errorf("without the word: %+v", l)
|
|
}
|
|
t.Setenv("MESH_BUILD_CPU_SHARE", "75")
|
|
if l := NodeLimits(3); l.Percent != 75 || l.Slots != 3 || l.Threads < 1 {
|
|
t.Errorf("from the word: %+v", l)
|
|
}
|
|
for _, c := range []struct {
|
|
name string
|
|
configured, quota int
|
|
cores float64
|
|
effective, byCPU int
|
|
}{
|
|
{"shanks: 12 cores, configured 3", 3, 1200, 6, 2, 2},
|
|
{"ace: 18 cores, configured 2", 2, 1800, 6, 2, 3},
|
|
{"g14: 8 cores, configured 2", 2, 800, 6, 1, 1},
|
|
{"node on shanks: 12 cores at 2", 3, 1200, 2, 3, 6},
|
|
{"no quota: the configured one", 3, 0, 6, 3, 0},
|
|
{"configured 0 pulls nothing", 0, 1200, 6, 0, 0},
|
|
{"a quota under one step still builds one", 2, 100, 6, 1, 1},
|
|
{"no cores a step is one", 2, 150, 0, 1, 1},
|
|
} {
|
|
got := EffectiveCapacity(c.configured, c.quota, "test", c.cores)
|
|
if got.Effective != c.effective || got.ByCPU != c.byCPU || got.Why == "" {
|
|
t.Errorf("%s: %+v, want effective %d, by CPU %d", c.name, got, c.effective, c.byCPU)
|
|
}
|
|
}
|
|
file = filepath.Join(t.TempDir(), "settings.json")
|
|
var said []string
|
|
cores := NodeCoresPerStep(file, 6, func(f string, a ...any) { said = append(said, f) })
|
|
for _, c := range []struct {
|
|
file string
|
|
want float64
|
|
}{{`{"capacity":"shanks=2"}`, 6}, {`{"cores-per-step":4}`, 4}, {`{"cores-per-step":"3.5"}`, 3.5}, {`{"cores-per-step":0}`, 6}, {`{"cores-per-step":0}`, 6}} {
|
|
os.WriteFile(file, []byte(c.file), 0o600)
|
|
if got := cores(); got != c.want {
|
|
t.Errorf("cores-per-step from %s = %g, want %g", c.file, got, c.want)
|
|
}
|
|
}
|
|
if len(said) != 1 {
|
|
t.Errorf("a value not above zero is said once, said %d times", len(said))
|
|
}
|
|
|
|
// The operator, 2026-10-26 21:18: g14 builds only from 18:00 to 06:00, its local time. Outside its build hours the
|
|
// capacity judged by is 0, said with the window and when it opens; a node not named, or a bad value, builds at any hour.
|
|
cet := time.FixedZone("CET", 3600)
|
|
day := func(hh, mm int) time.Time { return time.Date(2026, 10, 26, hh, mm, 0, 0, cet) }
|
|
was := now
|
|
defer func() { now = was }()
|
|
for _, c := range []struct {
|
|
name, setting, node string
|
|
at time.Time
|
|
effective int
|
|
edge string // the window's next edge, HH:MM; "" for no window
|
|
bad bool
|
|
}{
|
|
{"g14 in the office", "g14=18:00-06:00", "g14", day(10, 0), 0, "18:00", false},
|
|
{"g14 in the evening", "g14=18:00-06:00", "g14", day(18, 0), 2, "06:00", false},
|
|
{"g14 past midnight", "g14=18:00-06:00", "g14", day(5, 59), 2, "06:00", false},
|
|
{"g14 at the close", "g14=18:00-06:00", "g14", day(6, 0), 0, "18:00", false},
|
|
{"a day window", "shanks=09:00-17:30,g14=18:00-06:00", "shanks", day(17, 29), 2, "17:30", false},
|
|
{"a node not named", "g14=18:00-06:00", "ace", day(10, 0), 2, "", false},
|
|
{"a bad time", "g14=18:00-6", "g14", day(10, 0), 2, "", true},
|
|
{"no span", "g14=18:00-18:00", "g14", day(10, 0), 2, "", true},
|
|
} {
|
|
w, err := ParseBuildHours(c.setting, c.node)
|
|
if (err != nil) != c.bad {
|
|
t.Errorf("%s: parse %v", c.name, err)
|
|
}
|
|
if edge := ""; w != nil {
|
|
edge = w.NextEdge(c.at).Format("15:04")
|
|
if edge != c.edge {
|
|
t.Errorf("%s: next edge %s, want %s", c.name, edge, c.edge)
|
|
}
|
|
} else if c.edge != "" {
|
|
t.Errorf("%s: no window, want one", c.name)
|
|
}
|
|
file := filepath.Join(t.TempDir(), "settings.json")
|
|
os.WriteFile(file, []byte(`{"build-hours":"`+c.setting+`"}`), 0o600)
|
|
now = func() time.Time { return c.at }
|
|
p := &Puller{Language: "go", Node: c.node, Capacity: func() int { return 2 }, BuildHours: NodeBuildHours(file, c.node, nil)}
|
|
got := p.CapacityDetail()
|
|
if got.Effective != c.effective || got.OutsideHours != (c.effective == 0) || (c.effective == 0 && !strings.Contains(got.Why, "outside its build hours 18:00-06:00 CET until 18:00")) {
|
|
t.Errorf("%s: %+v, want capacity %d", c.name, got, c.effective)
|
|
}
|
|
// A value that cannot be read builds at any hour, loudly: the record names the value and says why.
|
|
if c.bad && (!strings.Contains(got.BuildHoursFault, c.setting) || !strings.Contains(got.Why, "at any hour, because its build hours cannot be read")) {
|
|
t.Errorf("%s: a bad value is not said in the record: %+v", c.name, got)
|
|
}
|
|
}
|
|
}
|