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") } }