Files
mesh-sdk/go/buildstep/workspaces_test.go
T
jschoubben 6147a20454
mesh/merge-gate head 6147a2045467 merged into trunk 7aacbc162014 — pass: builds mesh-sdk (+41 dependent(s)) → ace, g14, novox, shanks; no bus step; 9 wait(…
mesh/repo-check head 6147a2045467 merged into trunk 7aacbc162014 — pass: its merge-check.sh passed, each part in its toolchain: merge-check.sh (typescript)…
mesh/delivery delivered
mesh/check failed: integration: meshbuildnode/cmd/mesh-build-node.TestAFixtureBundleBuiltTwiceHasOneDigest: build_integration_test.go:71: no toolchain…
mesh/delivery-group group fix/1839-build-floor merged: every member is merged
Refuse a build step under a free-space floor and release workspaces, so a build node's disk is never filled (issue 1839)
buildstep.Floor refuses a step, naming the node, what is free and the floor
(20 GiB, build-agent's), while the work or cache filesystem is under it.
buildstep.Workspaces holds a workspace while a step runs on it, removes it
when its caller releases it, and sweeps one left unheld past ADR 0509's
two-hour bound.
2026-10-23 08:28:28 +02:00

114 lines
4.1 KiB
Go

package buildstep
import (
"errors"
"os"
"path/filepath"
"testing"
"time"
)
// 1839/AC1: a filesystem under the floor refuses the step, naming the node, what is free and the floor.
func TestAFilesystemUnderTheFloorRefusesNamingNodeFreeAndFloor(t *testing.T) {
dir := t.TempDir()
f := Floor{Node: "ace", Bytes: 20 << 30, Free: func(string) (uint64, error) { return 4<<30 + 5, nil }}
err := f.Check(dir)
var ref *Refusal
if !errors.As(err, &ref) {
t.Fatalf("Check = %v, want a *Refusal", err)
}
if want := "refused: ace has 4 GiB free under the floor of 20 GiB (on " + dir + ")"; err.Error() != want {
t.Errorf("Check = %q, want %q", err, want)
}
}
// 1839/AC1: at or above the floor, and with no floor set, nothing is refused.
func TestAtTheFloorOrWithNoFloorNothingIsRefused(t *testing.T) {
dir := t.TempDir()
if err := (Floor{Node: "ace", Bytes: 20 << 30, Free: func(string) (uint64, error) { return 20 << 30, nil }}).Check(dir); err != nil {
t.Errorf("at the floor: %v", err)
}
if err := (Floor{Node: "ace", Free: func(string) (uint64, error) { return 0, nil }}).Check(dir); err != nil {
t.Errorf("no floor: %v", err)
}
}
// 1839/AC1: a path not made yet is read at its nearest folder that is; a free space that cannot be read is a refusal.
func TestAPathNotMadeIsReadAtItsParentAndAnUnreadableOneRefuses(t *testing.T) {
dir := t.TempDir()
var read string
f := Floor{Node: "ace", Bytes: 1, Free: func(p string) (uint64, error) { read = p; return 2, nil }}
if err := f.Check(filepath.Join(dir, "not", "made")); err != nil || read != dir {
t.Errorf("Check = %v, read %q, want nil and %q", err, read, dir)
}
f.Free = func(string) (uint64, error) { return 0, errors.New("statfs: boom") }
var ref *Refusal
if err := f.Check(dir); !errors.As(err, &ref) {
t.Errorf("Check = %v, want a refusal", err)
}
}
func makeWorkspace(t *testing.T, w *Workspaces, id string, age time.Duration) string {
t.Helper()
p := filepath.Join(w.Dir, id)
if err := os.MkdirAll(filepath.Join(p, "tree"), 0o700); err != nil {
t.Fatal(err)
}
at := time.Now().Add(-age)
if err := os.Chtimes(p, at, at); err != nil {
t.Fatal(err)
}
return p
}
func gone(p string) bool { _, err := os.Stat(p); return os.IsNotExist(err) }
// 1839/AC2: a released workspace is gone at once; one already gone is no error; one a running step holds is refused
// and kept; an id that is not a workspace's is refused.
func TestReleaseRemovesAWorkspaceAtOnce(t *testing.T) {
w := &Workspaces{Dir: t.TempDir()}
p := makeWorkspace(t, w, "abc1", 0)
if err := w.Release("abc1"); err != nil || !gone(p) {
t.Fatalf("Release = %v, gone %v", err, gone(p))
}
if err := w.Release("abc1"); err != nil {
t.Errorf("a second release = %v, want nil", err)
}
held := makeWorkspace(t, w, "abc2", 0)
done := w.Hold("abc2")
if err := w.Release("abc2"); err == nil || gone(held) {
t.Errorf("Release of a held workspace = %v, gone %v; want refused and kept", err, gone(held))
}
done()
if err := w.Release("abc2"); err != nil || !gone(held) {
t.Errorf("Release after the hold ended = %v, gone %v", err, gone(held))
}
if err := w.Release("../etc"); err == nil {
t.Errorf("Release of ../etc answered nil")
}
}
// 1839/AC3: the sweep removes a workspace no step holds whose last use is past the bound, and keeps a held one and a
// recent one; a hold ending counts as a use.
func TestSweepRemovesUnheldWorkspacesPastTheBound(t *testing.T) {
w := &Workspaces{Dir: t.TempDir()}
old := makeWorkspace(t, w, "old1", 3*time.Hour)
heldOld := makeWorkspace(t, w, "held1", 3*time.Hour)
recent := makeWorkspace(t, w, "new1", time.Minute)
if err := os.WriteFile(filepath.Join(w.Dir, "not-a-workspace.txt"), nil, 0o600); err != nil {
t.Fatal(err)
}
done := w.Hold("held1")
removed := w.Sweep(time.Now(), 2*time.Hour)
if !gone(old) || gone(heldOld) || gone(recent) {
t.Errorf("after the sweep: old gone %v, held kept %v, recent kept %v", gone(old), !gone(heldOld), !gone(recent))
}
if len(removed) != 1 || removed[0] != "old1" {
t.Errorf("Sweep answered %v, want [old1]", removed)
}
done()
if w.Sweep(time.Now(), 2*time.Hour); gone(heldOld) {
t.Errorf("a workspace whose hold just ended was swept: its end is a use")
}
}