]> git.giacomolli.uk - onechat/blob - view_test.go
Send WhatsApp messages with an ID
[onechat] / view_test.go
1 package main
2
3 import (
4         "slices"
5         "strings"
6         "testing"
7
8         "github.com/stretchr/testify/require"
9 )
10
11 func TestViewMergesInTheOrderOfTheRollbackPush(t *testing.T) {
12         tree := ViewBuiltTree{}
13         view := newView(0, 0)
14         var states *ViewPushEntry
15         for tick := range 20001 {
16                 states = viewPush(tick, states)
17                 starts := states.starts()
18                 view.high = len(starts)
19                 view.low = len(starts)
20                 _, err := view.Append(tree)
21                 require.NoError(t, err)
22
23                 lines := make([]int, len(view.lines))
24                 for n, line := range view.lines {
25                         lines[n] = line.index << line.level
26                 }
27
28                 require.Equal(t, starts, lines, "t=%d", tick)
29         }
30 }
31
32 func TestViewMergesInOneBatchFromHighToLow(t *testing.T) {
33         tree := ViewBuiltTree{}
34         view := newView(10, 5)
35         sizes := make([]int, 0, 20)
36         for range 20 {
37                 _, err := view.Append(tree)
38                 require.NoError(t, err)
39                 sizes = append(sizes, view.Size())
40         }
41
42         require.Equal(t, []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 5, 6, 7, 8, 9, 10, 5, 6, 7, 8}, sizes)
43 }
44
45 func TestViewMergesOnlyBuiltParents(t *testing.T) {
46         tree := ViewPartTree{
47                 sizes: map[NodeKey]int{
48                         {0, 0}: 1,
49                         {0, 1}: 1,
50                         {0, 2}: 1,
51                         {0, 3}: 1,
52                         {0, 4}: 1,
53                         {0, 5}: 1,
54                 },
55         }
56
57         view := newView(3, 2)
58         for range 4 {
59                 _, err := view.Append(tree)
60                 require.NoError(t, err)
61         }
62
63         require.Equal(t, []NodeKey{{0, 0}, {0, 1}, {0, 2}, {0, 3}}, view.lines)
64         require.True(t, view.merging)
65
66         tree.sizes[NodeKey{1, 0}] = 1
67         _, err := view.Append(tree)
68         require.NoError(t, err)
69         require.Equal(t, []NodeKey{{1, 0}, {0, 2}, {0, 3}, {0, 4}}, view.lines)
70         require.True(t, view.merging)
71
72         tree.sizes[NodeKey{1, 1}] = 1
73         tree.sizes[NodeKey{1, 2}] = 1
74         tree.sizes[NodeKey{2, 0}] = 1
75         _, err = view.Append(tree)
76         require.NoError(t, err)
77         require.Equal(t, []NodeKey{{2, 0}, {1, 2}}, view.lines)
78         require.False(t, view.merging)
79 }
80
81 func TestViewRendersPendingLinesWithoutTheirMessage(t *testing.T) {
82         tree := ViewPartTree{
83                 sizes: map[NodeKey]int{
84                         {0, 0}: 3,
85                         {0, 1}: 3,
86                         {1, 0}: 7,
87                 },
88         }
89
90         view, err := restoreView(ViewState{
91                 Lines: [][2]int{{1, 0}, {0, 2}},
92         }, viewChatHigh, viewChatLow, tree)
93         require.NoError(t, err)
94         require.Equal(t, "<chat>\n0+2|aaa aaa\n2+1|"+viewPending+"\n</chat>", view.Render())
95         require.Equal(t, 7+len(viewPending), view.Size())
96 }
97
98 func TestViewCoarsenTakesTheCoarserLines(t *testing.T) {
99         view, err := restoreView(ViewState{
100                 Lines: [][2]int{{1, 0}, {0, 2}, {0, 3}, {0, 4}, {0, 5}},
101         }, 100, 100, ViewBuiltTree{})
102         require.NoError(t, err)
103
104         other, err := restoreView(ViewState{
105                 Lines: [][2]int{{0, 0}, {0, 1}, {1, 1}, {0, 4}, {0, 5}},
106         }, 100, 100, ViewBuiltTree{})
107         require.NoError(t, err)
108
109         require.NoError(t, view.Coarsen(other, ViewBuiltTree{}))
110         require.Equal(t, []NodeKey{{1, 0}, {1, 1}, {0, 4}, {0, 5}}, view.lines)
111
112         view.low = 2
113         require.NoError(t, view.Coarsen(other, ViewBuiltTree{}))
114         require.Equal(t, []NodeKey{{2, 0}, {1, 2}}, view.lines)
115         require.False(t, view.merging)
116 }
117
118 func TestViewContextStopsAtTheFirstPendingLine(t *testing.T) {
119         tree := ViewPartTree{
120                 sizes: map[NodeKey]int{
121                         {0, 0}: 1,
122                         {0, 1}: 1,
123                         {1, 0}: 1,
124                         {0, 3}: 1,
125                 },
126         }
127
128         view, err := restoreView(ViewState{
129                 Lines: [][2]int{{1, 0}, {0, 2}, {0, 3}},
130         }, 100, 100, tree)
131         require.NoError(t, err)
132         require.Equal(t, "<chat>\n</chat>", view.Context(0))
133         require.Equal(t, "<chat>\n0+2|a\n</chat>", view.Context(2))
134         require.Equal(t, "<chat>\n0+2|a\n</chat>", view.Context(3))
135 }
136
137 func TestRestoreViewRejectsAGap(t *testing.T) {
138         _, err := restoreView(ViewState{
139                 Lines: [][2]int{{0, 0}, {0, 2}},
140         }, viewChatHigh, viewChatLow, ViewBuiltTree{})
141         require.ErrorContains(t, err, "does not start at message 1")
142 }
143
144 // ViewBuiltTree is a tree with each node built, with 1 byte of text.
145 type ViewBuiltTree struct{}
146
147 func (ViewBuiltTree) Built(level, index int) (bool, error) {
148         return true, nil
149 }
150
151 func (ViewBuiltTree) Node(level, index int) (Node, bool, error) {
152         return treeTestNode(level, index, "a"), true, nil
153 }
154
155 // ViewPushEntry is one entry of the rollback state list of the reference
156 // design, newest first. Under push alone, life stays 0, so the entry leaves it
157 // out.
158 type ViewPushEntry struct {
159         keep  bool
160         state int
161         older *ViewPushEntry
162 }
163
164 // starts returns the states of the list, oldest first.
165 func (e *ViewPushEntry) starts() []int {
166         var starts []int
167         for entry := e; entry != nil; entry = entry.older {
168                 starts = append(starts, entry.state)
169         }
170
171         slices.Reverse(starts)
172
173         return starts
174 }
175
176 // ViewPartTree is a tree with the built nodes of its sizes.
177 type ViewPartTree struct {
178         sizes map[NodeKey]int
179 }
180
181 func (p ViewPartTree) Built(level, index int) (bool, error) {
182         _, built := p.sizes[NodeKey{level, index}]
183         return built, nil
184 }
185
186 func (p ViewPartTree) Node(level, index int) (Node, bool, error) {
187         size, built := p.sizes[NodeKey{level, index}]
188         if !built {
189                 return Node{}, false, nil
190         }
191
192         text := strings.Repeat("a", size)
193         if size == 7 {
194                 text = "aaa\naaa"
195         }
196
197         return treeTestNode(level, index, text), true, nil
198 }
199
200 // viewPush adds state to the newest end of states and returns the new list. An
201 // empty list gets one entry. When the newest entry is not kept, it becomes kept
202 // with its own state, and state is dropped. When it is kept, state becomes a
203 // new newest entry, and the state of the old newest entry goes to the older
204 // entries by the same push.
205 func viewPush(state int, states *ViewPushEntry) *ViewPushEntry {
206         if states == nil {
207                 return &ViewPushEntry{
208                         state: state,
209                 }
210         }
211
212         if !states.keep {
213                 return &ViewPushEntry{
214                         keep:  true,
215                         state: states.state,
216                         older: states.older,
217                 }
218         }
219
220         return &ViewPushEntry{
221                 state: state,
222                 older: viewPush(states.state, states.older),
223         }
224 }