1package uiterm
2
3import (
4 "bytes"
5 "context"
6 "testing"
7
8 "github.com/bnema/vev/internal/domain"
9 "github.com/bnema/vev/internal/ports"
10)
11
12// TestTerminalSnapshotReadsCommittedRevision pins the lazy conversion: a
13// Snapshot read after later uncommitted writes still returns the committed
14// screen of the latest revision, and each revision converts its own state.
15func TestTerminalSnapshotReadsCommittedRevision(t *testing.T) {
16 terminal := newTestTerminal(t, 4, 1)
17 for _, tc := range []struct {
18 write string
19 flush bool
20 picker bool
21 revision uint64
22 first string
23 }{
24 {write: "A", flush: true, revision: 1, first: "A"},
25 {write: "\rB", revision: 1, first: "A"},
26 {flush: true, revision: 2, first: "B"},
27 {picker: true, revision: 3, first: "B"},
28 {write: "\rC", revision: 3, first: "B"},
29 {flush: true, revision: 4, first: "C"},
30 } {
31 if tc.write != "" {
32 if _, err := terminal.Write([]byte(tc.write)); err != nil {
33 t.Fatal(err)
34 }
35 }
36 if tc.flush {
37 if err := terminal.Flush(); err != nil {
38 t.Fatal(err)
39 }
40 }
41 if tc.picker {
42 // A context-only publication reuses the committed frame.
43 if err := terminal.PublishContext(ports.UIContext{AttachmentHandle: "attachment", Status: ports.UIStatusPicker}); err != nil {
44 t.Fatal(err)
45 }
46 }
47 snapshot, err := terminal.Snapshot()
48 if err != nil {
49 t.Fatal(err)
50 }
51 if snapshot.Revision != tc.revision || snapshot.Cells[0].Text != tc.first {
52 t.Fatalf("after %+v: revision %d first %q, want %d %q", tc, snapshot.Revision, snapshot.Cells[0].Text, tc.revision, tc.first)
53 }
54 if tc.revision >= 3 && snapshot.Context.Status != ports.UIStatusPicker {
55 t.Fatalf("after %+v: context %+v lost the picker publication", tc, snapshot.Context)
56 }
57 }
58}
59
60// TestTerminalBoundaryMatchesSnapshotWithoutConverting pins the per-frame
61// bookkeeping read: Boundary reports the same revision and context as
62// Snapshot, follows the same availability rules, and never builds cells.
63func TestTerminalBoundaryMatchesSnapshotWithoutConverting(t *testing.T) {
64 terminal := newTestTerminal(t, 4, 1)
65 if _, err := terminal.Boundary(); err == nil {
66 t.Fatal("boundary before the first publication must be unavailable")
67 }
68 terminal.BeginOutput(ports.UIContext{AttachmentHandle: "attachment", Generation: 3, Status: ports.UIStatusAttached})
69 if _, err := terminal.Write([]byte("A")); err != nil {
70 t.Fatal(err)
71 }
72 terminal.EndOutput(true)
73
74 boundary, err := terminal.Boundary()
75 if err != nil {
76 t.Fatal(err)
77 }
78 terminal.mu.Lock()
79 converted := terminal.latest.Revision
80 terminal.mu.Unlock()
81 if converted != 0 {
82 t.Fatalf("Boundary converted revision %d", converted)
83 }
84 snapshot, err := terminal.Snapshot()
85 if err != nil {
86 t.Fatal(err)
87 }
88 if boundary.Revision != snapshot.Revision || boundary.Context != snapshot.Context {
89 t.Fatalf("boundary %+v, snapshot revision %d context %+v", boundary, snapshot.Revision, snapshot.Context)
90 }
91
92 terminal.InvalidateTerminalObservation()
93 if _, err := terminal.Boundary(); err == nil {
94 t.Fatal("boundary of an invalidated observation must be unavailable")
95 }
96}
97
98// BenchmarkTerminalMirrorFlush measures one mirrored flush that nobody reads,
99// the steady state of an interactive client with UI control enabled.
100func BenchmarkTerminalMirrorFlush(b *testing.B) {
101 terminal, err := NewMirror(context.Background(), domain.Geometry{Size: domain.Size{Cols: 200, Rows: 50}}, "attachment")
102 if err != nil {
103 b.Fatal(err)
104 }
105 defer terminal.Close()
106 line := append(bytes.Repeat([]byte("x"), 199), '\n')
107 b.ReportAllocs()
108 for b.Loop() {
109 terminal.ObserveTerminalWrite(line)
110 terminal.ObserveTerminalFlush()
111 }
112}