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>
210 lines
4.1 KiB
Go
210 lines
4.1 KiB
Go
// 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)
|
|
}
|