1 package d2parser_test
2
3 import (
4 "fmt"
5 "path/filepath"
6 "strings"
7 "testing"
8
9 "oss.terrastruct.com/util-go/assert"
10 "oss.terrastruct.com/util-go/diff"
11
12 "oss.terrastruct.com/d2/d2ast"
13 "oss.terrastruct.com/d2/d2format"
14 "oss.terrastruct.com/d2/d2parser"
15 )
16
17 type testCase struct {
18 name string
19 text string
20 assert func(t testing.TB, ast *d2ast.Map, err error)
21 }
22
23
24
25
26 func TestParse(t *testing.T) {
27 t.Parallel()
28
29 var testCases = []testCase{
30 {
31 name: "empty",
32 text: ``,
33 },
34 {
35 name: "semicolons",
36 text: `;;;;;`,
37 },
38 {
39 name: "bad_curly",
40 text: `;;;};;;`,
41 },
42 {
43 name: "one_line_comment",
44 text: `
45 # hello
46 `,
47 },
48 {
49 name: "multiline_comment",
50 text: `
51
52 # hello
53 # world
54 # earth
55 #
56 #globe
57 # very good
58 # not so bad
59 #
60 #yes indeed
61 #The good (I am convinced, for one)
62 #Is but the bad one leaves undone.
63 #Once your reputation's done
64 #You can live a life of fun.
65 # -- Wilhelm Busch
66
67
68 `,
69 },
70 {
71 name: "one_line_block_comment",
72 text: `
73 """ dmaslkmdlksa """
74 `,
75 },
76 {
77 name: "block_comment",
78 text: `
79 """ dmaslkmdlksa
80
81 dasmlkdas
82 mkdlasdmkas
83 dmsakldmklsadsa
84
85 dsmakldmaslk
86 damklsdmklas
87
88 echo hi
89 x """
90
91 """ ok
92 meow
93 """
94 `,
95 },
96 {
97 name: "key",
98 text: `
99 x
100 `,
101 },
102 {
103 name: "edge",
104 text: `
105 x -> y
106 `,
107 },
108 {
109 name: "multiple_edges",
110 text: `
111 x -> y -> z
112 `,
113 },
114 {
115 name: "key_with_edge",
116 text: `
117 x.(z->q)
118 `,
119 },
120 {
121 name: "edge_key",
122 text: `
123 x.(z->q)[343].hola: false
124 `,
125 },
126 {
127 name: "subst",
128 text: `
129 x -> y: ${meow.ok}
130 `,
131 },
132 {
133 name: "primary",
134 text: `
135 x -> y: ${meow.ok} {
136 label: |
137 "Hi, I'm Preston A. Mantis, president of Consumers Retail Law Outlet. As you
138 can see by my suit and the fact that I have all these books of equal height
139 on the shelves behind me, I am a trained legal attorney. Do you have a car
140 or a job? Do you ever walk around? If so, you probably have the makings of
141 an excellent legal case. Although of course every case is different, I
142 would definitely say that based on my experience and training, there's no
143 reason why you shouldn't come out of this thing with at least a cabin
144 cruiser.
145
146 "Remember, at the Preston A. Mantis Consumers Retail Law Outlet, our motto
147 is: 'It is very difficult to disprove certain kinds of pain.'"
148 -- Dave Barry, "Pain and Suffering"
149 |
150 }
151 `,
152 },
153 {
154 name: "()_keys",
155 text: `
156 my_fn() -> wowa()
157 meow.(x -> y -> z)[3].shape: "all hail corn"
158 `,
159 },
160 {
161 name: "errs",
162 text: `
163 --: meow]]] ` + `
164 meow][: ok ` + `
165 ok: "dmsadmakls" dsamkldkmsa ` + `
166 ` + `
167 s.shape: orochimaru ` + `
168 x.shape: dasdasdas ` + `
169
170 wow:
171
172 : ` + `
173 ` + `
174 []
175
176 {}
177
178 """
179 wsup
180 """
181
182 '
183
184 meow: ${ok}
185 meow.(x->)[:
186 x -> x
187
188 x: [][]𐀀𐀀𐀀𐀀𐀀𐀀
189 `,
190 },
191 {
192 name: "block_string",
193 text: `
194 x: ||
195 meow
196 meo
197 # ok
198 code
199 yes
200 ||
201 x: || meow
202 meo
203 # ok
204 code
205 yes ||
206
207 # compat
208 x: |` + "`" + `
209 meow
210 meow
211 meow
212 ` + "`" + `| {
213 }
214 `,
215 },
216 {
217 name: "trailing_whitespace",
218 text: `
219 s.shape: orochimaru ` + `
220 `,
221 },
222 {
223 name: "table_and_class",
224 text: `
225 sql_example: sql_example {
226 board: {
227 shape: sql_table
228 id: int {constraint: primary_key}
229 frame: int {constraint: foreign_key}
230 diagram: int {constraint: foreign_key}
231 board_objects: jsonb
232 last_updated: timestamp with time zone
233 last_thumbgen: timestamp with time zone
234 dsl: text
235 }
236
237 # Normal.
238 board.diagram -> diagrams.id
239
240 # Self referential.
241 diagrams.id -> diagrams.representation
242
243 # SrcArrow test.
244 diagrams.id <- views.diagram
245 diagrams.id <-> steps.diagram
246
247 diagrams: {
248 shape: sql_table
249 id: {type: int, constraint: primary_key}
250 representation: {type: jsonb}
251 }
252
253 views: {
254 shape: sql_table
255 id: {type: int, constraint: primary_key}
256 representation: {type: jsonb}
257 diagram: int {constraint: foreign_key}
258 }
259
260 # steps: {
261 # shape: sql_table
262 # id: {type: int, constraint: primary_key}
263 # representation: {type: jsonb}
264 # diagram: int {constraint: foreign_key}
265 # }
266 # Uncomment to make autolayout panic:
267 meow <- diagrams.id
268 }
269
270 D2 AST Parser: {
271 shape: class
272
273 +prevRune: rune
274 prevColumn: int
275
276 +eatSpace(eatNewlines bool): (rune, error)
277 unreadRune()
278
279 \#scanKey(r rune): (k Key, _ error)
280 }
281 `,
282 },
283 {
284 name: "missing_map_value",
285 text: `
286 x:
287 `,
288 },
289 {
290 name: "edge_line_continuation",
291 text: `
292 super long shape id here --\
293 -> super long shape id even longer here
294 `,
295 },
296 {
297 name: "edge_line_continuation_2",
298 text: `
299 super long shape id here --\
300 > super long shape id even longer here
301 `,
302 },
303 {
304 name: "field_line_continuation",
305 text: `
306 meow \
307 ok \
308 super: yes \
309 wow so cool
310 \
311 xd \
312 \
313 ok does it work: hopefully
314 `,
315 },
316 {
317 name: "block_with_delims",
318 text: `
319 a: ||
320 |pipe|
321 ||
322
323 """
324 b: ""
325 """
326 `,
327 },
328 {
329 name: "block_one_line",
330 text: `
331 a: | hello |
332 """ hello """
333 `,
334 },
335 {
336 name: "block_trailing_space",
337 text: `
338 x: |
339 meow ` + `
340 |
341 """ hello ` + `
342 """
343 `,
344 },
345 {
346 name: "block_edge_case",
347 text: `
348 x: | meow ` + `
349 hello
350 yes
351 |
352 `,
353 },
354 {
355 name: "single_quote_block_string",
356 text: `
357 x: |'
358 bs
359 '|
360 not part of block string
361 `,
362 },
363 {
364 name: "edge_group_value",
365 text: `
366 q.(x -> y).z: (rawr)
367 `,
368 },
369 {
370 name: "less_than_edge#955",
371 text: `
372 x <= y
373 `,
374 },
375 {
376 name: "merged_shapes_#322",
377 text: `
378 a-
379 b-
380 c-
381 `,
382 },
383 {
384 name: "not-amper",
385 text: `
386 &k: amper
387 !&k: not amper
388 `,
389 assert: func(t testing.TB, ast *d2ast.Map, err error) {
390 assert.Success(t, err)
391 assert.True(t, ast.Nodes[0].MapKey.Ampersand)
392 assert.True(t, ast.Nodes[1].MapKey.NotAmpersand)
393 },
394 },
395 {
396 name: "whitespace_range",
397 text: ` a -> b -> c `,
398 assert: func(t testing.TB, ast *d2ast.Map, err error) {
399 assert.Equal(t, "1:2", ast.Nodes[0].MapKey.Edges[0].Src.Range.Start.String())
400 assert.Equal(t, "1:3", ast.Nodes[0].MapKey.Edges[0].Src.Range.End.String())
401 assert.Equal(t, "1:7", ast.Nodes[0].MapKey.Edges[0].Dst.Range.Start.String())
402 assert.Equal(t, "1:8", ast.Nodes[0].MapKey.Edges[0].Dst.Range.End.String())
403 assert.Equal(t, "1:12", ast.Nodes[0].MapKey.Edges[1].Dst.Range.Start.String())
404 assert.Equal(t, "1:13", ast.Nodes[0].MapKey.Edges[1].Dst.Range.End.String())
405 },
406 },
407 {
408 name: "utf16-input",
409 text: "\xff\xfex\x00 \x00-\x00>\x00 \x00y\x00\r\x00\n\x00",
410 assert: func(t testing.TB, ast *d2ast.Map, err error) {
411 assert.Success(t, err)
412 assert.Equal(t, "x -> y\n", d2format.Format(ast))
413 },
414 },
415 }
416
417 t.Run("import", testImport)
418
419 runa(t, testCases)
420 }
421
422 func testImport(t *testing.T) {
423 t.Parallel()
424
425 tca := []testCase{
426 {
427 text: "x: @file",
428 assert: func(t testing.TB, ast *d2ast.Map, err error) {
429 assert.Success(t, err)
430 assert.Equal(t, "file", ast.Nodes[0].MapKey.Value.Import.Path[0].Unbox().ScalarString())
431 },
432 },
433 {
434 text: "x: @file.d2",
435 assert: func(t testing.TB, ast *d2ast.Map, err error) {
436 assert.Success(t, err)
437 assert.Equal(t, "file", ast.Nodes[0].MapKey.Value.Import.Path[0].Unbox().ScalarString())
438 },
439 },
440 {
441 text: "...@file.d2",
442 assert: func(t testing.TB, ast *d2ast.Map, err error) {
443 assert.Success(t, err)
444 assert.True(t, ast.Nodes[0].Import.Spread)
445 assert.Equal(t, "file", ast.Nodes[0].Import.Path[0].Unbox().ScalarString())
446 },
447 },
448 {
449 text: "x: [...@file.d2]",
450 assert: func(t testing.TB, ast *d2ast.Map, err error) {
451 assert.Success(t, err)
452 imp := ast.Nodes[0].MapKey.Value.Array.Nodes[0].Import
453 assert.True(t, imp.Spread)
454 assert.Equal(t, "file", imp.Path[0].Unbox().ScalarString())
455 },
456 },
457 {
458 text: "...@\"file\".d2",
459 assert: func(t testing.TB, ast *d2ast.Map, err error) {
460 assert.Success(t, err)
461 assert.True(t, ast.Nodes[0].Import.Spread)
462 assert.Equal(t, "file", ast.Nodes[0].Import.Path[0].Unbox().ScalarString())
463 assert.Equal(t, "d2", ast.Nodes[0].Import.Path[1].Unbox().ScalarString())
464 },
465 },
466 {
467 text: "...@file.\"d2\"",
468 assert: func(t testing.TB, ast *d2ast.Map, err error) {
469 assert.Success(t, err)
470 assert.True(t, ast.Nodes[0].Import.Spread)
471 assert.Equal(t, "file", ast.Nodes[0].Import.Path[0].Unbox().ScalarString())
472 assert.Equal(t, "d2", ast.Nodes[0].Import.Path[1].Unbox().ScalarString())
473 },
474 },
475 {
476 text: "...@../file",
477 assert: func(t testing.TB, ast *d2ast.Map, err error) {
478 assert.Success(t, err)
479 assert.True(t, ast.Nodes[0].Import.Spread)
480 assert.Equal(t, "../file", ast.Nodes[0].Import.PathWithPre())
481 },
482 },
483 {
484 text: "@file",
485 assert: func(t testing.TB, ast *d2ast.Map, err error) {
486 assert.ErrorString(t, err, "d2/testdata/d2parser/TestParse/import/#07.d2:1:1: @file is not a valid import, did you mean ...@file?")
487 },
488 },
489 {
490 text: "...@./../.././file",
491 assert: func(t testing.TB, ast *d2ast.Map, err error) {
492 assert.Success(t, err)
493 assert.True(t, ast.Nodes[0].Import.Spread)
494 assert.Equal(t, "../../file", ast.Nodes[0].Import.PathWithPre())
495 },
496 },
497 {
498 text: "meow: ...@file",
499 assert: func(t testing.TB, ast *d2ast.Map, err error) {
500 assert.ErrorString(t, err, "d2/testdata/d2parser/TestParse/import/#09.d2:1:7: unquoted strings cannot begin with ...@ as that's import spread syntax")
501 },
502 },
503 }
504
505 runa(t, tca)
506 }
507
508 func runa(t *testing.T, tca []testCase) {
509 for _, tc := range tca {
510 tc := tc
511 t.Run(tc.name, func(t *testing.T) {
512 t.Parallel()
513
514 d2Path := fmt.Sprintf("d2/testdata/d2parser/%v.d2", t.Name())
515 opts := &d2parser.ParseOptions{}
516 ast, err := d2parser.Parse(d2Path, strings.NewReader(tc.text), opts)
517
518 if tc.assert != nil {
519 tc.assert(t, ast, err)
520 }
521
522 got := struct {
523 AST *d2ast.Map `json:"ast"`
524 Err error `json:"err"`
525 }{
526 AST: ast,
527 Err: err,
528 }
529
530 err = diff.TestdataJSON(filepath.Join("..", "testdata", "d2parser", t.Name()), got)
531 assert.Success(t, err)
532 })
533 }
534 }
535
View as plain text