github.com/rogpeppe/go-internal@v1.12.1-0.20240509064211-c8567cf8e95f/lockedfile/internal/filelock/filelock_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 //go:build !js && !plan9 6 7 package filelock_test 8 9 import ( 10 "fmt" 11 "os" 12 "os/exec" 13 "path/filepath" 14 "runtime" 15 "testing" 16 "time" 17 18 "github.com/rogpeppe/go-internal/lockedfile/internal/filelock" 19 ) 20 21 func lock(t *testing.T, f *os.File) { 22 t.Helper() 23 err := filelock.Lock(f) 24 t.Logf("Lock(fd %d) = %v", f.Fd(), err) 25 if err != nil { 26 t.Fail() 27 } 28 } 29 30 func rLock(t *testing.T, f *os.File) { 31 t.Helper() 32 err := filelock.RLock(f) 33 t.Logf("RLock(fd %d) = %v", f.Fd(), err) 34 if err != nil { 35 t.Fail() 36 } 37 } 38 39 func unlock(t *testing.T, f *os.File) { 40 t.Helper() 41 err := filelock.Unlock(f) 42 t.Logf("Unlock(fd %d) = %v", f.Fd(), err) 43 if err != nil { 44 t.Fail() 45 } 46 } 47 48 func mustTempFile(t *testing.T) (f *os.File, remove func()) { 49 t.Helper() 50 51 base := filepath.Base(t.Name()) 52 f, err := os.CreateTemp("", base) 53 if err != nil { 54 t.Fatalf(`os.CreateTemp("", %q) = %v`, base, err) 55 } 56 t.Logf("fd %d = %s", f.Fd(), f.Name()) 57 58 return f, func() { 59 f.Close() 60 os.Remove(f.Name()) 61 } 62 } 63 64 func mustOpen(t *testing.T, name string) *os.File { 65 t.Helper() 66 67 f, err := os.OpenFile(name, os.O_RDWR, 0) 68 if err != nil { 69 t.Fatalf("os.Open(%q) = %v", name, err) 70 } 71 72 t.Logf("fd %d = os.Open(%q)", f.Fd(), name) 73 return f 74 } 75 76 const ( 77 quiescent = 10 * time.Millisecond 78 probablyStillBlocked = 10 * time.Second 79 ) 80 81 func mustBlock(t *testing.T, op string, f *os.File) (wait func(*testing.T)) { 82 t.Helper() 83 84 desc := fmt.Sprintf("%s(fd %d)", op, f.Fd()) 85 86 done := make(chan struct{}) 87 go func() { 88 t.Helper() 89 switch op { 90 case "Lock": 91 lock(t, f) 92 case "RLock": 93 rLock(t, f) 94 default: 95 panic("invalid op: " + op) 96 } 97 close(done) 98 }() 99 100 select { 101 case <-done: 102 t.Fatalf("%s unexpectedly did not block", desc) 103 return nil 104 105 case <-time.After(quiescent): 106 t.Logf("%s is blocked (as expected)", desc) 107 return func(t *testing.T) { 108 t.Helper() 109 select { 110 case <-time.After(probablyStillBlocked): 111 t.Fatalf("%s is unexpectedly still blocked", desc) 112 case <-done: 113 } 114 } 115 } 116 } 117 118 func TestLockExcludesLock(t *testing.T) { 119 t.Parallel() 120 121 f, remove := mustTempFile(t) 122 defer remove() 123 124 other := mustOpen(t, f.Name()) 125 defer other.Close() 126 127 lock(t, f) 128 lockOther := mustBlock(t, "Lock", other) 129 unlock(t, f) 130 lockOther(t) 131 unlock(t, other) 132 } 133 134 func TestLockExcludesRLock(t *testing.T) { 135 t.Parallel() 136 137 f, remove := mustTempFile(t) 138 defer remove() 139 140 other := mustOpen(t, f.Name()) 141 defer other.Close() 142 143 lock(t, f) 144 rLockOther := mustBlock(t, "RLock", other) 145 unlock(t, f) 146 rLockOther(t) 147 unlock(t, other) 148 } 149 150 func TestRLockExcludesOnlyLock(t *testing.T) { 151 t.Parallel() 152 153 f, remove := mustTempFile(t) 154 defer remove() 155 rLock(t, f) 156 157 f2 := mustOpen(t, f.Name()) 158 defer f2.Close() 159 160 doUnlockTF := false 161 switch runtime.GOOS { 162 case "aix", "solaris": 163 // When using POSIX locks (as on Solaris), we can't safely read-lock the 164 // same inode through two different descriptors at the same time: when the 165 // first descriptor is closed, the second descriptor would still be open but 166 // silently unlocked. So a second RLock must block instead of proceeding. 167 lockF2 := mustBlock(t, "RLock", f2) 168 unlock(t, f) 169 lockF2(t) 170 default: 171 rLock(t, f2) 172 doUnlockTF = true 173 } 174 175 other := mustOpen(t, f.Name()) 176 defer other.Close() 177 lockOther := mustBlock(t, "Lock", other) 178 179 unlock(t, f2) 180 if doUnlockTF { 181 unlock(t, f) 182 } 183 lockOther(t) 184 unlock(t, other) 185 } 186 187 func TestLockNotDroppedByExecCommand(t *testing.T) { 188 f, remove := mustTempFile(t) 189 defer remove() 190 191 lock(t, f) 192 193 other := mustOpen(t, f.Name()) 194 defer other.Close() 195 196 // Some kinds of file locks are dropped when a duplicated or forked file 197 // descriptor is unlocked. Double-check that the approach used by os/exec does 198 // not accidentally drop locks. 199 cmd := exec.Command(os.Args[0], "-test.run=^$") 200 if err := cmd.Run(); err != nil { 201 t.Fatalf("exec failed: %v", err) 202 } 203 204 lockOther := mustBlock(t, "Lock", other) 205 unlock(t, f) 206 lockOther(t) 207 unlock(t, other) 208 }