carlos / platform Public

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

Plain git — no account needed to clone.

Download

Download this file

1package moves
2
3import (
4 "go/ast"
5 "go/parser"
6 "go/token"
7 "os"
8 "path/filepath"
9 "reflect"
10 "strings"
11 "testing"
12 "time"
13)
14
15const (
16 tjHost = "a.go.example.com"
17 tjOp = "mv_abcdefghijklmnopqrstuvwxyz"
18)
19
20func tjRead(t *testing.T, dd string) *Journal {
21 t.Helper()
22 j, err := ReadJournal(dd, tjHost, tjOp)
23 if err != nil {
24 t.Fatal(err)
25 }
26 return j
27}
28
29func tjCommit(t *testing.T, dd string, j *Journal) {
30 t.Helper()
31 if err := j.Commit(dd); err != nil {
32 t.Fatal(err)
33 }
34}
35
36// Fix round 1, ruling 3, and fix round 2, ruling 2: a move's journal has
37// more than one writer on a box (the landing's pass, the probe owners and
38// attempts), each a read-modify-write through UpdateJournal: a writer that
39// read before another's write commits only its own changes, so the
40// landing's progress mark and the other writer's step both survive, in
41// either order. A step the writer itself removes is removed.
42func TestJournalWritersKeepEachOthersSteps(t *testing.T) {
43 dd := t.TempDir()
44 landing, other := tjRead(t, dd), tjRead(t, dd)
45 other.Mark("files-plain-g1", "sha256:f")
46 tjCommit(t, dd, other)
47 landing.Phase, landing.Attempt = "probing", 1
48 landing.Mark("probe-admitting-1", `{"session":7}`)
49 tjCommit(t, dd, landing)
50 got := tjRead(t, dd)
51 if !got.Done("files-plain-g1") || !got.Done("probe-admitting-1") || got.Phase != "probing" || got.Attempt != 1 {
52 t.Fatalf("after the landing's later commit: %+v", got)
53 }
54 if !landing.Done("files-plain-g1") {
55 t.Fatalf("a committed value is not the journal as written: %+v", landing.Steps)
56 }
57
58 // The other order: the other writer commits last.
59 landing, other = tjRead(t, dd), tjRead(t, dd)
60 landing.Mark("probe-passed-1", "ok")
61 tjCommit(t, dd, landing)
62 other.Mark("files-plain-g2", "sha256:g")
63 tjCommit(t, dd, other)
64 got = tjRead(t, dd)
65 if !got.Done("probe-passed-1") || !got.Done("files-plain-g2") || !got.Done("probe-admitting-1") || !got.Done("files-plain-g1") || got.Phase != "probing" {
66 t.Fatalf("after the other's later commit: %+v", got)
67 }
68
69 // A step the writer removes is removed; another's is not touched.
70 j := tjRead(t, dd)
71 j.Unmark("probe-passed-1")
72 tjCommit(t, dd, j)
73 if got := tjRead(t, dd); got.Done("probe-passed-1") || !got.Done("files-plain-g2") {
74 t.Fatalf("after an Unmark: %+v", got.Steps)
75 }
76}
77
78// Fix round 2, ruling 2: a stale writer never restores a value another
79// writer advanced or deleted after the stale writer read it. It writes only
80// what it changed itself.
81func TestStaleJournalWriterRestoresNothing(t *testing.T) {
82 dd := t.TempDir()
83 if err := UpdateJournal(dd, tjHost, tjOp, func(j *Journal) error {
84 j.Mark("probe-attempt-1", "started")
85 j.Mark("scratch", "here")
86 j.Phase = "restored"
87 return nil
88 }); err != nil {
89 t.Fatal(err)
90 }
91 stale := tjRead(t, dd)
92 // Another writer advances one step, deletes another and moves the phase.
93 if err := UpdateJournal(dd, tjHost, tjOp, func(j *Journal) error {
94 j.Mark("probe-attempt-1", "passed")
95 j.Unmark("scratch")
96 j.Phase = "probing"
97 return nil
98 }); err != nil {
99 t.Fatal(err)
100 }
101 stale.Mark("probe-admitting-2", "x")
102 tjCommit(t, dd, stale)
103 got := tjRead(t, dd)
104 if got.Steps["probe-attempt-1"] != "passed" {
105 t.Errorf("an advanced step was restored: %q", got.Steps["probe-attempt-1"])
106 }
107 if got.Done("scratch") {
108 t.Errorf("a deleted step was restored: %+v", got.Steps)
109 }
110 if got.Phase != "probing" {
111 t.Errorf("a phase the stale writer did not change was restored: %q", got.Phase)
112 }
113 if !got.Done("probe-admitting-2") {
114 t.Errorf("the stale writer's own change is lost: %+v", got)
115 }
116}
117
118// Fix round 1, ruling 3: one lock per journal. A writer that commits while
119// another holds the journal in UpdateJournal (the landing's admitting mark)
120// waits for it, and both changes are in the journal after.
121func TestJournalLockSerializesWriters(t *testing.T) {
122 dd := t.TempDir()
123 other := tjRead(t, dd)
124 other.Mark("files-plain-g1", "sha256:f")
125 done := make(chan error, 1)
126 err := UpdateJournal(dd, tjHost, tjOp, func(j *Journal) error {
127 go func() { done <- other.Commit(dd) }()
128 select {
129 case err := <-done:
130 t.Errorf("a commit wrote the journal (%v) while UpdateJournal held it", err)
131 case <-time.After(100 * time.Millisecond):
132 }
133 j.Phase, j.Attempt = "probing", 1
134 j.Mark("probe-admitting-1", `{"session":7}`)
135 return nil
136 })
137 if err != nil {
138 t.Fatal(err)
139 }
140 if err := <-done; err != nil {
141 t.Fatal(err)
142 }
143 got := tjRead(t, dd)
144 if !got.Done("probe-admitting-1") || !got.Done("files-plain-g1") || got.Phase != "probing" {
145 t.Fatalf("the journal lost a writer's change: %+v", got)
146 }
147}
148
149// Fix round 2, ruling 2: there is no plain write. Journal has no exported
150// Write, and the whole-file write (write) is called only by UpdateJournal,
151// which holds the lock across the read, the change and the write; every
152// other writer commits its own changes through it (Commit). A production
153// caller cannot read a journal and then write it whole.
154func TestJournalHasNoPlainWrite(t *testing.T) {
155 if _, ok := reflect.TypeOf(&Journal{}).MethodByName("Write"); ok {
156 t.Fatal("Journal has a plain Write")
157 }
158 files, err := filepath.Glob("*.go")
159 if err != nil {
160 t.Fatal(err)
161 }
162 fset := token.NewFileSet()
163 var callers []string
164 for _, f := range files {
165 if strings.HasSuffix(f, "_test.go") {
166 continue
167 }
168 af, err := parser.ParseFile(fset, f, nil, 0)
169 if err != nil {
170 t.Fatal(err)
171 }
172 for _, d := range af.Decls {
173 fd, ok := d.(*ast.FuncDecl)
174 if !ok || fd.Body == nil {
175 continue
176 }
177 ast.Inspect(fd.Body, func(n ast.Node) bool {
178 if c, ok := n.(*ast.CallExpr); ok {
179 if sel, ok := c.Fun.(*ast.SelectorExpr); ok && sel.Sel.Name == "write" {
180 callers = append(callers, f+":"+fd.Name.Name)
181 }
182 }
183 return true
184 })
185 }
186 }
187 if !reflect.DeepEqual(callers, []string{"journal.go:UpdateJournal"}) {
188 t.Fatalf("the journal's whole-file write is called by %v, want only UpdateJournal", callers)
189 }
190 // Every journal written in this package's tests reads back: a smoke
191 // check that Commit makes a journal where there was none.
192 dd := t.TempDir()
193 j := tjRead(t, dd)
194 j.Mark("a", "b")
195 tjCommit(t, dd, j)
196 if _, err := os.Stat(filepath.Join(JournalDir(dd, tjHost, tjOp), journalFile)); err != nil {
197 t.Fatalf("Commit made no journal: %v", err)
198 }
199}
200