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