fix(vt): handle history append failures
- commit
- a7d13d1b93e4f487359380886a55c3e96b660325
- author
- brice · · 2026-07-21 19:05 UTC
- parent
- 4dede25
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
17 files · +119 -53
M internal/usecase/copy/mode.go +22 -2
| @@ -2,6 +2,7 @@ package copy | |||
| 2 | 2 | ||
| 3 | 3 | import ( | |
| 4 | 4 | "encoding/base64" | |
| 5 | + | "math" | |
| 5 | 6 | "slices" | |
| 6 | 7 | "strings" | |
| 7 | 8 | "unicode" | |
| @@ -27,12 +28,31 @@ func NewSnapshot(historySource *vt.History, screen renderer.Frame) Snapshot { | |||
| 27 | 28 | return Snapshot{history: history, screen: screen.Clone(), Width: screen.Width, Height: screen.Height} | |
| 28 | 29 | } | |
| 29 | 30 | func NewSnapshotFromRows(rows [][]renderer.Cell, width, height int) Snapshot { | |
| 30 | - | history := vt.NewHistory(vt.HistoryConfig{MaxRows: len(rows), ChunkRows: 256}) | |
| 31 | + | history := vt.NewHistory(vt.HistoryConfig{ | |
| 32 | + | MaxRows: len(rows), | |
| 33 | + | MaxCells: snapshotRowsCellBudget(rows), | |
| 34 | + | ChunkRows: 256, | |
| 35 | + | }) | |
| 31 | 36 | for _, row := range rows { | |
| 32 | - | history.Append(row) | |
| 37 | + | if err := history.Append(row); err != nil { | |
| 38 | + | panic("copy snapshot: configured history rejected supplied row") | |
| 39 | + | } | |
| 33 | 40 | } | |
| 34 | 41 | return Snapshot{history: history.SealAndView(), Width: width, Height: height} | |
| 35 | 42 | } | |
| 43 | + | ||
| 44 | + | // snapshotRowsCellBudget returns a capacity that retains every supplied row. | |
| 45 | + | // Saturation is safe because a larger representable capacity does not exist. | |
| 46 | + | func snapshotRowsCellBudget(rows [][]renderer.Cell) int { | |
| 47 | + | cells := 0 | |
| 48 | + | for _, row := range rows { | |
| 49 | + | if len(row) > math.MaxInt-cells { | |
| 50 | + | return math.MaxInt | |
| 51 | + | } | |
| 52 | + | cells += len(row) | |
| 53 | + | } | |
| 54 | + | return cells | |
| 55 | + | } | |
| 36 | 56 | func (s Snapshot) Len() int { return s.history.Len() + s.screen.Height } | |
| 37 | 57 | func (s Snapshot) Row(i int) []renderer.Cell { | |
| 38 | 58 | if i < 0 { | |
| @@ -2,6 +2,7 @@ package copy | |||||
| 2 | 2 | ||||
| 3 | import ( | 3 | import ( | ||
| 4 | "encoding/base64" | 4 | "encoding/base64" | ||
| 5 | + | "math" | |||
| 5 | "slices" | 6 | "slices" | ||
| 6 | "strings" | 7 | "strings" | ||
| 7 | "unicode" | 8 | "unicode" | ||
| @@ -27,12 +28,31 @@ func NewSnapshot(historySource *vt.History, screen renderer.Frame) Snapshot { | |||||
| 27 | return Snapshot{history: history, screen: screen.Clone(), Width: screen.Width, Height: screen.Height} | 28 | return Snapshot{history: history, screen: screen.Clone(), Width: screen.Width, Height: screen.Height} | ||
| 28 | } | 29 | } | ||
| 29 | func NewSnapshotFromRows(rows [][]renderer.Cell, width, height int) Snapshot { | 30 | func NewSnapshotFromRows(rows [][]renderer.Cell, width, height int) Snapshot { | ||
| 30 | - | history := vt.NewHistory(vt.HistoryConfig{MaxRows: len(rows), ChunkRows: 256}) | 31 | + | history := vt.NewHistory(vt.HistoryConfig{ |
| 32 | + | MaxRows: len(rows), | |||
| 33 | + | MaxCells: snapshotRowsCellBudget(rows), | |||
| 34 | + | ChunkRows: 256, | |||
| 35 | + | }) | |||
| 31 | for _, row := range rows { | 36 | for _, row := range rows { | ||
| 32 | - | history.Append(row) | 37 | + | if err := history.Append(row); err != nil { |
| 38 | + | panic("copy snapshot: configured history rejected supplied row") | |||
| 39 | + | } | |||
| 33 | } | 40 | } | ||
| 34 | return Snapshot{history: history.SealAndView(), Width: width, Height: height} | 41 | return Snapshot{history: history.SealAndView(), Width: width, Height: height} | ||
| 35 | } | 42 | } | ||
| 43 | + | ||||
| 44 | + | // snapshotRowsCellBudget returns a capacity that retains every supplied row. | |||
| 45 | + | // Saturation is safe because a larger representable capacity does not exist. | |||
| 46 | + | func snapshotRowsCellBudget(rows [][]renderer.Cell) int { | |||
| 47 | + | cells := 0 | |||
| 48 | + | for _, row := range rows { | |||
| 49 | + | if len(row) > math.MaxInt-cells { | |||
| 50 | + | return math.MaxInt | |||
| 51 | + | } | |||
| 52 | + | cells += len(row) | |||
| 53 | + | } | |||
| 54 | + | return cells | |||
| 55 | + | } | |||
| 36 | func (s Snapshot) Len() int { return s.history.Len() + s.screen.Height } | 56 | func (s Snapshot) Len() int { return s.history.Len() + s.screen.Height } | ||
| 37 | func (s Snapshot) Row(i int) []renderer.Cell { | 57 | func (s Snapshot) Row(i int) []renderer.Cell { | ||
| 38 | if i < 0 { | 58 | if i < 0 { | ||
M internal/usecase/copy/mode_test.go +17 -2
| @@ -27,6 +27,21 @@ func modeFor(lines []string, height int) *Mode { | |||
| 27 | 27 | return NewMode(NewDocument(NewSnapshotFromRows(rows, 16, height), domain.DefaultWordSeparators)) | |
| 28 | 28 | } | |
| 29 | 29 | ||
| 30 | + | func TestNewSnapshotFromRowsPreservesWideRows(t *testing.T) { | |
| 31 | + | wide := make([]renderer.Cell, 321) | |
| 32 | + | for i := range wide { | |
| 33 | + | wide[i] = renderer.Cell{Rune: rune('a' + i%26)} | |
| 34 | + | } | |
| 35 | + | rows := [][]renderer.Cell{wide, row("tail")} | |
| 36 | + | ||
| 37 | + | snapshot := NewSnapshotFromRows(rows, len(wide), len(rows)) | |
| 38 | + | ||
| 39 | + | require.Equal(t, len(rows), snapshot.history.Len()) | |
| 40 | + | for i, want := range rows { | |
| 41 | + | require.Equal(t, want, snapshot.history.Row(i)) | |
| 42 | + | } | |
| 43 | + | } | |
| 44 | + | ||
| 30 | 45 | func TestCopyModeKeyboardSelection(t *testing.T) { | |
| 31 | 46 | m := modeFor([]string{"alpha", "xy", "bravo", "charlie"}, 2) | |
| 32 | 47 | require.Equal(t, Pos{Row: 3}, m.Cursor()) | |
| @@ -175,9 +190,9 @@ func TestFindMatchesUsesSealedScrollbackWithoutGlobalCopy(t *testing.T) { | |||
| 175 | 190 | const rows = 10_000 | |
| 176 | 191 | history := vt.NewHistory(vt.HistoryConfig{MaxRows: rows + 1, ChunkRows: 256}) | |
| 177 | 192 | for range rows { | |
| 178 | - | history.Append(row("unmatched")) | |
| 193 | + | require.NoError(t, history.Append(row("unmatched"))) | |
| 179 | 194 | } | |
| 180 | - | history.Append(row("target")) | |
| 195 | + | require.NoError(t, history.Append(row("target"))) | |
| 181 | 196 | snapshot := NewSnapshot(history, renderer.NewFrame(16, 1)) | |
| 182 | 197 | view := history.SealAndView() | |
| 183 | 198 | require.Same(t, view.Chunk(0), snapshot.history.Chunk(0)) | |
| @@ -27,6 +27,21 @@ func modeFor(lines []string, height int) *Mode { | |||||
| 27 | return NewMode(NewDocument(NewSnapshotFromRows(rows, 16, height), domain.DefaultWordSeparators)) | 27 | return NewMode(NewDocument(NewSnapshotFromRows(rows, 16, height), domain.DefaultWordSeparators)) | ||
| 28 | } | 28 | } | ||
| 29 | 29 | ||||
| 30 | + | func TestNewSnapshotFromRowsPreservesWideRows(t *testing.T) { | |||
| 31 | + | wide := make([]renderer.Cell, 321) | |||
| 32 | + | for i := range wide { | |||
| 33 | + | wide[i] = renderer.Cell{Rune: rune('a' + i%26)} | |||
| 34 | + | } | |||
| 35 | + | rows := [][]renderer.Cell{wide, row("tail")} | |||
| 36 | + | ||||
| 37 | + | snapshot := NewSnapshotFromRows(rows, len(wide), len(rows)) | |||
| 38 | + | ||||
| 39 | + | require.Equal(t, len(rows), snapshot.history.Len()) | |||
| 40 | + | for i, want := range rows { | |||
| 41 | + | require.Equal(t, want, snapshot.history.Row(i)) | |||
| 42 | + | } | |||
| 43 | + | } | |||
| 44 | + | ||||
| 30 | func TestCopyModeKeyboardSelection(t *testing.T) { | 45 | func TestCopyModeKeyboardSelection(t *testing.T) { | ||
| 31 | m := modeFor([]string{"alpha", "xy", "bravo", "charlie"}, 2) | 46 | m := modeFor([]string{"alpha", "xy", "bravo", "charlie"}, 2) | ||
| 32 | require.Equal(t, Pos{Row: 3}, m.Cursor()) | 47 | require.Equal(t, Pos{Row: 3}, m.Cursor()) | ||
| @@ -175,9 +190,9 @@ func TestFindMatchesUsesSealedScrollbackWithoutGlobalCopy(t *testing.T) { | |||||
| 175 | const rows = 10_000 | 190 | const rows = 10_000 | ||
| 176 | history := vt.NewHistory(vt.HistoryConfig{MaxRows: rows + 1, ChunkRows: 256}) | 191 | history := vt.NewHistory(vt.HistoryConfig{MaxRows: rows + 1, ChunkRows: 256}) | ||
| 177 | for range rows { | 192 | for range rows { | ||
| 178 | - | history.Append(row("unmatched")) | 193 | + | require.NoError(t, history.Append(row("unmatched"))) |
| 179 | } | 194 | } | ||
| 180 | - | history.Append(row("target")) | 195 | + | require.NoError(t, history.Append(row("target"))) |
| 181 | snapshot := NewSnapshot(history, renderer.NewFrame(16, 1)) | 196 | snapshot := NewSnapshot(history, renderer.NewFrame(16, 1)) | ||
| 182 | view := history.SealAndView() | 197 | view := history.SealAndView() | ||
| 183 | require.Same(t, view.Chunk(0), snapshot.history.Chunk(0)) | 198 | require.Same(t, view.Chunk(0), snapshot.history.Chunk(0)) | ||
M internal/usecase/daemon/copymode_test.go +9 -9
| @@ -301,7 +301,7 @@ func TestCopyModeSearchModalCapturesMouseAndClearsOnExit(t *testing.T) { | |||
| 301 | 301 | d, sess, ac, sends := newManualSessionWithPTYs(t, p) | |
| 302 | 302 | pane := sess.tabs[0].focusedPane() | |
| 303 | 303 | installTestHistory(pane, vt.HistoryConfig{MaxRows: 4}) | |
| 304 | - | pane.history.Append(testRow("old alpha")) | |
| 304 | + | require.NoError(t, pane.history.Append(testRow("old alpha"))) | |
| 305 | 305 | copy(pane.screen.Frame.Row(0), testRow("live alpha")) | |
| 306 | 306 | ||
| 307 | 307 | d.enterCopyMode(sess, ac) | |
| @@ -330,8 +330,8 @@ func TestCopyModeInputNotForwardedAndOSC52Copy(t *testing.T) { | |||
| 330 | 330 | p, _ := newBlockingPTYWithWrites(t, writes) | |
| 331 | 331 | d, sess, ac, sends := newManualSessionWithPTYs(t, p) | |
| 332 | 332 | installTestHistory(sess.tabs[0].focusedPane(), vt.HistoryConfig{MaxRows: 4}) | |
| 333 | - | sess.tabs[0].focusedPane().history.Append(testRow("old1 ")) | |
| 334 | - | sess.tabs[0].focusedPane().history.Append(testRow("old2 ")) | |
| 333 | + | require.NoError(t, sess.tabs[0].focusedPane().history.Append(testRow("old1 "))) | |
| 334 | + | require.NoError(t, sess.tabs[0].focusedPane().history.Append(testRow("old2 "))) | |
| 335 | 335 | sess.tabs[0].focusedPane().screen.Write([]byte("live")) | |
| 336 | 336 | ||
| 337 | 337 | d.enterCopyMode(sess, ac) | |
| @@ -505,8 +505,8 @@ func TestCopyModeSplitArrowDoesNotExit(t *testing.T) { | |||
| 505 | 505 | p, _ := newBlockingPTY(t) | |
| 506 | 506 | d, sess, ac, sends := newManualSessionWithPTYs(t, p) | |
| 507 | 507 | installTestHistory(sess.tabs[0].focusedPane(), vt.HistoryConfig{MaxRows: 4}) | |
| 508 | - | sess.tabs[0].focusedPane().history.Append(testRow("old1 ")) | |
| 509 | - | sess.tabs[0].focusedPane().history.Append(testRow("old2 ")) | |
| 508 | + | require.NoError(t, sess.tabs[0].focusedPane().history.Append(testRow("old1 "))) | |
| 509 | + | require.NoError(t, sess.tabs[0].focusedPane().history.Append(testRow("old2 "))) | |
| 510 | 510 | sess.tabs[0].focusedPane().screen.Write([]byte("live")) | |
| 511 | 511 | ||
| 512 | 512 | d.enterCopyMode(sess, ac) | |
| @@ -739,11 +739,11 @@ func TestCopyModeCapturesSourceAndRetainsItAcrossFocusMove(t *testing.T) { | |||
| 739 | 739 | var want *pane | |
| 740 | 740 | if tc.useFloating { | |
| 741 | 741 | want = installFloatingCopyFixture(t, sess, domain.Size{Cols: 20, Rows: 3}) | |
| 742 | - | want.history.Append(testRow("flt-old")) | |
| 742 | + | require.NoError(t, want.history.Append(testRow("flt-old"))) | |
| 743 | 743 | want.screen.Write([]byte("flt-live")) | |
| 744 | 744 | } else { | |
| 745 | 745 | want = main | |
| 746 | - | want.history.Append(testRow("one-old")) | |
| 746 | + | require.NoError(t, want.history.Append(testRow("one-old"))) | |
| 747 | 747 | want.screen.Write([]byte("one-live")) | |
| 748 | 748 | } | |
| 749 | 749 | ||
| @@ -780,7 +780,7 @@ func TestFloatingCopyModeWheelUsesCapturedSnapshot(t *testing.T) { | |||
| 780 | 780 | d, sess, ac, sends := newManualSessionWithPTYs(t, normal) | |
| 781 | 781 | fp := installFloatingCopyFixture(t, sess, domain.Size{Cols: 20, Rows: 3}) | |
| 782 | 782 | for i := range 30 { | |
| 783 | - | fp.history.Append(testRow(fmt.Sprintf("old-%02d", i))) | |
| 783 | + | require.NoError(t, fp.history.Append(testRow(fmt.Sprintf("old-%02d", i)))) | |
| 784 | 784 | } | |
| 785 | 785 | fp.screen.Write([]byte("live")) | |
| 786 | 786 | fp.mu.Lock() | |
| @@ -806,7 +806,7 @@ func TestFloatingCopyModeMouseSelectsFloatingRows(t *testing.T) { | |||
| 806 | 806 | d, sess, ac, sends := newManualSessionWithPTYs(t, normal) | |
| 807 | 807 | fp := installFloatingCopyFixture(t, sess, domain.Size{Cols: 20, Rows: 3}) | |
| 808 | 808 | for i := range 5 { | |
| 809 | - | fp.history.Append(testRow(fmt.Sprintf("old-%d", i))) | |
| 809 | + | require.NoError(t, fp.history.Append(testRow(fmt.Sprintf("old-%d", i)))) | |
| 810 | 810 | } | |
| 811 | 811 | fp.screen.Write([]byte("live")) | |
| 812 | 812 | ||
| @@ -301,7 +301,7 @@ func TestCopyModeSearchModalCapturesMouseAndClearsOnExit(t *testing.T) { | |||||
| 301 | d, sess, ac, sends := newManualSessionWithPTYs(t, p) | 301 | d, sess, ac, sends := newManualSessionWithPTYs(t, p) | ||
| 302 | pane := sess.tabs[0].focusedPane() | 302 | pane := sess.tabs[0].focusedPane() | ||
| 303 | installTestHistory(pane, vt.HistoryConfig{MaxRows: 4}) | 303 | installTestHistory(pane, vt.HistoryConfig{MaxRows: 4}) | ||
| 304 | - | pane.history.Append(testRow("old alpha")) | 304 | + | require.NoError(t, pane.history.Append(testRow("old alpha"))) |
| 305 | copy(pane.screen.Frame.Row(0), testRow("live alpha")) | 305 | copy(pane.screen.Frame.Row(0), testRow("live alpha")) | ||
| 306 | 306 | ||||
| 307 | d.enterCopyMode(sess, ac) | 307 | d.enterCopyMode(sess, ac) | ||
| @@ -330,8 +330,8 @@ func TestCopyModeInputNotForwardedAndOSC52Copy(t *testing.T) { | |||||
| 330 | p, _ := newBlockingPTYWithWrites(t, writes) | 330 | p, _ := newBlockingPTYWithWrites(t, writes) | ||
| 331 | d, sess, ac, sends := newManualSessionWithPTYs(t, p) | 331 | d, sess, ac, sends := newManualSessionWithPTYs(t, p) | ||
| 332 | installTestHistory(sess.tabs[0].focusedPane(), vt.HistoryConfig{MaxRows: 4}) | 332 | installTestHistory(sess.tabs[0].focusedPane(), vt.HistoryConfig{MaxRows: 4}) | ||
| 333 | - | sess.tabs[0].focusedPane().history.Append(testRow("old1 ")) | 333 | + | require.NoError(t, sess.tabs[0].focusedPane().history.Append(testRow("old1 "))) |
| 334 | - | sess.tabs[0].focusedPane().history.Append(testRow("old2 ")) | 334 | + | require.NoError(t, sess.tabs[0].focusedPane().history.Append(testRow("old2 "))) |
| 335 | sess.tabs[0].focusedPane().screen.Write([]byte("live")) | 335 | sess.tabs[0].focusedPane().screen.Write([]byte("live")) | ||
| 336 | 336 | ||||
| 337 | d.enterCopyMode(sess, ac) | 337 | d.enterCopyMode(sess, ac) | ||
| @@ -505,8 +505,8 @@ func TestCopyModeSplitArrowDoesNotExit(t *testing.T) { | |||||
| 505 | p, _ := newBlockingPTY(t) | 505 | p, _ := newBlockingPTY(t) | ||
| 506 | d, sess, ac, sends := newManualSessionWithPTYs(t, p) | 506 | d, sess, ac, sends := newManualSessionWithPTYs(t, p) | ||
| 507 | installTestHistory(sess.tabs[0].focusedPane(), vt.HistoryConfig{MaxRows: 4}) | 507 | installTestHistory(sess.tabs[0].focusedPane(), vt.HistoryConfig{MaxRows: 4}) | ||
| 508 | - | sess.tabs[0].focusedPane().history.Append(testRow("old1 ")) | 508 | + | require.NoError(t, sess.tabs[0].focusedPane().history.Append(testRow("old1 "))) |
| 509 | - | sess.tabs[0].focusedPane().history.Append(testRow("old2 ")) | 509 | + | require.NoError(t, sess.tabs[0].focusedPane().history.Append(testRow("old2 "))) |
| 510 | sess.tabs[0].focusedPane().screen.Write([]byte("live")) | 510 | sess.tabs[0].focusedPane().screen.Write([]byte("live")) | ||
| 511 | 511 | ||||
| 512 | d.enterCopyMode(sess, ac) | 512 | d.enterCopyMode(sess, ac) | ||
| @@ -739,11 +739,11 @@ func TestCopyModeCapturesSourceAndRetainsItAcrossFocusMove(t *testing.T) { | |||||
| 739 | var want *pane | 739 | var want *pane | ||
| 740 | if tc.useFloating { | 740 | if tc.useFloating { | ||
| 741 | want = installFloatingCopyFixture(t, sess, domain.Size{Cols: 20, Rows: 3}) | 741 | want = installFloatingCopyFixture(t, sess, domain.Size{Cols: 20, Rows: 3}) | ||
| 742 | - | want.history.Append(testRow("flt-old")) | 742 | + | require.NoError(t, want.history.Append(testRow("flt-old"))) |
| 743 | want.screen.Write([]byte("flt-live")) | 743 | want.screen.Write([]byte("flt-live")) | ||
| 744 | } else { | 744 | } else { | ||
| 745 | want = main | 745 | want = main | ||
| 746 | - | want.history.Append(testRow("one-old")) | 746 | + | require.NoError(t, want.history.Append(testRow("one-old"))) |
| 747 | want.screen.Write([]byte("one-live")) | 747 | want.screen.Write([]byte("one-live")) | ||
| 748 | } | 748 | } | ||
| 749 | 749 | ||||
| @@ -780,7 +780,7 @@ func TestFloatingCopyModeWheelUsesCapturedSnapshot(t *testing.T) { | |||||
| 780 | d, sess, ac, sends := newManualSessionWithPTYs(t, normal) | 780 | d, sess, ac, sends := newManualSessionWithPTYs(t, normal) | ||
| 781 | fp := installFloatingCopyFixture(t, sess, domain.Size{Cols: 20, Rows: 3}) | 781 | fp := installFloatingCopyFixture(t, sess, domain.Size{Cols: 20, Rows: 3}) | ||
| 782 | for i := range 30 { | 782 | for i := range 30 { | ||
| 783 | - | fp.history.Append(testRow(fmt.Sprintf("old-%02d", i))) | 783 | + | require.NoError(t, fp.history.Append(testRow(fmt.Sprintf("old-%02d", i)))) |
| 784 | } | 784 | } | ||
| 785 | fp.screen.Write([]byte("live")) | 785 | fp.screen.Write([]byte("live")) | ||
| 786 | fp.mu.Lock() | 786 | fp.mu.Lock() | ||
| @@ -806,7 +806,7 @@ func TestFloatingCopyModeMouseSelectsFloatingRows(t *testing.T) { | |||||
| 806 | d, sess, ac, sends := newManualSessionWithPTYs(t, normal) | 806 | d, sess, ac, sends := newManualSessionWithPTYs(t, normal) | ||
| 807 | fp := installFloatingCopyFixture(t, sess, domain.Size{Cols: 20, Rows: 3}) | 807 | fp := installFloatingCopyFixture(t, sess, domain.Size{Cols: 20, Rows: 3}) | ||
| 808 | for i := range 5 { | 808 | for i := range 5 { | ||
| 809 | - | fp.history.Append(testRow(fmt.Sprintf("old-%d", i))) | 809 | + | require.NoError(t, fp.history.Append(testRow(fmt.Sprintf("old-%d", i)))) |
| 810 | } | 810 | } | ||
| 811 | fp.screen.Write([]byte("live")) | 811 | fp.screen.Write([]byte("live")) | ||
| 812 | 812 | ||||
M internal/usecase/daemon/input_test.go +2 -2
| @@ -1237,7 +1237,7 @@ func TestMouseNormalScreenDragUsesPressOwnedDocumentAfterOutputEviction(t *testi | |||
| 1237 | 1237 | ||
| 1238 | 1238 | // Output evicts five lines after Press; the Document remains immutable. | |
| 1239 | 1239 | for range 5 { | |
| 1240 | - | sess.tabs[0].focusedPane().history.Append(testRow("evicted")) | |
| 1240 | + | require.NoError(t, sess.tabs[0].focusedPane().history.Append(testRow("evicted"))) | |
| 1241 | 1241 | } | |
| 1242 | 1242 | ||
| 1243 | 1243 | // Motion maps the next endpoint through the press-owned Document. | |
| @@ -1764,7 +1764,7 @@ func TestActiveCopyMouseRejectsViewportChangeAfterMappingSnapshot(t *testing.T) | |||
| 1764 | 1764 | d, sess, ac, sends := newManualSessionWithPTYs(t, p) | |
| 1765 | 1765 | pane := sess.activeTab().focusedPane() | |
| 1766 | 1766 | for range 8 { | |
| 1767 | - | pane.history.Append(testRow("history")) | |
| 1767 | + | require.NoError(t, pane.history.Append(testRow("history"))) | |
| 1768 | 1768 | } | |
| 1769 | 1769 | d.enterCopyMode(sess, ac) | |
| 1770 | 1770 | mustOutputData(t, sends) | |
| @@ -1237,7 +1237,7 @@ func TestMouseNormalScreenDragUsesPressOwnedDocumentAfterOutputEviction(t *testi | |||||
| 1237 | 1237 | ||||
| 1238 | // Output evicts five lines after Press; the Document remains immutable. | 1238 | // Output evicts five lines after Press; the Document remains immutable. | ||
| 1239 | for range 5 { | 1239 | for range 5 { | ||
| 1240 | - | sess.tabs[0].focusedPane().history.Append(testRow("evicted")) | 1240 | + | require.NoError(t, sess.tabs[0].focusedPane().history.Append(testRow("evicted"))) |
| 1241 | } | 1241 | } | ||
| 1242 | 1242 | ||||
| 1243 | // Motion maps the next endpoint through the press-owned Document. | 1243 | // Motion maps the next endpoint through the press-owned Document. | ||
| @@ -1764,7 +1764,7 @@ func TestActiveCopyMouseRejectsViewportChangeAfterMappingSnapshot(t *testing.T) | |||||
| 1764 | d, sess, ac, sends := newManualSessionWithPTYs(t, p) | 1764 | d, sess, ac, sends := newManualSessionWithPTYs(t, p) | ||
| 1765 | pane := sess.activeTab().focusedPane() | 1765 | pane := sess.activeTab().focusedPane() | ||
| 1766 | for range 8 { | 1766 | for range 8 { | ||
| 1767 | - | pane.history.Append(testRow("history")) | 1767 | + | require.NoError(t, pane.history.Append(testRow("history"))) |
| 1768 | } | 1768 | } | ||
| 1769 | d.enterCopyMode(sess, ac) | 1769 | d.enterCopyMode(sess, ac) | ||
| 1770 | mustOutputData(t, sends) | 1770 | mustOutputData(t, sends) | ||
M internal/usecase/daemon/overlay_runtime_test.go +2 -2
| @@ -199,7 +199,7 @@ func TestOverlayRuntimeCopyModeDocumentSurvivesRingOverwrite(t *testing.T) { | |||
| 199 | 199 | ac := &attachedClient{} | |
| 200 | 200 | ac.initOverlays() | |
| 201 | 201 | history := newTestHistory(1) | |
| 202 | - | history.Append(testRow("before")) | |
| 202 | + | require.NoError(t, history.Append(testRow("before"))) | |
| 203 | 203 | document := scopy.NewDocument(scopy.NewSnapshot(history, renderer.NewFrame(6, 1)), domain.DefaultWordSeparators) | |
| 204 | 204 | ||
| 205 | 205 | ac.overlays.copyMu.Lock() | |
| @@ -207,7 +207,7 @@ func TestOverlayRuntimeCopyModeDocumentSurvivesRingOverwrite(t *testing.T) { | |||
| 207 | 207 | ac.overlays.copyDocument = document | |
| 208 | 208 | ac.overlays.copyMu.Unlock() | |
| 209 | 209 | ||
| 210 | - | history.Append(testRow("after")) | |
| 210 | + | require.NoError(t, history.Append(testRow("after"))) | |
| 211 | 211 | renderSnap := ac.overlays.SnapshotForRender() | |
| 212 | 212 | defer renderSnap.Unlock() | |
| 213 | 213 | require.NotNil(t, renderSnap.copyMode) | |
| @@ -199,7 +199,7 @@ func TestOverlayRuntimeCopyModeDocumentSurvivesRingOverwrite(t *testing.T) { | |||||
| 199 | ac := &attachedClient{} | 199 | ac := &attachedClient{} | ||
| 200 | ac.initOverlays() | 200 | ac.initOverlays() | ||
| 201 | history := newTestHistory(1) | 201 | history := newTestHistory(1) | ||
| 202 | - | history.Append(testRow("before")) | 202 | + | require.NoError(t, history.Append(testRow("before"))) |
| 203 | document := scopy.NewDocument(scopy.NewSnapshot(history, renderer.NewFrame(6, 1)), domain.DefaultWordSeparators) | 203 | document := scopy.NewDocument(scopy.NewSnapshot(history, renderer.NewFrame(6, 1)), domain.DefaultWordSeparators) | ||
| 204 | 204 | ||||
| 205 | ac.overlays.copyMu.Lock() | 205 | ac.overlays.copyMu.Lock() | ||
| @@ -207,7 +207,7 @@ func TestOverlayRuntimeCopyModeDocumentSurvivesRingOverwrite(t *testing.T) { | |||||
| 207 | ac.overlays.copyDocument = document | 207 | ac.overlays.copyDocument = document | ||
| 208 | ac.overlays.copyMu.Unlock() | 208 | ac.overlays.copyMu.Unlock() | ||
| 209 | 209 | ||||
| 210 | - | history.Append(testRow("after")) | 210 | + | require.NoError(t, history.Append(testRow("after"))) |
| 211 | renderSnap := ac.overlays.SnapshotForRender() | 211 | renderSnap := ac.overlays.SnapshotForRender() | ||
| 212 | defer renderSnap.Unlock() | 212 | defer renderSnap.Unlock() | ||
| 213 | require.NotNil(t, renderSnap.copyMode) | 213 | require.NotNil(t, renderSnap.copyMode) | ||
M internal/usecase/daemon/picker_test.go +4 -3
| @@ -796,7 +796,7 @@ func TestCaptureOverlayLayersResizeRecomposesPickerWithoutStalePreview(t *testin | |||
| 796 | 796 | } | |
| 797 | 797 | ||
| 798 | 798 | func TestPickerPreviewContainsOnlyVisibleFrameRowsWithLargeScrollback(t *testing.T) { | |
| 799 | - | withScrollback := newPickerPreviewTabWithHistoryRows(10_000) | |
| 799 | + | withScrollback := newPickerPreviewTabWithHistoryRows(t, 10_000) | |
| 800 | 800 | require.Equal(t, 10_000, withScrollback.focusedPane().screen.History().Len()) | |
| 801 | 801 | ||
| 802 | 802 | preview := snapshotPickerPreview(withScrollback) | |
| @@ -820,11 +820,12 @@ func TestPickerPreviewContainsOnlyVisibleFrameRowsWithLargeScrollback(t *testing | |||
| 820 | 820 | require.NotContains(t, got, "history-only-marker") | |
| 821 | 821 | } | |
| 822 | 822 | ||
| 823 | - | func newPickerPreviewTabWithHistoryRows(historyRows int) *tab { | |
| 823 | + | func newPickerPreviewTabWithHistoryRows(t testing.TB, historyRows int) *tab { | |
| 824 | + | t.Helper() | |
| 824 | 825 | tb := newTab(nil, domain.Size{Cols: 10, Rows: 3}) | |
| 825 | 826 | p := tb.focusedPane() | |
| 826 | 827 | for range historyRows { | |
| 827 | - | p.screen.History().Append(testRow("history-only-marker")) | |
| 828 | + | require.NoError(t, p.screen.History().Append(testRow("history-only-marker"))) | |
| 828 | 829 | } | |
| 829 | 830 | p.screen.Write([]byte("NOW")) | |
| 830 | 831 | return tb | |
| @@ -796,7 +796,7 @@ func TestCaptureOverlayLayersResizeRecomposesPickerWithoutStalePreview(t *testin | |||||
| 796 | } | 796 | } | ||
| 797 | 797 | ||||
| 798 | func TestPickerPreviewContainsOnlyVisibleFrameRowsWithLargeScrollback(t *testing.T) { | 798 | func TestPickerPreviewContainsOnlyVisibleFrameRowsWithLargeScrollback(t *testing.T) { | ||
| 799 | - | withScrollback := newPickerPreviewTabWithHistoryRows(10_000) | 799 | + | withScrollback := newPickerPreviewTabWithHistoryRows(t, 10_000) |
| 800 | require.Equal(t, 10_000, withScrollback.focusedPane().screen.History().Len()) | 800 | require.Equal(t, 10_000, withScrollback.focusedPane().screen.History().Len()) | ||
| 801 | 801 | ||||
| 802 | preview := snapshotPickerPreview(withScrollback) | 802 | preview := snapshotPickerPreview(withScrollback) | ||
| @@ -820,11 +820,12 @@ func TestPickerPreviewContainsOnlyVisibleFrameRowsWithLargeScrollback(t *testing | |||||
| 820 | require.NotContains(t, got, "history-only-marker") | 820 | require.NotContains(t, got, "history-only-marker") | ||
| 821 | } | 821 | } | ||
| 822 | 822 | ||||
| 823 | - | func newPickerPreviewTabWithHistoryRows(historyRows int) *tab { | 823 | + | func newPickerPreviewTabWithHistoryRows(t testing.TB, historyRows int) *tab { |
| 824 | + | t.Helper() | |||
| 824 | tb := newTab(nil, domain.Size{Cols: 10, Rows: 3}) | 825 | tb := newTab(nil, domain.Size{Cols: 10, Rows: 3}) | ||
| 825 | p := tb.focusedPane() | 826 | p := tb.focusedPane() | ||
| 826 | for range historyRows { | 827 | for range historyRows { | ||
| 827 | - | p.screen.History().Append(testRow("history-only-marker")) | 828 | + | require.NoError(t, p.screen.History().Append(testRow("history-only-marker"))) |
| 828 | } | 829 | } | ||
| 829 | p.screen.Write([]byte("NOW")) | 830 | p.screen.Write([]byte("NOW")) | ||
| 830 | return tb | 831 | return tb | ||
M internal/usecase/daemon/render_test.go +1 -1
| @@ -1289,7 +1289,7 @@ func TestPaintComposesCopyBodyAboveFloating(t *testing.T) { | |||
| 1289 | 1289 | defer releaseFloating() | |
| 1290 | 1290 | d, sess, ac, sends := newManualSessionWithPTYs(t, normal) | |
| 1291 | 1291 | fp := newPane("floating", floatingPTY, domain.Size{Cols: 20, Rows: 3}) | |
| 1292 | - | fp.history.Append(testRow("flt-old")) | |
| 1292 | + | require.NoError(t, fp.history.Append(testRow("flt-old"))) | |
| 1293 | 1293 | fp.screen.Write([]byte("flt-live")) | |
| 1294 | 1294 | installTestFloating(sess.activeTab(), fp, true) | |
| 1295 | 1295 | ||
| @@ -1289,7 +1289,7 @@ func TestPaintComposesCopyBodyAboveFloating(t *testing.T) { | |||||
| 1289 | defer releaseFloating() | 1289 | defer releaseFloating() | ||
| 1290 | d, sess, ac, sends := newManualSessionWithPTYs(t, normal) | 1290 | d, sess, ac, sends := newManualSessionWithPTYs(t, normal) | ||
| 1291 | fp := newPane("floating", floatingPTY, domain.Size{Cols: 20, Rows: 3}) | 1291 | fp := newPane("floating", floatingPTY, domain.Size{Cols: 20, Rows: 3}) | ||
| 1292 | - | fp.history.Append(testRow("flt-old")) | 1292 | + | require.NoError(t, fp.history.Append(testRow("flt-old"))) |
| 1293 | fp.screen.Write([]byte("flt-live")) | 1293 | fp.screen.Write([]byte("flt-live")) | ||
| 1294 | installTestFloating(sess.activeTab(), fp, true) | 1294 | installTestFloating(sess.activeTab(), fp, true) | ||
| 1295 | 1295 | ||||
M internal/usecase/daemon/snapshot_acceptance_test.go +7 -7
| @@ -28,7 +28,7 @@ func TestSnapshotCaptureMarshalUnmarshalRestorePreservesTopologyAndTerminalState | |||
| 28 | 28 | first.nextPaneID = 3 | |
| 29 | 29 | first.panes["pane-1"].stableID = "pane-one" | |
| 30 | 30 | secondPane := newPaneWithStableID("pane-2", "pane-two", snapshotAcceptancePTY(t), domain.Size{Cols: 40, Rows: 24}) | |
| 31 | - | secondPane.history.Append([]renderer.Cell{{Rune: 't'}, {Rune: 'w'}, {Rune: 'o'}}) | |
| 31 | + | require.NoError(t, secondPane.history.Append([]renderer.Cell{{Rune: 't'}, {Rune: 'w'}, {Rune: 'o'}})) | |
| 32 | 32 | secondPane.screen.Write([]byte("two")) | |
| 33 | 33 | first.panes["pane-2"] = secondPane | |
| 34 | 34 | first.tree = &layout.Tree{Focus: "pane-2", Root: &layout.Node{Kind: layout.Split, Dir: layout.Horizontal, Children: []*layout.Node{ | |
| @@ -40,7 +40,7 @@ func TestSnapshotCaptureMarshalUnmarshalRestorePreservesTopologyAndTerminalState | |||
| 40 | 40 | second.stableID = "tab-two" | |
| 41 | 41 | second.nextPaneID = 2 | |
| 42 | 42 | second.panes["pane-1"].stableID = "pane-three" | |
| 43 | - | second.panes["pane-1"].history.Append([]renderer.Cell{{Rune: 't'}, {Rune: 'h'}, {Rune: 'r'}, {Rune: 'e'}, {Rune: 'e'}}) | |
| 43 | + | require.NoError(t, second.panes["pane-1"].history.Append([]renderer.Cell{{Rune: 't'}, {Rune: 'h'}, {Rune: 'r'}, {Rune: 'e'}, {Rune: 'e'}})) | |
| 44 | 44 | second.panes["pane-1"].screen.Write([]byte("three")) | |
| 45 | 45 | sess.tabs = []*tab{first, second} | |
| 46 | 46 | sess.active = 1 | |
| @@ -102,14 +102,14 @@ func TestSnapshotCaptureRetainsImmutableValuesAfterTailMutationAndEviction(t *te | |||
| 102 | 102 | pane := sess.tabs[0].panes["pane-1"] | |
| 103 | 103 | pane.screen = vt.NewScreenWithHistory(8, 3, vt.HistoryConfig{MaxRows: 2, ChunkRows: 1}) | |
| 104 | 104 | pane.history = pane.screen.History() | |
| 105 | - | pane.history.Append([]renderer.Cell{{Rune: 'o'}, {Rune: 'l'}, {Rune: 'd'}}) | |
| 105 | + | require.NoError(t, pane.history.Append([]renderer.Cell{{Rune: 'o'}, {Rune: 'l'}, {Rune: 'd'}})) | |
| 106 | 106 | pane.screen.Write([]byte("before")) | |
| 107 | 107 | ||
| 108 | 108 | require.True(t, d.captureSession(sess)) | |
| 109 | 109 | <-entered | |
| 110 | 110 | pane.mu.Lock() | |
| 111 | - | pane.history.Append([]renderer.Cell{{Rune: 'n'}, {Rune: 'e'}, {Rune: 'w'}}) | |
| 112 | - | pane.history.Append([]renderer.Cell{{Rune: 'e'}, {Rune: 'v'}, {Rune: 'i'}, {Rune: 'c'}, {Rune: 't'}}) | |
| 111 | + | require.NoError(t, pane.history.Append([]renderer.Cell{{Rune: 'n'}, {Rune: 'e'}, {Rune: 'w'}})) | |
| 112 | + | require.NoError(t, pane.history.Append([]renderer.Cell{{Rune: 'e'}, {Rune: 'v'}, {Rune: 'i'}, {Rune: 'c'}, {Rune: 't'}})) | |
| 113 | 113 | pane.screen.Write([]byte("\rafter")) | |
| 114 | 114 | pane.mu.Unlock() | |
| 115 | 115 | close(release) | |
| @@ -130,13 +130,13 @@ func TestDaemonHistoryEvictionLeavesCopyAndSearchSnapshotsUsable(t *testing.T) { | |||
| 130 | 130 | pane.screen = vt.NewScreenWithHistory(8, 1, vt.HistoryConfig{MaxRows: 2, ChunkRows: 1}) | |
| 131 | 131 | pane.history = pane.screen.History() | |
| 132 | 132 | for _, text := range []string{"first", "second"} { | |
| 133 | - | pane.history.Append(cells(text)) | |
| 133 | + | require.NoError(t, pane.history.Append(cells(text))) | |
| 134 | 134 | } | |
| 135 | 135 | pane.screen.Write([]byte("screen")) | |
| 136 | 136 | ||
| 137 | 137 | document := scopy.NewSnapshot(pane.history, pane.screen.Frame) | |
| 138 | 138 | for _, text := range []string{"third", "fourth"} { | |
| 139 | - | pane.history.Append(cells(text)) | |
| 139 | + | require.NoError(t, pane.history.Append(cells(text))) | |
| 140 | 140 | } | |
| 141 | 141 | ||
| 142 | 142 | for _, tt := range []struct { | |
| @@ -28,7 +28,7 @@ func TestSnapshotCaptureMarshalUnmarshalRestorePreservesTopologyAndTerminalState | |||||
| 28 | first.nextPaneID = 3 | 28 | first.nextPaneID = 3 | ||
| 29 | first.panes["pane-1"].stableID = "pane-one" | 29 | first.panes["pane-1"].stableID = "pane-one" | ||
| 30 | secondPane := newPaneWithStableID("pane-2", "pane-two", snapshotAcceptancePTY(t), domain.Size{Cols: 40, Rows: 24}) | 30 | secondPane := newPaneWithStableID("pane-2", "pane-two", snapshotAcceptancePTY(t), domain.Size{Cols: 40, Rows: 24}) | ||
| 31 | - | secondPane.history.Append([]renderer.Cell{{Rune: 't'}, {Rune: 'w'}, {Rune: 'o'}}) | 31 | + | require.NoError(t, secondPane.history.Append([]renderer.Cell{{Rune: 't'}, {Rune: 'w'}, {Rune: 'o'}})) |
| 32 | secondPane.screen.Write([]byte("two")) | 32 | secondPane.screen.Write([]byte("two")) | ||
| 33 | first.panes["pane-2"] = secondPane | 33 | first.panes["pane-2"] = secondPane | ||
| 34 | first.tree = &layout.Tree{Focus: "pane-2", Root: &layout.Node{Kind: layout.Split, Dir: layout.Horizontal, Children: []*layout.Node{ | 34 | first.tree = &layout.Tree{Focus: "pane-2", Root: &layout.Node{Kind: layout.Split, Dir: layout.Horizontal, Children: []*layout.Node{ | ||
| @@ -40,7 +40,7 @@ func TestSnapshotCaptureMarshalUnmarshalRestorePreservesTopologyAndTerminalState | |||||
| 40 | second.stableID = "tab-two" | 40 | second.stableID = "tab-two" | ||
| 41 | second.nextPaneID = 2 | 41 | second.nextPaneID = 2 | ||
| 42 | second.panes["pane-1"].stableID = "pane-three" | 42 | second.panes["pane-1"].stableID = "pane-three" | ||
| 43 | - | second.panes["pane-1"].history.Append([]renderer.Cell{{Rune: 't'}, {Rune: 'h'}, {Rune: 'r'}, {Rune: 'e'}, {Rune: 'e'}}) | 43 | + | require.NoError(t, second.panes["pane-1"].history.Append([]renderer.Cell{{Rune: 't'}, {Rune: 'h'}, {Rune: 'r'}, {Rune: 'e'}, {Rune: 'e'}})) |
| 44 | second.panes["pane-1"].screen.Write([]byte("three")) | 44 | second.panes["pane-1"].screen.Write([]byte("three")) | ||
| 45 | sess.tabs = []*tab{first, second} | 45 | sess.tabs = []*tab{first, second} | ||
| 46 | sess.active = 1 | 46 | sess.active = 1 | ||
| @@ -102,14 +102,14 @@ func TestSnapshotCaptureRetainsImmutableValuesAfterTailMutationAndEviction(t *te | |||||
| 102 | pane := sess.tabs[0].panes["pane-1"] | 102 | pane := sess.tabs[0].panes["pane-1"] | ||
| 103 | pane.screen = vt.NewScreenWithHistory(8, 3, vt.HistoryConfig{MaxRows: 2, ChunkRows: 1}) | 103 | pane.screen = vt.NewScreenWithHistory(8, 3, vt.HistoryConfig{MaxRows: 2, ChunkRows: 1}) | ||
| 104 | pane.history = pane.screen.History() | 104 | pane.history = pane.screen.History() | ||
| 105 | - | pane.history.Append([]renderer.Cell{{Rune: 'o'}, {Rune: 'l'}, {Rune: 'd'}}) | 105 | + | require.NoError(t, pane.history.Append([]renderer.Cell{{Rune: 'o'}, {Rune: 'l'}, {Rune: 'd'}})) |
| 106 | pane.screen.Write([]byte("before")) | 106 | pane.screen.Write([]byte("before")) | ||
| 107 | 107 | ||||
| 108 | require.True(t, d.captureSession(sess)) | 108 | require.True(t, d.captureSession(sess)) | ||
| 109 | <-entered | 109 | <-entered | ||
| 110 | pane.mu.Lock() | 110 | pane.mu.Lock() | ||
| 111 | - | pane.history.Append([]renderer.Cell{{Rune: 'n'}, {Rune: 'e'}, {Rune: 'w'}}) | 111 | + | require.NoError(t, pane.history.Append([]renderer.Cell{{Rune: 'n'}, {Rune: 'e'}, {Rune: 'w'}})) |
| 112 | - | pane.history.Append([]renderer.Cell{{Rune: 'e'}, {Rune: 'v'}, {Rune: 'i'}, {Rune: 'c'}, {Rune: 't'}}) | 112 | + | require.NoError(t, pane.history.Append([]renderer.Cell{{Rune: 'e'}, {Rune: 'v'}, {Rune: 'i'}, {Rune: 'c'}, {Rune: 't'}})) |
| 113 | pane.screen.Write([]byte("\rafter")) | 113 | pane.screen.Write([]byte("\rafter")) | ||
| 114 | pane.mu.Unlock() | 114 | pane.mu.Unlock() | ||
| 115 | close(release) | 115 | close(release) | ||
| @@ -130,13 +130,13 @@ func TestDaemonHistoryEvictionLeavesCopyAndSearchSnapshotsUsable(t *testing.T) { | |||||
| 130 | pane.screen = vt.NewScreenWithHistory(8, 1, vt.HistoryConfig{MaxRows: 2, ChunkRows: 1}) | 130 | pane.screen = vt.NewScreenWithHistory(8, 1, vt.HistoryConfig{MaxRows: 2, ChunkRows: 1}) | ||
| 131 | pane.history = pane.screen.History() | 131 | pane.history = pane.screen.History() | ||
| 132 | for _, text := range []string{"first", "second"} { | 132 | for _, text := range []string{"first", "second"} { | ||
| 133 | - | pane.history.Append(cells(text)) | 133 | + | require.NoError(t, pane.history.Append(cells(text))) |
| 134 | } | 134 | } | ||
| 135 | pane.screen.Write([]byte("screen")) | 135 | pane.screen.Write([]byte("screen")) | ||
| 136 | 136 | ||||
| 137 | document := scopy.NewSnapshot(pane.history, pane.screen.Frame) | 137 | document := scopy.NewSnapshot(pane.history, pane.screen.Frame) | ||
| 138 | for _, text := range []string{"third", "fourth"} { | 138 | for _, text := range []string{"third", "fourth"} { | ||
| 139 | - | pane.history.Append(cells(text)) | 139 | + | require.NoError(t, pane.history.Append(cells(text))) |
| 140 | } | 140 | } | ||
| 141 | 141 | ||||
| 142 | for _, tt := range []struct { | 142 | for _, tt := range []struct { | ||
M internal/usecase/daemon/snapshot_restore_test.go +1 -1
| @@ -25,7 +25,7 @@ func terminalPane(t *testing.T, id, stableID, cwd string, historyRows, visibleRo | |||
| 25 | 25 | t.Helper() | |
| 26 | 26 | h := vt.NewHistory(vt.HistoryConfig{MaxRows: defaultScrollbackRows}) | |
| 27 | 27 | for _, row := range historyRows { | |
| 28 | - | h.Append(row) | |
| 28 | + | require.NoError(t, h.Append(row)) | |
| 29 | 29 | } | |
| 30 | 30 | sealed, tail, err := vt.MarshalSealedHistory(h.SealAndView()) | |
| 31 | 31 | require.NoError(t, err) | |
| @@ -25,7 +25,7 @@ func terminalPane(t *testing.T, id, stableID, cwd string, historyRows, visibleRo | |||||
| 25 | t.Helper() | 25 | t.Helper() | ||
| 26 | h := vt.NewHistory(vt.HistoryConfig{MaxRows: defaultScrollbackRows}) | 26 | h := vt.NewHistory(vt.HistoryConfig{MaxRows: defaultScrollbackRows}) | ||
| 27 | for _, row := range historyRows { | 27 | for _, row := range historyRows { | ||
| 28 | - | h.Append(row) | 28 | + | require.NoError(t, h.Append(row)) |
| 29 | } | 29 | } | ||
| 30 | sealed, tail, err := vt.MarshalSealedHistory(h.SealAndView()) | 30 | sealed, tail, err := vt.MarshalSealedHistory(h.SealAndView()) | ||
| 31 | require.NoError(t, err) | 31 | require.NoError(t, err) | ||
M internal/usecase/daemon/snapshot_test.go +1 -1
| @@ -1034,7 +1034,7 @@ func newSnapshotTestSession(t *testing.T, name string, ephemeral bool, cwd strin | |||
| 1034 | 1034 | tb := newTab(pty, domain.Size{Cols: 8, Rows: 3}) | |
| 1035 | 1035 | p := tb.panes["pane-1"] | |
| 1036 | 1036 | p.screen.Write([]byte("hello")) | |
| 1037 | - | p.history.Append([]renderer.Cell{{Rune: 'h'}, {Rune: 'i'}}) | |
| 1037 | + | require.NoError(t, p.history.Append([]renderer.Cell{{Rune: 'h'}, {Rune: 'i'}})) | |
| 1038 | 1038 | sess := &session{ | |
| 1039 | 1039 | id: domain.SessionID("sess-" + name), | |
| 1040 | 1040 | name: name, | |
| @@ -1034,7 +1034,7 @@ func newSnapshotTestSession(t *testing.T, name string, ephemeral bool, cwd strin | |||||
| 1034 | tb := newTab(pty, domain.Size{Cols: 8, Rows: 3}) | 1034 | tb := newTab(pty, domain.Size{Cols: 8, Rows: 3}) | ||
| 1035 | p := tb.panes["pane-1"] | 1035 | p := tb.panes["pane-1"] | ||
| 1036 | p.screen.Write([]byte("hello")) | 1036 | p.screen.Write([]byte("hello")) | ||
| 1037 | - | p.history.Append([]renderer.Cell{{Rune: 'h'}, {Rune: 'i'}}) | 1037 | + | require.NoError(t, p.history.Append([]renderer.Cell{{Rune: 'h'}, {Rune: 'i'}})) |
| 1038 | sess := &session{ | 1038 | sess := &session{ | ||
| 1039 | id: domain.SessionID("sess-" + name), | 1039 | id: domain.SessionID("sess-" + name), | ||
| 1040 | name: name, | 1040 | name: name, | ||
M internal/usecase/snapshot/benchmark_test.go +3 -1
| @@ -63,7 +63,9 @@ func benchmarkV3Session(t testing.TB) (Session, []byte) { | |||
| 63 | 63 | for col := range cells { | |
| 64 | 64 | cells[col] = renderer.Cell{Rune: rune('a' + (row+col)%26)} | |
| 65 | 65 | } | |
| 66 | - | history.Append(cells) | |
| 66 | + | if err := history.Append(cells); err != nil { | |
| 67 | + | t.Fatal(err) | |
| 68 | + | } | |
| 67 | 69 | } | |
| 68 | 70 | sealed, tail, err := vt.MarshalSealedHistory(history.SealAndView()) | |
| 69 | 71 | if err != nil { | |
| @@ -63,7 +63,9 @@ func benchmarkV3Session(t testing.TB) (Session, []byte) { | |||||
| 63 | for col := range cells { | 63 | for col := range cells { | ||
| 64 | cells[col] = renderer.Cell{Rune: rune('a' + (row+col)%26)} | 64 | cells[col] = renderer.Cell{Rune: rune('a' + (row+col)%26)} | ||
| 65 | } | 65 | } | ||
| 66 | - | history.Append(cells) | 66 | + | if err := history.Append(cells); err != nil { |
| 67 | + | t.Fatal(err) | |||
| 68 | + | } | |||
| 67 | } | 69 | } | ||
| 68 | sealed, tail, err := vt.MarshalSealedHistory(history.SealAndView()) | 70 | sealed, tail, err := vt.MarshalSealedHistory(history.SealAndView()) | ||
| 69 | if err != nil { | 71 | if err != nil { | ||
M internal/usecase/snapshot/codec_test.go +2 -2
| @@ -322,7 +322,7 @@ func TestUnmarshalRejectsProcessWithoutArgv(t *testing.T) { | |||
| 322 | 322 | func TestUnmarshalGlobalBudgetRejectionDoesNotAllocatePerPane(t *testing.T) { | |
| 323 | 323 | history := vt.NewHistory(vt.HistoryConfig{MaxRows: 256, ChunkRows: 256}) | |
| 324 | 324 | for range 256 { | |
| 325 | - | history.Append([]renderer.Cell{{Rune: 'x'}}) | |
| 325 | + | requireNoError(t, history.Append([]renderer.Cell{{Rune: 'x'}})) | |
| 326 | 326 | } | |
| 327 | 327 | sealed, tail, err := vt.MarshalSealedHistory(history.SealAndView()) | |
| 328 | 328 | requireNoError(t, err) | |
| @@ -385,7 +385,7 @@ func historyBlobs(t *testing.T, rows [][]renderer.Cell) ([][]byte, []byte) { | |||
| 385 | 385 | t.Helper() | |
| 386 | 386 | h := vt.NewHistory(vt.HistoryConfig{MaxRows: 128, ChunkRows: 2}) | |
| 387 | 387 | for _, row := range rows { | |
| 388 | - | h.Append(row) | |
| 388 | + | requireNoError(t, h.Append(row)) | |
| 389 | 389 | } | |
| 390 | 390 | sealed, tail, err := vt.MarshalSealedHistory(h.SealAndView()) | |
| 391 | 391 | if err != nil { | |
| @@ -322,7 +322,7 @@ func TestUnmarshalRejectsProcessWithoutArgv(t *testing.T) { | |||||
| 322 | func TestUnmarshalGlobalBudgetRejectionDoesNotAllocatePerPane(t *testing.T) { | 322 | func TestUnmarshalGlobalBudgetRejectionDoesNotAllocatePerPane(t *testing.T) { | ||
| 323 | history := vt.NewHistory(vt.HistoryConfig{MaxRows: 256, ChunkRows: 256}) | 323 | history := vt.NewHistory(vt.HistoryConfig{MaxRows: 256, ChunkRows: 256}) | ||
| 324 | for range 256 { | 324 | for range 256 { | ||
| 325 | - | history.Append([]renderer.Cell{{Rune: 'x'}}) | 325 | + | requireNoError(t, history.Append([]renderer.Cell{{Rune: 'x'}})) |
| 326 | } | 326 | } | ||
| 327 | sealed, tail, err := vt.MarshalSealedHistory(history.SealAndView()) | 327 | sealed, tail, err := vt.MarshalSealedHistory(history.SealAndView()) | ||
| 328 | requireNoError(t, err) | 328 | requireNoError(t, err) | ||
| @@ -385,7 +385,7 @@ func historyBlobs(t *testing.T, rows [][]renderer.Cell) ([][]byte, []byte) { | |||||
| 385 | t.Helper() | 385 | t.Helper() | ||
| 386 | h := vt.NewHistory(vt.HistoryConfig{MaxRows: 128, ChunkRows: 2}) | 386 | h := vt.NewHistory(vt.HistoryConfig{MaxRows: 128, ChunkRows: 2}) | ||
| 387 | for _, row := range rows { | 387 | for _, row := range rows { | ||
| 388 | - | h.Append(row) | 388 | + | requireNoError(t, h.Append(row)) |
| 389 | } | 389 | } | ||
| 390 | sealed, tail, err := vt.MarshalSealedHistory(h.SealAndView()) | 390 | sealed, tail, err := vt.MarshalSealedHistory(h.SealAndView()) | ||
| 391 | if err != nil { | 391 | if err != nil { | ||
M pkg/vt/benchmark_test.go +2 -2
| @@ -175,7 +175,7 @@ func benchmarkHistory(t testing.TB, rows, width, chunkRows int) *History { | |||
| 175 | 175 | for col := range cells { | |
| 176 | 176 | cells[col] = renderer.Cell{Rune: rune('a' + (row+col)%26)} | |
| 177 | 177 | } | |
| 178 | - | history.Append(cells) | |
| 178 | + | requireHistoryAppend(t, history, cells) | |
| 179 | 179 | } | |
| 180 | 180 | return history | |
| 181 | 181 | } | |
| @@ -193,7 +193,7 @@ func benchmarkScreenResizeReflow(b *testing.B, historyRows int) { | |||
| 193 | 193 | s := NewScreenWithHistory(120, 40, HistoryConfig{MaxRows: historyRows + 500}) | |
| 194 | 194 | if historyRows > 0 { | |
| 195 | 195 | for range historyRows { | |
| 196 | - | s.history.Append(make([]renderer.Cell, 120)) | |
| 196 | + | requireHistoryAppend(b, s.history, make([]renderer.Cell, 120)) | |
| 197 | 197 | } | |
| 198 | 198 | } | |
| 199 | 199 | s.Write(bytes.Repeat([]byte("x"), 120*39)) | |
| @@ -175,7 +175,7 @@ func benchmarkHistory(t testing.TB, rows, width, chunkRows int) *History { | |||||
| 175 | for col := range cells { | 175 | for col := range cells { | ||
| 176 | cells[col] = renderer.Cell{Rune: rune('a' + (row+col)%26)} | 176 | cells[col] = renderer.Cell{Rune: rune('a' + (row+col)%26)} | ||
| 177 | } | 177 | } | ||
| 178 | - | history.Append(cells) | 178 | + | requireHistoryAppend(t, history, cells) |
| 179 | } | 179 | } | ||
| 180 | return history | 180 | return history | ||
| 181 | } | 181 | } | ||
| @@ -193,7 +193,7 @@ func benchmarkScreenResizeReflow(b *testing.B, historyRows int) { | |||||
| 193 | s := NewScreenWithHistory(120, 40, HistoryConfig{MaxRows: historyRows + 500}) | 193 | s := NewScreenWithHistory(120, 40, HistoryConfig{MaxRows: historyRows + 500}) | ||
| 194 | if historyRows > 0 { | 194 | if historyRows > 0 { | ||
| 195 | for range historyRows { | 195 | for range historyRows { | ||
| 196 | - | s.history.Append(make([]renderer.Cell, 120)) | 196 | + | requireHistoryAppend(b, s.history, make([]renderer.Cell, 120)) |
| 197 | } | 197 | } | ||
| 198 | } | 198 | } | ||
| 199 | s.Write(bytes.Repeat([]byte("x"), 120*39)) | 199 | s.Write(bytes.Repeat([]byte("x"), 120*39)) | ||
M pkg/vt/history_acceptance_test.go +1 -1
| @@ -62,7 +62,7 @@ func TestSnapshotCodecsPreserveEveryTerminalCellAcrossSealedTailAndVisible(t *te | |||
| 62 | 62 | makeRow('D', rgb), makeRow('E', indexed), makeRow('F', rgb), makeRow('G', indexed), | |
| 63 | 63 | } | |
| 64 | 64 | for _, row := range want { | |
| 65 | - | history.Append(row) | |
| 65 | + | requireHistoryAppend(t, history, row) | |
| 66 | 66 | } | |
| 67 | 67 | if got, wantChunks := history.View().ChunkCount(), 4; got != wantChunks { | |
| 68 | 68 | t.Fatalf("chunk count = %d, want three sealed chunks plus copied tail %d", got, wantChunks) | |
| @@ -62,7 +62,7 @@ func TestSnapshotCodecsPreserveEveryTerminalCellAcrossSealedTailAndVisible(t *te | |||||
| 62 | makeRow('D', rgb), makeRow('E', indexed), makeRow('F', rgb), makeRow('G', indexed), | 62 | makeRow('D', rgb), makeRow('E', indexed), makeRow('F', rgb), makeRow('G', indexed), | ||
| 63 | } | 63 | } | ||
| 64 | for _, row := range want { | 64 | for _, row := range want { | ||
| 65 | - | history.Append(row) | 65 | + | requireHistoryAppend(t, history, row) |
| 66 | } | 66 | } | ||
| 67 | if got, wantChunks := history.View().ChunkCount(), 4; got != wantChunks { | 67 | if got, wantChunks := history.View().ChunkCount(), 4; got != wantChunks { | ||
| 68 | t.Fatalf("chunk count = %d, want three sealed chunks plus copied tail %d", got, wantChunks) | 68 | t.Fatalf("chunk count = %d, want three sealed chunks plus copied tail %d", got, wantChunks) | ||
M pkg/vt/history_codec.go +2 -2
| @@ -194,10 +194,10 @@ func parseHistory(data []byte, populate bool) (HistoryView, historyDecodeStats, | |||
| 194 | 194 | cellCount, ok := p.uint32() | |
| 195 | 195 | cellBytes, validCellBytes := historyCellByteCount(uint64(cellCount)) | |
| 196 | 196 | if !ok || !validCellBytes || | |
| 197 | - | uint64(cellCount) > uint64(len(p.data))/historyCellBytes || | |
| 198 | 197 | !addHistoryDecodeBudget(&stats.cells, uint64(cellCount), maxHistoryCells) || | |
| 199 | 198 | !addHistoryDecodeBudget(&stats.styles, uint64(cellCount), maxHistoryDecodeStyles) || | |
| 200 | - | !addHistoryDecodeBudget(&stats.bytes, cellBytes, maxHistoryDecodedBytes) { | |
| 199 | + | !addHistoryDecodeBudget(&stats.bytes, cellBytes, maxHistoryDecodedBytes) || | |
| 200 | + | uint64(cellCount) > uint64(len(p.data))/historyCellBytes { | |
| 201 | 201 | return HistoryView{}, historyDecodeStats{}, false | |
| 202 | 202 | } | |
| 203 | 203 | ||
| @@ -194,10 +194,10 @@ func parseHistory(data []byte, populate bool) (HistoryView, historyDecodeStats, | |||||
| 194 | cellCount, ok := p.uint32() | 194 | cellCount, ok := p.uint32() | ||
| 195 | cellBytes, validCellBytes := historyCellByteCount(uint64(cellCount)) | 195 | cellBytes, validCellBytes := historyCellByteCount(uint64(cellCount)) | ||
| 196 | if !ok || !validCellBytes || | 196 | if !ok || !validCellBytes || | ||
| 197 | - | uint64(cellCount) > uint64(len(p.data))/historyCellBytes || | |||
| 198 | !addHistoryDecodeBudget(&stats.cells, uint64(cellCount), maxHistoryCells) || | 197 | !addHistoryDecodeBudget(&stats.cells, uint64(cellCount), maxHistoryCells) || | ||
| 199 | !addHistoryDecodeBudget(&stats.styles, uint64(cellCount), maxHistoryDecodeStyles) || | 198 | !addHistoryDecodeBudget(&stats.styles, uint64(cellCount), maxHistoryDecodeStyles) || | ||
| 200 | - | !addHistoryDecodeBudget(&stats.bytes, cellBytes, maxHistoryDecodedBytes) { | 199 | + | !addHistoryDecodeBudget(&stats.bytes, cellBytes, maxHistoryDecodedBytes) || |
| 200 | + | uint64(cellCount) > uint64(len(p.data))/historyCellBytes { | |||
| 201 | return HistoryView{}, historyDecodeStats{}, false | 201 | return HistoryView{}, historyDecodeStats{}, false | ||
| 202 | } | 202 | } | ||
| 203 | 203 | ||||
M pkg/vt/history_codec_test.go +29 -8
| @@ -57,7 +57,7 @@ func TestChunkCodecRoundTripsLosslessCellsAndStyles(t *testing.T) { | |||
| 57 | 57 | t.Run(tt.name, func(t *testing.T) { | |
| 58 | 58 | history := NewHistory(HistoryConfig{MaxRows: 8, ChunkRows: 2}) | |
| 59 | 59 | for _, row := range tt.rows { | |
| 60 | - | history.Append(row) | |
| 60 | + | requireHistoryAppend(t, history, row) | |
| 61 | 61 | } | |
| 62 | 62 | ||
| 63 | 63 | encoded, err := MarshalHistory(history.View()) | |
| @@ -75,8 +75,8 @@ func TestChunkCodecRoundTripsLosslessCellsAndStyles(t *testing.T) { | |||
| 75 | 75 | ||
| 76 | 76 | func TestChunkCodecRejectsTruncatedAndTrailingPayloads(t *testing.T) { | |
| 77 | 77 | history := NewHistory(HistoryConfig{MaxRows: 4, ChunkRows: 2}) | |
| 78 | - | history.Append(historyRow("AAAA")) | |
| 79 | - | history.Append(historyRow("BBBB")) | |
| 78 | + | requireHistoryAppend(t, history, historyRow("AAAA")) | |
| 79 | + | requireHistoryAppend(t, history, historyRow("BBBB")) | |
| 80 | 80 | encoded, err := MarshalHistory(history.View()) | |
| 81 | 81 | if err != nil { | |
| 82 | 82 | t.Fatalf("marshal history: %v", err) | |
| @@ -136,11 +136,6 @@ func TestChunkCodecRejectsDimensionsBeyondSupportedLimits(t *testing.T) { | |||
| 136 | 136 | view: historyViewWithDimensions(supportedRows+1, 0), | |
| 137 | 137 | data: historyPayloadWithDimensions(supportedRows+1, 0), | |
| 138 | 138 | }, | |
| 139 | - | { | |
| 140 | - | name: "too many aggregate cells within row budget", | |
| 141 | - | view: historyViewWithDimensions(maxHistoryRows, maxHistoryRowCells+1), | |
| 142 | - | data: historyPayloadWithDimensions(maxHistoryRows, maxHistoryRowCells+1), | |
| 143 | - | }, | |
| 144 | 139 | } | |
| 145 | 140 | ||
| 146 | 141 | for _, tt := range tests { | |
| @@ -155,6 +150,19 @@ func TestChunkCodecRejectsDimensionsBeyondSupportedLimits(t *testing.T) { | |||
| 155 | 150 | } | |
| 156 | 151 | } | |
| 157 | 152 | ||
| 153 | + | func TestChunkCodecRejectsAggregateCellDeclarationBeforePayloadAllocation(t *testing.T) { | |
| 154 | + | data := aggregateCellLimitDeclaration() | |
| 155 | + | if len(data) >= 1024 { | |
| 156 | + | t.Fatalf("aggregate declaration length = %d, want compact fixture", len(data)) | |
| 157 | + | } | |
| 158 | + | if _, err := PreflightHistoryBlob(data); err == nil { | |
| 159 | + | t.Fatal("preflight accepted aggregate cell declaration") | |
| 160 | + | } | |
| 161 | + | if _, err := UnmarshalHistory(data); err == nil { | |
| 162 | + | t.Fatal("unmarshal accepted aggregate cell declaration") | |
| 163 | + | } | |
| 164 | + | } | |
| 165 | + | ||
| 158 | 166 | func TestChunkCodecAcceptsWideRowsWithinAggregateBudget(t *testing.T) { | |
| 159 | 167 | for _, width := range []int{161, 293, math.MaxUint16} { | |
| 160 | 168 | t.Run(fmt.Sprintf("width_%d", width), func(t *testing.T) { | |
| @@ -234,6 +242,18 @@ func historyPayloadWithDimensions(rowCount, width int) []byte { | |||
| 234 | 242 | return data | |
| 235 | 243 | } | |
| 236 | 244 | ||
| 245 | + | // aggregateCellLimitDeclaration declares one row, which is within the row | |
| 246 | + | // budget, but its cells exceed the aggregate budget. It deliberately omits | |
| 247 | + | // cell payload bytes: the aggregate limit must reject it before allocation. | |
| 248 | + | func aggregateCellLimitDeclaration() []byte { | |
| 249 | + | data := make([]byte, 9, 21) | |
| 250 | + | copy(data, historyMagic) | |
| 251 | + | data[4] = historyVersion | |
| 252 | + | binary.BigEndian.PutUint32(data[5:], 1) | |
| 253 | + | data = binary.BigEndian.AppendUint32(data, 1) | |
| 254 | + | return binary.BigEndian.AppendUint32(data, uint32(maxHistoryCells+1)) | |
| 255 | + | } | |
| 256 | + | ||
| 237 | 257 | func hostileHistoryDeclarations(chunkCount, rowCount int) []byte { | |
| 238 | 258 | data := make([]byte, 9, 9+chunkCount*(4+rowCount*4)) | |
| 239 | 259 | copy(data, historyMagic) | |
| @@ -269,6 +289,7 @@ func TestHistoryPreflightMatchesUnmarshalMalformedInput(t *testing.T) { | |||
| 269 | 289 | {name: "invalid rune", data: invalidRune}, | |
| 270 | 290 | {name: "invalid underline style", data: invalidUnderlineStyle}, | |
| 271 | 291 | {name: "aggregate row budget", data: hostileHistoryDeclarations(47, maxHistoryChunkRows)}, | |
| 292 | + | {name: "aggregate cell budget", data: aggregateCellLimitDeclaration()}, | |
| 272 | 293 | {name: "zero row count", data: []byte{'V', 'T', 'H', '1', historyVersion, 0, 0, 0, 1, 0, 0, 0, 0}}, | |
| 273 | 294 | } | |
| 274 | 295 | for _, tt := range tests { | |
| @@ -57,7 +57,7 @@ func TestChunkCodecRoundTripsLosslessCellsAndStyles(t *testing.T) { | |||||
| 57 | t.Run(tt.name, func(t *testing.T) { | 57 | t.Run(tt.name, func(t *testing.T) { | ||
| 58 | history := NewHistory(HistoryConfig{MaxRows: 8, ChunkRows: 2}) | 58 | history := NewHistory(HistoryConfig{MaxRows: 8, ChunkRows: 2}) | ||
| 59 | for _, row := range tt.rows { | 59 | for _, row := range tt.rows { | ||
| 60 | - | history.Append(row) | 60 | + | requireHistoryAppend(t, history, row) |
| 61 | } | 61 | } | ||
| 62 | 62 | ||||
| 63 | encoded, err := MarshalHistory(history.View()) | 63 | encoded, err := MarshalHistory(history.View()) | ||
| @@ -75,8 +75,8 @@ func TestChunkCodecRoundTripsLosslessCellsAndStyles(t *testing.T) { | |||||
| 75 | 75 | ||||
| 76 | func TestChunkCodecRejectsTruncatedAndTrailingPayloads(t *testing.T) { | 76 | func TestChunkCodecRejectsTruncatedAndTrailingPayloads(t *testing.T) { | ||
| 77 | history := NewHistory(HistoryConfig{MaxRows: 4, ChunkRows: 2}) | 77 | history := NewHistory(HistoryConfig{MaxRows: 4, ChunkRows: 2}) | ||
| 78 | - | history.Append(historyRow("AAAA")) | 78 | + | requireHistoryAppend(t, history, historyRow("AAAA")) |
| 79 | - | history.Append(historyRow("BBBB")) | 79 | + | requireHistoryAppend(t, history, historyRow("BBBB")) |
| 80 | encoded, err := MarshalHistory(history.View()) | 80 | encoded, err := MarshalHistory(history.View()) | ||
| 81 | if err != nil { | 81 | if err != nil { | ||
| 82 | t.Fatalf("marshal history: %v", err) | 82 | t.Fatalf("marshal history: %v", err) | ||
| @@ -136,11 +136,6 @@ func TestChunkCodecRejectsDimensionsBeyondSupportedLimits(t *testing.T) { | |||||
| 136 | view: historyViewWithDimensions(supportedRows+1, 0), | 136 | view: historyViewWithDimensions(supportedRows+1, 0), | ||
| 137 | data: historyPayloadWithDimensions(supportedRows+1, 0), | 137 | data: historyPayloadWithDimensions(supportedRows+1, 0), | ||
| 138 | }, | 138 | }, | ||
| 139 | - | { | |||
| 140 | - | name: "too many aggregate cells within row budget", | |||
| 141 | - | view: historyViewWithDimensions(maxHistoryRows, maxHistoryRowCells+1), | |||
| 142 | - | data: historyPayloadWithDimensions(maxHistoryRows, maxHistoryRowCells+1), | |||
| 143 | - | }, | |||
| 144 | } | 139 | } | ||
| 145 | 140 | ||||
| 146 | for _, tt := range tests { | 141 | for _, tt := range tests { | ||
| @@ -155,6 +150,19 @@ func TestChunkCodecRejectsDimensionsBeyondSupportedLimits(t *testing.T) { | |||||
| 155 | } | 150 | } | ||
| 156 | } | 151 | } | ||
| 157 | 152 | ||||
| 153 | + | func TestChunkCodecRejectsAggregateCellDeclarationBeforePayloadAllocation(t *testing.T) { | |||
| 154 | + | data := aggregateCellLimitDeclaration() | |||
| 155 | + | if len(data) >= 1024 { | |||
| 156 | + | t.Fatalf("aggregate declaration length = %d, want compact fixture", len(data)) | |||
| 157 | + | } | |||
| 158 | + | if _, err := PreflightHistoryBlob(data); err == nil { | |||
| 159 | + | t.Fatal("preflight accepted aggregate cell declaration") | |||
| 160 | + | } | |||
| 161 | + | if _, err := UnmarshalHistory(data); err == nil { | |||
| 162 | + | t.Fatal("unmarshal accepted aggregate cell declaration") | |||
| 163 | + | } | |||
| 164 | + | } | |||
| 165 | + | ||||
| 158 | func TestChunkCodecAcceptsWideRowsWithinAggregateBudget(t *testing.T) { | 166 | func TestChunkCodecAcceptsWideRowsWithinAggregateBudget(t *testing.T) { | ||
| 159 | for _, width := range []int{161, 293, math.MaxUint16} { | 167 | for _, width := range []int{161, 293, math.MaxUint16} { | ||
| 160 | t.Run(fmt.Sprintf("width_%d", width), func(t *testing.T) { | 168 | t.Run(fmt.Sprintf("width_%d", width), func(t *testing.T) { | ||
| @@ -234,6 +242,18 @@ func historyPayloadWithDimensions(rowCount, width int) []byte { | |||||
| 234 | return data | 242 | return data | ||
| 235 | } | 243 | } | ||
| 236 | 244 | ||||
| 245 | + | // aggregateCellLimitDeclaration declares one row, which is within the row | |||
| 246 | + | // budget, but its cells exceed the aggregate budget. It deliberately omits | |||
| 247 | + | // cell payload bytes: the aggregate limit must reject it before allocation. | |||
| 248 | + | func aggregateCellLimitDeclaration() []byte { | |||
| 249 | + | data := make([]byte, 9, 21) | |||
| 250 | + | copy(data, historyMagic) | |||
| 251 | + | data[4] = historyVersion | |||
| 252 | + | binary.BigEndian.PutUint32(data[5:], 1) | |||
| 253 | + | data = binary.BigEndian.AppendUint32(data, 1) | |||
| 254 | + | return binary.BigEndian.AppendUint32(data, uint32(maxHistoryCells+1)) | |||
| 255 | + | } | |||
| 256 | + | ||||
| 237 | func hostileHistoryDeclarations(chunkCount, rowCount int) []byte { | 257 | func hostileHistoryDeclarations(chunkCount, rowCount int) []byte { | ||
| 238 | data := make([]byte, 9, 9+chunkCount*(4+rowCount*4)) | 258 | data := make([]byte, 9, 9+chunkCount*(4+rowCount*4)) | ||
| 239 | copy(data, historyMagic) | 259 | copy(data, historyMagic) | ||
| @@ -269,6 +289,7 @@ func TestHistoryPreflightMatchesUnmarshalMalformedInput(t *testing.T) { | |||||
| 269 | {name: "invalid rune", data: invalidRune}, | 289 | {name: "invalid rune", data: invalidRune}, | ||
| 270 | {name: "invalid underline style", data: invalidUnderlineStyle}, | 290 | {name: "invalid underline style", data: invalidUnderlineStyle}, | ||
| 271 | {name: "aggregate row budget", data: hostileHistoryDeclarations(47, maxHistoryChunkRows)}, | 291 | {name: "aggregate row budget", data: hostileHistoryDeclarations(47, maxHistoryChunkRows)}, | ||
| 292 | + | {name: "aggregate cell budget", data: aggregateCellLimitDeclaration()}, | |||
| 272 | {name: "zero row count", data: []byte{'V', 'T', 'H', '1', historyVersion, 0, 0, 0, 1, 0, 0, 0, 0}}, | 293 | {name: "zero row count", data: []byte{'V', 'T', 'H', '1', historyVersion, 0, 0, 0, 1, 0, 0, 0, 0}}, | ||
| 273 | } | 294 | } | ||
| 274 | for _, tt := range tests { | 295 | for _, tt := range tests { | ||
M pkg/vt/history_test.go +14 -7
| @@ -8,6 +8,13 @@ import ( | |||
| 8 | 8 | "github.com/bnema/vev/pkg/renderer" | |
| 9 | 9 | ) | |
| 10 | 10 | ||
| 11 | + | func requireHistoryAppend(t testing.TB, history *History, row []renderer.Cell) { | |
| 12 | + | t.Helper() | |
| 13 | + | if err := history.Append(row); err != nil { | |
| 14 | + | t.Fatalf("append history row: %v", err) | |
| 15 | + | } | |
| 16 | + | } | |
| 17 | + | ||
| 11 | 18 | func TestHistoryOrdersSealedChunksAndEvictsAtCapacity(t *testing.T) { | |
| 12 | 19 | tests := []struct { | |
| 13 | 20 | name string | |
| @@ -43,7 +50,7 @@ func TestHistoryOrdersSealedChunksAndEvictsAtCapacity(t *testing.T) { | |||
| 43 | 50 | t.Run(tt.name, func(t *testing.T) { | |
| 44 | 51 | history := NewHistory(HistoryConfig{MaxRows: tt.maxRows, ChunkRows: tt.chunkRows}) | |
| 45 | 52 | for _, text := range tt.rows { | |
| 46 | - | history.Append(historyRow(text)) | |
| 53 | + | requireHistoryAppend(t, history, historyRow(text)) | |
| 47 | 54 | } | |
| 48 | 55 | ||
| 49 | 56 | view := history.View() | |
| @@ -72,15 +79,15 @@ func TestHistoryTailRotationAndStableViews(t *testing.T) { | |||
| 72 | 79 | t.Run(tt.name, func(t *testing.T) { | |
| 73 | 80 | history := NewHistory(HistoryConfig{MaxRows: 6, ChunkRows: 2}) | |
| 74 | 81 | first := historyRow("AAAA") | |
| 75 | - | history.Append(first) | |
| 76 | - | history.Append(historyRow("BBBB")) | |
| 82 | + | requireHistoryAppend(t, history, first) | |
| 83 | + | requireHistoryAppend(t, history, historyRow("BBBB")) | |
| 77 | 84 | view := history.View() | |
| 78 | 85 | if got, want := view.ChunkCount(), 1; got != want { | |
| 79 | 86 | t.Fatalf("sealed chunk count = %d, want %d", got, want) | |
| 80 | 87 | } | |
| 81 | 88 | ||
| 82 | 89 | first[0].Rune = 'X' | |
| 83 | - | history.Append(historyRow("CCCC")) | |
| 90 | + | requireHistoryAppend(t, history, historyRow("CCCC")) | |
| 84 | 91 | ||
| 85 | 92 | if got, want := historyViewTexts(view), []string{"AAAA", "BBBB"}; !equalStrings(got, want) { | |
| 86 | 93 | t.Fatalf("stable view rows = %#v, want %#v", got, want) | |
| @@ -102,11 +109,11 @@ func TestHistoryReusesSealedChunkIdentityAcrossViews(t *testing.T) { | |||
| 102 | 109 | for _, tt := range tests { | |
| 103 | 110 | t.Run(tt.name, func(t *testing.T) { | |
| 104 | 111 | history := NewHistory(HistoryConfig{MaxRows: 8, ChunkRows: 2}) | |
| 105 | - | history.Append(historyRow("AAAA")) | |
| 106 | - | history.Append(historyRow("BBBB")) | |
| 112 | + | requireHistoryAppend(t, history, historyRow("AAAA")) | |
| 113 | + | requireHistoryAppend(t, history, historyRow("BBBB")) | |
| 107 | 114 | before := history.View() | |
| 108 | 115 | ||
| 109 | - | history.Append(historyRow("CCCC")) | |
| 116 | + | requireHistoryAppend(t, history, historyRow("CCCC")) | |
| 110 | 117 | after := history.View() | |
| 111 | 118 | ||
| 112 | 119 | if before.ChunkCount() == 0 || after.ChunkCount() == 0 { | |
| @@ -8,6 +8,13 @@ import ( | |||||
| 8 | "github.com/bnema/vev/pkg/renderer" | 8 | "github.com/bnema/vev/pkg/renderer" | ||
| 9 | ) | 9 | ) | ||
| 10 | 10 | ||||
| 11 | + | func requireHistoryAppend(t testing.TB, history *History, row []renderer.Cell) { | |||
| 12 | + | t.Helper() | |||
| 13 | + | if err := history.Append(row); err != nil { | |||
| 14 | + | t.Fatalf("append history row: %v", err) | |||
| 15 | + | } | |||
| 16 | + | } | |||
| 17 | + | ||||
| 11 | func TestHistoryOrdersSealedChunksAndEvictsAtCapacity(t *testing.T) { | 18 | func TestHistoryOrdersSealedChunksAndEvictsAtCapacity(t *testing.T) { | ||
| 12 | tests := []struct { | 19 | tests := []struct { | ||
| 13 | name string | 20 | name string | ||
| @@ -43,7 +50,7 @@ func TestHistoryOrdersSealedChunksAndEvictsAtCapacity(t *testing.T) { | |||||
| 43 | t.Run(tt.name, func(t *testing.T) { | 50 | t.Run(tt.name, func(t *testing.T) { | ||
| 44 | history := NewHistory(HistoryConfig{MaxRows: tt.maxRows, ChunkRows: tt.chunkRows}) | 51 | history := NewHistory(HistoryConfig{MaxRows: tt.maxRows, ChunkRows: tt.chunkRows}) | ||
| 45 | for _, text := range tt.rows { | 52 | for _, text := range tt.rows { | ||
| 46 | - | history.Append(historyRow(text)) | 53 | + | requireHistoryAppend(t, history, historyRow(text)) |
| 47 | } | 54 | } | ||
| 48 | 55 | ||||
| 49 | view := history.View() | 56 | view := history.View() | ||
| @@ -72,15 +79,15 @@ func TestHistoryTailRotationAndStableViews(t *testing.T) { | |||||
| 72 | t.Run(tt.name, func(t *testing.T) { | 79 | t.Run(tt.name, func(t *testing.T) { | ||
| 73 | history := NewHistory(HistoryConfig{MaxRows: 6, ChunkRows: 2}) | 80 | history := NewHistory(HistoryConfig{MaxRows: 6, ChunkRows: 2}) | ||
| 74 | first := historyRow("AAAA") | 81 | first := historyRow("AAAA") | ||
| 75 | - | history.Append(first) | 82 | + | requireHistoryAppend(t, history, first) |
| 76 | - | history.Append(historyRow("BBBB")) | 83 | + | requireHistoryAppend(t, history, historyRow("BBBB")) |
| 77 | view := history.View() | 84 | view := history.View() | ||
| 78 | if got, want := view.ChunkCount(), 1; got != want { | 85 | if got, want := view.ChunkCount(), 1; got != want { | ||
| 79 | t.Fatalf("sealed chunk count = %d, want %d", got, want) | 86 | t.Fatalf("sealed chunk count = %d, want %d", got, want) | ||
| 80 | } | 87 | } | ||
| 81 | 88 | ||||
| 82 | first[0].Rune = 'X' | 89 | first[0].Rune = 'X' | ||
| 83 | - | history.Append(historyRow("CCCC")) | 90 | + | requireHistoryAppend(t, history, historyRow("CCCC")) |
| 84 | 91 | ||||
| 85 | if got, want := historyViewTexts(view), []string{"AAAA", "BBBB"}; !equalStrings(got, want) { | 92 | if got, want := historyViewTexts(view), []string{"AAAA", "BBBB"}; !equalStrings(got, want) { | ||
| 86 | t.Fatalf("stable view rows = %#v, want %#v", got, want) | 93 | t.Fatalf("stable view rows = %#v, want %#v", got, want) | ||
| @@ -102,11 +109,11 @@ func TestHistoryReusesSealedChunkIdentityAcrossViews(t *testing.T) { | |||||
| 102 | for _, tt := range tests { | 109 | for _, tt := range tests { | ||
| 103 | t.Run(tt.name, func(t *testing.T) { | 110 | t.Run(tt.name, func(t *testing.T) { | ||
| 104 | history := NewHistory(HistoryConfig{MaxRows: 8, ChunkRows: 2}) | 111 | history := NewHistory(HistoryConfig{MaxRows: 8, ChunkRows: 2}) | ||
| 105 | - | history.Append(historyRow("AAAA")) | 112 | + | requireHistoryAppend(t, history, historyRow("AAAA")) |
| 106 | - | history.Append(historyRow("BBBB")) | 113 | + | requireHistoryAppend(t, history, historyRow("BBBB")) |
| 107 | before := history.View() | 114 | before := history.View() | ||
| 108 | 115 | ||||
| 109 | - | history.Append(historyRow("CCCC")) | 116 | + | requireHistoryAppend(t, history, historyRow("CCCC")) |
| 110 | after := history.View() | 117 | after := history.View() | ||
| 111 | 118 | ||||
| 112 | if before.ChunkCount() == 0 || after.ChunkCount() == 0 { | 119 | if before.ChunkCount() == 0 || after.ChunkCount() == 0 { | ||