move_transaction_test.go RawHistoryFolder

1package daemon
2
3import (
4	"sync"
5	"testing"
6
7	"github.com/stretchr/testify/require"
8
9	"github.com/bnema/vev/internal/domain"
10)
11
12func TestMoveTransactionRejectsStalePublicationWithoutLeakingAuthority(t *testing.T) {
13	tests := []struct {
14		name        string
15		installRace func(*Daemon, *tab)
16		move        func(*Daemon, *session, *tab, *pane, *session, *tab) error
17	}{
18		{
19			name: "pane",
20			installRace: func(d *Daemon, movedTab *tab) {
21				d.afterMovePaneSourceSnapshot = func() {
22					movedTab.mu.Lock()
23					movedTab.layoutGeneration++
24					movedTab.mu.Unlock()
25				}
26			},
27			move: func(d *Daemon, source *session, sourceTab *tab, moved *pane, destination *session, destinationTab *tab) error {
28				return d.movePane(movePaneRequest{
29					Source:           moveSessionLocator{ID: source.id, Incarnation: source.incarnation},
30					SourceTabID:      domain.TabStableID(sourceTab.stableID),
31					SourcePaneID:     domain.PaneStableID(moved.stableID),
32					Destination:      moveSessionLocator{ID: destination.id, Incarnation: destination.incarnation},
33					DestinationTabID: domain.TabStableID(destinationTab.stableID),
34				})
35			},
36		},
37		{
38			name: "tab",
39			installRace: func(d *Daemon, movedTab *tab) {
40				d.afterMoveTabSourceSnapshot = func() {
41					movedTab.mu.Lock()
42					movedTab.layoutGeneration++
43					movedTab.mu.Unlock()
44				}
45			},
46			move: func(d *Daemon, source *session, sourceTab *tab, _ *pane, destination *session, _ *tab) error {
47				return d.moveTab(moveTabRequest{
48					Source:      moveSessionLocator{ID: source.id, Incarnation: source.incarnation},
49					SourceTabID: domain.TabStableID(sourceTab.stableID),
50					Destination: moveSessionLocator{ID: destination.id, Incarnation: destination.incarnation},
51				})
52			},
53		},
54	}
55
56	for _, tt := range tests {
57		t.Run(tt.name, func(t *testing.T) {
58			d, source, sourceTab, moved, destination, destinationTab, releasePTYs := newMoveGateRaceFixture(t)
59			defer releasePTYs()
60
61			source.mu.Lock()
62			beforeSourceTabs := append([]*tab(nil), source.tabs...)
63			sourceAttachments := source.snapshotAttachmentsLocked()
64			source.mu.Unlock()
65			destination.mu.Lock()
66			beforeDestinationTabs := append([]*tab(nil), destination.tabs...)
67			destination.mu.Unlock()
68			beforeOwner := moved.ownerSnapshot()
69
70			tt.installRace(d, sourceTab)
71			err := tt.move(d, source, sourceTab, moved, destination, destinationTab)
72			require.ErrorIs(t, err, errMoveStaleTarget)
73
74			var rejection *moveRejection
75			require.ErrorAs(t, err, &rejection)
76			require.Equal(t, moveRejectionStaleTarget, rejection.Reason)
77
78			source.mu.Lock()
79			require.Equal(t, beforeSourceTabs, source.tabs)
80			require.True(t, sameMoveAttachmentsLocked(source, sourceAttachments))
81			source.mu.Unlock()
82			destination.mu.Lock()
83			require.Equal(t, beforeDestinationTabs, destination.tabs)
84			destination.mu.Unlock()
85			require.Same(t, beforeOwner, moved.ownerSnapshot())
86
87			d.moveLifecycleMu.Lock()
88			require.Zero(t, d.moveLifecycleActive)
89			d.moveLifecycleMu.Unlock()
90			for _, sess := range []*session{source, destination} {
91				sess.teardownMu.Lock()
92				require.Zero(t, sess.moveReservations)
93				sess.teardownMu.Unlock()
94			}
95			for name, lock := range map[string]*sync.Mutex{
96				"source dispatch":        &source.dispatchMu,
97				"destination dispatch":   &destination.dispatchMu,
98				"source layout":          &source.layoutApplyMu,
99				"destination layout":     &destination.layoutApplyMu,
100				"moved tab layout":       &sourceTab.layoutApplyMu,
101				"destination tab layout": &destinationTab.layoutApplyMu,
102				"moved pane resize":      &moved.resizeMu,
103			} {
104				require.True(t, lock.TryLock(), "%s authority leaked after rejection", name)
105				lock.Unlock()
106			}
107			for _, attachment := range sourceAttachments {
108				requireAttachmentEffectGateRetired(t, attachment)
109			}
110		})
111	}
112}

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