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