test(vt): prove screen snapshot ownership
- commit
- ff327ba2ff9986920b01694ec1f6073f56dd7bf0
- author
- brice · · 2026-07-28 14:10 UTC
- committer
- brice · · 2026-07-29 05:00 UTC
- parent
- 0461ab8
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
1 file · +133 -0
M pkg/vt/screen_snapshot_test.go +133 -0
| @@ -3,6 +3,7 @@ package vt | |||
| 3 | 3 | import ( | |
| 4 | 4 | "testing" | |
| 5 | 5 | ||
| 6 | + | "github.com/bnema/vev/pkg/renderer" | |
| 6 | 7 | "github.com/stretchr/testify/require" | |
| 7 | 8 | ) | |
| 8 | 9 | ||
| @@ -167,3 +168,135 @@ func TestScreenSnapshotTitle(t *testing.T) { | |||
| 167 | 168 | }) | |
| 168 | 169 | } | |
| 169 | 170 | } | |
| 171 | + | ||
| 172 | + | func TestScreenSnapshotFidelity(t *testing.T) { | |
| 173 | + | tests := []struct { | |
| 174 | + | name string | |
| 175 | + | make func() (*Screen, []renderer.Cell, []LineBound) | |
| 176 | + | wantModes ModeSnapshot | |
| 177 | + | }{ | |
| 178 | + | { | |
| 179 | + | name: "styles wide continuation and bounds are exact", | |
| 180 | + | make: func() (*Screen, []renderer.Cell, []LineBound) { | |
| 181 | + | screen := NewScreen(4, 2) | |
| 182 | + | style := renderer.Style{ | |
| 183 | + | Bold: true, Italic: true, Inverse: true, | |
| 184 | + | Attrs: renderer.AttrDim | renderer.AttrUnderline | renderer.AttrBlink | renderer.AttrStrikethrough, | |
| 185 | + | HasForegroundRGB: true, ForegroundRGB: renderer.RGB{R: 1, G: 2, B: 3}, | |
| 186 | + | Background: 17, | |
| 187 | + | UnderlineStyle: renderer.UnderlineCurly, | |
| 188 | + | HasUnderlineColorRGB: true, UnderlineColorRGB: renderer.RGB{R: 4, G: 5, B: 6}, | |
| 189 | + | } | |
| 190 | + | cells := []renderer.Cell{ | |
| 191 | + | {Rune: '界', Style: style}, | |
| 192 | + | {Continuation: true, Style: style}, | |
| 193 | + | {Rune: 'x', Style: renderer.DefaultStyle()}, | |
| 194 | + | renderer.BlankCell(), | |
| 195 | + | } | |
| 196 | + | copy(screen.Frame.Row(0), cells) | |
| 197 | + | screen.buffer.boundaries[0] = LineBound{End: 3, Soft: true} | |
| 198 | + | return screen, cells, []LineBound{LineBound{End: 3, Soft: true}, LineBound{}} | |
| 199 | + | }, | |
| 200 | + | }, | |
| 201 | + | { | |
| 202 | + | name: "active alternate screen is captured", | |
| 203 | + | make: func() (*Screen, []renderer.Cell, []LineBound) { | |
| 204 | + | screen := NewScreen(3, 1) | |
| 205 | + | screen.Write([]byte("primary\x1b[?1049halt")) | |
| 206 | + | cells := append([]renderer.Cell(nil), screen.Frame.Row(0)...) | |
| 207 | + | return screen, cells, screen.LineBounds() | |
| 208 | + | }, | |
| 209 | + | wantModes: ModeSnapshot{AlternateScreen: true}, | |
| 210 | + | }, | |
| 211 | + | } | |
| 212 | + | ||
| 213 | + | for _, test := range tests { | |
| 214 | + | t.Run(test.name, func(t *testing.T) { | |
| 215 | + | screen, wantCells, wantBounds := test.make() | |
| 216 | + | snapshot := screen.Snapshot() | |
| 217 | + | require.Equal(t, screen.Frame.Width, snapshot.Columns()) | |
| 218 | + | require.Equal(t, screen.Frame.Height, snapshot.Rows()) | |
| 219 | + | require.Equal(t, wantCells, snapshot.Row(0)) | |
| 220 | + | require.Equal(t, test.wantModes, snapshot.Modes()) | |
| 221 | + | for row, want := range wantBounds { | |
| 222 | + | require.Equal(t, want, snapshot.Bound(row)) | |
| 223 | + | } | |
| 224 | + | }) | |
| 225 | + | } | |
| 226 | + | } | |
| 227 | + | ||
| 228 | + | func TestScreenSnapshotOwnership(t *testing.T) { | |
| 229 | + | tests := []struct { | |
| 230 | + | name string | |
| 231 | + | mutate func(*Screen) | |
| 232 | + | }{ | |
| 233 | + | {name: "later write", mutate: func(screen *Screen) { screen.Write([]byte("later")) }}, | |
| 234 | + | {name: "later scroll", mutate: func(screen *Screen) { screen.Write([]byte("1111222233334444")) }}, | |
| 235 | + | {name: "later resize", mutate: func(screen *Screen) { screen.Resize(2, 4) }}, | |
| 236 | + | {name: "later alternate entry", mutate: func(screen *Screen) { screen.Write([]byte("\x1b[?1049halt")) }}, | |
| 237 | + | {name: "later alternate entry and exit", mutate: func(screen *Screen) { screen.Write([]byte("\x1b[?1049halt\x1b[?1049l")) }}, | |
| 238 | + | } | |
| 239 | + | ||
| 240 | + | for _, test := range tests { | |
| 241 | + | t.Run(test.name, func(t *testing.T) { | |
| 242 | + | screen := NewScreen(4, 2) | |
| 243 | + | screen.Write([]byte("abcdEF")) | |
| 244 | + | screen.buffer.boundaries[0] = LineBound{End: 4, Soft: true} | |
| 245 | + | snapshot := screen.Snapshot() | |
| 246 | + | wantRows := [][]renderer.Cell{snapshot.Row(0), snapshot.Row(1)} | |
| 247 | + | wantBounds := []LineBound{snapshot.Bound(0), snapshot.Bound(1)} | |
| 248 | + | ||
| 249 | + | test.mutate(screen) | |
| 250 | + | ||
| 251 | + | require.Equal(t, wantRows[0], snapshot.Row(0)) | |
| 252 | + | require.Equal(t, wantRows[1], snapshot.Row(1)) | |
| 253 | + | require.Equal(t, wantBounds[0], snapshot.Bound(0)) | |
| 254 | + | require.Equal(t, wantBounds[1], snapshot.Bound(1)) | |
| 255 | + | ||
| 256 | + | liveBeforeCallerMutation := append([]renderer.Cell(nil), screen.Frame.Row(0)...) | |
| 257 | + | secondSnapshot := screen.Snapshot() | |
| 258 | + | secondSnapshotBefore := secondSnapshot.Row(0) | |
| 259 | + | ||
| 260 | + | callerCopy := snapshot.Row(0) | |
| 261 | + | callerCopy[0] = renderer.Cell{Rune: 'z'} | |
| 262 | + | ||
| 263 | + | require.Equal(t, wantRows[0], snapshot.BorrowedRow(0)) | |
| 264 | + | require.Equal(t, liveBeforeCallerMutation, screen.Frame.Row(0), "mutating Row copy must not change live Screen") | |
| 265 | + | require.Equal(t, secondSnapshotBefore, secondSnapshot.Row(0), "mutating Row copy must not change another snapshot") | |
| 266 | + | }) | |
| 267 | + | } | |
| 268 | + | } | |
| 269 | + | ||
| 270 | + | func TestScreenSnapshotBoundsAreIndependent(t *testing.T) { | |
| 271 | + | tests := []struct { | |
| 272 | + | name string | |
| 273 | + | mutate func(*Screen) | |
| 274 | + | }{ | |
| 275 | + | { | |
| 276 | + | name: "mutating a single boundary entry after the snapshot", | |
| 277 | + | mutate: func(screen *Screen) { | |
| 278 | + | screen.buffer.boundaries[0] = LineBound{End: 1, Soft: false} | |
| 279 | + | }, | |
| 280 | + | }, | |
| 281 | + | { | |
| 282 | + | name: "replacing the whole boundaries slice after the snapshot", | |
| 283 | + | mutate: func(screen *Screen) { | |
| 284 | + | screen.buffer.boundaries = []LineBound{{End: 1, Soft: false}, {End: 1, Soft: false}} | |
| 285 | + | }, | |
| 286 | + | }, | |
| 287 | + | } | |
| 288 | + | ||
| 289 | + | for _, test := range tests { | |
| 290 | + | t.Run(test.name, func(t *testing.T) { | |
| 291 | + | screen := NewScreen(4, 2) | |
| 292 | + | screen.Write([]byte("abcdEF")) | |
| 293 | + | screen.buffer.boundaries[0] = LineBound{End: 4, Soft: true} | |
| 294 | + | snapshot := screen.Snapshot() | |
| 295 | + | want := snapshot.Bound(0) | |
| 296 | + | ||
| 297 | + | test.mutate(screen) | |
| 298 | + | ||
| 299 | + | require.Equal(t, want, snapshot.Bound(0), "snapshot bounds must not alias Screen.buffer.boundaries") | |
| 300 | + | }) | |
| 301 | + | } | |
| 302 | + | } | |
| @@ -3,6 +3,7 @@ package vt | |||||
| 3 | import ( | 3 | import ( | ||
| 4 | "testing" | 4 | "testing" | ||
| 5 | 5 | ||||
| 6 | + | "github.com/bnema/vev/pkg/renderer" | |||
| 6 | "github.com/stretchr/testify/require" | 7 | "github.com/stretchr/testify/require" | ||
| 7 | ) | 8 | ) | ||
| 8 | 9 | ||||
| @@ -167,3 +168,135 @@ func TestScreenSnapshotTitle(t *testing.T) { | |||||
| 167 | }) | 168 | }) | ||
| 168 | } | 169 | } | ||
| 169 | } | 170 | } | ||
| 171 | + | ||||
| 172 | + | func TestScreenSnapshotFidelity(t *testing.T) { | |||
| 173 | + | tests := []struct { | |||
| 174 | + | name string | |||
| 175 | + | make func() (*Screen, []renderer.Cell, []LineBound) | |||
| 176 | + | wantModes ModeSnapshot | |||
| 177 | + | }{ | |||
| 178 | + | { | |||
| 179 | + | name: "styles wide continuation and bounds are exact", | |||
| 180 | + | make: func() (*Screen, []renderer.Cell, []LineBound) { | |||
| 181 | + | screen := NewScreen(4, 2) | |||
| 182 | + | style := renderer.Style{ | |||
| 183 | + | Bold: true, Italic: true, Inverse: true, | |||
| 184 | + | Attrs: renderer.AttrDim | renderer.AttrUnderline | renderer.AttrBlink | renderer.AttrStrikethrough, | |||
| 185 | + | HasForegroundRGB: true, ForegroundRGB: renderer.RGB{R: 1, G: 2, B: 3}, | |||
| 186 | + | Background: 17, | |||
| 187 | + | UnderlineStyle: renderer.UnderlineCurly, | |||
| 188 | + | HasUnderlineColorRGB: true, UnderlineColorRGB: renderer.RGB{R: 4, G: 5, B: 6}, | |||
| 189 | + | } | |||
| 190 | + | cells := []renderer.Cell{ | |||
| 191 | + | {Rune: '界', Style: style}, | |||
| 192 | + | {Continuation: true, Style: style}, | |||
| 193 | + | {Rune: 'x', Style: renderer.DefaultStyle()}, | |||
| 194 | + | renderer.BlankCell(), | |||
| 195 | + | } | |||
| 196 | + | copy(screen.Frame.Row(0), cells) | |||
| 197 | + | screen.buffer.boundaries[0] = LineBound{End: 3, Soft: true} | |||
| 198 | + | return screen, cells, []LineBound{LineBound{End: 3, Soft: true}, LineBound{}} | |||
| 199 | + | }, | |||
| 200 | + | }, | |||
| 201 | + | { | |||
| 202 | + | name: "active alternate screen is captured", | |||
| 203 | + | make: func() (*Screen, []renderer.Cell, []LineBound) { | |||
| 204 | + | screen := NewScreen(3, 1) | |||
| 205 | + | screen.Write([]byte("primary\x1b[?1049halt")) | |||
| 206 | + | cells := append([]renderer.Cell(nil), screen.Frame.Row(0)...) | |||
| 207 | + | return screen, cells, screen.LineBounds() | |||
| 208 | + | }, | |||
| 209 | + | wantModes: ModeSnapshot{AlternateScreen: true}, | |||
| 210 | + | }, | |||
| 211 | + | } | |||
| 212 | + | ||||
| 213 | + | for _, test := range tests { | |||
| 214 | + | t.Run(test.name, func(t *testing.T) { | |||
| 215 | + | screen, wantCells, wantBounds := test.make() | |||
| 216 | + | snapshot := screen.Snapshot() | |||
| 217 | + | require.Equal(t, screen.Frame.Width, snapshot.Columns()) | |||
| 218 | + | require.Equal(t, screen.Frame.Height, snapshot.Rows()) | |||
| 219 | + | require.Equal(t, wantCells, snapshot.Row(0)) | |||
| 220 | + | require.Equal(t, test.wantModes, snapshot.Modes()) | |||
| 221 | + | for row, want := range wantBounds { | |||
| 222 | + | require.Equal(t, want, snapshot.Bound(row)) | |||
| 223 | + | } | |||
| 224 | + | }) | |||
| 225 | + | } | |||
| 226 | + | } | |||
| 227 | + | ||||
| 228 | + | func TestScreenSnapshotOwnership(t *testing.T) { | |||
| 229 | + | tests := []struct { | |||
| 230 | + | name string | |||
| 231 | + | mutate func(*Screen) | |||
| 232 | + | }{ | |||
| 233 | + | {name: "later write", mutate: func(screen *Screen) { screen.Write([]byte("later")) }}, | |||
| 234 | + | {name: "later scroll", mutate: func(screen *Screen) { screen.Write([]byte("1111222233334444")) }}, | |||
| 235 | + | {name: "later resize", mutate: func(screen *Screen) { screen.Resize(2, 4) }}, | |||
| 236 | + | {name: "later alternate entry", mutate: func(screen *Screen) { screen.Write([]byte("\x1b[?1049halt")) }}, | |||
| 237 | + | {name: "later alternate entry and exit", mutate: func(screen *Screen) { screen.Write([]byte("\x1b[?1049halt\x1b[?1049l")) }}, | |||
| 238 | + | } | |||
| 239 | + | ||||
| 240 | + | for _, test := range tests { | |||
| 241 | + | t.Run(test.name, func(t *testing.T) { | |||
| 242 | + | screen := NewScreen(4, 2) | |||
| 243 | + | screen.Write([]byte("abcdEF")) | |||
| 244 | + | screen.buffer.boundaries[0] = LineBound{End: 4, Soft: true} | |||
| 245 | + | snapshot := screen.Snapshot() | |||
| 246 | + | wantRows := [][]renderer.Cell{snapshot.Row(0), snapshot.Row(1)} | |||
| 247 | + | wantBounds := []LineBound{snapshot.Bound(0), snapshot.Bound(1)} | |||
| 248 | + | ||||
| 249 | + | test.mutate(screen) | |||
| 250 | + | ||||
| 251 | + | require.Equal(t, wantRows[0], snapshot.Row(0)) | |||
| 252 | + | require.Equal(t, wantRows[1], snapshot.Row(1)) | |||
| 253 | + | require.Equal(t, wantBounds[0], snapshot.Bound(0)) | |||
| 254 | + | require.Equal(t, wantBounds[1], snapshot.Bound(1)) | |||
| 255 | + | ||||
| 256 | + | liveBeforeCallerMutation := append([]renderer.Cell(nil), screen.Frame.Row(0)...) | |||
| 257 | + | secondSnapshot := screen.Snapshot() | |||
| 258 | + | secondSnapshotBefore := secondSnapshot.Row(0) | |||
| 259 | + | ||||
| 260 | + | callerCopy := snapshot.Row(0) | |||
| 261 | + | callerCopy[0] = renderer.Cell{Rune: 'z'} | |||
| 262 | + | ||||
| 263 | + | require.Equal(t, wantRows[0], snapshot.BorrowedRow(0)) | |||
| 264 | + | require.Equal(t, liveBeforeCallerMutation, screen.Frame.Row(0), "mutating Row copy must not change live Screen") | |||
| 265 | + | require.Equal(t, secondSnapshotBefore, secondSnapshot.Row(0), "mutating Row copy must not change another snapshot") | |||
| 266 | + | }) | |||
| 267 | + | } | |||
| 268 | + | } | |||
| 269 | + | ||||
| 270 | + | func TestScreenSnapshotBoundsAreIndependent(t *testing.T) { | |||
| 271 | + | tests := []struct { | |||
| 272 | + | name string | |||
| 273 | + | mutate func(*Screen) | |||
| 274 | + | }{ | |||
| 275 | + | { | |||
| 276 | + | name: "mutating a single boundary entry after the snapshot", | |||
| 277 | + | mutate: func(screen *Screen) { | |||
| 278 | + | screen.buffer.boundaries[0] = LineBound{End: 1, Soft: false} | |||
| 279 | + | }, | |||
| 280 | + | }, | |||
| 281 | + | { | |||
| 282 | + | name: "replacing the whole boundaries slice after the snapshot", | |||
| 283 | + | mutate: func(screen *Screen) { | |||
| 284 | + | screen.buffer.boundaries = []LineBound{{End: 1, Soft: false}, {End: 1, Soft: false}} | |||
| 285 | + | }, | |||
| 286 | + | }, | |||
| 287 | + | } | |||
| 288 | + | ||||
| 289 | + | for _, test := range tests { | |||
| 290 | + | t.Run(test.name, func(t *testing.T) { | |||
| 291 | + | screen := NewScreen(4, 2) | |||
| 292 | + | screen.Write([]byte("abcdEF")) | |||
| 293 | + | screen.buffer.boundaries[0] = LineBound{End: 4, Soft: true} | |||
| 294 | + | snapshot := screen.Snapshot() | |||
| 295 | + | want := snapshot.Bound(0) | |||
| 296 | + | ||||
| 297 | + | test.mutate(screen) | |||
| 298 | + | ||||
| 299 | + | require.Equal(t, want, snapshot.Bound(0), "snapshot bounds must not alias Screen.buffer.boundaries") | |||
| 300 | + | }) | |||
| 301 | + | } | |||
| 302 | + | } | |||