1package sheet23import (4 "strconv"5 "testing"6)78// fillColumn writes v into rows 0..n-1 of column A as one step.9func fillColumn(t *testing.T, s *Sheet, n int, v string) {10 t.Helper()11 col := NewRect(Addr{}, Addr{Row: n - 1})12 if err := s.FillEntry(col, col.From, v); err != nil {13 t.Fatal(err)14 }15}1617// The history drops its oldest steps once it holds more than the byte18// budget, keeps a running total that matches its steps, and always keeps19// the newest step.20func TestUndoByteBudget(t *testing.T) {21 s := New()22 fillColumn(t, s, 1000, "x")23 w := s.Book()24 w.ClearHistory()25 fillColumn(t, s, 1000, "0") // before-images of 1000 cells, slots and one string26 one := w.HistoryBytes()27 if one < 1000*slotBytes || one > 1000*slotBytes+8<<10 {28 t.Fatalf("a 1000-cell step is estimated at %d bytes", one)29 }30 defer func(b int64) { undoBudget = b }(undoBudget)31 undoBudget = 5 * one3233 for i := 1; i < 20; i++ {34 fillColumn(t, s, 1000, strconv.Itoa(i))35 }36 if w.HistoryBytes() > undoBudget {37 t.Errorf("history holds %d bytes, over the budget of %d", w.HistoryBytes(), undoBudget)38 }39 if got := historySum(w); got != w.HistoryBytes() {40 t.Errorf("running total %d, steps add up to %d", w.HistoryBytes(), got)41 }42 n := 043 for s.CanUndo() {44 s.Undo()45 n++46 }47 if n < 3 || n > 5 {48 t.Errorf("kept %d steps under a budget of about 5", n)49 }50 // The oldest steps went first: undoing everything kept lands on the51 // value written n steps before the last.52 if want := strconv.Itoa(19 - n); inputs(s)["A1"] != want {53 t.Errorf("after undoing all: A1 %q, want %q", inputs(s)["A1"], want)54 }55 if w.HistoryBytes() != 0 {56 t.Errorf("empty undo stack holds %d bytes", w.HistoryBytes())57 }58 // Redoing brings the steps back onto the undo stack, counted again.59 for s.CanRedo() {60 s.Redo()61 }62 if got := historySum(w); got != w.HistoryBytes() || got == 0 || got > undoBudget {63 t.Errorf("after redo: running total %d, steps %d", w.HistoryBytes(), got)64 }6566 // A single step bigger than the budget is still kept.67 undoBudget = one / 268 fillColumn(t, s, 1000, "big")69 if !s.CanUndo() {70 t.Fatal("the newest step was dropped")71 }72 s.Undo()73 if s.CanUndo() {74 t.Error("older steps survived a budget smaller than one step")75 }76 if inputs(s)["A1"] != "19" {77 t.Errorf("undo big: A1 %q", inputs(s)["A1"])78 }79}8081// Formulas are estimated larger than numbers: their parsed trees stay82// alive in the before-image.83func TestUndoBytesFormulas(t *testing.T) {84 nums, forms := New(), New()85 fillColumn(t, nums, 100, "1")86 fillColumn(t, forms, 100, "=SUM(B1:B10)*2")87 nums.Book().ClearHistory()88 forms.Book().ClearHistory()89 fillColumn(t, nums, 100, "")90 fillColumn(t, forms, 100, "")91 if n, f := nums.Book().HistoryBytes(), forms.Book().HistoryBytes(); f < n+100*formulaBytes {92 t.Errorf("clearing 100 numbers %d bytes, 100 formulas %d", n, f)93 }94}9596// A deleted sheet keeps its cells for undo, and the step that deleted it97// counts them.98func TestUndoBytesCountDeletedSheet(t *testing.T) {99 s := New()100 w := s.Book()101 data, err := w.AddSheet("Data", 1)102 if err != nil {103 t.Fatal(err)104 }105 for i := range 500 {106 data.Set(Addr{Row: i}, "x")107 }108 w.ClearHistory()109 if err := w.DeleteSheet(data); err != nil {110 t.Fatal(err)111 }112 if got := w.HistoryBytes(); got < 500*slotBytes {113 t.Errorf("deleting a 500-cell sheet is estimated at %d bytes", got)114 }115}116117// historySum adds up the undo steps' sizes, checking each step's118// running count of its cells against a walk over them.119func historySum(w *Workbook) int64 {120 var n int64121 for _, st := range w.hist.undo {122 var cells int64123 for _, img := range st.cells {124 cells += img.walkSize()125 }126 if cells != st.cellBytes {127 return -1128 }129 n += st.bytes130 }131 return n132}133134// walkSize is what keep counts for the image, found by walking it.135func (img *image) walkSize() int64 {136 t := img.tables()137 n := int64(imageBytes + t.stored.n*slotBytes)138 for _, ic := range img.small {139 if !ic.blank {140 n += slotBytes141 }142 }143 n += int64(t.stored.blocks()+img.blank.blocks()) * blockBytes144 for _, rc := range t.rich {145 n += richBytes + cellSize(rc.c)146 }147 for _, s := range t.strs.strs[min(1, len(t.strs.strs)):] {148 n += strBytes + int64(len(s))149 }150 return n151}