v0.3.0 99dc266

move_lifecycle_test.go RawHistoryFolder

1package daemon
2
3import (
4	"testing"
5	"time"
6
7	"github.com/stretchr/testify/require"
8
9	"github.com/bnema/vev/internal/ports"
10)
11
12func awaitMoveTeardownState(t *testing.T, sess *session, reservations, waiters uint) {
13	t.Helper()
14	for {
15		sess.teardownMu.Lock()
16		gotReservations := sess.moveReservations
17		gotWaiters := sess.teardownWaiters
18		changed := sess.teardownChangeLocked()
19		sess.teardownMu.Unlock()
20		if gotReservations == reservations && gotWaiters == waiters {
21			return
22		}
23		select {
24		case <-changed:
25		case <-time.After(testWaitTimeout):
26			t.Fatalf("move reservations/waiters = %d/%d, want %d/%d", gotReservations, gotWaiters, reservations, waiters)
27		}
28	}
29}
30
31func awaitDaemonMoveActive(t *testing.T, d *Daemon, want uint) {
32	t.Helper()
33	for {
34		d.moveLifecycleMu.Lock()
35		active := d.moveLifecycleActive
36		changed := d.moveLifecycleChangeLocked()
37		d.moveLifecycleMu.Unlock()
38		if active == want {
39			return
40		}
41		select {
42		case <-changed:
43		case <-time.After(testWaitTimeout):
44			t.Fatalf("active move lifecycles = %d, want %d", active, want)
45		}
46	}
47}
48
49func awaitDaemonMoveClosing(t *testing.T, d *Daemon) {
50	t.Helper()
51	for {
52		d.moveLifecycleMu.Lock()
53		closing := d.moveLifecycleClosing
54		changed := d.moveLifecycleChangeLocked()
55		d.moveLifecycleMu.Unlock()
56		if closing {
57			return
58		}
59		select {
60		case <-changed:
61		case <-time.After(testWaitTimeout):
62			t.Fatal("daemon move lifecycle gate did not begin closing")
63		}
64	}
65}
66
67func TestMoveLifecycleReservationMakesTeardownWait(t *testing.T) {
68	d := newTestDaemon(t, nil, stubClock{})
69	sess := &session{sessionCore: sessionCore{id: "source"}}
70
71	reservation, err := d.reserveMoveLifecycles(sess, sess)
72	require.NoError(t, err)
73	awaitMoveTeardownState(t, sess, 1, 0)
74
75	teardown := make(chan bool, 1)
76	go func() { teardown <- sess.beginTeardown(nil) }()
77	awaitMoveTeardownState(t, sess, 1, 1)
78	select {
79	case <-teardown:
80		t.Fatal("teardown claimed ownership while the move lifecycle was reserved")
81	default:
82	}
83
84	reservation.Release()
85	require.True(t, awaitTestValue(t, teardown, "teardown did not wake after move release"))
86	sess.finishTeardown()
87}
88
89func TestMoveLifecycleReservationsUseStableSessionOrder(t *testing.T) {
90	d := newTestDaemon(t, nil, stubClock{})
91	first := &session{sessionCore: sessionCore{id: "a"}}
92	second := &session{sessionCore:
93
94	// Both opposite-direction reservations must block on a before either takes
95	// b. This proves order from immutable IDs rather than argument direction.
96	sessionCore{id: "b"}}
97
98	first.teardownMu.Lock()
99	beforeTeardownLocks := make(chan struct{}, 2)
100	d.afterMoveLifecycleGateBeforeTeardownLocks = func() { beforeTeardownLocks <- struct{}{} }
101	defer func() { d.afterMoveLifecycleGateBeforeTeardownLocks = nil }()
102	results := make(chan *moveLifecycleReservation, 2)
103	errs := make(chan error, 2)
104	for _, pair := range [][2]*session{{first, second}, {second, first}} {
105		go func() {
106			reservation, err := d.reserveMoveLifecycles(pair[0], pair[1])
107			results <- reservation
108			errs <- err
109		}()
110	}
111	awaitDaemonMoveActive(t, d, 2)
112	for range 2 {
113		awaitTestCompletion(t, beforeTeardownLocks, "move did not reach ordered teardown-lock admission")
114	}
115	require.True(t, second.teardownMu.TryLock(), "opposite move locked b before the shared lowest-ID session")
116	second.teardownMu.Unlock()
117	first.teardownMu.Unlock()
118
119	for range 2 {
120		require.NoError(t, awaitTestValue(t, errs, "opposite move reservation did not finish"))
121		reservation := awaitTestValue(t, results, "opposite move reservation token missing")
122		require.NotNil(t, reservation)
123		reservation.Release()
124	}
125	awaitDaemonMoveActive(t, d, 0)
126}
127
128func TestMoveLifecycleReservationRejectsTeardownWithoutLeakingCounts(t *testing.T) {
129	for _, teardownSession := range []string{"source", "destination"} {
130		t.Run(teardownSession, func(t *testing.T) {
131			d := newTestDaemon(t, nil, stubClock{})
132			source := &session{sessionCore: sessionCore{id: "source"}}
133			destination := &session{sessionCore: sessionCore{id: "destination"}}
134			tearingDown := source
135			if teardownSession == "destination" {
136				tearingDown = destination
137			}
138			require.True(t, tearingDown.beginTeardown(nil))
139
140			reservation, err := d.reserveMoveLifecycles(source, destination)
141			require.ErrorIs(t, err, errMoveLifecycleUnavailable)
142			require.Nil(t, reservation)
143			awaitMoveTeardownState(t, source, 0, 0)
144			awaitMoveTeardownState(t, destination, 0, 0)
145			awaitDaemonMoveActive(t, d, 0)
146			tearingDown.finishTeardown()
147		})
148	}
149}
150
151func TestMoveLifecycleReservationDeduplicatesAndReleasesExactlyOnce(t *testing.T) {
152	d := newTestDaemon(t, nil, stubClock{})
153	sess := &session{sessionCore: sessionCore{id: "same"}}
154	reservation, err := d.reserveMoveLifecycles(sess, sess)
155	require.NoError(t, err)
156	awaitMoveTeardownState(t, sess, 1, 0)
157
158	const releasers = 8
159	released := make(chan struct{}, releasers)
160	for range releasers {
161		go func() {
162			reservation.Release()
163			released <- struct{}{}
164		}()
165	}
166	for range releasers {
167		awaitTestCompletion(t, released, "concurrent reservation release did not return")
168	}
169	awaitMoveTeardownState(t, sess, 0, 0)
170	awaitDaemonMoveActive(t, d, 0)
171}
172
173func TestMoveLifecycleReservationWaitCanBeCancelledWithoutChangingCounts(t *testing.T) {
174	d := newTestDaemon(t, nil, stubClock{})
175	sess := &session{sessionCore: sessionCore{id: "source"}}
176	reservation, err := d.reserveMoveLifecycles(sess, sess)
177	require.NoError(t, err)
178	deadline := &snapshotShutdownDeadline{done: make(chan struct{})}
179
180	teardown := make(chan bool, 1)
181	go func() { teardown <- sess.beginTeardown(deadline) }()
182	awaitMoveTeardownState(t, sess, 1, 1)
183	close(deadline.done)
184	require.False(t, awaitTestValue(t, teardown, "cancelled teardown waiter did not return"))
185	awaitMoveTeardownState(t, sess, 1, 0)
186	awaitDaemonMoveActive(t, d, 1)
187
188	reservation.Release()
189	awaitMoveTeardownState(t, sess, 0, 0)
190	awaitDaemonMoveActive(t, d, 0)
191}
192
193func TestMoveLifecycleReservationWakesAllTeardownWaiters(t *testing.T) {
194	d := newTestDaemon(t, nil, stubClock{})
195	sess := &session{sessionCore: sessionCore{id: "source"}}
196	reservation, err := d.reserveMoveLifecycles(sess, sess)
197	require.NoError(t, err)
198
199	claimed := make(chan struct{}, 2)
200	finish := make(chan struct{}, 2)
201	done := make(chan struct{}, 2)
202	for range 2 {
203		go func() {
204			if sess.beginTeardown(nil) {
205				claimed <- struct{}{}
206				<-finish
207				sess.finishTeardown()
208			}
209			done <- struct{}{}
210		}()
211	}
212	awaitMoveTeardownState(t, sess, 1, 2)
213	reservation.Release()
214	for range 2 {
215		awaitTestCompletion(t, claimed, "teardown waiter did not claim ownership")
216		finish <- struct{}{}
217	}
218	for range 2 {
219		awaitTestCompletion(t, done, "teardown waiter did not complete")
220	}
221	awaitMoveTeardownState(t, sess, 0, 0)
222}
223
224func TestDaemonShutdownDrainsMoveBeforePaneLifetimeCancellation(t *testing.T) {
225	d := newTestDaemon(t, nil, stubClock{})
226	source := &session{sessionCore: sessionCore{id: "source"}}
227	destination := &session{sessionCore: sessionCore{id: "destination"}}
228	reservation, err := d.reserveMoveLifecycles(source, destination)
229	require.NoError(t, err)
230	paneLifetime := d.paneProcessCtx
231	require.NotNil(t, paneLifetime)
232
233	shutdown := make(chan bool, 1)
234	go func() { shutdown <- d.shutdownAll(ports.ReasonServerShutdown) }()
235	awaitDaemonMoveClosing(t, d)
236	select {
237	case <-paneLifetime.Done():
238		t.Fatal("shutdown cancelled transferable pane lifetime before the move released")
239	default:
240	}
241	select {
242	case <-shutdown:
243		t.Fatal("shutdown completed while a move lifecycle remained reserved")
244	default:
245	}
246
247	reservation.Release()
248	awaitTestCompletion(t, paneLifetime.Done(), "shutdown did not cancel pane lifetime after move drain")
249	require.False(t, awaitTestValue(t, shutdown, "shutdown did not continue after move drain"))
250
251	reservation, err = d.reserveMoveLifecycles(source, destination)
252	require.ErrorIs(t, err, errMoveLifecycleUnavailable)
253	require.Nil(t, reservation)
254	awaitDaemonMoveActive(t, d, 0)
255}

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