| 1 | package store_test |
| 2 | |
| 3 | import ( |
| 4 | "math" |
| 5 | "strconv" |
| 6 | "testing" |
| 7 | |
| 8 | "amadan.net/oficina/sheets/internal/sheets/store" |
| 9 | ) |
| 10 | |
| 11 | func TestVersionWatermarkNeverLowersAndRequiresTab(t *testing.T) { |
| 12 | db, _, id := seeded(t) |
| 13 | if err := store.AdvanceContentVersionForTest(db, id, 100); err != nil { |
| 14 | t.Fatal(err) |
| 15 | } |
| 16 | if err := store.AdvanceContentVersionForTest(db, id, 4); err != nil { |
| 17 | t.Fatal(err) |
| 18 | } |
| 19 | var mark uint64 |
| 20 | if err := db.Raw("SELECT content_version FROM tabs WHERE id = ?", id).Scan(&mark).Error; err != nil { |
| 21 | t.Fatal(err) |
| 22 | } |
| 23 | if mark != 100 { |
| 24 | t.Fatalf("watermark lowered to %d", mark) |
| 25 | } |
| 26 | if err := store.AdvanceContentVersionForTest(db, id+1000, 4); err == nil { |
| 27 | t.Fatal("missing tab accepted") |
| 28 | } |
| 29 | } |
| 30 | |
| 31 | func TestOpenRequiresTabVersionRow(t *testing.T) { |
| 32 | db, s, id := seeded(t) |
| 33 | h, err := store.Open(db, s, id+1000) |
| 34 | if err == nil { |
| 35 | h.Release() |
| 36 | t.Fatal("missing tab opened") |
| 37 | } |
| 38 | } |
| 39 | |
| 40 | func TestMissingTabRefusesRetainedHandleWrite(t *testing.T) { |
| 41 | db, s, id := seeded(t) |
| 42 | h, err := store.Open(db, s, id) |
| 43 | if err != nil { |
| 44 | t.Fatal(err) |
| 45 | } |
| 46 | defer h.Release() |
| 47 | if err = db.Delete(&store.Tab{}, id).Error; err != nil { |
| 48 | t.Fatal(err) |
| 49 | } |
| 50 | if _, err = h.SetReport(rowA, colA, "9", actor); err == nil { |
| 51 | t.Fatal("write accepted without version row") |
| 52 | } |
| 53 | if h.Value(rowA, colA).Num != 5 { |
| 54 | t.Fatal("failed write changed graph") |
| 55 | } |
| 56 | } |
| 57 | |
| 58 | func TestHighWatermarkSurvivesEmptyTabOpen(t *testing.T) { |
| 59 | db, s, id := seeded(t) |
| 60 | if err := db.Where("tab_id = ?", id).Delete(&store.Cell{}).Error; err != nil { |
| 61 | t.Fatal(err) |
| 62 | } |
| 63 | const high = math.MaxInt64 - 10 |
| 64 | if err := store.AdvanceContentVersionForTest(db, id, high); err != nil { |
| 65 | t.Fatal(err) |
| 66 | } |
| 67 | h, err := store.Open(db, s, id) |
| 68 | if err != nil { |
| 69 | t.Fatal(err) |
| 70 | } |
| 71 | defer h.Release() |
| 72 | rep, err := h.SetReport(rowA, colA, "9", actor) |
| 73 | if err != nil { |
| 74 | t.Fatal(err) |
| 75 | } |
| 76 | if rep.Version <= high { |
| 77 | t.Fatalf("high watermark lost: %d", rep.Version) |
| 78 | } |
| 79 | } |
| 80 | |
| 81 | func TestVersionExhaustionRefusesWithoutMutation(t *testing.T) { |
| 82 | for _, mark := range []int64{math.MaxInt64, math.MaxInt64 - 1, -1} { |
| 83 | t.Run(strconv.FormatInt(mark, 10), func(t *testing.T) { |
| 84 | db, s, id := seeded(t) |
| 85 | if err := db.Model(&store.Tab{}).Where("id = ?", id).Update("content_version", mark).Error; err != nil { |
| 86 | t.Fatal(err) |
| 87 | } |
| 88 | h, err := store.Open(db, s, id) |
| 89 | if err == nil { |
| 90 | h.Release() |
| 91 | t.Fatal("invalid watermark opened") |
| 92 | } |
| 93 | }) |
| 94 | } |
| 95 | for _, batch := range []bool{false, true} { |
| 96 | t.Run(map[bool]string{false: "single", true: "batch"}[batch], func(t *testing.T) { |
| 97 | db, s, id := seeded(t) |
| 98 | h, err := store.Open(db, s, id) |
| 99 | if err != nil { |
| 100 | t.Fatal(err) |
| 101 | } |
| 102 | defer h.Release() |
| 103 | var before store.Tab |
| 104 | if err = db.First(&before, id).Error; err != nil { |
| 105 | t.Fatal(err) |
| 106 | } |
| 107 | store.SetNextVersionForTest(h, math.MaxInt64) |
| 108 | if batch { |
| 109 | _, err = h.SetBatch([]store.CellWrite{{Row: 0, Col: 0, Raw: "9"}}, actor) |
| 110 | } else { |
| 111 | _, err = h.SetReport(rowA, colA, "9", actor) |
| 112 | } |
| 113 | if err == nil { |
| 114 | t.Fatal("exhausted allocation accepted") |
| 115 | } |
| 116 | var after store.Tab |
| 117 | if err = db.First(&after, id).Error; err != nil { |
| 118 | t.Fatal(err) |
| 119 | } |
| 120 | if after.ContentVersion != before.ContentVersion || h.Value(rowA, colA).Num != 5 { |
| 121 | t.Fatal("exhausted write mutated state") |
| 122 | } |
| 123 | }) |
| 124 | } |
| 125 | } |
| 126 | |
| 127 | func TestFormulaRecoveryVersionSurvivesClearingNewestCaches(t *testing.T) { |
| 128 | db, s, id := seeded(t) |
| 129 | h, err := store.Open(db, s, id) |
| 130 | if err != nil { |
| 131 | t.Fatal(err) |
| 132 | } |
| 133 | if err = h.Set(rowB, colA, "=A1*2", actor); err != nil { |
| 134 | t.Fatal(err) |
| 135 | } |
| 136 | if err = h.Set(rowA, colA, "8", actor); err != nil { |
| 137 | t.Fatal(err) |
| 138 | } |
| 139 | if h.Value(rowB, colA).Num != 16 { |
| 140 | t.Fatal("formula did not recalculate") |
| 141 | } |
| 142 | h.Release() |
| 143 | if err = db.Model(&store.Cell{}).Where("tab_id = ? AND row_id = ?", id, rowB).Update("cache_version", 0).Error; err != nil { |
| 144 | t.Fatal(err) |
| 145 | } |
| 146 | h, err = store.Open(db, s, id) |
| 147 | if err != nil { |
| 148 | t.Fatal(err) |
| 149 | } |
| 150 | var tab store.Tab |
| 151 | if err = db.First(&tab, id).Error; err != nil { |
| 152 | t.Fatal(err) |
| 153 | } |
| 154 | var cell store.Cell |
| 155 | if err = db.Where("tab_id = ? AND row_id = ?", id, rowB).First(&cell).Error; err != nil { |
| 156 | t.Fatal(err) |
| 157 | } |
| 158 | if tab.ContentVersion < cell.CacheVersion { |
| 159 | t.Fatal("recovery stamp not retained") |
| 160 | } |
| 161 | if err = h.Set(rowA, colA, "", actor); err != nil { |
| 162 | t.Fatal(err) |
| 163 | } |
| 164 | if h.Value(rowB, colA).Num != 0 { |
| 165 | t.Fatal("blank feeder did not recalculate") |
| 166 | } |
| 167 | last, err := h.SetReport(rowB, colA, "", actor) |
| 168 | if err != nil { |
| 169 | t.Fatal(err) |
| 170 | } |
| 171 | h.Release() |
| 172 | h, err = store.Open(db, s, id) |
| 173 | if err != nil { |
| 174 | t.Fatal(err) |
| 175 | } |
| 176 | defer h.Release() |
| 177 | next, err := h.SetReport(rowA, colA, "9", actor) |
| 178 | if err != nil { |
| 179 | t.Fatal(err) |
| 180 | } |
| 181 | if next.Version <= last.Version { |
| 182 | t.Fatal("deleting recovered cells lost watermark") |
| 183 | } |
| 184 | } |
| 185 | |