github.com/go-asm/go@v1.21.1-0.20240213172139-40c5ead50c48/cmd/go/lockedfile/lockedfile_test.go (about) 1 // Copyright 2018 The Go Authors. All rights reserved. 2 // Use of this source code is governed by a BSD-style 3 // license that can be found in the LICENSE file. 4 5 // js and wasip1 do not support inter-process file locking. 6 // 7 //go:build !js && !wasip1 8 9 package lockedfile_test 10 11 import ( 12 "fmt" 13 "os" 14 "path/filepath" 15 "testing" 16 "time" 17 18 "github.com/go-asm/go/testenv" 19 20 "github.com/go-asm/go/cmd/go/lockedfile" 21 ) 22 23 func mustTempDir(t *testing.T) (dir string, remove func()) { 24 t.Helper() 25 26 dir, err := os.MkdirTemp("", filepath.Base(t.Name())) 27 if err != nil { 28 t.Fatal(err) 29 } 30 return dir, func() { os.RemoveAll(dir) } 31 } 32 33 const ( 34 quiescent = 10 * time.Millisecond 35 probablyStillBlocked = 10 * time.Second 36 ) 37 38 func mustBlock(t *testing.T, desc string, f func()) (wait func(*testing.T)) { 39 t.Helper() 40 41 done := make(chan struct{}) 42 go func() { 43 f() 44 close(done) 45 }() 46 47 timer := time.NewTimer(quiescent) 48 defer timer.Stop() 49 select { 50 case <-done: 51 t.Fatalf("%s unexpectedly did not block", desc) 52 case <-timer.C: 53 } 54 55 return func(t *testing.T) { 56 logTimer := time.NewTimer(quiescent) 57 defer logTimer.Stop() 58 59 select { 60 case <-logTimer.C: 61 // We expect the operation to have unblocked by now, 62 // but maybe it's just slow. Write to the test log 63 // in case the test times out, but don't fail it. 64 t.Helper() 65 t.Logf("%s is unexpectedly still blocked after %v", desc, quiescent) 66 67 // Wait for the operation to actually complete, no matter how long it 68 // takes. If the test has deadlocked, this will cause the test to time out 69 // and dump goroutines. 70 <-done 71 72 case <-done: 73 } 74 } 75 } 76 77 func TestMutexExcludes(t *testing.T) { 78 t.Parallel() 79 80 dir, remove := mustTempDir(t) 81 defer remove() 82 83 path := filepath.Join(dir, "lock") 84 85 mu := lockedfile.MutexAt(path) 86 t.Logf("mu := MutexAt(_)") 87 88 unlock, err := mu.Lock() 89 if err != nil { 90 t.Fatalf("mu.Lock: %v", err) 91 } 92 t.Logf("unlock, _ := mu.Lock()") 93 94 mu2 := lockedfile.MutexAt(mu.Path) 95 t.Logf("mu2 := MutexAt(mu.Path)") 96 97 wait := mustBlock(t, "mu2.Lock()", func() { 98 unlock2, err := mu2.Lock() 99 if err != nil { 100 t.Errorf("mu2.Lock: %v", err) 101 return 102 } 103 t.Logf("unlock2, _ := mu2.Lock()") 104 t.Logf("unlock2()") 105 unlock2() 106 }) 107 108 t.Logf("unlock()") 109 unlock() 110 wait(t) 111 } 112 113 func TestReadWaitsForLock(t *testing.T) { 114 t.Parallel() 115 116 dir, remove := mustTempDir(t) 117 defer remove() 118 119 path := filepath.Join(dir, "timestamp.txt") 120 121 f, err := lockedfile.Create(path) 122 if err != nil { 123 t.Fatalf("Create: %v", err) 124 } 125 defer f.Close() 126 127 const ( 128 part1 = "part 1\n" 129 part2 = "part 2\n" 130 ) 131 _, err = f.WriteString(part1) 132 if err != nil { 133 t.Fatalf("WriteString: %v", err) 134 } 135 t.Logf("WriteString(%q) = <nil>", part1) 136 137 wait := mustBlock(t, "Read", func() { 138 b, err := lockedfile.Read(path) 139 if err != nil { 140 t.Errorf("Read: %v", err) 141 return 142 } 143 144 const want = part1 + part2 145 got := string(b) 146 if got == want { 147 t.Logf("Read(_) = %q", got) 148 } else { 149 t.Errorf("Read(_) = %q, _; want %q", got, want) 150 } 151 }) 152 153 _, err = f.WriteString(part2) 154 if err != nil { 155 t.Errorf("WriteString: %v", err) 156 } else { 157 t.Logf("WriteString(%q) = <nil>", part2) 158 } 159 f.Close() 160 161 wait(t) 162 } 163 164 func TestCanLockExistingFile(t *testing.T) { 165 t.Parallel() 166 167 dir, remove := mustTempDir(t) 168 defer remove() 169 path := filepath.Join(dir, "existing.txt") 170 171 if err := os.WriteFile(path, []byte("ok"), 0777); err != nil { 172 t.Fatalf("os.WriteFile: %v", err) 173 } 174 175 f, err := lockedfile.Edit(path) 176 if err != nil { 177 t.Fatalf("first Edit: %v", err) 178 } 179 180 wait := mustBlock(t, "Edit", func() { 181 other, err := lockedfile.Edit(path) 182 if err != nil { 183 t.Errorf("second Edit: %v", err) 184 } 185 other.Close() 186 }) 187 188 f.Close() 189 wait(t) 190 } 191 192 // TestSpuriousEDEADLK verifies that the spurious EDEADLK reported in 193 // https://golang.org/issue/32817 no longer occurs. 194 func TestSpuriousEDEADLK(t *testing.T) { 195 // P.1 locks file A. 196 // Q.3 locks file B. 197 // Q.3 blocks on file A. 198 // P.2 blocks on file B. (Spurious EDEADLK occurs here.) 199 // P.1 unlocks file A. 200 // Q.3 unblocks and locks file A. 201 // Q.3 unlocks files A and B. 202 // P.2 unblocks and locks file B. 203 // P.2 unlocks file B. 204 205 testenv.MustHaveExec(t) 206 207 dirVar := t.Name() + "DIR" 208 209 if dir := os.Getenv(dirVar); dir != "" { 210 // Q.3 locks file B. 211 b, err := lockedfile.Edit(filepath.Join(dir, "B")) 212 if err != nil { 213 t.Fatal(err) 214 } 215 defer b.Close() 216 217 if err := os.WriteFile(filepath.Join(dir, "locked"), []byte("ok"), 0666); err != nil { 218 t.Fatal(err) 219 } 220 221 // Q.3 blocks on file A. 222 a, err := lockedfile.Edit(filepath.Join(dir, "A")) 223 // Q.3 unblocks and locks file A. 224 if err != nil { 225 t.Fatal(err) 226 } 227 defer a.Close() 228 229 // Q.3 unlocks files A and B. 230 return 231 } 232 233 dir, remove := mustTempDir(t) 234 defer remove() 235 236 // P.1 locks file A. 237 a, err := lockedfile.Edit(filepath.Join(dir, "A")) 238 if err != nil { 239 t.Fatal(err) 240 } 241 242 cmd := testenv.Command(t, os.Args[0], "-test.run=^"+t.Name()+"$") 243 cmd.Env = append(os.Environ(), fmt.Sprintf("%s=%s", dirVar, dir)) 244 245 qDone := make(chan struct{}) 246 waitQ := mustBlock(t, "Edit A and B in subprocess", func() { 247 out, err := cmd.CombinedOutput() 248 if err != nil { 249 t.Errorf("%v:\n%s", err, out) 250 } 251 close(qDone) 252 }) 253 254 // Wait until process Q has either failed or locked file B. 255 // Otherwise, P.2 might not block on file B as intended. 256 locked: 257 for { 258 if _, err := os.Stat(filepath.Join(dir, "locked")); !os.IsNotExist(err) { 259 break locked 260 } 261 timer := time.NewTimer(1 * time.Millisecond) 262 select { 263 case <-qDone: 264 timer.Stop() 265 break locked 266 case <-timer.C: 267 } 268 } 269 270 waitP2 := mustBlock(t, "Edit B", func() { 271 // P.2 blocks on file B. (Spurious EDEADLK occurs here.) 272 b, err := lockedfile.Edit(filepath.Join(dir, "B")) 273 // P.2 unblocks and locks file B. 274 if err != nil { 275 t.Error(err) 276 return 277 } 278 // P.2 unlocks file B. 279 b.Close() 280 }) 281 282 // P.1 unlocks file A. 283 a.Close() 284 285 waitQ(t) 286 waitP2(t) 287 }