package buildcpu import ( "os" "path/filepath" "reflect" "runtime" "testing" ) // The node's share as a quota, the threads one step is given across every build slot of the node, and the words the // composer hands, in one table (ADR 0553, issue 1806). func TestShareQuotaAndThreads(t *testing.T) { for _, c := range []struct { name string percent, cores, slots int quota, threads int }{ {"shanks: half of 24 across 9 slots", 50, 24, 9, 1200, 1}, {"shanks: half of 24 across 4 slots", 50, 24, 4, 1200, 3}, {"ace: three quarters of 24 across 6 slots", 75, 24, 6, 1800, 3}, {"g14: half of 16 across 1 slot", 50, 16, 1, 800, 8}, {"one processor at least", 10, 2, 4, 100, 1}, {"no slots counts as one", 100, 4, 0, 400, 4}, {"a share out of range is the default", 0, 8, 1, 0, 4}, } { if c.quota != 0 { if q := Quota(c.percent, c.cores); q != c.quota { t.Errorf("%s: Quota = %d, want %d", c.name, q, c.quota) } } if th := Threads(c.percent, c.cores, c.slots); th != c.threads { t.Errorf("%s: Threads = %d, want %d", c.name, th, c.threads) } } words := map[string]string{ShareWord: "75", SlotsWord: "6"} l := FromWords(func(k string) string { return words[k] }, 2) if l.Percent != 75 || l.From != ShareWord || l.Slots != 6 || l.SlotsFrom != SlotsWord || l.Threads != Threads(75, runtime.NumCPU(), 6) { t.Errorf("from the words: %+v", l) } // Told nothing (or nonsense), a module assumes every build seat beside it at its own capacity, at the default share. words = map[string]string{ShareWord: "250", SlotsWord: "many"} if l := FromWords(func(k string) string { return words[k] }, 3); l.Percent != DefaultPercent || l.Slots != 3*BuildSeats || l.From != "default" { t.Errorf("without usable words: %+v", l) } if got := Env(0); !reflect.DeepEqual(got, []string{"GOMAXPROCS=1", "UV_THREADPOOL_SIZE=1"}) { t.Errorf("Env = %q", got) } } // What the kernel holds a process to is read from its unit's and its slice's cgroup and its stat file, not assumed. func TestReadInForce(t *testing.T) { dir := t.TempDir() cg := filepath.Join(dir, "cg") slice := filepath.Join(cg, "meshyield.slice") unit := filepath.Join(slice, "mesh-build-go-runtime.service") if err := os.MkdirAll(unit, 0o755); err != nil { t.Fatal(err) } for at, files := range map[string]map[string]string{ slice: {"cpu.idle": "1\n", "cpu.weight": "0\n", "cpu.max": "1200000 100000\n", "io.weight": "default 1\n"}, unit: {"cpu.idle": "0\n", "cpu.weight": "100\n", "cpu.max": "max 100000\n", "io.weight": "default 100\n"}, } { for name, v := range files { if err := os.WriteFile(filepath.Join(at, name), []byte(v), 0o644); err != nil { t.Fatal(err) } } } self := filepath.Join(dir, "cgroup") stat := filepath.Join(dir, "stat") _ = os.WriteFile(self, []byte("0::/meshyield.slice/mesh-build-go-runtime.service\n"), 0o644) // A command with a space and a parenthesis in it, then nice 19 in field 19. _ = os.WriteFile(stat, []byte("42 (mesh (build) go) S 1 42 42 0 -1 4194560 1 0 0 0 0 0 0 0 39 19 8 0 1 2 3\n"), 0o644) cgroupRoot, procSelfCgroup, procSelfStat = cg, self, stat t.Cleanup(func() { cgroupRoot, procSelfCgroup, procSelfStat = "/sys/fs/cgroup", "/proc/self/cgroup", "/proc/self/stat" }) got := ReadInForce() want := InForce{Nice: 19, Cgroup: "/meshyield.slice/mesh-build-go-runtime.service", Unit: Group{Path: "/meshyield.slice/mesh-build-go-runtime.service", CPUIdle: "0", CPUWeight: "100", CPUMax: "max 100000", IOWeight: "default 100"}, Slice: Group{Path: "/meshyield.slice", CPUIdle: "1", CPUWeight: "0", CPUMax: "1200000 100000", IOWeight: "default 1"}, Quota: 1200} if !reflect.DeepEqual(got, want) { t.Fatalf("ReadInForce = %+v\nwant %+v", got, want) } }