1 package d2ir_test
2
3 import (
4 "fmt"
5 "math/big"
6 "path/filepath"
7 "strings"
8 "testing"
9
10 "oss.terrastruct.com/util-go/assert"
11 "oss.terrastruct.com/util-go/diff"
12 "oss.terrastruct.com/util-go/mapfs"
13
14 "oss.terrastruct.com/d2/d2ast"
15 "oss.terrastruct.com/d2/d2ir"
16 "oss.terrastruct.com/d2/d2parser"
17 )
18
19 func TestCompile(t *testing.T) {
20 t.Parallel()
21
22 t.Run("fields", testCompileFields)
23 t.Run("classes", testCompileClasses)
24 t.Run("edges", testCompileEdges)
25 t.Run("layers", testCompileLayers)
26 t.Run("scenarios", testCompileScenarios)
27 t.Run("steps", testCompileSteps)
28 t.Run("imports", testCompileImports)
29 t.Run("patterns", testCompilePatterns)
30 t.Run("filters", testCompileFilters)
31 }
32
33 type testCase struct {
34 name string
35 run func(testing.TB)
36 }
37
38 func runa(t *testing.T, tca []testCase) {
39 for _, tc := range tca {
40 tc := tc
41 t.Run(tc.name, func(t *testing.T) {
42 t.Parallel()
43 tc.run(t)
44 })
45 }
46 }
47
48 func compile(t testing.TB, text string) (*d2ir.Map, error) {
49 t.Helper()
50
51 d2Path := fmt.Sprintf("%v.d2", t.Name())
52 return compileFS(t, d2Path, map[string]string{d2Path: text})
53 }
54
55 func compileFS(t testing.TB, path string, mfs map[string]string) (*d2ir.Map, error) {
56 t.Helper()
57
58 ast, err := d2parser.Parse(path, strings.NewReader(mfs[path]), nil)
59 if err != nil {
60 return nil, err
61 }
62
63 fs, err := mapfs.New(mfs)
64 assert.Success(t, err)
65 t.Cleanup(func() {
66 err = fs.Close()
67 assert.Success(t, err)
68 })
69 m, err := d2ir.Compile(ast, &d2ir.CompileOptions{
70 FS: fs,
71 })
72 if err != nil {
73 return nil, err
74 }
75
76 err = diff.TestdataJSON(filepath.Join("..", "testdata", "d2ir", t.Name()), m)
77 if err != nil {
78 return nil, err
79 }
80 return m, nil
81 }
82
83 func assertQuery(t testing.TB, n d2ir.Node, nfields, nedges int, primary interface{}, idStr string) d2ir.Node {
84 t.Helper()
85
86 m := n.Map()
87 p := n.Primary()
88
89 var na []d2ir.Node
90 if idStr != "" {
91 var err error
92 na, err = m.QueryAll(idStr)
93 assert.Success(t, err)
94 assert.NotEqual(t, n, nil)
95 } else {
96 na = append(na, n)
97 }
98
99 for _, n := range na {
100 m = n.Map()
101 p = n.Primary()
102 assert.Equal(t, nfields, m.FieldCountRecursive())
103 assert.Equal(t, nedges, m.EdgeCountRecursive())
104 if !makeScalar(p).Equal(makeScalar(primary)) {
105 t.Fatalf("expected primary %#v but got %s", primary, p)
106 }
107 }
108
109 if len(na) == 0 {
110 t.Fatalf("query didn't match anything")
111 }
112
113 return na[0]
114 }
115
116 func makeScalar(v interface{}) *d2ir.Scalar {
117 s := &d2ir.Scalar{}
118 switch v := v.(type) {
119 case *d2ir.Scalar:
120 if v == nil {
121 s.Value = &d2ast.Null{}
122 return s
123 }
124 return v
125 case bool:
126 s.Value = &d2ast.Boolean{
127 Value: v,
128 }
129 case float64:
130 bv := &big.Rat{}
131 bv.SetFloat64(v)
132 s.Value = &d2ast.Number{
133 Raw: fmt.Sprint(v),
134 Value: bv,
135 }
136 case int:
137 s.Value = &d2ast.Number{
138 Raw: fmt.Sprint(v),
139 Value: big.NewRat(int64(v), 1),
140 }
141 case string:
142 s.Value = d2ast.FlatDoubleQuotedString(v)
143 default:
144 if v != nil {
145 panic(fmt.Sprintf("d2ir: unexpected type to makeScalar: %#v", v))
146 }
147 s.Value = &d2ast.Null{}
148 }
149 return s
150 }
151
152 func testCompileFields(t *testing.T) {
153 t.Parallel()
154 tca := []testCase{
155 {
156 name: "root",
157 run: func(t testing.TB) {
158 m, err := compile(t, `x`)
159 assert.Success(t, err)
160 assertQuery(t, m, 1, 0, nil, "")
161
162 assertQuery(t, m, 0, 0, nil, "x")
163 },
164 },
165 {
166 name: "label",
167 run: func(t testing.TB) {
168 m, err := compile(t, `x: yes`)
169 assert.Success(t, err)
170 assertQuery(t, m, 1, 0, nil, "")
171
172 assertQuery(t, m, 0, 0, "yes", "x")
173 },
174 },
175 {
176 name: "nested",
177 run: func(t testing.TB) {
178 m, err := compile(t, `x.y: yes`)
179 assert.Success(t, err)
180 assertQuery(t, m, 2, 0, nil, "")
181
182 assertQuery(t, m, 1, 0, nil, "x")
183 assertQuery(t, m, 0, 0, "yes", "x.y")
184 },
185 },
186 {
187 name: "array",
188 run: func(t testing.TB) {
189 m, err := compile(t, `x: [1;2;3;4]`)
190 assert.Success(t, err)
191 assertQuery(t, m, 1, 0, nil, "")
192
193 f := assertQuery(t, m, 0, 0, nil, "x").(*d2ir.Field)
194 assert.String(t, `[1; 2; 3; 4]`, f.Composite.String())
195 },
196 },
197 {
198 name: "null",
199 run: func(t testing.TB) {
200 m, err := compile(t, `pq: pq
201 pq: null`)
202 assert.Success(t, err)
203 assertQuery(t, m, 0, 0, nil, "")
204 },
205 },
206 }
207 runa(t, tca)
208 t.Run("primary", func(t *testing.T) {
209 t.Parallel()
210 tca := []testCase{
211 {
212 name: "root",
213 run: func(t testing.TB) {
214 m, err := compile(t, `x: yes { pqrs }`)
215 assert.Success(t, err)
216 assertQuery(t, m, 2, 0, nil, "")
217
218 assertQuery(t, m, 1, 0, "yes", "x")
219 assertQuery(t, m, 0, 0, nil, "x.pqrs")
220 },
221 },
222 {
223 name: "nested",
224 run: func(t testing.TB) {
225 m, err := compile(t, `x.y: yes { pqrs }`)
226 assert.Success(t, err)
227 assertQuery(t, m, 3, 0, nil, "")
228
229 assertQuery(t, m, 2, 0, nil, "x")
230 assertQuery(t, m, 1, 0, "yes", "x.y")
231 assertQuery(t, m, 0, 0, nil, "x.y.pqrs")
232 },
233 },
234 }
235 runa(t, tca)
236 })
237 }
238
239 func testCompileEdges(t *testing.T) {
240 t.Parallel()
241 tca := []testCase{
242 {
243 name: "root",
244 run: func(t testing.TB) {
245 m, err := compile(t, `x -> y`)
246 assert.Success(t, err)
247 assertQuery(t, m, 2, 1, nil, "")
248 assertQuery(t, m, 0, 0, nil, `(x -> y)[0]`)
249
250 assertQuery(t, m, 0, 0, nil, "x")
251 assertQuery(t, m, 0, 0, nil, "y")
252 },
253 },
254 {
255 name: "nested",
256 run: func(t testing.TB) {
257 m, err := compile(t, `x.y -> z.p`)
258 assert.Success(t, err)
259 assertQuery(t, m, 4, 1, nil, "")
260
261 assertQuery(t, m, 1, 0, nil, "x")
262 assertQuery(t, m, 0, 0, nil, "x.y")
263
264 assertQuery(t, m, 1, 0, nil, "z")
265 assertQuery(t, m, 0, 0, nil, "z.p")
266
267 assertQuery(t, m, 0, 0, nil, "(x.y -> z.p)[0]")
268 },
269 },
270 {
271 name: "underscore",
272 run: func(t testing.TB) {
273 m, err := compile(t, `p: { _.x -> z }`)
274 assert.Success(t, err)
275 assertQuery(t, m, 3, 1, nil, "")
276
277 assertQuery(t, m, 0, 0, nil, "x")
278 assertQuery(t, m, 1, 0, nil, "p")
279
280 assertQuery(t, m, 0, 0, nil, "(x -> p.z)[0]")
281 },
282 },
283 {
284 name: "chain",
285 run: func(t testing.TB) {
286 m, err := compile(t, `a -> b -> c -> d`)
287 assert.Success(t, err)
288 assertQuery(t, m, 4, 3, nil, "")
289
290 assertQuery(t, m, 0, 0, nil, "a")
291 assertQuery(t, m, 0, 0, nil, "b")
292 assertQuery(t, m, 0, 0, nil, "c")
293 assertQuery(t, m, 0, 0, nil, "d")
294 assertQuery(t, m, 0, 0, nil, "(a -> b)[0]")
295 assertQuery(t, m, 0, 0, nil, "(b -> c)[0]")
296 assertQuery(t, m, 0, 0, nil, "(c -> d)[0]")
297 },
298 },
299 }
300 runa(t, tca)
301 t.Run("errs", func(t *testing.T) {
302 t.Parallel()
303 tca := []testCase{
304 {
305 name: "bad_edge",
306 run: func(t testing.TB) {
307 _, err := compile(t, `(x -> y): { p -> q }`)
308 assert.ErrorString(t, err, `TestCompile/edges/errs/bad_edge.d2:1:13: cannot create edge inside edge`)
309 },
310 },
311 }
312 runa(t, tca)
313 })
314 }
315
316 func testCompileLayers(t *testing.T) {
317 t.Parallel()
318 tca := []testCase{
319 {
320 name: "root",
321 run: func(t testing.TB) {
322 m, err := compile(t, `x -> y
323 layers: {
324 bingo: { p.q.z }
325 }`)
326 assert.Success(t, err)
327
328 assertQuery(t, m, 7, 1, nil, "")
329 assertQuery(t, m, 0, 0, nil, `(x -> y)[0]`)
330
331 assertQuery(t, m, 0, 0, nil, "x")
332 assertQuery(t, m, 0, 0, nil, "y")
333
334 assertQuery(t, m, 3, 0, nil, "layers.bingo")
335 },
336 },
337 }
338 runa(t, tca)
339 t.Run("errs", func(t *testing.T) {
340 t.Parallel()
341 tca := []testCase{
342 {
343 name: "1/bad_edge",
344 run: func(t testing.TB) {
345 _, err := compile(t, `layers.x -> layers.y`)
346 assert.ErrorString(t, err, `TestCompile/layers/errs/1/bad_edge.d2:1:1: cannot create edges between boards`)
347 },
348 },
349 {
350 name: "2/bad_edge",
351 run: func(t testing.TB) {
352 _, err := compile(t, `layers -> scenarios`)
353 assert.ErrorString(t, err, `TestCompile/layers/errs/2/bad_edge.d2:1:1: edge with board keyword alone doesn't make sense`)
354 },
355 },
356 {
357 name: "3/bad_edge",
358 run: func(t testing.TB) {
359 _, err := compile(t, `layers.x.y -> steps.z.p`)
360 assert.ErrorString(t, err, `TestCompile/layers/errs/3/bad_edge.d2:1:1: cannot create edges between boards`)
361 },
362 },
363 {
364 name: "4/good_edge",
365 run: func(t testing.TB) {
366 _, err := compile(t, `layers.x.y -> layers.x.y`)
367 assert.Success(t, err)
368 },
369 },
370 }
371 runa(t, tca)
372 })
373 }
374
375 func testCompileScenarios(t *testing.T) {
376 t.Parallel()
377 tca := []testCase{
378 {
379 name: "root",
380 run: func(t testing.TB) {
381 m, err := compile(t, `x -> y
382 scenarios: {
383 bingo: { p.q.z }
384 nuclear: { quiche }
385 }`)
386 assert.Success(t, err)
387
388 assertQuery(t, m, 13, 3, nil, "")
389
390 assertQuery(t, m, 0, 0, nil, "x")
391 assertQuery(t, m, 0, 0, nil, "y")
392 assertQuery(t, m, 0, 0, nil, `(x -> y)[0]`)
393
394 assertQuery(t, m, 5, 1, nil, "scenarios.bingo")
395 assertQuery(t, m, 0, 0, nil, "scenarios.bingo.x")
396 assertQuery(t, m, 0, 0, nil, "scenarios.bingo.y")
397 assertQuery(t, m, 0, 0, nil, `scenarios.bingo.(x -> y)[0]`)
398 assertQuery(t, m, 2, 0, nil, "scenarios.bingo.p")
399 assertQuery(t, m, 1, 0, nil, "scenarios.bingo.p.q")
400 assertQuery(t, m, 0, 0, nil, "scenarios.bingo.p.q.z")
401
402 assertQuery(t, m, 3, 1, nil, "scenarios.nuclear")
403 assertQuery(t, m, 0, 0, nil, "scenarios.nuclear.x")
404 assertQuery(t, m, 0, 0, nil, "scenarios.nuclear.y")
405 assertQuery(t, m, 0, 0, nil, `scenarios.nuclear.(x -> y)[0]`)
406 assertQuery(t, m, 0, 0, nil, "scenarios.nuclear.quiche")
407 },
408 },
409 {
410 name: "edge",
411 run: func(t testing.TB) {
412 m, err := compile(t, `a -> b
413 scenarios: {
414 1: {
415 (a -> b)[0].style.opacity: 0.1
416 }
417 }`)
418 assert.Success(t, err)
419 assertQuery(t, m, 8, 2, nil, "")
420 assertQuery(t, m, 0, 0, nil, "(a -> b)[0]")
421 },
422 },
423 {
424 name: "multiple-scenario-map",
425 run: func(t testing.TB) {
426 m, err := compile(t, `a -> b: { style.opacity: 0.3 }
427 scenarios: {
428 1: {
429 (a -> b)[0].style.opacity: 0.1
430 }
431 1: {
432 z
433 }
434 }`)
435 assert.Success(t, err)
436 assertQuery(t, m, 11, 2, nil, "")
437 assertQuery(t, m, 0, 0, 0.1, "scenarios.1.(a -> b)[0].style.opacity")
438 },
439 },
440 }
441 runa(t, tca)
442 }
443
444 func testCompileSteps(t *testing.T) {
445 t.Parallel()
446 tca := []testCase{
447 {
448 name: "root",
449 run: func(t testing.TB) {
450 m, err := compile(t, `x -> y
451 steps: {
452 bingo: { p.q.z }
453 nuclear: { quiche }
454 }`)
455 assert.Success(t, err)
456
457 assertQuery(t, m, 16, 3, nil, "")
458
459 assertQuery(t, m, 0, 0, nil, "x")
460 assertQuery(t, m, 0, 0, nil, "y")
461 assertQuery(t, m, 0, 0, nil, `(x -> y)[0]`)
462
463 assertQuery(t, m, 5, 1, nil, "steps.bingo")
464 assertQuery(t, m, 0, 0, nil, "steps.bingo.x")
465 assertQuery(t, m, 0, 0, nil, "steps.bingo.y")
466 assertQuery(t, m, 0, 0, nil, `steps.bingo.(x -> y)[0]`)
467 assertQuery(t, m, 2, 0, nil, "steps.bingo.p")
468 assertQuery(t, m, 1, 0, nil, "steps.bingo.p.q")
469 assertQuery(t, m, 0, 0, nil, "steps.bingo.p.q.z")
470
471 assertQuery(t, m, 6, 1, nil, "steps.nuclear")
472 assertQuery(t, m, 0, 0, nil, "steps.nuclear.x")
473 assertQuery(t, m, 0, 0, nil, "steps.nuclear.y")
474 assertQuery(t, m, 0, 0, nil, `steps.nuclear.(x -> y)[0]`)
475 assertQuery(t, m, 2, 0, nil, "steps.nuclear.p")
476 assertQuery(t, m, 1, 0, nil, "steps.nuclear.p.q")
477 assertQuery(t, m, 0, 0, nil, "steps.nuclear.p.q.z")
478 assertQuery(t, m, 0, 0, nil, "steps.nuclear.quiche")
479 },
480 },
481 {
482 name: "steps_panic",
483 run: func(t testing.TB) {
484 _, err := compile(t, `steps: {
485 shape: sql_table
486 id: int {constraint: primary_key}
487 }
488 scenarios: {
489 shape: sql_table
490 hey: int {constraint: primary_key}
491 }`)
492 assert.ErrorString(t, err, `TestCompile/steps/steps_panic.d2:3:3: invalid step
493 TestCompile/steps/steps_panic.d2:7:3: invalid scenario`)
494 },
495 },
496 {
497 name: "recursive",
498 run: func(t testing.TB) {
499 m, err := compile(t, `x -> y
500 steps: {
501 bingo: { p.q.z }
502 nuclear: {
503 quiche
504 scenarios: {
505 bavarian: {
506 perseverance
507 }
508 }
509 }
510 }`)
511 assert.Success(t, err)
512
513 assertQuery(t, m, 25, 4, nil, "")
514
515 assertQuery(t, m, 0, 0, nil, "x")
516 assertQuery(t, m, 0, 0, nil, "y")
517 assertQuery(t, m, 0, 0, nil, `(x -> y)[0]`)
518
519 assertQuery(t, m, 5, 1, nil, "steps.bingo")
520 assertQuery(t, m, 0, 0, nil, "steps.bingo.x")
521 assertQuery(t, m, 0, 0, nil, "steps.bingo.y")
522 assertQuery(t, m, 0, 0, nil, `steps.bingo.(x -> y)[0]`)
523 assertQuery(t, m, 2, 0, nil, "steps.bingo.p")
524 assertQuery(t, m, 1, 0, nil, "steps.bingo.p.q")
525 assertQuery(t, m, 0, 0, nil, "steps.bingo.p.q.z")
526
527 assertQuery(t, m, 15, 2, nil, "steps.nuclear")
528 assertQuery(t, m, 0, 0, nil, "steps.nuclear.x")
529 assertQuery(t, m, 0, 0, nil, "steps.nuclear.y")
530 assertQuery(t, m, 0, 0, nil, `steps.nuclear.(x -> y)[0]`)
531 assertQuery(t, m, 2, 0, nil, "steps.nuclear.p")
532 assertQuery(t, m, 1, 0, nil, "steps.nuclear.p.q")
533 assertQuery(t, m, 0, 0, nil, "steps.nuclear.p.q.z")
534 assertQuery(t, m, 0, 0, nil, "steps.nuclear.quiche")
535
536 assertQuery(t, m, 7, 1, nil, "steps.nuclear.scenarios.bavarian")
537 assertQuery(t, m, 0, 0, nil, "steps.nuclear.scenarios.bavarian.x")
538 assertQuery(t, m, 0, 0, nil, "steps.nuclear.scenarios.bavarian.y")
539 assertQuery(t, m, 0, 0, nil, `steps.nuclear.scenarios.bavarian.(x -> y)[0]`)
540 assertQuery(t, m, 2, 0, nil, "steps.nuclear.scenarios.bavarian.p")
541 assertQuery(t, m, 1, 0, nil, "steps.nuclear.scenarios.bavarian.p.q")
542 assertQuery(t, m, 0, 0, nil, "steps.nuclear.scenarios.bavarian.p.q.z")
543 assertQuery(t, m, 0, 0, nil, "steps.nuclear.scenarios.bavarian.quiche")
544 assertQuery(t, m, 0, 0, nil, "steps.nuclear.scenarios.bavarian.perseverance")
545 },
546 },
547 }
548 runa(t, tca)
549 }
550
551 func testCompileClasses(t *testing.T) {
552 t.Parallel()
553 tca := []testCase{
554 {
555 name: "basic",
556 run: func(t testing.TB) {
557 _, err := compile(t, `x
558 classes: {
559 mango: {
560 style.fill: orange
561 }
562 }
563 `)
564 assert.Success(t, err)
565 },
566 },
567 {
568 name: "nonroot",
569 run: func(t testing.TB) {
570 _, err := compile(t, `x: {
571 classes: {
572 mango: {
573 style.fill: orange
574 }
575 }
576 }
577 `)
578 assert.ErrorString(t, err, `TestCompile/classes/nonroot.d2:2:3: classes is only allowed at a board root`)
579 },
580 },
581 {
582 name: "merge",
583 run: func(t testing.TB) {
584 m, err := compile(t, `classes: {
585 mango: {
586 style.fill: orange
587 width: 10
588 }
589 }
590 layers: {
591 hawaii: {
592 classes: {
593 mango: {
594 width: 9000
595 }
596 }
597 }
598 }
599 `)
600 assert.Success(t, err)
601 assertQuery(t, m, 3, 0, nil, "layers.hawaii.classes.mango")
602 assertQuery(t, m, 0, 0, "orange", "layers.hawaii.classes.mango.style.fill")
603 assertQuery(t, m, 0, 0, 9000, "layers.hawaii.classes.mango.width")
604 },
605 },
606 {
607 name: "nested",
608 run: func(t testing.TB) {
609 m, err := compile(t, `classes: {
610 mango: {
611 style.fill: orange
612 }
613 }
614 layers: {
615 hawaii: {
616 layers: {
617 maui: {
618 x
619 }
620 }
621 }
622 }
623 `)
624 assert.Success(t, err)
625 assertQuery(t, m, 3, 0, nil, "layers.hawaii.classes")
626 assertQuery(t, m, 3, 0, nil, "layers.hawaii.layers.maui.classes")
627 },
628 },
629 {
630 name: "inherited",
631 run: func(t testing.TB) {
632 m, err := compile(t, `classes: {
633 mango: {
634 style.fill: orange
635 }
636 }
637 scenarios: {
638 hawaii: {
639 steps: {
640 1: {
641 classes: {
642 cherry: {
643 style.fill: red
644 }
645 }
646 x
647 }
648 2: {
649 y
650 }
651 3: {
652 classes: {
653 cherry: {
654 style.fill: blue
655 }
656 }
657 y
658 }
659 4: {
660 layers: {
661 deep: {
662 x
663 }
664 }
665 x
666 }
667 }
668 }
669 }
670 `)
671 assert.Success(t, err)
672 assertQuery(t, m, 3, 0, nil, "scenarios.hawaii.classes")
673 assertQuery(t, m, 2, 0, nil, "scenarios.hawaii.steps.2.classes.mango")
674 assertQuery(t, m, 2, 0, nil, "scenarios.hawaii.steps.2.classes.cherry")
675 assertQuery(t, m, 0, 0, "blue", "scenarios.hawaii.steps.4.classes.cherry.style.fill")
676 assertQuery(t, m, 0, 0, "blue", "scenarios.hawaii.steps.4.layers.deep.classes.cherry.style.fill")
677 },
678 },
679 {
680 name: "layer-modify",
681 run: func(t testing.TB) {
682 m, err := compile(t, `classes: {
683 orb: {
684 style.fill: yellow
685 }
686 }
687 layers: {
688 x: {
689 classes.orb.style.stroke: red
690 }
691 }
692 `)
693 assert.Success(t, err)
694 assertQuery(t, m, 0, 0, "yellow", "layers.x.classes.orb.style.fill")
695 assertQuery(t, m, 0, 0, "red", "layers.x.classes.orb.style.stroke")
696 },
697 },
698 }
699 runa(t, tca)
700 }
701
View as plain text