Review of 62a50eb: the layer lay in the workspace, which every step binds read-write,
so a change's command could swap a kept directory for a symlink and the move, run
outside the sandbox, would write into the shared cache (ADR 0541 §2). The layer now
lies beside the workspace (Workspaces.CacheLayer, removed by Release and Sweep),
Command refuses a layer within reach of the workspace, a writable directory or a
cache, and every move goes through an os.Root on the layer and keeps no symlink. Only
LayerCaches (the build cache) are layered; a module cache keeps its throwaway layer
(issue 1921).
132 lines
5.1 KiB
Go
132 lines
5.1 KiB
Go
package buildstep
|
|
|
|
import (
|
|
"errors"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"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)
|
|
layer := w.CacheLayer("abc1") // issue 1921/S2: the cache layer lies beside the workspace and goes with it
|
|
if filepath.Dir(layer) != w.Dir || strings.HasPrefix(layer, p+"/") {
|
|
t.Fatalf("the cache layer %s does not lie beside the workspace %s", layer, p)
|
|
}
|
|
if err := os.MkdirAll(filepath.Join(layer, "kept", "0"), 0o700); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := w.Release("abc1"); err != nil || !gone(p) || !gone(layer) {
|
|
t.Fatalf("Release = %v, gone %v, its cache layer gone %v", err, gone(p), gone(layer))
|
|
}
|
|
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)
|
|
}
|
|
// issue 1921/S2: a swept workspace's cache layer goes with it, and so does a layer whose workspace is gone; a held
|
|
// workspace's stays
|
|
for _, id := range []string{"old1", "held1", "orphan1"} {
|
|
if err := os.MkdirAll(w.CacheLayer(id), 0o700); 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 !gone(w.CacheLayer("old1")) || !gone(w.CacheLayer("orphan1")) || gone(w.CacheLayer("held1")) {
|
|
t.Errorf("after the sweep: old1's layer gone %v, orphan1's gone %v, held1's kept %v", gone(w.CacheLayer("old1")), gone(w.CacheLayer("orphan1")), !gone(w.CacheLayer("held1")))
|
|
}
|
|
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")
|
|
}
|
|
}
|