internal/app/state: use internal/lockedfile
All checks were successful
Test / Create distribution (push) Successful in 33s
Test / Sandbox (push) Successful in 2m15s
Test / Hakurei (push) Successful in 3m11s
Test / Hpkg (push) Successful in 4m0s
Test / Sandbox (race detector) (push) Successful in 4m4s
Test / Hakurei (race detector) (push) Successful in 4m52s
Test / Flake checks (push) Successful in 1m30s
All checks were successful
Test / Create distribution (push) Successful in 33s
Test / Sandbox (push) Successful in 2m15s
Test / Hakurei (push) Successful in 3m11s
Test / Hpkg (push) Successful in 4m0s
Test / Sandbox (race detector) (push) Successful in 4m4s
Test / Hakurei (race detector) (push) Successful in 4m52s
Test / Flake checks (push) Successful in 1m30s
This is a pretty solid implementation backed by robust tests, with a much cleaner interface. Signed-off-by: Ophestra <cat@gensokyo.uk>
This commit is contained in:
209
internal/lockedfile/internal/filelock/filelock_test.go
Normal file
209
internal/lockedfile/internal/filelock/filelock_test.go
Normal file
@@ -0,0 +1,209 @@
|
||||
// Copyright 2018 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
//go:build !js && !plan9 && !wasip1
|
||||
|
||||
package filelock_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"hakurei.app/container"
|
||||
"hakurei.app/internal/lockedfile/internal/filelock"
|
||||
"hakurei.app/internal/lockedfile/internal/testexec"
|
||||
)
|
||||
|
||||
func lock(t *testing.T, f *os.File) {
|
||||
t.Helper()
|
||||
err := filelock.Lock(f)
|
||||
t.Logf("Lock(fd %d) = %v", f.Fd(), err)
|
||||
if err != nil {
|
||||
t.Fail()
|
||||
}
|
||||
}
|
||||
|
||||
func rLock(t *testing.T, f *os.File) {
|
||||
t.Helper()
|
||||
err := filelock.RLock(f)
|
||||
t.Logf("RLock(fd %d) = %v", f.Fd(), err)
|
||||
if err != nil {
|
||||
t.Fail()
|
||||
}
|
||||
}
|
||||
|
||||
func unlock(t *testing.T, f *os.File) {
|
||||
t.Helper()
|
||||
err := filelock.Unlock(f)
|
||||
t.Logf("Unlock(fd %d) = %v", f.Fd(), err)
|
||||
if err != nil {
|
||||
t.Fail()
|
||||
}
|
||||
}
|
||||
|
||||
func mustTempFile(t *testing.T) (f *os.File, remove func()) {
|
||||
t.Helper()
|
||||
|
||||
base := filepath.Base(t.Name())
|
||||
f, err := os.CreateTemp("", base)
|
||||
if err != nil {
|
||||
t.Fatalf(`os.CreateTemp("", %q) = %v`, base, err)
|
||||
}
|
||||
t.Logf("fd %d = %s", f.Fd(), f.Name())
|
||||
|
||||
return f, func() {
|
||||
f.Close()
|
||||
os.Remove(f.Name())
|
||||
}
|
||||
}
|
||||
|
||||
func mustOpen(t *testing.T, name string) *os.File {
|
||||
t.Helper()
|
||||
|
||||
f, err := os.OpenFile(name, os.O_RDWR, 0)
|
||||
if err != nil {
|
||||
t.Fatalf("os.OpenFile(%q) = %v", name, err)
|
||||
}
|
||||
|
||||
t.Logf("fd %d = os.OpenFile(%q)", f.Fd(), name)
|
||||
return f
|
||||
}
|
||||
|
||||
const (
|
||||
quiescent = 10 * time.Millisecond
|
||||
probablyStillBlocked = 10 * time.Second
|
||||
)
|
||||
|
||||
func mustBlock(t *testing.T, op string, f *os.File) (wait func(*testing.T)) {
|
||||
t.Helper()
|
||||
|
||||
desc := fmt.Sprintf("%s(fd %d)", op, f.Fd())
|
||||
|
||||
done := make(chan struct{})
|
||||
go func() {
|
||||
t.Helper()
|
||||
switch op {
|
||||
case "Lock":
|
||||
lock(t, f)
|
||||
case "RLock":
|
||||
rLock(t, f)
|
||||
default:
|
||||
panic("invalid op: " + op)
|
||||
}
|
||||
close(done)
|
||||
}()
|
||||
|
||||
select {
|
||||
case <-done:
|
||||
t.Fatalf("%s unexpectedly did not block", desc)
|
||||
return nil
|
||||
|
||||
case <-time.After(quiescent):
|
||||
t.Logf("%s is blocked (as expected)", desc)
|
||||
return func(t *testing.T) {
|
||||
t.Helper()
|
||||
select {
|
||||
case <-time.After(probablyStillBlocked):
|
||||
t.Fatalf("%s is unexpectedly still blocked", desc)
|
||||
case <-done:
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestLockExcludesLock(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
f, remove := mustTempFile(t)
|
||||
defer remove()
|
||||
|
||||
other := mustOpen(t, f.Name())
|
||||
defer other.Close()
|
||||
|
||||
lock(t, f)
|
||||
lockOther := mustBlock(t, "Lock", other)
|
||||
unlock(t, f)
|
||||
lockOther(t)
|
||||
unlock(t, other)
|
||||
}
|
||||
|
||||
func TestLockExcludesRLock(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
f, remove := mustTempFile(t)
|
||||
defer remove()
|
||||
|
||||
other := mustOpen(t, f.Name())
|
||||
defer other.Close()
|
||||
|
||||
lock(t, f)
|
||||
rLockOther := mustBlock(t, "RLock", other)
|
||||
unlock(t, f)
|
||||
rLockOther(t)
|
||||
unlock(t, other)
|
||||
}
|
||||
|
||||
func TestRLockExcludesOnlyLock(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
f, remove := mustTempFile(t)
|
||||
defer remove()
|
||||
rLock(t, f)
|
||||
|
||||
f2 := mustOpen(t, f.Name())
|
||||
defer f2.Close()
|
||||
|
||||
doUnlockTF := false
|
||||
switch runtime.GOOS {
|
||||
case "aix", "solaris":
|
||||
// When using POSIX locks (as on Solaris), we can't safely read-lock the
|
||||
// same inode through two different descriptors at the same time: when the
|
||||
// first descriptor is closed, the second descriptor would still be open but
|
||||
// silently unlocked. So a second RLock must block instead of proceeding.
|
||||
lockF2 := mustBlock(t, "RLock", f2)
|
||||
unlock(t, f)
|
||||
lockF2(t)
|
||||
default:
|
||||
rLock(t, f2)
|
||||
doUnlockTF = true
|
||||
}
|
||||
|
||||
other := mustOpen(t, f.Name())
|
||||
defer other.Close()
|
||||
lockOther := mustBlock(t, "Lock", other)
|
||||
|
||||
unlock(t, f2)
|
||||
if doUnlockTF {
|
||||
unlock(t, f)
|
||||
}
|
||||
lockOther(t)
|
||||
unlock(t, other)
|
||||
}
|
||||
|
||||
func TestLockNotDroppedByExecCommand(t *testing.T) {
|
||||
f, remove := mustTempFile(t)
|
||||
defer remove()
|
||||
|
||||
lock(t, f)
|
||||
|
||||
other := mustOpen(t, f.Name())
|
||||
defer other.Close()
|
||||
|
||||
// Some kinds of file locks are dropped when a duplicated or forked file
|
||||
// descriptor is unlocked. Double-check that the approach used by os/exec does
|
||||
// not accidentally drop locks.
|
||||
cmd := testexec.CommandContext(t, t.Context(), container.MustExecutable(nil), "-test.run=^$")
|
||||
if err := cmd.Run(); err != nil {
|
||||
t.Fatalf("exec failed: %v", err)
|
||||
}
|
||||
|
||||
lockOther := mustBlock(t, "Lock", other)
|
||||
unlock(t, f)
|
||||
lockOther(t)
|
||||
unlock(t, other)
|
||||
}
|
||||
Reference in New Issue
Block a user