carlos / platform Public

Clone
git clone https://amadan.net/carlos/platform

Plain git — no account needed to clone.

Download

Download this file

1//go:build linux
2
3package host
4
5import (
6 "context"
7 "errors"
8 "fmt"
9 "os"
10 "path/filepath"
11 "syscall"
12 "testing"
13
14 "github.com/carlosframework/platform/internal/moves"
15)
16
17// Every creating open of .moves fsyncs the data dir, not only the one that
18// made the directory (fix round 2, finding 4): a second writer must not
19// report a floor durable on the strength of a first writer's unsynced mkdir.
20func TestEveryFloorWriteSyncsTheDataDir(t *testing.T) {
21 n := 0
22 orig := syncParent
23 syncParent = func(fd int) error { n++; return orig(fd) }
24 defer func() { syncParent = orig }()
25 dd := t.TempDir()
26 armLocalFloor(t, dd, "v0.30.0")
27 first := n
28 armLocalFloor(t, dd, "v0.31.0")
29 if first == 0 || n <= first {
30 t.Fatalf("data dir fsyncs: %d after the first write, %d after the second", first, n)
31 }
32 syncParent = func(int) error { return errors.New("EIO") }
33 if err := writeLocalFloor(dd, moves.Floor{Min: 1, MinRelease: "v0.32.0"}); err == nil {
34 t.Fatal("a write whose data dir fsync failed reported success")
35 }
36 if f, _ := readLocalFloor(dd); f == nil || f.MinRelease != "v0.31.0" {
37 t.Fatalf("floor after a failed fsync = %+v", f)
38 }
39}
40
41// Only a lock that cannot be created counts as "cannot exist" (the F15
42// fallback). Contention never does: a writer is active (fix round 3).
43func TestFloorLockCannotExistIsOnlyForAnUncreatableLock(t *testing.T) {
44 for _, tc := range []struct {
45 err error
46 want bool
47 }{
48 {fmt.Errorf("mkdir /data/.moves: %w", syscall.EROFS), true},
49 {fmt.Errorf("mkdir /data/.moves: %w", syscall.ENOSPC), true},
50 {fmt.Errorf("open floor.lock: %w", syscall.EACCES), true},
51 {fmt.Errorf("mkdir: %w", syscall.EPERM), true},
52 {fmt.Errorf("mkdir: %w", syscall.EDQUOT), true},
53 {fmt.Errorf("lock /data/.moves/floor.lock: %w", errFloorLockBusy), false},
54 {fmt.Errorf("open /data/.moves: %w", syscall.ELOOP), false},
55 {fmt.Errorf("fsync /data: %w", syscall.EIO), false},
56 {errors.Join(syscall.EROFS, errFloorLockBusy), false},
57 } {
58 if got := floorLockCannotExist(tc.err); got != tc.want {
59 t.Errorf("floorLockCannotExist(%v) = %v, want %v", tc.err, got, tc.want)
60 }
61 }
62}
63
64// TestActivationGuardLockOrder pins the lock order the merge of claim Plan 8
65// relies on: at an activation (the install's rename, inside
66// activationGuard, with a floor armed so the holds are frozen too), Run's
67// apply flock and .moves/floor.lock are both held. activationGuard takes
68// floor.lock only while the apply flock is already held; it never takes the
69// apply flock (withFloorLock's comment has the whole order).
70func TestActivationGuardLockOrder(t *testing.T) {
71 f, _ := floorFixture(t, "v0.31.0", "v0.32.0")
72 armLocalFloor(t, f.a.dataDir, "v0.30.0")
73 busy := func(path string, flags int) error {
74 fd, err := syscall.Open(path, flags|syscall.O_CLOEXEC, 0)
75 if err != nil {
76 return fmt.Errorf("open %s: %w", path, err)
77 }
78 defer syscall.Close(fd)
79 if err := syscall.Flock(fd, syscall.LOCK_EX|syscall.LOCK_NB); !errors.Is(err, syscall.EWOULDBLOCK) {
80 return fmt.Errorf("%s: flock = %v, want it held (EWOULDBLOCK)", path, err)
81 }
82 return nil
83 }
84 swapped := false
85 f.a.beforeSwap = func() {
86 swapped = true
87 if err := busy(f.a.lockPath(), syscall.O_RDWR); err != nil {
88 t.Errorf("apply flock: %v", err)
89 }
90 if err := busy(filepath.Join(f.a.dataDir, movesDirName, floorLockFile), syscall.O_RDONLY); err != nil {
91 t.Errorf("floor lock: %v", err)
92 }
93 }
94 if err := f.a.Run(context.Background(), false); err != nil {
95 t.Fatal(err)
96 }
97 if !swapped {
98 t.Fatal("the install never reached its guarded rename")
99 }
100 if r := f.result(); r == nil || r.Outcome != systemOutcomeApplied {
101 t.Fatalf("result = %+v", r)
102 }
103}
104
105func TestMovesDirRefusesLinkedRootAndLock(t *testing.T) {
106 root := t.TempDir()
107 alias := filepath.Join(t.TempDir(), "data")
108 if err := os.Symlink(root, alias); err != nil {
109 t.Fatal(err)
110 }
111 if d, err := openMovesDir(alias, true); err == nil {
112 d.Close()
113 t.Fatal("opened symlinked data root")
114 }
115 d, err := openMovesDir(root, true)
116 if err != nil {
117 t.Fatal(err)
118 }
119 defer d.Close()
120 victim := filepath.Join(root, "victim")
121 if err := os.WriteFile(victim, []byte("unchanged"), 0o600); err != nil {
122 t.Fatal(err)
123 }
124 if err := os.Link(victim, filepath.Join(root, movesDirName, floorLockFile)); err != nil {
125 t.Fatal(err)
126 }
127 if err := d.Lock(); err == nil {
128 t.Fatal("locked a multiply linked file")
129 }
130 body, err := os.ReadFile(victim)
131 if err != nil || string(body) != "unchanged" {
132 t.Fatalf("victim = %q, %v", body, err)
133 }
134}
135
136func TestMovesDirWritesStayWithLockedDirectory(t *testing.T) {
137 root := t.TempDir()
138 d, err := openMovesDir(root, true)
139 if err != nil {
140 t.Fatal(err)
141 }
142 defer d.Close()
143 if err := d.Lock(); err != nil {
144 t.Fatal(err)
145 }
146 moved := filepath.Join(root, "old-moves")
147 if err := os.Rename(filepath.Join(root, movesDirName), moved); err != nil {
148 t.Fatal(err)
149 }
150 if err := os.Mkdir(filepath.Join(root, movesDirName), 0o755); err != nil {
151 t.Fatal(err)
152 }
153 if err := d.WriteDurable("floor.json", []byte("written"), 0o644); err != nil {
154 t.Fatal(err)
155 }
156 body, err := os.ReadFile(filepath.Join(moved, "floor.json"))
157 if err != nil || string(body) != "written" {
158 t.Fatalf("pinned directory = %q, %v", body, err)
159 }
160 if _, err := os.Stat(filepath.Join(root, movesDirName, "floor.json")); !errors.Is(err, os.ErrNotExist) {
161 t.Fatalf("write reached unlocked replacement: %v", err)
162 }
163}
164