Migration This is my new public git source viewer. Source coming soon. Please contact with concerns/bugs. Contact me

internal/modules/structured_test.go

main · 6.5 KB · 274 lines raw

1 package modules
2
3 import (
4 "html/template"
5 "strings"
6 "testing"
7 )
8
9 // lines builds the Line slice annotateStructure expects, mirroring what
10 // blobMod produces before the structure pass runs.
11 func lines(raw string) []Line {
12 out := []Line{}
13 for i, t := range strings.Split(raw, "\n") {
14 out = append(out, Line{N: i + 1, Text: template.HTML(t)})
15 }
16 return out
17 }
18
19 const sampleYAML = `defaults: &defaults
20 adapter: postgres
21 pool: 5
22
23 development:
24 <<: *defaults
25 database: dev
26
27 test:
28 <<: *defaults
29 database: test
30 hosts:
31 - a
32 - b
33 `
34
35 func TestFoldRangesYAML(t *testing.T) {
36 ls := lines(sampleYAML)
37 annotateStructure(ls, []byte(sampleYAML), "yaml")
38
39 want := map[int]int{
40 1: 3, // defaults: block ends at "pool: 5", not the blank line after
41 5: 7, // development:
42 9: 14, // test:, stopping short of the empty line 15
43 12: 14, // hosts:
44 }
45 for _, l := range ls {
46 if got, ok := want[l.N]; ok {
47 if l.FoldTo != got {
48 t.Errorf("line %d: FoldTo = %d, want %d", l.N, l.FoldTo, got)
49 }
50 } else if l.FoldTo != 0 {
51 t.Errorf("line %d: FoldTo = %d, want none", l.N, l.FoldTo)
52 }
53 }
54 if ls[1].Depth != 1 {
55 t.Errorf("line 2 depth = %d, want 1", ls[1].Depth)
56 }
57 if ls[13].Depth != 2 {
58 t.Errorf("line 14 depth = %d, want 2", ls[13].Depth)
59 }
60 }
61
62 func TestKeysAndAnchors(t *testing.T) {
63 ls := lines(sampleYAML)
64 annotateStructure(ls, []byte(sampleYAML), "yaml")
65
66 if ls[0].Key != "defaults" {
67 t.Errorf("line 1 key = %q, want %q", ls[0].Key, "defaults")
68 }
69 if ls[13].Key != "" {
70 t.Errorf("line 14 (sequence item) key = %q, want none", ls[13].Key)
71 }
72
73 // The anchor is reported on the line the &token is written on, not on
74 // the line the mapping it names begins.
75 a := ls[0].Anchor
76 if a == nil {
77 t.Fatal("line 1: no anchor")
78 }
79 if a.Name != "defaults" || a.Uses != 2 {
80 t.Errorf("anchor = %+v, want {defaults 2}", *a)
81 }
82
83 for _, n := range []int{6, 10} {
84 r := ls[n-1].Ref
85 if r == nil {
86 t.Fatalf("line %d: no ref", n)
87 }
88 if r.Name != "defaults" || r.Line != 1 {
89 t.Errorf("line %d ref = %+v, want defaults@1", n, *r)
90 }
91 if !strings.Contains(r.Preview, "adapter: postgres") {
92 t.Errorf("line %d preview = %q, want the resolved mapping", n, r.Preview)
93 }
94 }
95 }
96
97 // Anchors defined in the second document of a stream are still resolved:
98 // yaml.Unmarshal would only ever see the first.
99 func TestMultiDocumentAnchors(t *testing.T) {
100 const src = `---
101 kind: First
102 ---
103 base: &b
104 x: 1
105 copy: *b
106 `
107 ls := lines(src)
108 annotateStructure(ls, []byte(src), "yaml")
109
110 if ls[3].Anchor == nil || ls[3].Anchor.Name != "b" {
111 t.Fatalf("line 4 anchor = %+v, want b", ls[3].Anchor)
112 }
113 if r := ls[5].Ref; r == nil || r.Line != 4 {
114 t.Fatalf("line 6 ref = %+v, want b@4", r)
115 }
116 }
117
118 func TestFoldRangesJSON(t *testing.T) {
119 const src = `{
120 "name": "gsg",
121 "deps": {
122 "a": 1
123 },
124 "list": [
125 2
126 ]
127 }`
128 ls := lines(src)
129 annotateStructure(ls, []byte(src), "json")
130
131 if ls[0].FoldTo != 8 {
132 t.Errorf("line 1 FoldTo = %d, want 8", ls[0].FoldTo)
133 }
134 if ls[2].FoldTo != 4 {
135 t.Errorf("line 3 FoldTo = %d, want 4", ls[2].FoldTo)
136 }
137 if ls[5].FoldTo != 7 {
138 t.Errorf("line 6 FoldTo = %d, want 7", ls[5].FoldTo)
139 }
140 if ls[1].Key != "name" {
141 t.Errorf("line 2 key = %q, want %q", ls[1].Key, "name")
142 }
143 // No anchors outside YAML, and a brace inside a string is not structure.
144 if ls[1].Anchor != nil || ls[1].Ref != nil {
145 t.Error("JSON line carries anchor/ref data")
146 }
147 }
148
149 // Minified JSON has no indentation to fold on; it must degrade to a plain
150 // view rather than producing bogus ranges.
151 func TestMinifiedJSONHasNoFolds(t *testing.T) {
152 const src = `{"a":{"b":[1,2]},"c":3}`
153 ls := lines(src)
154 annotateStructure(ls, []byte(src), "json")
155 if ls[0].FoldTo != 0 {
156 t.Errorf("FoldTo = %d, want 0", ls[0].FoldTo)
157 }
158 }
159
160 // Nested arrays of objects are the shape a path query like
161 // obj1.property3.items[4].val is written against.
162 func TestJSONPaths(t *testing.T) {
163 const src = `{
164 "obj1": {
165 "property3": {
166 "items": [
167 {
168 "val": 0
169 },
170 {
171 "val": 1
172 }
173 ]
174 }
175 }
176 }`
177 ls := lines(src)
178 annotateStructure(ls, []byte(src), "json")
179
180 want := map[int]string{
181 2: "obj1",
182 3: "obj1.property3",
183 4: "obj1.property3.items",
184 5: "obj1.property3.items[0]",
185 6: "obj1.property3.items[0].val",
186 8: "obj1.property3.items[1]",
187 9: "obj1.property3.items[1].val",
188 }
189 for _, l := range ls {
190 if got, ok := want[l.N]; ok {
191 if l.Path != got {
192 t.Errorf("line %d Path = %q, want %q", l.N, l.Path, got)
193 }
194 } else if l.Path != "" {
195 t.Errorf("line %d Path = %q, want none", l.N, l.Path)
196 }
197 }
198 }
199
200 // A flow-style row carries several paths at once; the deepest is kept, so
201 // a query naming the final member still resolves. The shallow ones stay
202 // reachable when a container query prefix-matches the deep path.
203 func TestJSONFlowPathKeepsDeepest(t *testing.T) {
204 const src = `{"a": {"b": 1}}`
205 ls := lines(src)
206 annotateStructure(ls, []byte(src), "json")
207 if ls[0].Path != "a.b" {
208 t.Errorf("Path = %q, want a.b", ls[0].Path)
209 }
210 }
211
212 func TestYAMLPaths(t *testing.T) {
213 ls := lines(sampleYAML)
214 annotateStructure(ls, []byte(sampleYAML), "yaml")
215
216 want := map[int]string{
217 1: "defaults",
218 2: "defaults.adapter",
219 3: "defaults.pool",
220 5: "development",
221 6: "development", // merge key keeps the parent path
222 7: "development.database",
223 9: "test",
224 10: "test",
225 11: "test.database",
226 12: "test.hosts",
227 13: "test.hosts[0]",
228 14: "test.hosts[1]",
229 }
230 for _, l := range ls {
231 if got, ok := want[l.N]; ok {
232 if l.Path != got {
233 t.Errorf("line %d Path = %q, want %q", l.N, l.Path, got)
234 }
235 } else if l.Path != "" {
236 t.Errorf("line %d Path = %q, want none", l.N, l.Path)
237 }
238 }
239 }
240
241 func TestYAMLSequenceOfMappingsPaths(t *testing.T) {
242 const src = `obj1:
243 property3:
244 items:
245 - val: 0
246 - val: 1
247 `
248 ls := lines(src)
249 annotateStructure(ls, []byte(src), "yaml")
250 if ls[3].Path != "obj1.property3.items[0].val" {
251 t.Errorf("line 4 Path = %q", ls[3].Path)
252 }
253 if ls[4].Path != "obj1.property3.items[1].val" {
254 t.Errorf("line 5 Path = %q", ls[4].Path)
255 }
256 }
257
258 func TestPathKeyQuoting(t *testing.T) {
259 tests := []struct{ parent, key, want string }{
260 {"", "obj1", "obj1"},
261 {"obj1", "items", "obj1.items"},
262 {"obj1", "a.b", `obj1["a.b"]`},
263 {"", "weird key", `["weird key"]`},
264 {"obj1", `quo"te`, `obj1["quo\"te"]`},
265 }
266 for _, tc := range tests {
267 if got := joinPath(tc.parent, tc.key); got != tc.want {
268 t.Errorf("joinPath(%q, %q) = %q, want %q", tc.parent, tc.key, got, tc.want)
269 }
270 }
271 if got := joinIndex("obj1.items", 4); got != "obj1.items[4]" {
272 t.Errorf("joinIndex = %q", got)
273 }
274 }