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 "path/filepath"
5 "testing"
6
7 "gorm.io/gorm"
8
9 "amadan.net/oficina/sheets/internal/sheets/seal"
10 "amadan.net/oficina/sheets/internal/sheets/store"
11)
12
13// emptyTab builds a real database with an empty tab — no rows, no
14// columns, no cells — the shape a brand-new tab that XLSX import is
15// about to populate actually has. Unlike seeded() (handle_test.go), this
16// deliberately seeds nothing beyond the Tab row itself, because
17// EnsureRow/EnsureCol exist to grow exactly this.
18func emptyTab(t *testing.T) (*gorm.DB, *seal.Sealer, uint64) {
19 t.Helper()
20 dir := t.TempDir()
21 db := openTestDB(t, filepath.Join(dir, "test.db"))
22 s, err := seal.New(make32())
23 if err != nil {
24 t.Fatalf("seal.New: %v", err)
25 }
26 const emptyTabID uint64 = 55
27 if err := db.Create(&store.Tab{ID: emptyTabID, SheetID: 1}).Error; err != nil {
28 t.Fatalf("seed tab: %v", err)
29 }
30 return db, s, emptyTabID
31}
32
33// EnsureRow/EnsureCol are the grid-growth primitive XLSX import needs:
34// a tab starts with no rows or columns at all, and cells arrive addressed
35// by spreadsheet position, not by a pre-existing stable id.
36func TestEnsureRowColCreatesAndIsIdempotent(t *testing.T) {
37 db, sealer, tabID := emptyTab(t)
38 h, err := store.Open(db, sealer, tabID)
39 if err != nil {
40 t.Fatalf("Open: %v", err)
41 }
42 defer h.Release()
43
44 r1, err := h.EnsureRow(4)
45 if err != nil {
46 t.Fatalf("EnsureRow(4): %v", err)
47 }
48 r2, err := h.EnsureRow(4)
49 if err != nil {
50 t.Fatalf("EnsureRow(4) again: %v", err)
51 }
52 if r1 != r2 {
53 t.Fatalf("EnsureRow(4) not idempotent: %d != %d", r1, r2)
54 }
55
56 c1, err := h.EnsureCol(2)
57 if err != nil {
58 t.Fatalf("EnsureCol(2): %v", err)
59 }
60 c2, err := h.EnsureCol(2)
61 if err != nil {
62 t.Fatalf("EnsureCol(2) again: %v", err)
63 }
64 if c1 != c2 {
65 t.Fatalf("EnsureCol(2) not idempotent: %d != %d", c1, c2)
66 }
67
68 // A cell can now be set at the row/col this just created — proving
69 // the new ids are wired into the handle's binder (cellKey), not just
70 // persisted rows nothing else knows about.
71 if err := h.Set(r1, c1, "42", actor); err != nil {
72 t.Fatalf("Set at newly-ensured coordinate: %v", err)
73 }
74 if got := h.Value(r1, c1); got.Kind != 1 || got.Num != 42 { // calc.KindNum == 1
75 t.Fatalf("Value at newly-ensured coordinate = %+v, want 42", got)
76 }
77}
78
79// A row created via EnsureRow survives a Release/re-Open — proving it
80// was actually persisted, not just cached in the live handle.
81func TestEnsureRowPersistsAcrossReopen(t *testing.T) {
82 db, sealer, tabID := emptyTab(t)
83 h, err := store.Open(db, sealer, tabID)
84 if err != nil {
85 t.Fatalf("Open: %v", err)
86 }
87 rowID, err := h.EnsureRow(0)
88 if err != nil {
89 t.Fatalf("EnsureRow: %v", err)
90 }
91 h.Release()
92
93 h2, err := store.Open(db, sealer, tabID)
94 if err != nil {
95 t.Fatalf("re-Open: %v", err)
96 }
97 defer h2.Release()
98
99 rows := h2.GridRows()
100 if len(rows) != 1 || rows[0].ID != rowID || rows[0].Pos != 0 {
101 t.Fatalf("GridRows after reopen = %+v, want one row {Pos:0 ID:%d}", rows, rowID)
102 }
103}
104
105// RawInput is what XLSX export needs to tell a formula cell from a
106// literal one and to re-emit the exact formula text a person entered,
107// not just its computed result.
108func TestRawInputReturnsWhatWasSet(t *testing.T) {
109 db, sealer, tabID := seeded(t)
110 h, err := store.Open(db, sealer, tabID)
111 if err != nil {
112 t.Fatalf("Open: %v", err)
113 }
114 defer h.Release()
115
116 if err := h.Set(rowB, colA, "=A1*2", actor); err != nil {
117 t.Fatalf("Set: %v", err)
118 }
119 raw, ok := h.RawInput(rowB, colA)
120 if !ok || raw != "=A1*2" {
121 t.Fatalf("RawInput(B1) = %q, %v, want \"=A1*2\", true", raw, ok)
122 }
123
124 // seeded() itself put "5" at (rowA, colA).
125 raw, ok = h.RawInput(rowA, colA)
126 if !ok || raw != "5" {
127 t.Fatalf("RawInput(A1) = %q, %v, want \"5\", true", raw, ok)
128 }
129
130 // A cell nothing ever touched has no raw input.
131 if _, ok := h.RawInput(rowB, colA+1); ok {
132 t.Fatalf("RawInput on an unknown coordinate reported ok=true")
133 }
134}
135
136// SetFormat interns by content: two cells given field-for-field identical
137// Format values share one Format row, not two — the whole point of
138// interning (models.go's comment on Format).
139func TestSetFormatInterns(t *testing.T) {
140 db, sealer, tabID := seeded(t)
141 h, err := store.Open(db, sealer, tabID)
142 if err != nil {
143 t.Fatalf("Open: %v", err)
144 }
145 defer h.Release()
146
147 rowC, err := h.EnsureRow(2)
148 if err != nil {
149 t.Fatalf("EnsureRow: %v", err)
150 }
151 if err := h.Set(rowC, colA, "7", actor); err != nil {
152 t.Fatalf("Set: %v", err)
153 }
154
155 want := store.Format{NumberFmt: "0.00", Bold: true}
156 if err := h.SetFormat(rowA, colA, want); err != nil {
157 t.Fatalf("SetFormat(A1): %v", err)
158 }
159 if err := h.SetFormat(rowC, colA, want); err != nil {
160 t.Fatalf("SetFormat(C1): %v", err)
161 }
162
163 fa := h.Format(rowA, colA)
164 fc := h.Format(rowC, colA)
165 if fa == nil || fc == nil {
166 t.Fatalf("Format returned nil: A1=%v C1=%v", fa, fc)
167 }
168 if fa.ID != fc.ID {
169 t.Fatalf("two identical formats got different ids: %d != %d — not interned", fa.ID, fc.ID)
170 }
171 if fa.NumberFmt != "0.00" || !fa.Bold {
172 t.Fatalf("Format fields did not round-trip: %+v", fa)
173 }
174}
175
176// A cell with no content at all can still carry a format — a formatted
177// but empty cell, e.g. a coloured header with nothing typed into it yet.
178// This is the one case models.go's "Cell is sparse: only non-empty cells
179// exist" comment does not cover: FormatID lives on Cell, so a format-only
180// cell needs a Cell row purely to carry it, sealed input empty.
181func TestSetFormatOnBlankCell(t *testing.T) {
182 db, sealer, tabID := emptyTab(t)
183 h, err := store.Open(db, sealer, tabID)
184 if err != nil {
185 t.Fatalf("Open: %v", err)
186 }
187 defer h.Release()
188
189 rowID, err := h.EnsureRow(0)
190 if err != nil {
191 t.Fatalf("EnsureRow: %v", err)
192 }
193 colID, err := h.EnsureCol(0)
194 if err != nil {
195 t.Fatalf("EnsureCol: %v", err)
196 }
197
198 if err := h.SetFormat(rowID, colID, store.Format{FillHex: "FFFF0000"}); err != nil {
199 t.Fatalf("SetFormat on blank cell: %v", err)
200 }
201
202 f := h.Format(rowID, colID)
203 if f == nil || f.FillHex != "FFFF0000" {
204 t.Fatalf("Format(blank cell) = %+v, want FillHex FFFF0000", f)
205 }
206 // It is still blank — no value.
207 if v := h.Value(rowID, colID); v.Kind != 0 { // calc.KindEmpty == 0
208 t.Fatalf("Value(formatted-but-blank cell) = %+v, want KindEmpty", v)
209 }
210}
211
212// Cells is the enumeration an exporter needs: exactly the tab's
213// non-default cells (content or format), not the full rows x cols cross
214// product, which would be quadratic for a sparse, scattered tab.
215func TestCellsListsContentAndFormatOnlyCells(t *testing.T) {
216 db, sealer, tabID := seeded(t)
217 h, err := store.Open(db, sealer, tabID)
218 if err != nil {
219 t.Fatalf("Open: %v", err)
220 }
221 defer h.Release()
222
223 // seeded() already put a cell at (rowA, colA).
224 rowFmt, err := h.EnsureRow(5)
225 if err != nil {
226 t.Fatalf("EnsureRow: %v", err)
227 }
228 colFmt, err := h.EnsureCol(5)
229 if err != nil {
230 t.Fatalf("EnsureCol: %v", err)
231 }
232 if err := h.SetFormat(rowFmt, colFmt, store.Format{Bold: true}); err != nil {
233 t.Fatalf("SetFormat: %v", err)
234 }
235
236 cells, err := h.Cells()
237 if err != nil {
238 t.Fatalf("Cells: %v", err)
239 }
240 want := map[store.CellCoord]bool{
241 {RowID: rowA, ColID: colA}: false,
242 {RowID: rowFmt, ColID: colFmt}: false,
243 }
244 if len(cells) != len(want) {
245 t.Fatalf("Cells() = %v, want exactly %d entries", cells, len(want))
246 }
247 for _, c := range cells {
248 if _, ok := want[c]; !ok {
249 t.Fatalf("Cells() returned unexpected coordinate %+v", c)
250 }
251 want[c] = true
252 }
253 for coord, seen := range want {
254 if !seen {
255 t.Fatalf("Cells() missing expected coordinate %+v", coord)
256 }
257 }
258}
259