oficina / sheets Public

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

Plain git — no account needed to clone.

Download

Download this file

1package sheets
2
3import (
4 "fmt"
5 "net/http"
6 "net/http/httptest"
7 "testing"
8)
9
10// versionFollower mirrors native SheetModel.accept (native/apple/Sources/
11// SheetsKit/SheetModel.swift), the one client that detects missed edits
12// from the version: drop anything older than what it holds, refetch on
13// resync or when the version moved further than the response accounts
14// for, otherwise apply. Change the two together.
15type versionFollower struct {
16 have uint64
17 refetches int
18}
19
20func (f *versionFollower) missed(g gridResponse) bool {
21 allowed := uint64(max(g.Stamps, 1))
22 return g.Resync || g.Version > f.have+allowed
23}
24
25// accept returns whether the response made the follower refetch.
26func (f *versionFollower) accept(t *testing.T, g gridResponse, refetch func() gridResponse) bool {
27 t.Helper()
28 if g.Version < f.have {
29 return false
30 }
31 if f.missed(g) {
32 f.refetches++
33 f.have = refetch().Version
34 return true
35 }
36 f.have = g.Version
37 return false
38}
39
40// B12 and its review (#35): replay REAL server responses — cell writes,
41// batches (with a derived formula cell), a format, a size, snapshots —
42// through the native rule. With nothing missed, nothing may refetch: an
43// Open used to take a stamp every time, and the handle is evicted between
44// requests, so sequential writes jumped by two and every other one
45// refetched. With one write missed, the next response of every kind must
46// refetch, including a format, whose N cells consume no stamp.
47func TestNativeVersionContractAgainstRealResponses(t *testing.T) {
48 app, cookies := signIn(t)
49 s, cells := newSheet(t, app, cookies, "Budget")
50 snapshot := func() gridResponse {
51 t.Helper()
52 rec := do(t, app, as(httptest.NewRequest(http.MethodGet, cells+"?rows=20&cols=10", nil), cookies))
53 if rec.Code != http.StatusOK {
54 t.Fatalf("GET cells = %d: %s", rec.Code, rec.Body.String())
55 }
56 return decodeGrid(t, rec)
57 }
58 ok := func(rec *httptest.ResponseRecorder) gridResponse {
59 t.Helper()
60 if rec.Code != http.StatusOK {
61 t.Fatalf("write = %d: %s", rec.Code, rec.Body.String())
62 }
63 return decodeGrid(t, rec)
64 }
65 cell := func(r, c int, raw string) gridResponse {
66 return ok(postCell(t, app, cookies, cells, fmt.Sprintf(`{"r":%d,"c":%d,"raw":%q}`, r, c, raw)))
67 }
68 batch := func(body string) gridResponse { return ok(postCell(t, app, cookies, cells+"/batch", body)) }
69 format := func(body string) gridResponse { return ok(postFormat(t, app, cookies, s, body)) }
70 size := func(body string) gridResponse { return ok(postSize(t, app, cookies, s, body)) }
71
72 t.Run("nothing missed, nothing refetched", func(t *testing.T) {
73 f := &versionFollower{have: snapshot().Version}
74 steps := []struct {
75 name string
76 resp func() gridResponse
77 }{
78 {"cell", func() gridResponse { return cell(0, 0, "1") }},
79 {"cell", func() gridResponse { return cell(1, 0, "2") }},
80 {"cell", func() gridResponse { return cell(2, 0, "3") }},
81 {"batch of 3 with a derived formula", func() gridResponse {
82 return batch(`{"cells":[{"r":3,"c":0,"raw":"=SUM(A1:A3)"},{"r":0,"c":1,"raw":"b"},{"r":0,"c":0,"raw":"10"}]}`)
83 }},
84 {"format over 6 cells", func() gridResponse { return format(`{"r1":0,"c1":0,"r2":1,"c2":2,"bold":true}`) }},
85 {"cell after format", func() gridResponse { return cell(1, 1, "x") }},
86 {"size", func() gridResponse { return size(`{"col":2,"size":220}`) }},
87 {"batch of 2", func() gridResponse { return batch(`{"cells":[{"r":5,"c":0,"raw":"5"},{"r":6,"c":0,"raw":"6"}]}`) }},
88 {"clear", func() gridResponse { return cell(5, 0, "") }},
89 {"snapshot", snapshot},
90 {"cell after snapshot", func() gridResponse { return cell(7, 0, "7") }},
91 {"batch of 4", func() gridResponse {
92 return batch(`{"cells":[{"r":8,"c":0,"raw":"8"},{"r":8,"c":1,"raw":"8"},{"r":8,"c":2,"raw":"8"},{"r":8,"c":3,"raw":"8"}]}`)
93 }},
94 }
95 for _, step := range steps {
96 before := f.have
97 g := step.resp()
98 if f.accept(t, g, snapshot) {
99 t.Errorf("%s: refetched with nothing missed (held %d, got version %d, stamps %d)",
100 step.name, before, g.Version, g.Stamps)
101 }
102 }
103 })
104
105 t.Run("one write missed, the next response refetches", func(t *testing.T) {
106 for _, next := range []struct {
107 name string
108 resp func() gridResponse
109 }{
110 {"cell", func() gridResponse { return cell(10, 0, "n") }},
111 {"batch", func() gridResponse { return batch(`{"cells":[{"r":10,"c":1,"raw":"n"},{"r":10,"c":2,"raw":"n"}]}`) }},
112 {"format over 20 cells", func() gridResponse { return format(`{"r1":11,"c1":0,"r2":14,"c2":4,"italic":true}`) }},
113 {"size", func() gridResponse { return size(`{"col":3,"size":140}`) }},
114 } {
115 // Hold the version of a write this follower saw, then miss one.
116 f := &versionFollower{have: cell(9, 0, "seen").Version}
117 cell(9, 1, "missed")
118 g := next.resp()
119 if !f.missed(g) {
120 t.Errorf("%s after a missed write was applied, not refetched (held %d, got version %d, stamps %d)",
121 next.name, f.have, g.Version, g.Stamps)
122 }
123 }
124 })
125}
126