main ece7682

flock_test.go RawHistoryFolder

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	"syscall"
15	"testing"
16	"time"
17
18	"github.com/stretchr/testify/require"
19)
20
21func TestLifecycleLockProcessHelper(t *testing.T) {
22	if os.Getenv("VEV_LIFECYCLE_LOCK_HELPER") != "1" {
23		t.Skip("helper process")
24	}
25
26	owner, err := TryAcquire(os.Getenv("VEV_LIFECYCLE_LOCK_DIR"))
27	require.NoError(t, err)
28	defer func() { require.NoError(t, owner.Release()) }()
29
30	_, err = fmt.Fprintln(os.Stdout, "locked")
31	require.NoError(t, err)
32	_, err = io.Copy(io.Discard, os.Stdin)
33	require.NoError(t, err)
34}
35
36func TestLifecycleLock(t *testing.T) {
37	t.Run("process contention and reacquire", testLifecycleLockProcessContentionAndReacquire)
38	t.Run("acquire cancellation", testLifecycleLockAcquireCancellation)
39	t.Run("unavailable runtime directory", testLifecycleLockUnavailableRuntimeDirectory)
40	t.Run("lock file is owner-only regular", testLifecycleLockIsOwnerOnlyRegular)
41	t.Run("owner-only variant is accepted", testLifecycleLockOwnerOnlyVariantAccepted)
42	t.Run("restrictive umask normalizes to owner-only", testLifecycleLockRestrictiveUmaskNormalizes)
43	t.Run("symlinked lock path fails closed", testLifecycleLockSymlinkFailsClosed)
44	t.Run("nonregular lock path fails closed", testLifecycleLockNonRegularFailsClosed)
45	t.Run("loose permissions fail closed", testLifecycleLockLoosePermissionsFailClosed)
46}
47
48func testLifecycleLockIsOwnerOnlyRegular(t *testing.T) {
49	runtimeDir := filepath.Join(t.TempDir(), "runtime")
50	owner, err := TryAcquire(runtimeDir)
51	require.NoError(t, err)
52	defer func() { require.NoError(t, owner.Release()) }()
53
54	info, err := os.Lstat(Path(runtimeDir))
55	require.NoError(t, err)
56	require.True(t, info.Mode().IsRegular(), "the lock must be a regular file")
57	require.Equal(t, os.FileMode(0o600), info.Mode().Perm(), "the lock must be owner-only")
58}
59
60// testLifecycleLockOwnerOnlyVariantAccepted proves a pre-existing owner-only
61// lock with a different owner bit pattern is trusted, so an installation whose
62// lock was created under a variant umask can still start the daemon.
63func testLifecycleLockOwnerOnlyVariantAccepted(t *testing.T) {
64	runtimeDir := filepath.Join(t.TempDir(), "runtime")
65	require.NoError(t, os.MkdirAll(runtimeDir, 0o700))
66	require.NoError(t, os.WriteFile(Path(runtimeDir), nil, 0o600))
67	require.NoError(t, os.Chmod(Path(runtimeDir), 0o700))
68
69	owner, err := TryAcquire(runtimeDir)
70	require.NoError(t, err)
71	require.NoError(t, owner.Release())
72}
73
74// testLifecycleLockRestrictiveUmaskNormalizes proves a newly created lock is
75// chmodded to 0600 even when the process umask would have stripped owner bits,
76// so the lock is always exactly owner-only after creation.
77func testLifecycleLockRestrictiveUmaskNormalizes(t *testing.T) {
78	runtimeDir := filepath.Join(t.TempDir(), "runtime")
79	require.NoError(t, os.MkdirAll(runtimeDir, 0o700))
80	previous := syscall.Umask(0o400)
81	defer syscall.Umask(previous)
82
83	owner, err := TryAcquire(runtimeDir)
84	require.NoError(t, err)
85	defer func() { require.NoError(t, owner.Release()) }()
86
87	info, err := os.Lstat(Path(runtimeDir))
88	require.NoError(t, err)
89	require.Equal(t, os.FileMode(0o600), info.Mode().Perm(), "a newly created lock must be normalized to owner-only")
90}
91
92func testLifecycleLockSymlinkFailsClosed(t *testing.T) {
93	runtimeDir := filepath.Join(t.TempDir(), "runtime")
94	require.NoError(t, os.MkdirAll(runtimeDir, 0o700))
95	target := filepath.Join(t.TempDir(), "target.lock")
96	require.NoError(t, os.WriteFile(target, nil, 0o600))
97	require.NoError(t, os.Symlink(target, Path(runtimeDir)))
98
99	_, err := TryAcquire(runtimeDir)
100	require.Error(t, err)
101	require.False(t, errors.Is(err, ErrBusy), "a symlinked lock path is an error, not contention")
102	require.Contains(t, err.Error(), "symlink")
103}
104
105func testLifecycleLockNonRegularFailsClosed(t *testing.T) {
106	runtimeDir := filepath.Join(t.TempDir(), "runtime")
107	require.NoError(t, os.MkdirAll(runtimeDir, 0o700))
108	require.NoError(t, syscall.Mkfifo(Path(runtimeDir), 0o600))
109
110	_, err := TryAcquire(runtimeDir)
111	require.Error(t, err)
112	require.False(t, errors.Is(err, ErrBusy), "a non-regular lock path is an error, not contention")
113	require.Contains(t, err.Error(), "not a regular file")
114}
115
116func testLifecycleLockLoosePermissionsFailClosed(t *testing.T) {
117	runtimeDir := filepath.Join(t.TempDir(), "runtime")
118	require.NoError(t, os.MkdirAll(runtimeDir, 0o700))
119	require.NoError(t, os.WriteFile(Path(runtimeDir), nil, 0o600))
120	require.NoError(t, os.Chmod(Path(runtimeDir), 0o644))
121
122	_, err := TryAcquire(runtimeDir)
123	require.Error(t, err)
124	require.False(t, errors.Is(err, ErrBusy), "a loose lock file is an error, not contention")
125	require.Contains(t, err.Error(), "permissions")
126}
127
128func testLifecycleLockProcessContentionAndReacquire(t *testing.T) {
129	runtimeDir := filepath.Join(t.TempDir(), "runtime")
130	cmd := exec.Command(os.Args[0], "-test.run=^TestLifecycleLockProcessHelper$")
131	cmd.Env = append(os.Environ(),
132		"VEV_LIFECYCLE_LOCK_HELPER=1",
133		"VEV_LIFECYCLE_LOCK_DIR="+runtimeDir,
134	)
135	stdin, err := cmd.StdinPipe()
136	require.NoError(t, err)
137	stdout, err := cmd.StdoutPipe()
138	require.NoError(t, err)
139	cmd.Stderr = os.Stderr
140	require.NoError(t, cmd.Start())
141	defer func() {
142		_ = stdin.Close()
143		_ = cmd.Wait()
144	}()
145
146	line, err := bufio.NewReader(stdout).ReadString('\n')
147	require.NoError(t, err)
148	require.Equal(t, "locked\n", line)
149
150	_, err = TryAcquire(runtimeDir)
151	require.ErrorIs(t, err, ErrBusy)
152
153	require.NoError(t, stdin.Close())
154	require.NoError(t, cmd.Wait())
155
156	owner, err := TryAcquire(runtimeDir)
157	require.NoError(t, err)
158	require.NoError(t, owner.Release())
159	require.NoError(t, owner.Release(), "release is idempotent")
160}
161
162func testLifecycleLockAcquireCancellation(t *testing.T) {
163	runtimeDir := filepath.Join(t.TempDir(), "runtime")
164	owner, err := TryAcquire(runtimeDir)
165	require.NoError(t, err)
166	defer func() { require.NoError(t, owner.Release()) }()
167
168	ctx, cancel := context.WithCancel(context.Background())
169	cancel()
170
171	_, err = Acquire(ctx, runtimeDir, time.Millisecond)
172	require.ErrorIs(t, err, context.Canceled)
173}
174
175func testLifecycleLockUnavailableRuntimeDirectory(t *testing.T) {
176	runtimePath := t.TempDir() + "/not-a-directory"
177	require.NoError(t, os.WriteFile(runtimePath, []byte("occupied"), 0o600))
178
179	_, err := TryAcquire(runtimePath)
180	require.Error(t, err)
181	require.False(t, errors.Is(err, ErrBusy))
182}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 12

Tags 11

↑↓ moveenter openesc close