oficina / slides Public

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

Plain git — no account needed to clone.

Download

Download this file

1package core_test
2
3import (
4 "bytes"
5 "context"
6 "encoding/json"
7 "fmt"
8 "os"
9 "path/filepath"
10 "sort"
11 "strings"
12 "testing"
13
14 "amadan.net/oficina/slides/internal/slides/core"
15)
16
17// The contract between the browser and the core (spec §2.8): for every
18// vector fixtures/generate.mjs wrote, the core's view of the document the
19// JS side built must equal the JS side's own view, and the core's own
20// Ops applied to the same batch must arrive at that view as well. A
21// difference here is a bug in one implementation or the other, never
22// something to fix by editing a vector.
23
24const vectorsDir = "../../../fixtures/vectors"
25const dataDir = "../../../fixtures/data"
26
27func vectorNames(t *testing.T) []string {
28 t.Helper()
29 entries, err := filepath.Glob(filepath.Join(vectorsDir, "*.yjs"))
30 if err != nil {
31 t.Fatalf("glob: %v", err)
32 }
33 if len(entries) == 0 {
34 t.Fatalf("no vectors under %s: run node generate.mjs in fixtures/", vectorsDir)
35 }
36 names := make([]string, 0, len(entries))
37 for _, e := range entries {
38 names = append(names, strings.TrimSuffix(filepath.Base(e), ".yjs"))
39 }
40 sort.Strings(names)
41 return names
42}
43
44// canonical parses JSON and re-encodes it with sorted keys and one entry
45// per line, so two views compare as values and a mismatch reads as a diff.
46func canonical(t *testing.T, raw []byte) string {
47 t.Helper()
48 var v any
49 if err := json.Unmarshal(raw, &v); err != nil {
50 t.Fatalf("not JSON: %v\n%s", err, raw)
51 }
52 var b strings.Builder
53 writeCanonical(&b, v, "")
54 return b.String()
55}
56
57func writeCanonical(b *strings.Builder, v any, indent string) {
58 switch x := v.(type) {
59 case map[string]any:
60 keys := make([]string, 0, len(x))
61 for k := range x {
62 keys = append(keys, k)
63 }
64 sort.Strings(keys)
65 b.WriteString("{\n")
66 for _, k := range keys {
67 fmt.Fprintf(b, "%s %q: ", indent, k)
68 writeCanonical(b, x[k], indent+" ")
69 b.WriteString("\n")
70 }
71 b.WriteString(indent + "}")
72 case []any:
73 b.WriteString("[\n")
74 for _, item := range x {
75 b.WriteString(indent + " ")
76 writeCanonical(b, item, indent+" ")
77 b.WriteString("\n")
78 }
79 b.WriteString(indent + "]")
80 default:
81 enc, _ := json.Marshal(x)
82 b.Write(enc)
83 }
84}
85
86// diffLines reports the first lines where want and got differ, with a
87// little context, so a failing vector says where rather than dumping
88// two whole views.
89func diffLines(want, got string) string {
90 w := strings.Split(want, "\n")
91 g := strings.Split(got, "\n")
92 i := 0
93 for i < len(w) && i < len(g) && w[i] == g[i] {
94 i++
95 }
96 from := i - 3
97 if from < 0 {
98 from = 0
99 }
100 var b strings.Builder
101 fmt.Fprintf(&b, "first difference at line %d\n", i+1)
102 for j := from; j < i; j++ {
103 fmt.Fprintf(&b, " %s\n", w[j])
104 }
105 for j := i; j < i+6 && j < len(w); j++ {
106 fmt.Fprintf(&b, "- %s\n", w[j])
107 }
108 for j := i; j < i+6 && j < len(g); j++ {
109 fmt.Fprintf(&b, "+ %s\n", g[j])
110 }
111 return b.String()
112}
113
114func wantView(t *testing.T, name string) string {
115 t.Helper()
116 raw, err := os.ReadFile(filepath.Join(vectorsDir, name+".view.json"))
117 if err != nil {
118 t.Fatalf("read view: %v", err)
119 }
120 return canonical(t, raw)
121}
122
123func assertView(t *testing.T, ctx context.Context, h *core.Handle, want string) {
124 t.Helper()
125 view, err := h.JSON(ctx)
126 if err != nil {
127 t.Fatalf("JSON: %v", err)
128 }
129 got := canonical(t, view)
130 if got != want {
131 t.Errorf("the core's view differs from the JS view (- want, + got):\n%s", diffLines(want, got))
132 }
133}
134
135// TestVectorsViewAsTheJSSideViewsThem: each .yjs update, applied to a
136// fresh document, views exactly as the JS side that wrote it says.
137func TestVectorsViewAsTheJSSideViewsThem(t *testing.T) {
138 ctx, rt := newRuntime(t)
139 for _, name := range vectorNames(t) {
140 t.Run(name, func(t *testing.T) {
141 update, err := os.ReadFile(filepath.Join(vectorsDir, name+".yjs"))
142 if err != nil {
143 t.Fatalf("read update: %v", err)
144 }
145 h := newDoc(t, ctx, rt, name, 1)
146 if err := h.Apply(ctx, update); err != nil {
147 t.Fatalf("Apply: %v", err)
148 }
149 assertView(t, ctx, h, wantView(t, name))
150 })
151 }
152}
153
154// TestVectorsOpsAgreeWithTheJSOps: the same batch through the core's own
155// Ops arrives at the same view, so the two implementations of the §7.2
156// vocabulary are one vocabulary.
157func TestVectorsOpsAgreeWithTheJSOps(t *testing.T) {
158 ctx, rt := newRuntime(t)
159 for _, name := range vectorNames(t) {
160 t.Run(name, func(t *testing.T) {
161 raw, err := os.ReadFile(filepath.Join(dataDir, name+".json"))
162 if err != nil {
163 t.Fatalf("read data: %v", err)
164 }
165 var data struct {
166 GUID string `json:"guid"`
167 Ops json.RawMessage `json:"ops"`
168 }
169 if err := json.Unmarshal(raw, &data); err != nil {
170 t.Fatalf("parse data: %v", err)
171 }
172 if data.GUID != name {
173 t.Fatalf("guid %q does not match the file name %q", data.GUID, name)
174 }
175 h := newDoc(t, ctx, rt, name, 1)
176 update, err := h.Ops(ctx, data.Ops)
177 if err != nil {
178 t.Fatalf("Ops: %v", err)
179 }
180 if len(bytes.TrimSpace(update)) == 0 {
181 t.Fatal("Ops produced an empty update")
182 }
183 assertView(t, ctx, h, wantView(t, name))
184 })
185 }
186}
187