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 unix
2
3package hostguard
4
5import (
6 "errors"
7 "os"
8 "path/filepath"
9 "reflect"
10 "testing"
11
12 "golang.org/x/sys/unix"
13)
14
15func TestRetireInstallationWithSyncsInactiveRecord(t *testing.T) {
16 f := newGuardFixture(t)
17 host := f.obs.Host
18 if err := f.g.Activate(t.Context(), host, "uk", nil); err != nil {
19 t.Fatal(err)
20 }
21 captured, err := Capture(f.g.DataDir, host)
22 if err != nil {
23 t.Fatal(err)
24 }
25 failure := errors.New("injected retirement sync failure")
26 failKind := "directory"
27 var calls []string
28 f.g.openRecords = func(path string) (*recordDir, error) {
29 d, err := openRecordDir(path)
30 if err != nil {
31 return nil, err
32 }
33 d.fsync = func(fd int) error {
34 kind := "file"
35 if fd == d.fd {
36 kind = "directory"
37 }
38 calls = append(calls, kind)
39 if kind == failKind {
40 return failure
41 }
42 return unix.Fsync(fd)
43 }
44 return d, nil
45 }
46 callbacks := 0
47 retire := func() error {
48 return f.g.RetireInstallationWith(t.Context(), host, captured, allow, func() error {
49 callbacks++
50 return nil
51 })
52 }
53 if err := retire(); !errors.Is(err, failure) || callbacks != 0 {
54 t.Fatalf("initial failure: %v, callbacks=%d", err, callbacks)
55 }
56 path := filepath.Join(f.g.DataDir, "hostguard", host+".json")
57 // Hold the inode so replacement writes cannot reuse its number.
58 original, err := os.Open(path)
59 if err != nil {
60 t.Fatal(err)
61 }
62 defer original.Close()
63 defer unchangedFile(t, path)()
64 for _, kind := range []string{"file", "directory", ""} {
65 failKind, calls = kind, nil
66 err := retire()
67 if kind == "" {
68 if err != nil || callbacks != 1 {
69 t.Fatalf("durable retry: %v, callbacks=%d", err, callbacks)
70 }
71 } else if !errors.Is(err, failure) || callbacks != 0 {
72 t.Fatalf("%s failure: %v, callbacks=%d", kind, err, callbacks)
73 }
74 want := []string{"file", "directory"}
75 if kind == "file" {
76 want = []string{"file"}
77 }
78 if !reflect.DeepEqual(calls, want) {
79 t.Fatalf("syncs=%v, want=%v", calls, want)
80 }
81 }
82}
83
84func TestRetireInstallationWithRepairsDurability(t *testing.T) {
85 f := newGuardFixture(t)
86 if err := f.g.Activate(t.Context(), f.obs.Host, "uk", nil); err != nil {
87 t.Fatal(err)
88 }
89 captured, err := Capture(f.g.DataDir, f.obs.Host)
90 if err != nil {
91 t.Fatal(err)
92 }
93 crash := errors.New("crash after record rename before directory fsync")
94 callbacks, writes := 0, 0
95 callback := func() error { callbacks++; return nil }
96 persist := func(d *recordDir, r record) error {
97 writes++
98 if r.ActiveStamp != 0 || len(r.RetiredStamps) != 1 || r.RetiredStamps[0] != captured.Stamp {
99 t.Fatal("incorrect inactive record", r)
100 }
101 return d.writeUsing(r, ".retire-crash", func(a int, old string, b int, new string) error {
102 if err := unix.Renameat(a, old, b, new); err != nil {
103 return err
104 }
105 return crash
106 })
107 }
108 for attempt := 0; attempt < 2; attempt++ {
109 if err := f.g.retireInstallationWith(t.Context(), f.obs.Host, captured, allow, callback, persist); !errors.Is(err, crash) {
110 t.Fatalf("attempt %d: %v", attempt, err)
111 }
112 if callbacks != 0 || writes != attempt+1 {
113 t.Fatal("callback ran without durable record", callbacks, writes)
114 }
115 visible, err := Capture(f.g.DataDir, f.obs.Host)
116 if err != nil || visible.Stamp != 0 {
117 t.Fatal("did not reach rename boundary", visible, err)
118 }
119 }
120 // Exercise the public entry point with its real writer after both failures.
121 if err := f.g.RetireInstallationWith(t.Context(), f.obs.Host, captured, allow, callback); err != nil {
122 t.Fatal(err)
123 }
124 if callbacks != 1 {
125 t.Fatal("durable retry did not invoke callback")
126 }
127}
128