1package ansi_test
2
3import (
4 "fmt"
5 "math/rand/v2"
6 "strings"
7 "testing"
8
9 vt "github.com/bnema/vev-vt"
10 "github.com/bnema/vev-vt/ansi"
11 core "github.com/bnema/vev-vt/core"
12)
13
14type hiddenCellSource struct{ core.CellSource }
15
16// Random VT scroll workloads rendered with real screen damage must replay to
17// the exact source grid for every CellSource shape the renderer dispatches on.
18func TestRandomScrollWorkloadsReplayExactly(t *testing.T) {
19 seeds := 400
20 if testing.Short() {
21 seeds = 60
22 }
23 for _, kind := range []string{"screen", "frame", "pointer", "hidden"} {
24 t.Run(kind, func(t *testing.T) {
25 for seed := range seeds {
26 runReplayWorkload(t, kind, uint64(seed))
27 }
28 })
29 }
30}
31
32func runReplayWorkload(t *testing.T, kind string, seed uint64) {
33 t.Helper()
34 r := rand.New(rand.NewPCG(seed, 0x5c4011))
35 cols, rows := 4+r.IntN(14), 3+r.IntN(10)
36 s := vt.NewScreen(cols, rows)
37 renderer := ansi.New(ansi.Capabilities{})
38 replay := vt.NewScreen(cols, rows)
39 mirror := core.NewFrame(cols, rows)
40 for step := range 30 {
41 input := randomScrollInput(r, cols, rows)
42 s.Write(input)
43 var source core.CellSource = s
44 if kind != "screen" {
45 for y := range rows {
46 for x := range cols {
47 mirror.Set(x, y, s.Cell(x, y))
48 }
49 }
50 switch kind {
51 case "frame":
52 source = mirror
53 case "pointer":
54 source = &mirror
55 default:
56 source = hiddenCellSource{mirror}
57 }
58 }
59 prepared, err := renderer.Prepare(source, s.Damage(), false)
60 if err != nil {
61 t.Fatal(err)
62 }
63 prepared.Commit()
64 replay.Write(prepared.Bytes())
65 s.ClearDamage()
66 for y := range rows {
67 for x := range cols {
68 want, got := s.Cell(x, y), replay.Cell(x, y)
69 if want.Rune != got.Rune || want.Continuation != got.Continuation || !want.Style.Equal(got.Style) {
70 t.Fatalf("seed %d step %d input %q: cell (%d,%d) = %+v, want %+v", seed, step, input, x, y, got, want)
71 }
72 }
73 }
74 }
75}
76
77func randomScrollInput(r *rand.Rand, cols, rows int) []byte {
78 var b strings.Builder
79 for range 1 + r.IntN(12) {
80 switch r.IntN(14) {
81 case 0:
82 top := 1 + r.IntN(rows)
83 fmt.Fprintf(&b, "\x1b[%d;%dr", top, top+r.IntN(rows-top+1))
84 case 1:
85 b.WriteString("\x1b[r")
86 case 2:
87 b.WriteString("\x1bD")
88 case 3:
89 b.WriteString("\x1bM")
90 case 4:
91 fmt.Fprintf(&b, "\x1b[%dS", 1+r.IntN(3))
92 case 5:
93 fmt.Fprintf(&b, "\x1b[%dT", 1+r.IntN(3))
94 case 6:
95 fmt.Fprintf(&b, "\x1b[%dL", 1+r.IntN(3))
96 case 7:
97 fmt.Fprintf(&b, "\x1b[%dM", 1+r.IntN(3))
98 case 8:
99 b.WriteString("\r\n")
100 case 9:
101 b.WriteString([]string{"\x1b[K", "\x1b[1K", "\x1b[2K", "\x1b[J"}[r.IntN(4)])
102 case 10:
103 fmt.Fprintf(&b, "\x1b[%dm", []int{0, 1, 7, 31, 42}[r.IntN(5)])
104 case 11:
105 fmt.Fprintf(&b, "\x1b[%d;%dH", 1+r.IntN(rows), 1+r.IntN(cols))
106 case 12:
107 b.WriteString("界")
108 default:
109 for range 1 + r.IntN(cols) {
110 b.WriteByte(byte('a' + r.IntN(26)))
111 }
112 }
113 }
114 return []byte(b.String())
115}