Migration
This is my new public git source viewer. Source coming soon. Please contact with concerns/bugs.
Contact me
internal/modules/structured_test.go
4bda28df3ab3dbfcc789dcbbb184cabc8cac9398
· 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 | } |