test(daemon): cover resize action adapter boundaries
- commit
- 10f16b25e98755a43123b0398c21b12412357568
- author
- brice · · 2026-07-25 05:03 UTC
- parent
- 563179c
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
1 file · +68 -8
M internal/usecase/daemon/input_test.go +68 -8
| @@ -69,6 +69,7 @@ func TestResizeActionAdaptersProduceEquivalentGeometry(t *testing.T) { | |||
| 69 | 69 | type adapterState struct { | |
| 70 | 70 | tree *layout.Tree | |
| 71 | 71 | placements []layout.Placement | |
| 72 | + | ptySizes map[layout.PaneID][]domain.Size | |
| 72 | 73 | generationDelta uint64 | |
| 73 | 74 | snapshotDirtyDelta uint64 | |
| 74 | 75 | } | |
| @@ -87,11 +88,15 @@ func TestResizeActionAdaptersProduceEquivalentGeometry(t *testing.T) { | |||
| 87 | 88 | ||
| 88 | 89 | for _, tt := range tests { | |
| 89 | 90 | t.Run(tt.name, func(t *testing.T) { | |
| 90 | - | run := func(t *testing.T, adapter string) adapterState { | |
| 91 | - | d, sess, ac, _ := newManualSessionWithPTYs(t, nil) | |
| 91 | + | run := func(t *testing.T, adapter string, attached bool) adapterState { | |
| 92 | + | t.Helper() | |
| 93 | + | ptys := map[layout.PaneID]*resizeActionPTY{ | |
| 94 | + | "pane-1": {}, "pane-2": {}, "pane-3": {}, | |
| 95 | + | } | |
| 96 | + | d, sess, ac, _ := newManualSessionWithPTYs(t, ptys["pane-1"]) | |
| 92 | 97 | tb := sess.activeTab() | |
| 93 | - | p2 := newPane("pane-2", nil, tb.size) | |
| 94 | - | p3 := newPane("pane-3", nil, tb.size) | |
| 98 | + | p2 := newPane("pane-2", ptys["pane-2"], tb.size) | |
| 99 | + | p3 := newPane("pane-3", ptys["pane-3"], tb.size) | |
| 95 | 100 | tb.mu.Lock() | |
| 96 | 101 | tb.panes[p2.id] = p2 | |
| 97 | 102 | tb.panes[p3.id] = p3 | |
| @@ -114,6 +119,20 @@ func TestResizeActionAdaptersProduceEquivalentGeometry(t *testing.T) { | |||
| 114 | 119 | dirty := sess.snapshotGeneration | |
| 115 | 120 | sess.snapshotMu.Unlock() | |
| 116 | 121 | ||
| 122 | + | invalidations := make(chan renderInvalidation, 2) | |
| 123 | + | rc := newRenderCoordinator(renderCoordinatorOptions{ | |
| 124 | + | wake: func(renderWake) {}, | |
| 125 | + | onInvalidate: func(inv renderInvalidation) { invalidations <- inv }, | |
| 126 | + | }) | |
| 127 | + | sess.installRenderCoordinator(rc) | |
| 128 | + | if attached { | |
| 129 | + | rc.attach(ac) | |
| 130 | + | } else { | |
| 131 | + | sess.mu.Lock() | |
| 132 | + | sess.client = nil | |
| 133 | + | sess.mu.Unlock() | |
| 134 | + | } | |
| 135 | + | ||
| 117 | 136 | switch adapter { | |
| 118 | 137 | case "key": | |
| 119 | 138 | daemonKeyHandler{d: d, ac: ac}.Action(tt.action) | |
| @@ -135,18 +154,59 @@ func TestResizeActionAdaptersProduceEquivalentGeometry(t *testing.T) { | |||
| 135 | 154 | sess.snapshotMu.Lock() | |
| 136 | 155 | gotDirty := sess.snapshotGeneration | |
| 137 | 156 | sess.snapshotMu.Unlock() | |
| 138 | - | return adapterState{tree: tree, placements: placements, generationDelta: gotGeneration - generation, snapshotDirtyDelta: gotDirty - dirty} | |
| 157 | + | ||
| 158 | + | gotPTY := make(map[layout.PaneID][]domain.Size, len(ptys)) | |
| 159 | + | for id, pty := range ptys { | |
| 160 | + | gotPTY[id] = pty.Sizes() | |
| 161 | + | } | |
| 162 | + | for _, placement := range placements { | |
| 163 | + | require.Equal(t, []domain.Size{{Cols: placement.Content.Width, Rows: placement.Content.Height}}, gotPTY[placement.ID], "canonical PTY resize for %s", placement.ID) | |
| 164 | + | } | |
| 165 | + | if attached { | |
| 166 | + | awaitInvalidation(t, invalidations) | |
| 167 | + | requireNoInvalidation(t, invalidations) | |
| 168 | + | } else { | |
| 169 | + | requireNoInvalidation(t, invalidations) | |
| 170 | + | } | |
| 171 | + | return adapterState{tree: tree, placements: placements, ptySizes: gotPTY, generationDelta: gotGeneration - generation, snapshotDirtyDelta: gotDirty - dirty} | |
| 139 | 172 | } | |
| 140 | 173 | ||
| 141 | - | keyState := run(t, "key") | |
| 174 | + | keyState := run(t, "key", true) | |
| 142 | 175 | require.Equal(t, uint64(1), keyState.generationDelta) | |
| 143 | 176 | require.Equal(t, uint64(1), keyState.snapshotDirtyDelta) | |
| 144 | - | require.Equal(t, keyState, run(t, "palette")) | |
| 145 | - | require.Equal(t, keyState, run(t, "control")) | |
| 177 | + | require.Equal(t, keyState, run(t, "palette", true)) | |
| 178 | + | require.Equal(t, keyState, run(t, "control", true)) | |
| 179 | + | headless := run(t, "control", false) | |
| 180 | + | require.Equal(t, uint64(1), headless.generationDelta) | |
| 181 | + | require.Equal(t, uint64(1), headless.snapshotDirtyDelta) | |
| 182 | + | require.Equal(t, keyState, headless) | |
| 146 | 183 | }) | |
| 147 | 184 | } | |
| 148 | 185 | } | |
| 149 | 186 | ||
| 187 | + | type resizeActionPTY struct { | |
| 188 | + | mu sync.Mutex | |
| 189 | + | sizes []domain.Size | |
| 190 | + | } | |
| 191 | + | ||
| 192 | + | func (*resizeActionPTY) Read([]byte) (int, error) { return 0, nil } | |
| 193 | + | func (*resizeActionPTY) Write(b []byte) (int, error) { return len(b), nil } | |
| 194 | + | func (*resizeActionPTY) Close() error { return nil } | |
| 195 | + | func (*resizeActionPTY) Pid() int { return 0 } | |
| 196 | + | func (*resizeActionPTY) ForegroundPgid() (int, error) { return 0, nil } | |
| 197 | + | func (p *resizeActionPTY) Resize(size domain.Size) error { | |
| 198 | + | p.mu.Lock() | |
| 199 | + | p.sizes = append(p.sizes, size) | |
| 200 | + | p.mu.Unlock() | |
| 201 | + | return nil | |
| 202 | + | } | |
| 203 | + | ||
| 204 | + | func (p *resizeActionPTY) Sizes() []domain.Size { | |
| 205 | + | p.mu.Lock() | |
| 206 | + | defer p.mu.Unlock() | |
| 207 | + | return append([]domain.Size(nil), p.sizes...) | |
| 208 | + | } | |
| 209 | + | ||
| 150 | 210 | // stubClock returns timers whose channel never fires, so a scheduler under it | |
| 151 | 211 | // blocks in its debounce loop until the session context is cancelled. Used by | |
| 152 | 212 | ||
| @@ -69,6 +69,7 @@ func TestResizeActionAdaptersProduceEquivalentGeometry(t *testing.T) { | |||||
| 69 | type adapterState struct { | 69 | type adapterState struct { | ||
| 70 | tree *layout.Tree | 70 | tree *layout.Tree | ||
| 71 | placements []layout.Placement | 71 | placements []layout.Placement | ||
| 72 | + | ptySizes map[layout.PaneID][]domain.Size | |||
| 72 | generationDelta uint64 | 73 | generationDelta uint64 | ||
| 73 | snapshotDirtyDelta uint64 | 74 | snapshotDirtyDelta uint64 | ||
| 74 | } | 75 | } | ||
| @@ -87,11 +88,15 @@ func TestResizeActionAdaptersProduceEquivalentGeometry(t *testing.T) { | |||||
| 87 | 88 | ||||
| 88 | for _, tt := range tests { | 89 | for _, tt := range tests { | ||
| 89 | t.Run(tt.name, func(t *testing.T) { | 90 | t.Run(tt.name, func(t *testing.T) { | ||
| 90 | - | run := func(t *testing.T, adapter string) adapterState { | 91 | + | run := func(t *testing.T, adapter string, attached bool) adapterState { |
| 91 | - | d, sess, ac, _ := newManualSessionWithPTYs(t, nil) | 92 | + | t.Helper() |
| 93 | + | ptys := map[layout.PaneID]*resizeActionPTY{ | |||
| 94 | + | "pane-1": {}, "pane-2": {}, "pane-3": {}, | |||
| 95 | + | } | |||
| 96 | + | d, sess, ac, _ := newManualSessionWithPTYs(t, ptys["pane-1"]) | |||
| 92 | tb := sess.activeTab() | 97 | tb := sess.activeTab() | ||
| 93 | - | p2 := newPane("pane-2", nil, tb.size) | 98 | + | p2 := newPane("pane-2", ptys["pane-2"], tb.size) |
| 94 | - | p3 := newPane("pane-3", nil, tb.size) | 99 | + | p3 := newPane("pane-3", ptys["pane-3"], tb.size) |
| 95 | tb.mu.Lock() | 100 | tb.mu.Lock() | ||
| 96 | tb.panes[p2.id] = p2 | 101 | tb.panes[p2.id] = p2 | ||
| 97 | tb.panes[p3.id] = p3 | 102 | tb.panes[p3.id] = p3 | ||
| @@ -114,6 +119,20 @@ func TestResizeActionAdaptersProduceEquivalentGeometry(t *testing.T) { | |||||
| 114 | dirty := sess.snapshotGeneration | 119 | dirty := sess.snapshotGeneration | ||
| 115 | sess.snapshotMu.Unlock() | 120 | sess.snapshotMu.Unlock() | ||
| 116 | 121 | ||||
| 122 | + | invalidations := make(chan renderInvalidation, 2) | |||
| 123 | + | rc := newRenderCoordinator(renderCoordinatorOptions{ | |||
| 124 | + | wake: func(renderWake) {}, | |||
| 125 | + | onInvalidate: func(inv renderInvalidation) { invalidations <- inv }, | |||
| 126 | + | }) | |||
| 127 | + | sess.installRenderCoordinator(rc) | |||
| 128 | + | if attached { | |||
| 129 | + | rc.attach(ac) | |||
| 130 | + | } else { | |||
| 131 | + | sess.mu.Lock() | |||
| 132 | + | sess.client = nil | |||
| 133 | + | sess.mu.Unlock() | |||
| 134 | + | } | |||
| 135 | + | ||||
| 117 | switch adapter { | 136 | switch adapter { | ||
| 118 | case "key": | 137 | case "key": | ||
| 119 | daemonKeyHandler{d: d, ac: ac}.Action(tt.action) | 138 | daemonKeyHandler{d: d, ac: ac}.Action(tt.action) | ||
| @@ -135,18 +154,59 @@ func TestResizeActionAdaptersProduceEquivalentGeometry(t *testing.T) { | |||||
| 135 | sess.snapshotMu.Lock() | 154 | sess.snapshotMu.Lock() | ||
| 136 | gotDirty := sess.snapshotGeneration | 155 | gotDirty := sess.snapshotGeneration | ||
| 137 | sess.snapshotMu.Unlock() | 156 | sess.snapshotMu.Unlock() | ||
| 138 | - | return adapterState{tree: tree, placements: placements, generationDelta: gotGeneration - generation, snapshotDirtyDelta: gotDirty - dirty} | 157 | + | |
| 158 | + | gotPTY := make(map[layout.PaneID][]domain.Size, len(ptys)) | |||
| 159 | + | for id, pty := range ptys { | |||
| 160 | + | gotPTY[id] = pty.Sizes() | |||
| 161 | + | } | |||
| 162 | + | for _, placement := range placements { | |||
| 163 | + | require.Equal(t, []domain.Size{{Cols: placement.Content.Width, Rows: placement.Content.Height}}, gotPTY[placement.ID], "canonical PTY resize for %s", placement.ID) | |||
| 164 | + | } | |||
| 165 | + | if attached { | |||
| 166 | + | awaitInvalidation(t, invalidations) | |||
| 167 | + | requireNoInvalidation(t, invalidations) | |||
| 168 | + | } else { | |||
| 169 | + | requireNoInvalidation(t, invalidations) | |||
| 170 | + | } | |||
| 171 | + | return adapterState{tree: tree, placements: placements, ptySizes: gotPTY, generationDelta: gotGeneration - generation, snapshotDirtyDelta: gotDirty - dirty} | |||
| 139 | } | 172 | } | ||
| 140 | 173 | ||||
| 141 | - | keyState := run(t, "key") | 174 | + | keyState := run(t, "key", true) |
| 142 | require.Equal(t, uint64(1), keyState.generationDelta) | 175 | require.Equal(t, uint64(1), keyState.generationDelta) | ||
| 143 | require.Equal(t, uint64(1), keyState.snapshotDirtyDelta) | 176 | require.Equal(t, uint64(1), keyState.snapshotDirtyDelta) | ||
| 144 | - | require.Equal(t, keyState, run(t, "palette")) | 177 | + | require.Equal(t, keyState, run(t, "palette", true)) |
| 145 | - | require.Equal(t, keyState, run(t, "control")) | 178 | + | require.Equal(t, keyState, run(t, "control", true)) |
| 179 | + | headless := run(t, "control", false) | |||
| 180 | + | require.Equal(t, uint64(1), headless.generationDelta) | |||
| 181 | + | require.Equal(t, uint64(1), headless.snapshotDirtyDelta) | |||
| 182 | + | require.Equal(t, keyState, headless) | |||
| 146 | }) | 183 | }) | ||
| 147 | } | 184 | } | ||
| 148 | } | 185 | } | ||
| 149 | 186 | ||||
| 187 | + | type resizeActionPTY struct { | |||
| 188 | + | mu sync.Mutex | |||
| 189 | + | sizes []domain.Size | |||
| 190 | + | } | |||
| 191 | + | ||||
| 192 | + | func (*resizeActionPTY) Read([]byte) (int, error) { return 0, nil } | |||
| 193 | + | func (*resizeActionPTY) Write(b []byte) (int, error) { return len(b), nil } | |||
| 194 | + | func (*resizeActionPTY) Close() error { return nil } | |||
| 195 | + | func (*resizeActionPTY) Pid() int { return 0 } | |||
| 196 | + | func (*resizeActionPTY) ForegroundPgid() (int, error) { return 0, nil } | |||
| 197 | + | func (p *resizeActionPTY) Resize(size domain.Size) error { | |||
| 198 | + | p.mu.Lock() | |||
| 199 | + | p.sizes = append(p.sizes, size) | |||
| 200 | + | p.mu.Unlock() | |||
| 201 | + | return nil | |||
| 202 | + | } | |||
| 203 | + | ||||
| 204 | + | func (p *resizeActionPTY) Sizes() []domain.Size { | |||
| 205 | + | p.mu.Lock() | |||
| 206 | + | defer p.mu.Unlock() | |||
| 207 | + | return append([]domain.Size(nil), p.sizes...) | |||
| 208 | + | } | |||
| 209 | + | ||||
| 150 | // stubClock returns timers whose channel never fires, so a scheduler under it | 210 | // stubClock returns timers whose channel never fires, so a scheduler under it | ||
| 151 | // blocks in its debounce loop until the session context is cancelled. Used by | 211 | // blocks in its debounce loop until the session context is cancelled. Used by | ||
| 152 | 212 | ||||