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 (ViewBuiltTree) Built(level, index int) (bool, error) {
return true, nil
}
func (ViewBuiltTree) Node(level, index int) (Node, bool, error) {
return testNode(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 testNode(level, index, text), true, nil
}
// viewPush is the push of rollback_state_list.js.
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),
}
}