package main import ( "slices" "strings" "testing" "github.com/stretchr/testify/require" ) func TestViewMergesInTheOrderOfTheRollbackPush(t *testing.T) { tree := ViewBuiltTree{} view := newView(0, 0) var states *ViewPushEntry for tick := range 20001 { states = viewPush(tick, states) starts := states.starts() view.high = len(starts) view.low = len(starts) _, err := view.Append(tree) require.NoError(t, err) lines := make([]int, len(view.lines)) for n, line := range view.lines { lines[n] = line.index << line.level } require.Equal(t, starts, lines, "t=%d", tick) } } func TestViewMergesInOneBatchFromHighToLow(t *testing.T) { tree := ViewBuiltTree{} view := newView(10, 5) sizes := make([]int, 0, 20) for range 20 { _, err := view.Append(tree) require.NoError(t, err) sizes = append(sizes, view.Size()) } 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) } func TestViewMergesOnlyBuiltParents(t *testing.T) { tree := ViewPartTree{ sizes: map[NodeKey]int{ {0, 0}: 1, {0, 1}: 1, {0, 2}: 1, {0, 3}: 1, {0, 4}: 1, {0, 5}: 1, }, } view := newView(3, 2) for range 4 { _, err := view.Append(tree) require.NoError(t, err) } require.Equal(t, []NodeKey{{0, 0}, {0, 1}, {0, 2}, {0, 3}}, view.lines) require.True(t, view.merging) tree.sizes[NodeKey{1, 0}] = 1 _, err := view.Append(tree) require.NoError(t, err) require.Equal(t, []NodeKey{{1, 0}, {0, 2}, {0, 3}, {0, 4}}, view.lines) require.True(t, view.merging) tree.sizes[NodeKey{1, 1}] = 1 tree.sizes[NodeKey{1, 2}] = 1 tree.sizes[NodeKey{2, 0}] = 1 _, err = view.Append(tree) require.NoError(t, err) require.Equal(t, []NodeKey{{2, 0}, {1, 2}}, view.lines) require.False(t, view.merging) } func TestViewRendersPendingLinesWithoutTheirMessage(t *testing.T) { tree := ViewPartTree{ sizes: map[NodeKey]int{ {0, 0}: 3, {0, 1}: 3, {1, 0}: 7, }, } view, err := restoreView(ViewState{ Lines: [][2]int{{1, 0}, {0, 2}}, }, viewChatHigh, viewChatLow, tree) require.NoError(t, err) require.Equal(t, "\n0+2|aaa aaa\n2+1|"+viewPending+"\n", view.Render()) require.Equal(t, 7+len(viewPending), view.Size()) } func TestViewCoarsenTakesTheCoarserLines(t *testing.T) { view, err := restoreView(ViewState{ Lines: [][2]int{{1, 0}, {0, 2}, {0, 3}, {0, 4}, {0, 5}}, }, 100, 100, ViewBuiltTree{}) require.NoError(t, err) other, err := restoreView(ViewState{ Lines: [][2]int{{0, 0}, {0, 1}, {1, 1}, {0, 4}, {0, 5}}, }, 100, 100, ViewBuiltTree{}) require.NoError(t, err) require.NoError(t, view.Coarsen(other, ViewBuiltTree{})) require.Equal(t, []NodeKey{{1, 0}, {1, 1}, {0, 4}, {0, 5}}, view.lines) view.low = 2 require.NoError(t, view.Coarsen(other, ViewBuiltTree{})) require.Equal(t, []NodeKey{{2, 0}, {1, 2}}, view.lines) require.False(t, view.merging) } func TestViewContextStopsAtTheFirstPendingLine(t *testing.T) { tree := ViewPartTree{ sizes: map[NodeKey]int{ {0, 0}: 1, {0, 1}: 1, {1, 0}: 1, {0, 3}: 1, }, } view, err := restoreView(ViewState{ Lines: [][2]int{{1, 0}, {0, 2}, {0, 3}}, }, 100, 100, tree) require.NoError(t, err) require.Equal(t, "\n", view.Context(0)) require.Equal(t, "\n0+2|a\n", view.Context(2)) require.Equal(t, "\n0+2|a\n", view.Context(3)) } func TestRestoreViewRejectsAGap(t *testing.T) { _, err := restoreView(ViewState{ Lines: [][2]int{{0, 0}, {0, 2}}, }, viewChatHigh, viewChatLow, ViewBuiltTree{}) require.ErrorContains(t, err, "does not start at message 1") } // ViewBuiltTree is a tree with each node built, with 1 byte of text. type ViewBuiltTree struct{} func (v ViewBuiltTree) Built(_, _ int) (bool, error) { return true, nil } func (ViewBuiltTree) Node(level, index int) (Node, bool, error) { return treeTestNode(level, index, "a"), true, nil } // ViewPushEntry is one entry of the rollback state list of the reference // design, newest first. Under push alone, life stays 0, so the entry leaves it // out. type ViewPushEntry struct { keep bool state int older *ViewPushEntry } // starts returns the states of the list, oldest first. func (e *ViewPushEntry) starts() []int { var starts []int for entry := e; entry != nil; entry = entry.older { starts = append(starts, entry.state) } slices.Reverse(starts) return starts } // ViewPartTree is a tree with the built nodes of its sizes. type ViewPartTree struct { sizes map[NodeKey]int } func (p ViewPartTree) Built(level, index int) (bool, error) { _, built := p.sizes[NodeKey{level, index}] return built, nil } func (p ViewPartTree) Node(level, index int) (Node, bool, error) { size, built := p.sizes[NodeKey{level, index}] if !built { return Node{}, false, nil } text := strings.Repeat("a", size) if size == 7 { text = "aaa\naaa" } return treeTestNode(level, index, text), true, nil } // viewPush adds state to the newest end of states and returns the new list. An // empty list gets one entry. When the newest entry is not kept, it becomes kept // with its own state, and state is dropped. When it is kept, state becomes a // new newest entry, and the state of the old newest entry goes to the older // entries by the same push. func viewPush(state int, states *ViewPushEntry) *ViewPushEntry { if states == nil { return &ViewPushEntry{ state: state, } } if !states.keep { return &ViewPushEntry{ keep: true, state: states.state, older: states.older, } } return &ViewPushEntry{ state: state, older: viewPush(states.state, states.older), } }