oficina / sheets Public

Clone
git clone https://amadan.net/oficina/sheets

Plain git — no account needed to clone.

Download

Download this file

1package store_test
2
3import (
4 "amadan.net/oficina/sheets/internal/sheets/seal"
5 "amadan.net/oficina/sheets/internal/sheets/sealtest"
6 "errors"
7 "os"
8 "path/filepath"
9 "strconv"
10 "testing"
11 "time"
12
13 "amadan.net/oficina/sheets/internal/sheets/store"
14 "gorm.io/gorm"
15)
16
17func TestCellBatchFinalValuesAndDirectOrigins(t *testing.T) {
18 h := openSeeded(t)
19 before := h.Version()
20 rep, err := h.SetBatch([]store.CellWrite{{Row: 1, Col: 0, Raw: "=A1*2"}, {Row: 0, Col: 0, Raw: "21"}}, actor)
21 if err != nil {
22 t.Fatal(err)
23 }
24 if rep.Version <= before || h.Version() <= rep.Version {
25 t.Fatalf("versions %d %d %d", before, rep.Version, h.Version())
26 }
27 for _, c := range rep.Cells {
28 if c.Derived {
29 t.Fatalf("edited origin marked derived: %+v", c)
30 }
31 }
32 if got := findCell(t, rep, rowB, colA); got.Value.Num != 42 {
33 t.Fatalf("final formula = %+v", got)
34 }
35 rep, err = h.SetBatch([]store.CellWrite{{Row: 0, Col: 0, Raw: ""}}, actor)
36 if err != nil {
37 t.Fatal(err)
38 }
39 if c := findCell(t, rep, rowB, colA); c.Value.Num != 0 || !c.Derived {
40 t.Fatalf("clear dependent = %+v", c)
41 }
42}
43
44func TestCellBatchRollsBackStorageHistoryAxesAndLiveHandle(t *testing.T) {
45 db, sealer, id := seeded(t)
46 h, err := store.Open(db, sealer, id)
47 if err != nil {
48 t.Fatal(err)
49 }
50 defer h.Release()
51 other, err := store.Open(db, sealer, id)
52 if err != nil {
53 t.Fatal(err)
54 }
55 defer other.Release()
56 before := h.Version()
57 var history int64
58 db.Model(&store.Event{}).Count(&history)
59 calls := 0
60 if err := db.Callback().Create().Before("gorm:create").Register("batch-failure", func(tx *gorm.DB) {
61 if tx.Statement.Table == "events" {
62 calls++
63 if calls == 3 {
64 tx.AddError(errors.New("injected failure"))
65 }
66 }
67 }); err != nil {
68 t.Fatal(err)
69 }
70 defer db.Callback().Create().Remove("batch-failure")
71 _, err = h.SetBatch([]store.CellWrite{{Row: 0, Col: 0, Raw: "21"}, {Row: 30, Col: 40, Raw: "99"}}, actor)
72 if err == nil {
73 t.Fatal("injected persistence failure accepted")
74 }
75 if got := other.Value(rowA, colA).Num; got != 5 {
76 t.Fatalf("live graph changed: %v", got)
77 }
78 if h.Version() != before {
79 t.Fatal("failed batch advanced version")
80 }
81 var now int64
82 db.Model(&store.Event{}).Count(&now)
83 if now != history {
84 t.Fatalf("history %d -> %d", history, now)
85 }
86 db.Model(&store.Row{}).Where("tab_id = ? AND position = ?", id, 30).Count(&now)
87 if now != 0 {
88 t.Fatal("row survived rollback")
89 }
90 db.Model(&store.Col{}).Where("tab_id = ? AND position = ?", id, 40).Count(&now)
91 if now != 0 {
92 t.Fatal("column survived rollback")
93 }
94 db.Callback().Create().Remove("batch-failure")
95 rep, err := h.SetBatch([]store.CellWrite{{Row: 0, Col: 0, Raw: "8"}}, actor)
96 if err != nil {
97 t.Fatal(err)
98 }
99 if findCell(t, rep, rowA, colA).Value.Num != 8 || other.Value(rowA, colA).Num != 8 {
100 t.Fatal("live handle failed to recover")
101 }
102}
103
104func TestCellBatchValidatesAllInputsBeforeWriting(t *testing.T) {
105 for _, writes := range [][]store.CellWrite{
106 nil,
107 {{Row: 0, Col: 0, Raw: "7"}, {Row: 30, Col: 20, Raw: "=SUM(("}},
108 {{Row: 0, Col: 0, Raw: "7"}, {Row: 0, Col: 0, Raw: "8"}},
109 {{Row: -1, Col: 0, Raw: "7"}},
110 make([]store.CellWrite, store.MaxCellBatch+1),
111 } {
112 h := openSeeded(t)
113 v := h.Version()
114 if _, err := h.SetBatch(writes, actor); err == nil {
115 t.Fatal("invalid batch accepted")
116 }
117 if h.Value(rowA, colA).Num != 5 || h.Version() != v {
118 t.Fatal("invalid batch changed cell")
119 }
120 }
121}
122
123func TestCellBatchFiveHundredCells(t *testing.T) {
124 h := openSeeded(t)
125 writes := make([]store.CellWrite, store.MaxCellBatch)
126 for i := range writes {
127 writes[i] = store.CellWrite{Row: i, Col: 1, Raw: "=A1*2"}
128 }
129 start := time.Now()
130 rep, err := h.SetBatch(writes, actor)
131 if err != nil {
132 t.Fatal(err)
133 }
134 if len(rep.Cells) != store.MaxCellBatch {
135 t.Fatalf("got %d cells", len(rep.Cells))
136 }
137 for _, c := range rep.Cells {
138 if c.Value.Num != 10 {
139 t.Fatalf("value = %+v", c)
140 }
141 }
142 t.Logf("500-cell atomic batch: %s", time.Since(start))
143}
144
145func TestCellBatchKeepsCellsAndHistorySealed(t *testing.T) {
146 path := filepath.Join(t.TempDir(), "test.db")
147 db := openTestDB(t, path)
148 sealer, err := seal.New(make32())
149 if err != nil {
150 t.Fatal(err)
151 }
152 if err := db.Create(&store.Tab{ID: tabID, SheetID: 1}).Error; err != nil {
153 t.Fatal(err)
154 }
155 h, err := store.Open(db, sealer, tabID)
156 if err != nil {
157 t.Fatal(err)
158 }
159 defer h.Release()
160 if _, err := h.SetBatch([]store.CellWrite{{Row: 0, Col: 0, Raw: string(sealtest.Canaries[0])}, {Row: 1, Col: 0, Raw: "=A1"}}, actor); err != nil {
161 t.Fatal(err)
162 }
163 sealtest.AssertNoPlaintext(t, path)
164}
165
166func TestCellBatchRecoversStaleCacheInsideTransaction(t *testing.T) {
167 db, s, id := seeded(t)
168 h, err := store.Open(db, s, id)
169 if err != nil {
170 t.Fatal(err)
171 }
172 defer h.Release()
173 if err := h.Set(rowB, colA, "=A1*2", actor); err != nil {
174 t.Fatal(err)
175 }
176 sealed, err := s.Seal("cell.input", []byte("9"))
177 if err != nil {
178 t.Fatal(err)
179 }
180 if err := db.Model(&store.Cell{}).Where("tab_id = ? AND row_id = ? AND col_id = ?", id, rowA, colA).Updates(map[string]any{"input_sealed": sealed, "input_version": h.Version() + 1}).Error; err != nil {
181 t.Fatal(err)
182 }
183 done := make(chan error, 1)
184 go func() { _, err := h.SetBatch([]store.CellWrite{{Row: 0, Col: 1, Raw: "=A2"}}, actor); done <- err }()
185 select {
186 case err := <-done:
187 if err != nil {
188 t.Fatal(err)
189 }
190 case <-time.After(3 * time.Second):
191 panic("batch recovery deadlocked")
192 }
193 if h.Value(rowB, colA).Num != 18 {
194 t.Fatal("stale dependent not recovered")
195 }
196}
197
198func TestCellBatchLargeTabTiming(t *testing.T) {
199 raw := os.Getenv("CELL_BATCH_BENCH_N")
200 if raw == "" {
201 t.Skip("set CELL_BATCH_BENCH_N to measure full-tab rebuild")
202 }
203 n, err := strconv.Atoi(raw)
204 if err != nil || n < 501 || n > store.MaxCellsPerTab {
205 t.Fatal("invalid CELL_BATCH_BENCH_N")
206 }
207 db := openTestDB(t, filepath.Join(t.TempDir(), "test.db"))
208 s, err := seal.New(make32())
209 if err != nil {
210 t.Fatal(err)
211 }
212 id := tabID
213 seedBlankRecoveryFixture(t, db, s, id, rowA, colA, n-1)
214 h, err := store.Open(db, s, id)
215 if err != nil {
216 t.Fatal(err)
217 }
218 defer h.Release()
219 writes := make([]store.CellWrite, 500)
220 for i := range writes {
221 writes[i] = store.CellWrite{Row: i + 1, Col: 0, Raw: "8"}
222 }
223 start := time.Now()
224 _, err = h.SetBatch(writes, actor)
225 elapsed := time.Since(start)
226 if err != nil {
227 t.Fatal(err)
228 }
229 t.Logf("%d-cell tab, 500 writes with transaction rebuild: %s", n, elapsed)
230}
231
232func TestCellBatchRejectsMergedReportPastCalculationCap(t *testing.T) {
233 db := openTestDB(t, filepath.Join(t.TempDir(), "test.db"))
234 s, err := seal.New(make32())
235 if err != nil {
236 t.Fatal(err)
237 }
238 seedBlankRecoveryFixture(t, db, s, tabID, rowA, colA, 4999)
239 h, err := store.Open(db, s, tabID)
240 if err != nil {
241 t.Fatal(err)
242 }
243 defer h.Release()
244 _, err = h.SetBatch([]store.CellWrite{{Row: 0, Col: 0, Raw: "7"}, {Row: 0, Col: 1, Raw: "8"}}, actor)
245 if err == nil {
246 t.Fatal("oversized merged report accepted")
247 }
248 if h.Value(rowA, colA).Num != 5 {
249 t.Fatal("oversized report committed partial write")
250 }
251}
252