cat / 012
clone

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

main
012 / internal / sheet / historyscale_test.go
historyscale_test.go 5.0K
1package sheet23import (4 "bytes"5 "fmt"6 "testing"7)89// scaleBook is a workbook of two sheets, the first holding rows rows of10// every kind of cell a before-image keeps: numbers as typed, text,11// formatted numbers, formulas, booleans, notes and formatting alone.12func scaleBook(t testing.TB, rows int) *Sheet {13 s := New()14 cur := Format{Kind: FmtCurrency, Decimals: 2}15 for r := range rows {16 n := r + 117 load := func(col int, in string, f Format, st Style) {18 if err := s.Load(Addr{Col: col, Row: r}, in, f, st); err != nil {19 t.Fatal(err)20 }21 }22 load(0, fmt.Sprintf("%d.50", r%1000), Format{}, Style{})23 load(1, fmt.Sprintf("item %d", r%500), Format{}, Style{Bold: r%3 == 0})24 load(2, fmt.Sprint(r*7%10000), cur, Style{})25 load(3, fmt.Sprintf("=A%d*2+C%d", n, n), Format{}, Style{})26 switch r % 4 {27 case 0:28 load(4, "TRUE", Format{}, Style{})29 case 1:30 load(4, "", Format{Kind: FmtPercent}, Style{Italic: true})31 }32 }33 for r := 0; r < rows; r += 100 {34 if err := s.SetNote(Addr{Col: 1, Row: r}, fmt.Sprintf("note %d", r)); err != nil {35 t.Fatal(err)36 }37 }38 other, err := s.Book().AddSheet("Other", 1)39 if err != nil {40 t.Fatal(err)41 }42 other.Set(Addr{}, "=SUM(Sheet1!A:A)")43 s.RecalcAll()44 s.ClearHistory()45 return s46}4748// saved is the workbook as its file, and the value of Other!A1, which49// reads every number of the first sheet.50func saved(t *testing.T, s *Sheet) string {51 t.Helper()52 var b bytes.Buffer53 if err := s.Book().Write(&b); err != nil {54 t.Fatal(err)55 }56 w := s.Book()57 return b.String() + fmt.Sprint(w.Sheet(w.Len()-1).Value(Addr{}))58}5960// Every whole-range operation over a large sheet undoes to the workbook61// as it was and redoes to the workbook as it made it, and its step is62// estimated at a slot or so a plain cell, not a Cell.63func TestUndoWholeRangeAtScale(t *testing.T) {64 const rows = 1000065 s := scaleBook(t, rows)66 w := s.Book()67 all := NewRect(Addr{}, Addr{Col: 4, Row: rows - 1})68 ops := []struct {69 name string70 op func(t *testing.T)71 }{72 {"clear", func(*testing.T) { s.EraseRange(all) }},73 {"format", func(*testing.T) { s.SetFormat(all, Format{Kind: FmtPercent, Decimals: 1}) }},74 {"style", func(*testing.T) { s.SetStyle(all, func(st *Style) { st.Underline = true }) }},75 {"clear formatting", func(*testing.T) { s.ClearFormatting(all) }},76 {"fill", func(t *testing.T) {77 if err := s.FillEntry(all, all.From, "7"); err != nil {78 t.Fatal(err)79 }80 }},81 {"insert rows", func(t *testing.T) {82 if err := s.InsertRows(10, 5); err != nil {83 t.Fatal(err)84 }85 }},86 {"delete rows", func(*testing.T) { s.DeleteRows(3, 100) }},87 {"insert columns", func(t *testing.T) {88 if err := s.InsertCols(1, 2); err != nil {89 t.Fatal(err)90 }91 }},92 {"delete columns", func(*testing.T) { s.DeleteCols(0, 1) }},93 {"sort", func(*testing.T) { s.SortRange(all, []SortKey{{Col: 2, Desc: true}}) }},94 {"replace sheet", func(t *testing.T) {95 src := New()96 src.Set(Addr{}, "42")97 if _, err := w.ReplaceSheet(s, src, "import"); err != nil {98 t.Fatal(err)99 }100 }},101 {"delete sheet", func(t *testing.T) {102 if err := w.DeleteSheet(s); err != nil {103 t.Fatal(err)104 }105 }},106 }107 for _, o := range ops {108 before := saved(t, s)109 o.op(t)110 after := saved(t, s)111 if after == before {112 t.Fatalf("%s changed nothing", o.name)113 }114 if n := w.HistoryBytes(); n > int64(rows*5)*(slotBytes+32)+int64(rows)*(richBytes+cellBytes+formulaBytes+perFormulaByte*16) {115 t.Errorf("%s: step estimated at %d bytes", o.name, n)116 }117 if _, ok := w.Undo(); !ok || saved(t, s) != before {118 t.Fatalf("%s: undo doesn't restore the workbook", o.name)119 }120 if _, ok := w.Redo(); !ok || saved(t, s) != after {121 t.Fatalf("%s: redo doesn't make it again", o.name)122 }123 w.Undo()124 w.ClearHistory()125 }126}127128// A step over a sheet of plain numbers holds about a slot a cell.129func TestUndoPlainCellsAsSlots(t *testing.T) {130 s := New()131 const n = 50000132 for r := range n {133 s.Load(Addr{Row: r}, fmt.Sprintf("%d.25", r), Format{}, Style{})134 }135 s.RecalcAll()136 r := NewRect(Addr{}, Addr{Row: n - 1})137 cost := s.UndoCost(r)138 s.EraseRange(r)139 got := s.Book().HistoryBytes()140 if got > n*(slotBytes+4) {141 t.Errorf("clearing %d numbers is estimated at %d bytes", n, got)142 }143 if cost < got*9/10 || cost > got*11/10 {144 t.Errorf("clearing %d numbers was to cost %d bytes, and took %d", n, cost, got)145 }146}147148// A change too large to undo runs without recording, forgets the history149// and still reads as a change to the workbook.150func TestWithoutUndo(t *testing.T) {151 s := New()152 s.Set(Addr{}, "1")153 w := s.Book()154 id := w.StateID()155 w.WithoutUndo(func() { s.Set(Addr{}, "2") })156 if w.CanUndo() || w.CanRedo() || w.HistoryBytes() != 0 {157 t.Error("the history outlived a change without undo")158 }159 if inputs(s)["A1"] != "2" || w.StateID() == id || w.StateID() == 0 {160 t.Errorf("A1 %q, state %d (was %d)", inputs(s)["A1"], w.StateID(), id)161 }162 s.Set(Addr{}, "3")163 if s.Undo(); inputs(s)["A1"] != "2" {164 t.Errorf("undo after: A1 %q", inputs(s)["A1"])165 }166 if cost := s.UndoCost(NewRect(Addr{}, Addr{Col: 9, Row: 9})); cost < slotBytes || cost > 1024 {167 t.Errorf("one cell's undo cost %d", cost)168 }169}