cat / 012
clone

a spreadsheet for the terminal: lotus 1-2-3 looks, google sheets keys and formulas, one go binary

cat/module-path
012 / internal / sheet / store_test.go
store_test.go 5.1K
1package sheet23import (4 "math/rand/v2"5 "slices"6 "testing"7)89// The store agrees with a map of what was stored: get returns each10// cell's entry, inRange yields exactly the stored cells of a range in11// row-major order, bounds is their bounding box and nextRow finds the12// neighbours, through random sets and deletes of plain and rich cells13// that cross block boundaries.14func TestCellStoreIndex(t *testing.T) {15 rng := rand.New(rand.NewPCG(1, 2))16 st := newCellStore()17 model := map[Addr]string{}18 rows := []int{0, 1, 63, 64, 1023, 1024, 1025, 5000, 70000, MaxRows - 1}19 inputs := []string{"x", "", "5", "2.50", "=A1", "TRUE"} // "" is formatting only20 for i := range 3000 {21 a := Addr{Col: rng.IntN(40), Row: rows[rng.IntN(len(rows))] + rng.IntN(3)}22 a.Row = min(a.Row, MaxRows-1)23 if rng.IntN(3) == 0 {24 st.delete(a)25 delete(model, a)26 } else {27 input := inputs[rng.IntN(len(inputs))]28 c, err := newCell(input, Format{}, Style{Bold: input == ""}, true)29 if err != nil {30 t.Fatal(err)31 }32 st.set(a, c)33 model[a] = input34 }35 if i%100 == 0 {36 checkStore(t, &st, model, rng)37 }38 }39 for a := range st.all() {40 st.delete(a)41 }42 if st.len() != 0 || slices.ContainsFunc(st.stored.cols, func(c *colIndex) bool { return c != nil }) || len(st.stored.colIDs) != 0 || len(st.filled.colIDs) != 0 {43 t.Errorf("empty store keeps %d columns", len(st.stored.colIDs))44 }45}4647func checkStore(t *testing.T, st *cellStore, model map[Addr]string, rng *rand.Rand) {48 t.Helper()49 if st.len() != len(model) {50 t.Fatalf("%d cells stored, want %d", st.len(), len(model))51 }52 for a, input := range model {53 if c := st.get(a); c == nil || c.Input != input {54 t.Fatalf("get(%v) = %+v, want %q", a, c, input)55 }56 }57 for range 20 {58 r := NewRect(Addr{Col: rng.IntN(45), Row: rng.IntN(80000)}, Addr{Col: rng.IntN(45), Row: rng.IntN(MaxRows)})59 checkRange(t, st, model, r)60 checkFilled(t, st, model, r)61 checkNextRow(t, st, model, rng.IntN(40), rng.IntN(MaxRows))62 }63}6465// checkRange compares inRange and bounds with the model.66func checkRange(t *testing.T, st *cellStore, model map[Addr]string, r Rect) {67 t.Helper()68 var want []Addr69 for a := range model {70 if r.Contains(a) {71 want = append(want, a)72 }73 }74 sortAddrs(want)75 var got []Addr76 for a := range st.inRange(r) {77 got = append(got, a)78 }79 if !slices.Equal(got, want) {80 t.Fatalf("inRange(%v): %d cells, want %d", r, len(got), len(want))81 }82 got = got[:0]83 for a := range st.anyKeysIn(r) {84 got = append(got, a)85 }86 sortAddrs(got)87 if !slices.Equal(got, want) {88 t.Fatalf("anyKeysIn(%v): %d cells, want %d", r, len(got), len(want))89 }90 b, ok := st.bounds(r)91 if ok != (len(want) > 0) {92 t.Fatalf("bounds(%v) found %v with %d cells", r, ok, len(want))93 }94 for _, a := range want {95 if !b.Contains(a) {96 t.Fatalf("bounds(%v) = %v misses %v", r, b, a)97 }98 }99 if ok && (!slices.ContainsFunc(want, func(a Addr) bool { return a.Row == b.From.Row }) ||100 !slices.ContainsFunc(want, func(a Addr) bool { return a.Col == b.To.Col })) {101 t.Fatalf("bounds(%v) = %v isn't tight", r, b)102 }103}104105// checkFilled compares the index of cells with contents with the model.106func checkFilled(t *testing.T, st *cellStore, model map[Addr]string, r Rect) {107 t.Helper()108 var filled []Addr109 for a, input := range model {110 if r.Contains(a) && input != "" {111 filled = append(filled, a)112 }113 if st.filled.has(a) != (input != "") {114 t.Fatalf("filled index says %v for %v", st.filled.has(a), a)115 }116 }117 fb, ok := st.filled.bounds(r)118 if ok != (len(filled) > 0) || ok && !slices.ContainsFunc(filled, func(a Addr) bool { return a.Row == fb.To.Row }) {119 t.Fatalf("filled bounds(%v) = %v %v", r, fb, ok)120 }121}122123// checkNextRow compares nextRow both ways with the model.124func checkNextRow(t *testing.T, st *cellStore, model map[Addr]string, c, row int) {125 t.Helper()126 next, ok := st.stored.nextRow(c, row, 1)127 prev, okp := st.stored.nextRow(c, row, -1)128 want1, ok1 := MaxRows, false129 want2, ok2 := -1, false130 for a := range model {131 if a.Col != c {132 continue133 }134 if a.Row >= row && a.Row < want1 {135 want1, ok1 = a.Row, true136 }137 if a.Row <= row && a.Row > want2 {138 want2, ok2 = a.Row, true139 }140 }141 if ok != ok1 || ok && next != want1 || okp != ok2 || okp && prev != want2 {142 t.Fatalf("nextRow(%d, %d) = %d %v / %d %v, want %d %v / %d %v", c, row, next, ok, prev, okp, want1, ok1, want2, ok2)143 }144}145146// Cells set and deleted while iterating follow a map's rules: a cell147// deleted before it is reached isn't yielded, and every cell stored148// throughout is yielded once.149func TestCellStoreChangeWhileIterating(t *testing.T) {150 st := newCellStore()151 for row := range 3000 {152 for col := range 3 {153 st.set(Addr{Col: col, Row: row}, &Cell{Input: "x"})154 }155 }156 seen := map[Addr]bool{}157 for a := range st.keysIn(NewRect(Addr{}, Addr{Col: 2, Row: 2999})) {158 if seen[a] {159 t.Fatalf("%v yielded twice", a)160 }161 seen[a] = true162 if a.Col == 0 {163 st.delete(Addr{Col: 2, Row: a.Row}) // not yet reached164 st.delete(Addr{Col: 1, Row: a.Row + 1}) // nor this165 }166 }167 for a := range seen {168 if !st.has(a) {169 t.Fatalf("deleted %v was yielded", a)170 }171 }172 if len(seen) != st.len() {173 t.Errorf("%d yielded, %d stored", len(seen), st.len())174 }175}