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}