1//go:build unix
2
3package lifecycle
4
5import (
6 "bufio"
7 "context"
8 "errors"
9 "fmt"
10 "io"
11 "os"
12 "os/exec"
13 "path/filepath"
14 "testing"
15 "time"
16
17 "github.com/stretchr/testify/require"
18)
19
20func TestLifecycleLockProcessHelper(t *testing.T) {
21 if os.Getenv("VEV_LIFECYCLE_LOCK_HELPER") != "1" {
22 t.Skip("helper process")
23 }
24
25 owner, err := TryAcquire(os.Getenv("VEV_LIFECYCLE_LOCK_DIR"))
26 require.NoError(t, err)
27 defer func() { require.NoError(t, owner.Release()) }()
28
29 _, err = fmt.Fprintln(os.Stdout, "locked")
30 require.NoError(t, err)
31 _, err = io.Copy(io.Discard, os.Stdin)
32 require.NoError(t, err)
33}
34
35func TestLifecycleLock(t *testing.T) {
36 t.Run("process contention and reacquire", testLifecycleLockProcessContentionAndReacquire)
37 t.Run("acquire cancellation", testLifecycleLockAcquireCancellation)
38 t.Run("unavailable runtime directory", testLifecycleLockUnavailableRuntimeDirectory)
39}
40
41func testLifecycleLockProcessContentionAndReacquire(t *testing.T) {
42 runtimeDir := filepath.Join(t.TempDir(), "runtime")
43 cmd := exec.Command(os.Args[0], "-test.run=^TestLifecycleLockProcessHelper$")
44 cmd.Env = append(os.Environ(),
45 "VEV_LIFECYCLE_LOCK_HELPER=1",
46 "VEV_LIFECYCLE_LOCK_DIR="+runtimeDir,
47 )
48 stdin, err := cmd.StdinPipe()
49 require.NoError(t, err)
50 stdout, err := cmd.StdoutPipe()
51 require.NoError(t, err)
52 cmd.Stderr = os.Stderr
53 require.NoError(t, cmd.Start())
54 defer func() {
55 _ = stdin.Close()
56 _ = cmd.Wait()
57 }()
58
59 line, err := bufio.NewReader(stdout).ReadString('\n')
60 require.NoError(t, err)
61 require.Equal(t, "locked\n", line)
62
63 _, err = TryAcquire(runtimeDir)
64 require.ErrorIs(t, err, ErrBusy)
65
66 require.NoError(t, stdin.Close())
67 require.NoError(t, cmd.Wait())
68
69 owner, err := TryAcquire(runtimeDir)
70 require.NoError(t, err)
71 require.NoError(t, owner.Release())
72 require.NoError(t, owner.Release(), "release is idempotent")
73}
74
75func testLifecycleLockAcquireCancellation(t *testing.T) {
76 runtimeDir := filepath.Join(t.TempDir(), "runtime")
77 owner, err := TryAcquire(runtimeDir)
78 require.NoError(t, err)
79 defer func() { require.NoError(t, owner.Release()) }()
80
81 ctx, cancel := context.WithCancel(context.Background())
82 cancel()
83
84 _, err = Acquire(ctx, runtimeDir, time.Millisecond)
85 require.ErrorIs(t, err, context.Canceled)
86}
87
88func testLifecycleLockUnavailableRuntimeDirectory(t *testing.T) {
89 runtimePath := t.TempDir() + "/not-a-directory"
90 require.NoError(t, os.WriteFile(runtimePath, []byte("occupied"), 0o600))
91
92 _, err := TryAcquire(runtimePath)
93 require.Error(t, err)
94 require.False(t, errors.Is(err, ErrBusy))
95}