1 package d2compiler_test
2
3 import (
4 "fmt"
5 "path/filepath"
6 "strings"
7 "testing"
8
9 tassert "github.com/stretchr/testify/assert"
10
11 "oss.terrastruct.com/util-go/assert"
12 "oss.terrastruct.com/util-go/diff"
13
14 "oss.terrastruct.com/d2/d2compiler"
15 "oss.terrastruct.com/d2/d2format"
16 "oss.terrastruct.com/d2/d2graph"
17 "oss.terrastruct.com/d2/d2target"
18 )
19
20 func TestCompile(t *testing.T) {
21 t.Parallel()
22
23 testCases := []struct {
24 name string
25 text string
26
27 expErr string
28 assertions func(t *testing.T, g *d2graph.Graph)
29 }{
30 {
31 name: "basic_shape",
32
33 text: `
34 x: {
35 shape: circle
36 }
37 `,
38 assertions: func(t *testing.T, g *d2graph.Graph) {
39 if len(g.Objects) != 1 {
40 t.Fatalf("expected 1 objects: %#v", g.Objects)
41 }
42 if g.Objects[0].ID != "x" {
43 t.Fatalf("expected g.Objects[0].ID to be x: %#v", g.Objects[0])
44 }
45
46 if g.Objects[0].Shape.Value != d2target.ShapeCircle {
47 t.Fatalf("expected g.Objects[0].Shape.Value to be circle: %#v", g.Objects[0].Shape.Value)
48 }
49
50 },
51 },
52 {
53 name: "basic_style",
54
55 text: `
56 x: {
57 style.opacity: 0.4
58 }
59 `,
60 assertions: func(t *testing.T, g *d2graph.Graph) {
61 if len(g.Objects) != 1 {
62 t.Fatalf("expected 1 objects: %#v", g.Objects)
63 }
64 if g.Objects[0].ID != "x" {
65 t.Fatalf("expected g.Objects[0].ID to be x: %#v", g.Objects[0])
66 }
67
68 if g.Objects[0].Style.Opacity.Value != "0.4" {
69 t.Fatalf("expected g.Objects[0].Style.Opacity.Value to be 0.4: %#v", g.Objects[0].Style.Opacity.Value)
70 }
71
72 },
73 },
74 {
75 name: "image_style",
76
77 text: `hey: "" {
78 icon: https://icons.terrastruct.com/essentials/004-picture.svg
79 shape: image
80 style.stroke: "#0D32B2"
81 }
82 `,
83 assertions: func(t *testing.T, g *d2graph.Graph) {
84 if len(g.Objects) != 1 {
85 t.Fatalf("expected 1 objects: %#v", g.Objects)
86 }
87 },
88 },
89 {
90 name: "dimensions_on_nonimage",
91
92 text: `hey: "" {
93 shape: hexagon
94 width: 200
95 height: 230
96 }
97 `,
98 assertions: func(t *testing.T, g *d2graph.Graph) {
99 if len(g.Objects) != 1 {
100 t.Fatalf("expected 1 objects: %#v", g.Objects)
101 }
102 if g.Objects[0].ID != "hey" {
103 t.Fatalf("expected g.Objects[0].ID to be 'hey': %#v", g.Objects[0])
104 }
105 if g.Objects[0].Shape.Value != d2target.ShapeHexagon {
106 t.Fatalf("expected g.Objects[0].Shape.Value to be hexagon: %#v", g.Objects[0].Shape.Value)
107 }
108 if g.Objects[0].WidthAttr.Value != "200" {
109 t.Fatalf("expected g.Objects[0].Width.Value to be 200: %#v", g.Objects[0].WidthAttr.Value)
110 }
111 if g.Objects[0].HeightAttr.Value != "230" {
112 t.Fatalf("expected g.Objects[0].Height.Value to be 230: %#v", g.Objects[0].HeightAttr.Value)
113 }
114 },
115 },
116 {
117 name: "positions",
118 text: `hey: {
119 top: 200
120 left: 230
121 }
122 `,
123 assertions: func(t *testing.T, g *d2graph.Graph) {
124 tassert.Equal(t, "200", g.Objects[0].Top.Value)
125 },
126 },
127 {
128 name: "positions_negative",
129 text: `hey: {
130 top: 200
131 left: -200
132 }
133 `,
134 expErr: `d2/testdata/d2compiler/TestCompile/positions_negative.d2:3:8: left must be a non-negative integer: "-200"`,
135 },
136 {
137 name: "equal_dimensions_on_circle",
138
139 text: `hey: "" {
140 shape: circle
141 width: 200
142 height: 230
143 }
144 `,
145 expErr: `d2/testdata/d2compiler/TestCompile/equal_dimensions_on_circle.d2:3:2: width and height must be equal for circle shapes
146 d2/testdata/d2compiler/TestCompile/equal_dimensions_on_circle.d2:4:2: width and height must be equal for circle shapes`,
147 },
148 {
149 name: "single_dimension_on_circle",
150
151 text: `hey: "" {
152 shape: circle
153 height: 230
154 }
155 `,
156 assertions: func(t *testing.T, g *d2graph.Graph) {
157 if len(g.Objects) != 1 {
158 t.Fatalf("expected 1 objects: %#v", g.Objects)
159 }
160 if g.Objects[0].ID != "hey" {
161 t.Fatalf("expected ID to be 'hey': %#v", g.Objects[0])
162 }
163 if g.Objects[0].Shape.Value != d2target.ShapeCircle {
164 t.Fatalf("expected Attributes.Shape.Value to be circle: %#v", g.Objects[0].Shape.Value)
165 }
166 if g.Objects[0].WidthAttr != nil {
167 t.Fatalf("expected Attributes.Width to be nil: %#v", g.Objects[0].WidthAttr)
168 }
169 if g.Objects[0].HeightAttr == nil {
170 t.Fatalf("Attributes.Height is nil")
171 }
172 },
173 },
174 {
175 name: "dimensions_on_containers",
176 text: `
177 containers: {
178 circle container: {
179 shape: circle
180 width: 512
181
182 diamond: {
183 shape: diamond
184 width: 128
185 height: 64
186 }
187 }
188 diamond container: {
189 shape: diamond
190 width: 512
191 height: 256
192
193 circle: {
194 shape: circle
195 width: 128
196 }
197 }
198 oval container: {
199 shape: oval
200 width: 512
201 height: 256
202
203 hexagon: {
204 shape: hexagon
205 width: 128
206 height: 64
207 }
208 }
209 hexagon container: {
210 shape: hexagon
211 width: 512
212 height: 256
213
214 oval: {
215 shape: oval
216 width: 128
217 height: 64
218 }
219 }
220 }
221 `,
222 },
223 {
224 name: "dimension_with_style",
225
226 text: `x: {
227 width: 200
228 style.multiple: true
229 }
230 `,
231 },
232 {
233 name: "basic_icon",
234
235 text: `hey: "" {
236 icon: https://icons.terrastruct.com/essentials/004-picture.svg
237 }
238 `,
239 assertions: func(t *testing.T, g *d2graph.Graph) {
240 if g.Objects[0].Icon == nil {
241 t.Fatal("Attribute icon is nil")
242 }
243 },
244 },
245 {
246 name: "fill-pattern",
247 text: `x: {
248 style: {
249 fill-pattern: dots
250 }
251 }
252 `,
253 },
254 {
255 name: "invalid-fill-pattern",
256 text: `x: {
257 style: {
258 fill-pattern: ddots
259 }
260 }
261 `,
262 expErr: `d2/testdata/d2compiler/TestCompile/invalid-fill-pattern.d2:3:19: expected "fill-pattern" to be one of: dots, lines, grain, paper`,
263 },
264 {
265 name: "shape_unquoted_hex",
266
267 text: `x: {
268 style: {
269 fill: #ffffff
270 }
271 }
272 `,
273 expErr: `d2/testdata/d2compiler/TestCompile/shape_unquoted_hex.d2:3:10: missing value after colon`,
274 },
275 {
276 name: "edge_unquoted_hex",
277
278 text: `x -> y: {
279 style: {
280 fill: #ffffff
281 }
282 }
283 `,
284 expErr: `d2/testdata/d2compiler/TestCompile/edge_unquoted_hex.d2:3:10: missing value after colon`,
285 },
286 {
287 name: "blank_underscore",
288
289 text: `x: {
290 y
291 _
292 }
293 `,
294 expErr: `d2/testdata/d2compiler/TestCompile/blank_underscore.d2:3:3: field key must contain more than underscores`,
295 },
296 {
297 name: "image_non_style",
298
299 text: `x: {
300 shape: image
301 icon: https://icons.terrastruct.com/aws/_Group%20Icons/EC2-instance-container_light-bg.svg
302 name: y
303 }
304 `,
305 expErr: `d2/testdata/d2compiler/TestCompile/image_non_style.d2:4:3: image shapes cannot have children.`,
306 },
307 {
308 name: "image_children_Steps",
309
310 text: `x: {
311 icon: https://icons.terrastruct.com/aws/_Group%20Icons/EC2-instance-container_light-bg.svg
312 shape: image
313 Steps
314 }
315 `,
316 expErr: `d2/testdata/d2compiler/TestCompile/image_children_Steps.d2:4:3: steps is only allowed at a board root`,
317 },
318 {
319 name: "name-with-dot-underscore",
320 text: `A: {
321 _.C
322 }
323
324 "D.E": {
325 _.C
326 }
327 `,
328 assertions: func(t *testing.T, g *d2graph.Graph) {
329 tassert.Equal(t, 3, len(g.Objects))
330 },
331 },
332 {
333 name: "stroke-width",
334
335 text: `hey {
336 style.stroke-width: 0
337 }
338 `,
339 assertions: func(t *testing.T, g *d2graph.Graph) {
340 if len(g.Objects) != 1 {
341 t.Fatalf("expected 1 objects: %#v", g.Objects)
342 }
343 if g.Objects[0].Style.StrokeWidth.Value != "0" {
344 t.Fatalf("unexpected")
345 }
346 },
347 },
348 {
349 name: "illegal-stroke-width",
350
351 text: `hey {
352 style.stroke-width: -1
353 }
354 `,
355 expErr: `d2/testdata/d2compiler/TestCompile/illegal-stroke-width.d2:2:23: expected "stroke-width" to be a number between 0 and 15`,
356 },
357 {
358 name: "underscore_parent_create",
359
360 text: `
361 x: {
362 _.y
363 }
364 `,
365 assertions: func(t *testing.T, g *d2graph.Graph) {
366 if len(g.Objects) != 2 {
367 t.Fatalf("expected 2 objects: %#v", g.Objects)
368 }
369 if g.Objects[0].ID != "x" {
370 t.Fatalf("expected g.Objects[0].ID to be x: %#v", g.Objects[0])
371 }
372 if g.Objects[1].ID != "y" {
373 t.Fatalf("expected g.Objects[1].ID to be y: %#v", g.Objects[1])
374 }
375
376 if len(g.Root.ChildrenArray) != 2 {
377 t.Fatalf("expected 2 objects at the root: %#v", len(g.Root.ChildrenArray))
378 }
379
380 },
381 },
382 {
383 name: "underscore_unresolved_obj",
384
385 text: `
386 x: {
387 _.y
388 }
389 `,
390 assertions: func(t *testing.T, g *d2graph.Graph) {
391 tassert.Equal(t, "y", g.Objects[1].ID)
392 tassert.Equal(t, g.Objects[0].AbsID(), g.Objects[1].References[0].ScopeObj.AbsID())
393 },
394 },
395 {
396 name: "underscore_connection",
397 text: `a: {
398 _.c.d -> _.c.b
399 }
400 `,
401 assertions: func(t *testing.T, g *d2graph.Graph) {
402 tassert.Equal(t, 4, len(g.Objects))
403 tassert.Equal(t, 1, len(g.Edges))
404 },
405 },
406 {
407 name: "underscore_parent_not_root",
408
409 text: `
410 x: {
411 y: {
412 _.z
413 }
414 }
415 `,
416 assertions: func(t *testing.T, g *d2graph.Graph) {
417 if len(g.Objects) != 3 {
418 t.Fatalf("expected 3 objects: %#v", g.Objects)
419 }
420 if g.Objects[0].ID != "x" {
421 t.Fatalf("expected g.Objects[0].ID to be x: %#v", g.Objects[0])
422 }
423 if g.Objects[1].ID != "y" {
424 t.Fatalf("expected g.Objects[1].ID to be y: %#v", g.Objects[1])
425 }
426
427 if len(g.Root.ChildrenArray) != 1 {
428 t.Fatalf("expected 1 object at the root: %#v", len(g.Root.ChildrenArray))
429 }
430 if len(g.Objects[0].ChildrenArray) != 2 {
431 t.Fatalf("expected 2 objects within x: %v", len(g.Objects[0].ChildrenArray))
432 }
433
434 },
435 },
436 {
437 name: "underscore_parent_preference_1",
438
439 text: `
440 x: {
441 _.y: "All we are given is possibilities -- to make ourselves one thing or another."
442 }
443 y: "But it's real. And if it's real it can be affected ... we may not be able"
444 `,
445 assertions: func(t *testing.T, g *d2graph.Graph) {
446 if len(g.Objects) != 2 {
447 t.Fatalf("expected 2 objects: %#v", g.Objects)
448 }
449 if g.Objects[0].ID != "x" {
450 t.Fatalf("expected g.Objects[0].ID to be x: %#v", g.Objects[0])
451 }
452 if g.Objects[1].ID != "y" {
453 t.Fatalf("expected g.Objects[1].ID to be y: %#v", g.Objects[1])
454 }
455
456 if len(g.Root.ChildrenArray) != 2 {
457 t.Fatalf("expected 2 objects at the root: %#v", len(g.Root.ChildrenArray))
458 }
459 if g.Objects[1].Label.Value != "But it's real. And if it's real it can be affected ... we may not be able" {
460 t.Fatalf("expected g.Objects[1].Label.Value to be last value: %#v", g.Objects[1].Label.Value)
461 }
462 },
463 },
464 {
465 name: "underscore_parent_preference_2",
466
467 text: `
468 y: "But it's real. And if it's real it can be affected ... we may not be able"
469 x: {
470 _.y: "All we are given is possibilities -- to make ourselves one thing or another."
471 }
472 `,
473 assertions: func(t *testing.T, g *d2graph.Graph) {
474 if len(g.Objects) != 2 {
475 t.Fatalf("expected 2 objects: %#v", g.Objects)
476 }
477 if g.Objects[0].ID != "y" {
478 t.Fatalf("expected g.Objects[0].ID to be y: %#v", g.Objects[0])
479 }
480 if g.Objects[1].ID != "x" {
481 t.Fatalf("expected g.Objects[1].ID to be x: %#v", g.Objects[1])
482 }
483
484 if len(g.Root.ChildrenArray) != 2 {
485 t.Fatalf("expected 2 objects at the root: %#v", len(g.Root.ChildrenArray))
486 }
487 if g.Objects[0].Label.Value != "All we are given is possibilities -- to make ourselves one thing or another." {
488 t.Fatalf("expected g.Objects[0].Label.Value to be last value: %#v", g.Objects[0].Label.Value)
489 }
490 },
491 },
492 {
493 name: "underscore_parent_squared",
494
495 text: `
496 x: {
497 y: {
498 _._.z
499 }
500 }
501 `,
502 assertions: func(t *testing.T, g *d2graph.Graph) {
503 if len(g.Objects) != 3 {
504 t.Fatalf("expected 3 objects: %#v", len(g.Objects))
505 }
506
507 if len(g.Root.ChildrenArray) != 2 {
508 t.Fatalf("expected 2 objects at the root: %#v", len(g.Root.ChildrenArray))
509 }
510 },
511 },
512 {
513 name: "underscore_parent_root",
514
515 text: `
516 _.x
517 `,
518 expErr: `d2/testdata/d2compiler/TestCompile/underscore_parent_root.d2:2:1: invalid underscore: no parent`,
519 },
520 {
521 name: "underscore_parent_middle_path",
522
523 text: `
524 x: {
525 y._.z
526 }
527 `,
528 expErr: `d2/testdata/d2compiler/TestCompile/underscore_parent_middle_path.d2:3:5: parent "_" can only be used in the beginning of paths, e.g. "_.x"`,
529 },
530 {
531 name: "underscore_parent_sandwich_path",
532
533 text: `
534 x: {
535 _.z._
536 }
537 `,
538 expErr: `d2/testdata/d2compiler/TestCompile/underscore_parent_sandwich_path.d2:3:7: parent "_" can only be used in the beginning of paths, e.g. "_.x"`,
539 },
540 {
541 name: "underscore_edge",
542
543 text: `
544 x: {
545 _.y -> _.x
546 }
547 `,
548 assertions: func(t *testing.T, g *d2graph.Graph) {
549 if len(g.Objects) != 2 {
550 t.Fatalf("expected 2 objects: %#v", g.Objects)
551 }
552 if g.Objects[0].ID != "x" {
553 t.Fatalf("expected g.Objects[0].ID to be x: %#v", g.Objects[0])
554 }
555 if g.Objects[1].ID != "y" {
556 t.Fatalf("expected g.Objects[1].ID to be y: %#v", g.Objects[1])
557 }
558
559 if len(g.Edges) != 1 {
560 t.Fatalf("expected 1 edge: %#v", g.Edges)
561 }
562 if g.Edges[0].Src.ID != "y" {
563 t.Fatalf("expected g.Edges[0].Src.ID to be y: %#v", g.Edges[0])
564 }
565 if g.Edges[0].Dst.ID != "x" {
566 t.Fatalf("expected g.Edges[0].Dst.ID to be x: %#v", g.Edges[0])
567 }
568 if g.Edges[0].SrcArrow {
569 t.Fatalf("expected g.Edges[0].SrcArrow to be false: %#v", g.Edges[0])
570 }
571 if !g.Edges[0].DstArrow {
572 t.Fatalf("expected g.Edges[0].DstArrow to be true: %#v", g.Edges[0])
573 }
574 },
575 },
576 {
577 name: "underscore_edge_chain",
578
579 text: `
580 x: {
581 _.y -> _.x -> _.z
582 }
583 `,
584 assertions: func(t *testing.T, g *d2graph.Graph) {
585 if len(g.Objects) != 3 {
586 t.Fatalf("expected 3 objects: %#v", g.Objects)
587 }
588 if g.Objects[0].ID != "x" {
589 t.Fatalf("expected g.Objects[0].ID to be x: %#v", g.Objects[0])
590 }
591 if g.Objects[1].ID != "y" {
592 t.Fatalf("expected g.Objects[1].ID to be y: %#v", g.Objects[1])
593 }
594 if g.Objects[2].ID != "z" {
595 t.Fatalf("expected g.Objects[2].ID to be z: %#v", g.Objects[2])
596 }
597
598 if len(g.Edges) != 2 {
599 t.Fatalf("expected 2 edge: %#v", g.Edges)
600 }
601 if g.Edges[0].Src.ID != "y" {
602 t.Fatalf("expected g.Edges[0].Src.ID to be y: %#v", g.Edges[0])
603 }
604 if g.Edges[0].Dst.ID != "x" {
605 t.Fatalf("expected g.Edges[0].Dst.ID to be x: %#v", g.Edges[0])
606 }
607 if g.Edges[1].Src.ID != "x" {
608 t.Fatalf("expected g.Edges[1].Src.ID to be x: %#v", g.Edges[1])
609 }
610 if g.Edges[1].Dst.ID != "z" {
611 t.Fatalf("expected g.Edges[1].Dst.ID to be z: %#v", g.Edges[1])
612 }
613 },
614 },
615 {
616 name: "md_block_string_err",
617
618 text: `test: |md
619 # What about pipes
620
621 Will escaping \| work?
622 |
623 `,
624 expErr: `d2/testdata/d2compiler/TestCompile/md_block_string_err.d2:4:19: unexpected text after md block string. See https://d2lang.com/tour/text#advanced-block-strings.
625 d2/testdata/d2compiler/TestCompile/md_block_string_err.d2:5:1: block string must be terminated with |`,
626 },
627 {
628 name: "no_empty_block_string",
629 text: `Text: |md |`,
630 expErr: `d2/testdata/d2compiler/TestCompile/no_empty_block_string.d2:1:1: block string cannot be empty`,
631 },
632 {
633 name: "no_white_spaces_only_block_string",
634 text: `Text: |md |`,
635 expErr: `d2/testdata/d2compiler/TestCompile/no_white_spaces_only_block_string.d2:1:1: block string cannot be empty`,
636 },
637 {
638 name: "no_new_lines_only_block_string",
639 text: `Text: |md
640
641
642 |`,
643 expErr: `d2/testdata/d2compiler/TestCompile/no_new_lines_only_block_string.d2:1:1: block string cannot be empty`,
644 },
645 {
646 name: "underscore_edge_existing",
647
648 text: `
649 a -> b: "Can you imagine how life could be improved if we could do away with"
650 x: {
651 _.a -> _.b: "Well, it's garish, ugly, and derelicts have used it for a toilet."
652 }
653 `,
654 assertions: func(t *testing.T, g *d2graph.Graph) {
655 if len(g.Objects) != 3 {
656 t.Fatalf("expected 3 objects: %#v", g.Objects)
657 }
658 if len(g.Edges) != 2 {
659 t.Fatalf("expected 2 edge: %#v", g.Edges)
660 }
661 if g.Edges[0].Src.ID != "a" {
662 t.Fatalf("expected g.Edges[0].Src.ID to be a: %#v", g.Edges[0])
663 }
664 if g.Edges[0].Dst.ID != "b" {
665 t.Fatalf("expected g.Edges[0].Dst.ID to be b: %#v", g.Edges[0])
666 }
667 if g.Edges[1].Src.ID != "a" {
668 t.Fatalf("expected g.Edges[1].Src.ID to be a: %#v", g.Edges[1])
669 }
670 if g.Edges[1].Dst.ID != "b" {
671 t.Fatalf("expected g.Edges[1].Dst.ID to be b: %#v", g.Edges[1])
672 }
673 if g.Edges[0].Label.Value != "Can you imagine how life could be improved if we could do away with" {
674 t.Fatalf("unexpected g.Edges[0].Label: %#v", g.Edges[0].Label)
675 }
676 if g.Edges[1].Label.Value != "Well, it's garish, ugly, and derelicts have used it for a toilet." {
677 t.Fatalf("unexpected g.Edges[1].Label: %#v", g.Edges[1].Label)
678 }
679 },
680 },
681 {
682 name: "underscore_edge_index",
683
684 text: `
685 a -> b: "Can you imagine how life could be improved if we could do away with"
686 x: {
687 (_.a -> _.b)[0]: "Well, it's garish, ugly, and derelicts have used it for a toilet."
688 }
689 `,
690 assertions: func(t *testing.T, g *d2graph.Graph) {
691 if len(g.Objects) != 3 {
692 t.Fatalf("expected 3 objects: %#v", g.Objects)
693 }
694 if len(g.Edges) != 1 {
695 t.Fatalf("expected 1 edge: %#v", g.Edges)
696 }
697 if g.Edges[0].Src.ID != "a" {
698 t.Fatalf("expected g.Edges[0].Src.ID to be a: %#v", g.Edges[0])
699 }
700 if g.Edges[0].Dst.ID != "b" {
701 t.Fatalf("expected g.Edges[0].Dst.ID to be b: %#v", g.Edges[0])
702 }
703 if g.Edges[0].Label.Value != "Well, it's garish, ugly, and derelicts have used it for a toilet." {
704 t.Fatalf("unexpected g.Edges[0].Label: %#v", g.Edges[0].Label)
705 }
706 },
707 },
708 {
709 name: "underscore_edge_nested",
710
711 text: `
712 x: {
713 y: {
714 _._.z -> _.y
715 }
716 }
717 `,
718 assertions: func(t *testing.T, g *d2graph.Graph) {
719 if len(g.Objects) != 3 {
720 t.Fatalf("expected 3 objects: %#v", g.Objects)
721 }
722 if len(g.Edges) != 1 {
723 t.Fatalf("expected 1 edge: %#v", g.Edges)
724 }
725 if g.Edges[0].Src.AbsID() != "z" {
726 t.Fatalf("expected g.Edges[0].Src.AbsID() to be z: %#v", g.Edges[0].Src.AbsID())
727 }
728 if g.Edges[0].Dst.AbsID() != "x.y" {
729 t.Fatalf("expected g.Edges[0].Dst.AbsID() to be x.y: %#v", g.Edges[0].Dst.AbsID())
730 }
731 },
732 },
733 {
734 name: "edge",
735
736 text: `
737 x -> y
738 `,
739 assertions: func(t *testing.T, g *d2graph.Graph) {
740 if len(g.Objects) != 2 {
741 t.Fatalf("expected 2 objects: %#v", g.Objects)
742 }
743 if g.Objects[0].ID != "x" {
744 t.Fatalf("expected g.Objects[0].ID to be x: %#v", g.Objects[0])
745 }
746 if g.Objects[1].ID != "y" {
747 t.Fatalf("expected g.Objects[1].ID to be y: %#v", g.Objects[1])
748 }
749
750 if len(g.Edges) != 1 {
751 t.Fatalf("expected 1 edge: %#v", g.Edges)
752 }
753 if g.Edges[0].Src.ID != "x" {
754 t.Fatalf("expected g.Edges[0].Src.ID to be x: %#v", g.Edges[0])
755 }
756 if g.Edges[0].Dst.ID != "y" {
757 t.Fatalf("expected g.Edges[0].Dst.ID to be y: %#v", g.Edges[0])
758 }
759 if g.Edges[0].SrcArrow {
760 t.Fatalf("expected g.Edges[0].SrcArrow to be false: %#v", g.Edges[0])
761 }
762 if !g.Edges[0].DstArrow {
763 t.Fatalf("expected g.Edges[0].DstArrow to be true: %#v", g.Edges[0])
764 }
765 },
766 },
767 {
768 name: "edge_chain",
769
770 text: `
771 x -> y -> z: "The kids will love our inflatable slides"
772 `,
773 assertions: func(t *testing.T, g *d2graph.Graph) {
774 if len(g.Objects) != 3 {
775 t.Fatalf("expected 2 objects: %#v", g.Objects)
776 }
777 if g.Objects[0].ID != "x" {
778 t.Fatalf("expected g.Objects[0].ID to be x: %#v", g.Objects[0])
779 }
780 if g.Objects[1].ID != "y" {
781 t.Fatalf("expected g.Objects[1].ID to be y: %#v", g.Objects[1])
782 }
783 if g.Objects[2].ID != "z" {
784 t.Fatalf("expected g.Objects[2].ID to be z: %#v", g.Objects[2])
785 }
786
787 if len(g.Edges) != 2 {
788 t.Fatalf("expected 2 edge: %#v", g.Edges)
789 }
790 if g.Edges[0].Src.ID != "x" {
791 t.Fatalf("expected g.Edges[0].Src.ID to be x: %#v", g.Edges[0])
792 }
793 if g.Edges[0].Dst.ID != "y" {
794 t.Fatalf("expected g.Edges[0].Dst.ID to be y: %#v", g.Edges[0])
795 }
796 if g.Edges[1].Src.ID != "y" {
797 t.Fatalf("expected g.Edges[1].Src.ID to be x: %#v", g.Edges[1])
798 }
799 if g.Edges[1].Dst.ID != "z" {
800 t.Fatalf("expected g.Edges[1].Dst.ID to be y: %#v", g.Edges[1])
801 }
802
803 if g.Edges[0].Label.Value != "The kids will love our inflatable slides" {
804 t.Fatalf("unexpected g.Edges[0].Label: %#v", g.Edges[0].Label.Value)
805 }
806 if g.Edges[1].Label.Value != "The kids will love our inflatable slides" {
807 t.Fatalf("unexpected g.Edges[1].Label: %#v", g.Edges[1].Label.Value)
808 }
809 },
810 },
811 {
812 name: "edge_index",
813
814 text: `
815 x -> y: one
816 (x -> y)[0]: two
817 `,
818 assertions: func(t *testing.T, g *d2graph.Graph) {
819 if len(g.Objects) != 2 {
820 t.Fatalf("expected 2 objects: %#v", g.Objects)
821 }
822 if g.Objects[0].ID != "x" {
823 t.Fatalf("expected g.Objects[0].ID to be x: %#v", g.Objects[0])
824 }
825 if g.Objects[1].ID != "y" {
826 t.Fatalf("expected g.Objects[1].ID to be y: %#v", g.Objects[1])
827 }
828
829 if len(g.Edges) != 1 {
830 t.Fatalf("expected 1 edge: %#v", g.Edges)
831 }
832 if g.Edges[0].Src.ID != "x" {
833 t.Fatalf("expected g.Edges[0].Src.ID to be x: %#v", g.Edges[0].Src)
834 }
835 if g.Edges[0].Dst.ID != "y" {
836 t.Fatalf("expected g.Edges[0].Dst.ID to be y: %#v", g.Edges[0].Dst)
837 }
838 if g.Edges[0].SrcArrow {
839 t.Fatalf("expected g.Edges[0].SrcArrow to be false: %#v", g.Edges[0].SrcArrow)
840 }
841 if !g.Edges[0].DstArrow {
842 t.Fatalf("expected g.Edges[0].DstArrow to be true: %#v", g.Edges[0].DstArrow)
843 }
844 if g.Edges[0].Label.Value != "two" {
845 t.Fatalf("expected g.Edges[0].Label to be two: %#v", g.Edges[0].Label)
846 }
847 },
848 },
849 {
850 name: "edge_index_nested",
851
852 text: `
853 b: {
854 x -> y: one
855 (x -> y)[0]: two
856 }
857 `,
858 assertions: func(t *testing.T, g *d2graph.Graph) {
859 if len(g.Objects) != 3 {
860 t.Fatalf("expected 3 objects: %#v", g.Objects)
861 }
862 if g.Objects[0].ID != "b" {
863 t.Fatalf("expected g.Objects[0].ID to be b: %#v", g.Objects[0])
864 }
865 if g.Objects[1].ID != "x" {
866 t.Fatalf("expected g.Objects[1].ID to be x: %#v", g.Objects[0])
867 }
868 if g.Objects[2].ID != "y" {
869 t.Fatalf("expected g.Objects[2].ID to be y: %#v", g.Objects[1])
870 }
871
872 if len(g.Edges) != 1 {
873 t.Fatalf("expected 1 edge: %#v", g.Edges)
874 }
875 if g.Edges[0].Src.AbsID() != "b.x" {
876 t.Fatalf("expected g.Edges[0].Src.AbsoluteID() to be x: %#v", g.Edges[0].Src)
877 }
878 if g.Edges[0].Dst.AbsID() != "b.y" {
879 t.Fatalf("expected g.Edges[0].Dst.AbsoluteID() to be y: %#v", g.Edges[0].Dst)
880 }
881 if g.Edges[0].SrcArrow {
882 t.Fatalf("expected g.Edges[0].SrcArrow to be false: %#v", g.Edges[0].SrcArrow)
883 }
884 if !g.Edges[0].DstArrow {
885 t.Fatalf("expected g.Edges[0].DstArrow to be true: %#v", g.Edges[0].DstArrow)
886 }
887 if g.Edges[0].Label.Value != "two" {
888 t.Fatalf("expected g.Edges[0].Label to be two: %#v", g.Edges[0].Label)
889 }
890 },
891 },
892 {
893 name: "edge_index_nested_cross_scope",
894
895 text: `
896 b: {
897 x -> y: one
898 }
899 b.(x -> y)[0]: two
900 `,
901 assertions: func(t *testing.T, g *d2graph.Graph) {
902 if len(g.Objects) != 3 {
903 t.Fatalf("expected 3 objects: %#v", g.Objects)
904 }
905 if g.Objects[0].ID != "b" {
906 t.Fatalf("expected g.Objects[0].ID to be b: %#v", g.Objects[0])
907 }
908 if g.Objects[1].ID != "x" {
909 t.Fatalf("expected g.Objects[1].ID to be x: %#v", g.Objects[0])
910 }
911 if g.Objects[2].ID != "y" {
912 t.Fatalf("expected g.Objects[2].ID to be y: %#v", g.Objects[1])
913 }
914
915 if len(g.Edges) != 1 {
916 t.Fatalf("expected 1 edge: %#v", g.Edges)
917 }
918 if g.Edges[0].Src.AbsID() != "b.x" {
919 t.Fatalf("expected g.Edges[0].Src.AbsoluteID() to be x: %#v", g.Edges[0].Src)
920 }
921 if g.Edges[0].Dst.AbsID() != "b.y" {
922 t.Fatalf("expected g.Edges[0].Dst.AbsoluteID() to be y: %#v", g.Edges[0].Dst)
923 }
924 if g.Edges[0].SrcArrow {
925 t.Fatalf("expected g.Edges[0].SrcArrow to be false: %#v", g.Edges[0].SrcArrow)
926 }
927 if !g.Edges[0].DstArrow {
928 t.Fatalf("expected g.Edges[0].DstArrow to be true: %#v", g.Edges[0].DstArrow)
929 }
930 if g.Edges[0].Label.Value != "two" {
931 t.Fatalf("expected g.Edges[0].Label to be two: %#v", g.Edges[0].Label)
932 }
933 },
934 },
935 {
936 name: "unsemantic_markdown",
937
938 text: `test:|
939 foobar
940 <p>
941 |
942 `,
943 expErr: `d2/testdata/d2compiler/TestCompile/unsemantic_markdown.d2:1:1: malformed Markdown: element <p> closed by </div>`,
944 },
945 {
946 name: "unsemantic_markdown_2",
947
948 text: `test:|
949 foo<br>
950 bar
951 |
952 `,
953 expErr: `d2/testdata/d2compiler/TestCompile/unsemantic_markdown_2.d2:1:1: malformed Markdown: element <br> closed by </p>`,
954 },
955 {
956 name: "edge_map",
957
958 text: `
959 x -> y: {
960 label: "Space: the final frontier. These are the voyages of the starship Enterprise."
961 }
962 `,
963 assertions: func(t *testing.T, g *d2graph.Graph) {
964 if len(g.Objects) != 2 {
965 t.Fatalf("expected 2 objects: %#v", g.Objects)
966 }
967 if g.Objects[0].ID != "x" {
968 t.Fatalf("expected g.Objects[0].ID to be x: %#v", g.Objects[0])
969 }
970 if g.Objects[1].ID != "y" {
971 t.Fatalf("expected g.Objects[1].ID to be y: %#v", g.Objects[1])
972 }
973
974 if len(g.Edges) != 1 {
975 t.Fatalf("expected 1 edge: %#v", g.Edges)
976 }
977 if g.Edges[0].Src.ID != "x" {
978 t.Fatalf("expected g.Edges[0].Src.ID to be x: %#v", g.Edges[0])
979 }
980 if g.Edges[0].Dst.ID != "y" {
981 t.Fatalf("expected g.Edges[0].Dst.ID to be y: %#v", g.Edges[0])
982 }
983 if g.Edges[0].Label.Value != "Space: the final frontier. These are the voyages of the starship Enterprise." {
984 t.Fatalf("unexpected g.Edges[0].Label.Value : %#v", g.Edges[0].Label.Value)
985 }
986 },
987 },
988 {
989 name: "edge_label_map",
990
991 text: `hey y9 -> qwer: asdf {style.opacity: 0.5}
992 `,
993 assertions: func(t *testing.T, g *d2graph.Graph) {
994 if len(g.Edges) != 1 {
995 t.Fatalf("expected 1 edge: %#v", g.Edges)
996 }
997 if g.Edges[0].Label.Value != "asdf" {
998 t.Fatalf("unexpected g.Edges[0].Label.Value : %#v", g.Edges[0].Label.Value)
999 }
1000 },
1001 },
1002 {
1003 name: "edge_map_arrowhead",
1004
1005 text: `x -> y: {
1006 source-arrowhead: {
1007 shape: diamond
1008 }
1009 }
1010 `,
1011 assertions: func(t *testing.T, g *d2graph.Graph) {
1012 if len(g.Edges) != 1 {
1013 t.Fatalf("expected 1 edge: %#v", g.Edges)
1014 }
1015 if len(g.Objects) != 2 {
1016 t.Fatalf("expected 2 objects: %#v", g.Objects)
1017 }
1018 assert.String(t, "diamond", g.Edges[0].SrcArrowhead.Shape.Value)
1019 assert.String(t, "", g.Edges[0].Shape.Value)
1020
1021 exp := `x -> y: {
1022 source-arrowhead: {
1023 shape: diamond
1024 }
1025 }
1026 `
1027 newText := d2format.Format(g.AST)
1028 ds, err := diff.Strings(exp, newText)
1029 if err != nil {
1030 t.Fatal(err)
1031 }
1032 if ds != "" {
1033 t.Fatalf("exp != newText:\n%s", ds)
1034 }
1035 },
1036 },
1037 {
1038 name: "edge_arrowhead_primary",
1039
1040 text: `x -> y: {
1041 source-arrowhead: Reisner's Rule of Conceptual Inertia
1042 }
1043 `,
1044 assertions: func(t *testing.T, g *d2graph.Graph) {
1045 assert.String(t, "Reisner's Rule of Conceptual Inertia", g.Edges[0].SrcArrowhead.Label.Value)
1046 },
1047 },
1048 {
1049 name: "edge_arrowhead_fields",
1050
1051 text: `x -> y: {
1052 source-arrowhead: Reisner's Rule of Conceptual Inertia {
1053 shape: diamond
1054 }
1055 target-arrowhead: QOTD
1056 target-arrowhead: {
1057 style.filled: true
1058 }
1059 }
1060 `,
1061 assertions: func(t *testing.T, g *d2graph.Graph) {
1062 if len(g.Edges) != 1 {
1063 t.Fatalf("expected 1 edge: %#v", g.Edges)
1064 }
1065 if len(g.Objects) != 2 {
1066 t.Fatalf("expected 2 objects: %#v", g.Objects)
1067 }
1068 assert.String(t, "diamond", g.Edges[0].SrcArrowhead.Shape.Value)
1069 assert.String(t, "Reisner's Rule of Conceptual Inertia", g.Edges[0].SrcArrowhead.Label.Value)
1070 assert.String(t, "QOTD", g.Edges[0].DstArrowhead.Label.Value)
1071 assert.String(t, "true", g.Edges[0].DstArrowhead.Style.Filled.Value)
1072 assert.String(t, "", g.Edges[0].Shape.Value)
1073 assert.String(t, "", g.Edges[0].Label.Value)
1074 assert.JSON(t, nil, g.Edges[0].Style.Filled)
1075 },
1076 },
1077 {
1078 name: "edge_flat_arrowhead",
1079
1080 text: `x -> y
1081 (x -> y)[0].source-arrowhead.shape: diamond
1082 `,
1083 assertions: func(t *testing.T, g *d2graph.Graph) {
1084 if len(g.Edges) != 1 {
1085 t.Fatalf("expected 1 edge: %#v", g.Edges)
1086 }
1087 if len(g.Objects) != 2 {
1088 t.Fatalf("expected 2 objects: %#v", g.Objects)
1089 }
1090 assert.String(t, "diamond", g.Edges[0].SrcArrowhead.Shape.Value)
1091 assert.String(t, "", g.Edges[0].Shape.Value)
1092 },
1093 },
1094 {
1095
1096 name: "edge_non_shape_arrowhead",
1097
1098 text: `x -> y: { source-arrowhead.shape: triangle }
1099 `,
1100 assertions: func(t *testing.T, g *d2graph.Graph) {
1101 if len(g.Edges) != 1 {
1102 t.Fatalf("expected 1 edge: %#v", g.Edges)
1103 }
1104 if len(g.Objects) != 2 {
1105 t.Fatalf("expected 2 objects: %#v", g.Objects)
1106 }
1107 assert.String(t, "triangle", g.Edges[0].SrcArrowhead.Shape.Value)
1108 assert.String(t, "", g.Edges[0].Shape.Value)
1109 },
1110 },
1111 {
1112 name: "object_arrowhead_shape",
1113
1114 text: `x: {shape: triangle}
1115 `,
1116 expErr: `d2/testdata/d2compiler/TestCompile/object_arrowhead_shape.d2:1:5: invalid shape, can only set "triangle" for arrowheads`,
1117 },
1118 {
1119 name: "edge_flat_label_arrowhead",
1120
1121 text: `x -> y: {
1122 # comment
1123 source-arrowhead.label: yo
1124 }
1125 `,
1126 assertions: func(t *testing.T, g *d2graph.Graph) {
1127 if len(g.Edges) != 1 {
1128 t.Fatalf("expected 1 edge: %#v", g.Edges)
1129 }
1130 if len(g.Objects) != 2 {
1131 t.Fatalf("expected 2 objects: %#v", g.Objects)
1132 }
1133 assert.String(t, "yo", g.Edges[0].SrcArrowhead.Label.Value)
1134 assert.String(t, "", g.Edges[0].Label.Value)
1135 },
1136 },
1137 {
1138 name: "edge_semiflat_arrowhead",
1139
1140 text: `x -> y
1141 (x -> y)[0].source-arrowhead: {
1142 shape: diamond
1143 }
1144 `,
1145 assertions: func(t *testing.T, g *d2graph.Graph) {
1146 if len(g.Edges) != 1 {
1147 t.Fatalf("expected 1 edge: %#v", g.Edges)
1148 }
1149 if len(g.Objects) != 2 {
1150 t.Fatalf("expected 2 objects: %#v", g.Objects)
1151 }
1152 assert.String(t, "diamond", g.Edges[0].SrcArrowhead.Shape.Value)
1153 assert.String(t, "", g.Edges[0].Shape.Value)
1154 },
1155 },
1156 {
1157 name: "edge_mixed_arrowhead",
1158
1159 text: `x -> y: {
1160 target-arrowhead.shape: diamond
1161 }
1162 (x -> y)[0].source-arrowhead: {
1163 shape: diamond
1164 }
1165 `,
1166 assertions: func(t *testing.T, g *d2graph.Graph) {
1167 if len(g.Edges) != 1 {
1168 t.Fatalf("expected 1 edge: %#v", g.Edges)
1169 }
1170 if len(g.Objects) != 2 {
1171 t.Fatalf("expected 2 objects: %#v", g.Objects)
1172 }
1173 assert.String(t, "diamond", g.Edges[0].SrcArrowhead.Shape.Value)
1174 assert.String(t, "diamond", g.Edges[0].DstArrowhead.Shape.Value)
1175 assert.String(t, "", g.Edges[0].Shape.Value)
1176 },
1177 },
1178 {
1179 name: "edge_exclusive_style",
1180
1181 text: `
1182 x -> y: {
1183 style.animated: true
1184 }
1185 `,
1186 assertions: func(t *testing.T, g *d2graph.Graph) {
1187 if len(g.Edges) != 1 {
1188 t.Fatalf("expected 1 edge: %#v", g.Edges)
1189 }
1190 if g.Edges[0].Style.Animated.Value != "true" {
1191 t.Fatalf("Edges[0].Style.Animated.Value: %#v", g.Edges[0].Style.Animated.Value)
1192 }
1193 },
1194 },
1195 {
1196 name: "nested_edge",
1197
1198 text: `sequence -> quest: {
1199 space -> stars
1200 }
1201 `,
1202 expErr: `d2/testdata/d2compiler/TestCompile/nested_edge.d2:2:3: cannot create edge inside edge`,
1203 },
1204 {
1205 name: "shape_edge_style",
1206
1207 text: `
1208 x: {
1209 style.animated: true
1210 }
1211 `,
1212 expErr: `d2/testdata/d2compiler/TestCompile/shape_edge_style.d2:3:2: key "animated" can only be applied to edges`,
1213 },
1214 {
1215 name: "edge_invalid_style",
1216
1217 text: `x -> y: {
1218 opacity: 0.5
1219 }
1220 `,
1221 expErr: `d2/testdata/d2compiler/TestCompile/edge_invalid_style.d2:2:3: opacity must be style.opacity`,
1222 },
1223 {
1224 name: "obj_invalid_style",
1225
1226 text: `x: {
1227 opacity: 0.5
1228 }
1229 `,
1230 expErr: `d2/testdata/d2compiler/TestCompile/obj_invalid_style.d2:2:3: opacity must be style.opacity`,
1231 },
1232 {
1233 name: "edge_chain_map",
1234
1235 text: `
1236 x -> y -> z: {
1237 label: "Space: the final frontier. These are the voyages of the starship Enterprise."
1238 }
1239 `,
1240 assertions: func(t *testing.T, g *d2graph.Graph) {
1241 if len(g.Objects) != 3 {
1242 t.Fatalf("expected 3 objects: %#v", g.Objects)
1243 }
1244
1245 if len(g.Edges) != 2 {
1246 t.Fatalf("expected 2 edge: %#v", g.Edges)
1247 }
1248 if g.Edges[0].Label.Value != "Space: the final frontier. These are the voyages of the starship Enterprise." {
1249 t.Fatalf("unexpected g.Edges[0].Label.Value : %#v", g.Edges[0].Label.Value)
1250 }
1251 if g.Edges[1].Label.Value != "Space: the final frontier. These are the voyages of the starship Enterprise." {
1252 t.Fatalf("unexpected g.Edges[0].Label.Value : %#v", g.Edges[1].Label.Value)
1253 }
1254 },
1255 },
1256 {
1257 name: "edge_index_map",
1258
1259 text: `
1260 x -> y
1261 (x -> y)[0]: {
1262 label: "Space: the final frontier. These are the voyages of the starship Enterprise."
1263 }
1264 `,
1265 assertions: func(t *testing.T, g *d2graph.Graph) {
1266 if len(g.Objects) != 2 {
1267 t.Fatalf("expected 2 objects: %#v", g.Objects)
1268 }
1269
1270 if len(g.Edges) != 1 {
1271 t.Fatalf("expected 1 edge: %#v", g.Edges)
1272 }
1273 if g.Edges[0].Label.Value != "Space: the final frontier. These are the voyages of the starship Enterprise." {
1274 t.Fatalf("unexpected g.Edges[0].Label.Value : %#v", g.Edges[0].Label.Value)
1275 }
1276 },
1277 },
1278 {
1279 name: "edge_map_nested",
1280
1281 text: `
1282 x -> y: {
1283 style: {
1284 opacity: 0.4
1285 }
1286 }
1287 `,
1288 assertions: func(t *testing.T, g *d2graph.Graph) {
1289 if len(g.Objects) != 2 {
1290 t.Fatalf("expected 2 objects: %#v", g.Objects)
1291 }
1292
1293 if len(g.Edges) != 1 {
1294 t.Fatalf("expected 1 edge: %#v", g.Edges)
1295 }
1296 if g.Edges[0].Style.Opacity.Value != "0.4" {
1297 t.Fatalf("unexpected g.Edges[0].Style.Opacity.Value: %#v", g.Edges[0].Style.Opacity.Value)
1298 }
1299 },
1300 },
1301 {
1302 name: "edge_map_nested_flat",
1303
1304 text: `
1305 x -> y: {
1306 style.opacity: 0.4
1307 }
1308 `,
1309 assertions: func(t *testing.T, g *d2graph.Graph) {
1310 if len(g.Objects) != 2 {
1311 t.Fatalf("expected 2 objects: %#v", g.Objects)
1312 }
1313
1314 if len(g.Edges) != 1 {
1315 t.Fatalf("expected 1 edge: %#v", g.Edges)
1316 }
1317 if g.Edges[0].Style.Opacity.Value != "0.4" {
1318 t.Fatalf("unexpected g.Edges[0].Style.Opacity.Value: %#v", g.Edges[0].Style.Opacity.Value)
1319 }
1320 if g.Edges[0].Label.Value != "" {
1321 t.Fatalf("unexpected g.Edges[0].Label.Value : %#v", g.Edges[0].Label.Value)
1322 }
1323 },
1324 },
1325 {
1326 name: "edge_map_group_flat",
1327
1328 text: `
1329 x -> y
1330 (x -> y)[0].style.opacity: 0.4
1331 `,
1332 assertions: func(t *testing.T, g *d2graph.Graph) {
1333 if len(g.Objects) != 2 {
1334 t.Fatalf("expected 2 objects: %#v", g.Objects)
1335 }
1336
1337 if len(g.Edges) != 1 {
1338 t.Fatalf("expected 1 edge: %#v", g.Edges)
1339 }
1340 if g.Edges[0].Style.Opacity.Value != "0.4" {
1341 t.Fatalf("unexpected g.Edges[0].Style.Opacity.Value: %#v", g.Edges[0].Style.Opacity.Value)
1342 }
1343 if g.Edges[0].Label.Value != "" {
1344 t.Fatalf("unexpected g.Edges[0].Label.Value : %#v", g.Edges[0].Label.Value)
1345 }
1346 },
1347 },
1348 {
1349 name: "edge_map_group_semiflat",
1350
1351 text: `x -> y
1352 (x -> y)[0].style: {
1353 opacity: 0.4
1354 }
1355 `,
1356 assertions: func(t *testing.T, g *d2graph.Graph) {
1357 if len(g.Objects) != 2 {
1358 t.Fatalf("expected 2 objects: %#v", g.Objects)
1359 }
1360
1361 if len(g.Edges) != 1 {
1362 t.Fatalf("expected 1 edge: %#v", g.Edges)
1363 }
1364 if g.Edges[0].Style.Opacity.Value != "0.4" {
1365 t.Fatalf("unexpected g.Edges[0].Style.Opacity.Value: %#v", g.Edges[0].Style.Opacity.Value)
1366 }
1367 if g.Edges[0].Label.Value != "" {
1368 t.Fatalf("unexpected g.Edges[0].Label.Value : %#v", g.Edges[0].Label.Value)
1369 }
1370 },
1371 },
1372 {
1373 name: "edge_key_group_flat_nested",
1374
1375 text: `
1376 x: {
1377 a -> b
1378 }
1379 x.(a -> b)[0].style.opacity: 0.4
1380 `,
1381 assertions: func(t *testing.T, g *d2graph.Graph) {
1382 if len(g.Objects) != 3 {
1383 t.Fatalf("expected 3 objects: %#v", g.Objects)
1384 }
1385
1386 if len(g.Edges) != 1 {
1387 t.Fatalf("expected 1 edge: %#v", g.Edges)
1388 }
1389 if g.Edges[0].Style.Opacity.Value != "0.4" {
1390 t.Fatalf("unexpected g.Edges[0].Style.Opacity.Value: %#v", g.Edges[0].Style.Opacity.Value)
1391 }
1392 if g.Edges[0].Label.Value != "" {
1393 t.Fatalf("unexpected g.Edges[0].Label.Value : %#v", g.Edges[0].Label.Value)
1394 }
1395 },
1396 },
1397 {
1398 name: "edge_key_group_flat_nested_underscore",
1399
1400 text: `
1401 a -> b
1402 x: {
1403 (_.a -> _.b)[0].style.opacity: 0.4
1404 }
1405 `,
1406 assertions: func(t *testing.T, g *d2graph.Graph) {
1407 if len(g.Objects) != 3 {
1408 t.Fatalf("expected 3 objects: %#v", g.Objects)
1409 }
1410
1411 if len(g.Edges) != 1 {
1412 t.Fatalf("expected 1 edge: %#v", g.Edges)
1413 }
1414 if g.Edges[0].Style.Opacity.Value != "0.4" {
1415 t.Fatalf("unexpected g.Edges[0].Style.Opacity.Value: %#v", g.Edges[0].Style.Opacity.Value)
1416 }
1417 if g.Edges[0].Label.Value != "" {
1418 t.Fatalf("unexpected g.Edges[0].Label.Value : %#v", g.Edges[0].Label.Value)
1419 }
1420 },
1421 },
1422 {
1423 name: "edge_key_group_map_nested_underscore",
1424
1425 text: `
1426 a -> b
1427 x: {
1428 (_.a -> _.b)[0]: {
1429 style: {
1430 opacity: 0.4
1431 }
1432 }
1433 }
1434 `,
1435 assertions: func(t *testing.T, g *d2graph.Graph) {
1436 if len(g.Objects) != 3 {
1437 t.Fatalf("expected 3 objects: %#v", g.Objects)
1438 }
1439
1440 if len(g.Edges) != 1 {
1441 t.Fatalf("expected 1 edge: %#v", g.Edges)
1442 }
1443 if g.Edges[0].Style.Opacity.Value != "0.4" {
1444 t.Fatalf("unexpected g.Edges[0].Style.Opacity.Value: %#v", g.Edges[0].Style.Opacity.Value)
1445 }
1446 if g.Edges[0].Label.Value != "" {
1447 t.Fatalf("unexpected g.Edges[0].Label.Value : %#v", g.Edges[0].Label.Value)
1448 }
1449 },
1450 },
1451 {
1452 name: "edge_key_group_map_flat_nested_underscore",
1453
1454 text: `
1455 a -> b
1456 x: {
1457 (_.a -> _.b)[0]: {
1458 style.opacity: 0.4
1459 }
1460 }
1461 `,
1462 assertions: func(t *testing.T, g *d2graph.Graph) {
1463 if len(g.Objects) != 3 {
1464 t.Fatalf("expected 3 objects: %#v", g.Objects)
1465 }
1466
1467 if len(g.Edges) != 1 {
1468 t.Fatalf("expected 1 edge: %#v", g.Edges)
1469 }
1470 if g.Edges[0].Style.Opacity.Value != "0.4" {
1471 t.Fatalf("unexpected g.Edges[0].Style.Opacity.Value: %#v", g.Edges[0].Style.Opacity.Value)
1472 }
1473 if g.Edges[0].Label.Value != "" {
1474 t.Fatalf("unexpected g.Edges[0].Label.Value : %#v", g.Edges[0].Label.Value)
1475 }
1476 },
1477 },
1478 {
1479 name: "edge_map_non_reserved",
1480
1481 text: `
1482 x -> y: {
1483 z
1484 }
1485 `,
1486 expErr: `d2/testdata/d2compiler/TestCompile/edge_map_non_reserved.d2:3:3: edge map keys must be reserved keywords`,
1487 },
1488 {
1489 name: "url_link",
1490
1491 text: `x: {
1492 link: https://google.com
1493 }
1494 `,
1495 assertions: func(t *testing.T, g *d2graph.Graph) {
1496 if len(g.Objects) != 1 {
1497 t.Fatal(g.Objects)
1498 }
1499 if g.Objects[0].Link.Value != "https://google.com" {
1500 t.Fatal(g.Objects[0].Link.Value)
1501 }
1502 },
1503 },
1504 {
1505 name: "url_tooltip",
1506 text: `x: {tooltip: https://google.com}`,
1507 assertions: func(t *testing.T, g *d2graph.Graph) {
1508 if len(g.Objects) != 1 {
1509 t.Fatal(g.Objects)
1510 }
1511
1512 if g.Objects[0].Tooltip.Value != "https://google.com" {
1513 t.Fatal(g.Objects[0].Tooltip.Value)
1514 }
1515 },
1516 },
1517 {
1518 name: "no_url_link_and_url_tooltip_concurrently",
1519 text: `x: {link: https://not-google.com; tooltip: https://google.com}`,
1520 expErr: `d2/testdata/d2compiler/TestCompile/no_url_link_and_url_tooltip_concurrently.d2:1:44: Tooltip cannot be set to URL when link is also set (for security)`,
1521 },
1522 {
1523 name: "url_link_non_url_tooltip_ok",
1524 text: `x: {link: https://not-google.com; tooltip: note: url.ParseRequestURI might see this as a URL}`,
1525 expErr: ``,
1526 },
1527 {
1528 name: "url_link_and_not_url_tooltip_concurrently",
1529 text: `x: {link: https://google.com; tooltip: hello world}`,
1530 assertions: func(t *testing.T, g *d2graph.Graph) {
1531 if len(g.Objects) != 1 {
1532 t.Fatal(g.Objects)
1533 }
1534 if g.Objects[0].Link.Value != "https://google.com" {
1535 t.Fatal(g.Objects[0].Link.Value)
1536 }
1537
1538 if g.Objects[0].Tooltip.Value != "hello world" {
1539 t.Fatal(g.Objects[0].Tooltip.Value)
1540 }
1541 },
1542 },
1543 {
1544 name: "nil_scope_obj_regression",
1545
1546 text: `a
1547 b: {
1548 _.a
1549 }
1550 `,
1551 assertions: func(t *testing.T, g *d2graph.Graph) {
1552 tassert.Equal(t, "a", g.Objects[0].ID)
1553 for _, ref := range g.Objects[0].References {
1554 tassert.NotNil(t, ref.ScopeObj)
1555 }
1556 },
1557 },
1558 {
1559 name: "path_link",
1560
1561 text: `x: {
1562 link: Overview.Untitled board 7.zzzzz
1563 }
1564 `,
1565 assertions: func(t *testing.T, g *d2graph.Graph) {
1566 if len(g.Objects) != 1 {
1567 t.Fatal(g.Objects)
1568 }
1569 if g.Objects[0].Link.Value != "Overview.Untitled board 7.zzzzz" {
1570 t.Fatal(g.Objects[0].Link.Value)
1571 }
1572 },
1573 },
1574 {
1575 name: "near_constant",
1576
1577 text: `x.near: top-center
1578 `,
1579 },
1580 {
1581 name: "near-invalid",
1582
1583 text: `mongodb: MongoDB {
1584 perspective: perspective (View) {
1585 password
1586 }
1587
1588 explanation: |md
1589 perspective.model.js
1590 | {
1591 near: mongodb
1592 }
1593 }
1594
1595 a: {
1596 near: a.b
1597 b
1598 }
1599 `,
1600 expErr: `d2/testdata/d2compiler/TestCompile/near-invalid.d2:9:11: near keys cannot be set to an ancestor
1601 d2/testdata/d2compiler/TestCompile/near-invalid.d2:14:9: near keys cannot be set to an descendant`,
1602 },
1603 {
1604 name: "near_bad_constant",
1605
1606 text: `x.near: txop-center
1607 `,
1608 expErr: `d2/testdata/d2compiler/TestCompile/near_bad_constant.d2:1:9: near key "txop-center" must be the absolute path to a shape or one of the following constants: top-left, top-center, top-right, center-left, center-right, bottom-left, bottom-center, bottom-right`,
1609 },
1610 {
1611 name: "near_bad_connected",
1612
1613 text: `
1614 x: {
1615 near: top-center
1616 }
1617 x -> y
1618 `,
1619 expErr: ``,
1620 },
1621 {
1622 name: "near_descendant_connect_to_outside",
1623 text: `
1624 x: {
1625 near: top-left
1626 y
1627 }
1628 x.y -> z
1629 `,
1630 expErr: "",
1631 },
1632 {
1633 name: "nested_near_constant",
1634
1635 text: `x.y.near: top-center
1636 `,
1637 expErr: `d2/testdata/d2compiler/TestCompile/nested_near_constant.d2:1:11: constant near keys can only be set on root level shapes`,
1638 },
1639 {
1640 name: "reserved_icon_near_style",
1641
1642 text: `x: {
1643 icon: orange
1644 style.opacity: 0.5
1645 style.stroke: red
1646 style.fill: green
1647 }
1648 x.near: y
1649 y
1650 `,
1651 assertions: func(t *testing.T, g *d2graph.Graph) {
1652 if len(g.Objects) != 2 {
1653 t.Fatal(g.Objects)
1654 }
1655 if g.Objects[0].NearKey == nil {
1656 t.Fatal("missing near key")
1657 }
1658 if g.Objects[0].Icon.Path != "orange" {
1659 t.Fatal(g.Objects[0].Icon)
1660 }
1661 if g.Objects[0].Style.Opacity.Value != "0.5" {
1662 t.Fatal(g.Objects[0].Style.Opacity)
1663 }
1664 if g.Objects[0].Style.Stroke.Value != "red" {
1665 t.Fatal(g.Objects[0].Style.Stroke)
1666 }
1667 if g.Objects[0].Style.Fill.Value != "green" {
1668 t.Fatal(g.Objects[0].Style.Fill)
1669 }
1670 },
1671 },
1672 {
1673 name: "errors/reserved_icon_style",
1674
1675 text: `x: {
1676 near: y
1677 icon: "::????:::%%orange"
1678 style.opacity: -1
1679 style.opacity: 232
1680 }
1681 `,
1682 expErr: `d2/testdata/d2compiler/TestCompile/errors/reserved_icon_style.d2:3:9: bad icon url "::????:::%%orange": parse "::????:::%%orange": missing protocol scheme
1683 d2/testdata/d2compiler/TestCompile/errors/reserved_icon_style.d2:5:18: expected "opacity" to be a number between 0.0 and 1.0
1684 d2/testdata/d2compiler/TestCompile/errors/reserved_icon_style.d2:2:9: near key "y" must be the absolute path to a shape or one of the following constants: top-left, top-center, top-right, center-left, center-right, bottom-left, bottom-center, bottom-right`,
1685 },
1686 {
1687 name: "errors/missing_shape_icon",
1688
1689 text: `x.shape: image`,
1690 expErr: `d2/testdata/d2compiler/TestCompile/errors/missing_shape_icon.d2:1:1: image shape must include an "icon" field`,
1691 },
1692 {
1693 name: "edge_in_column",
1694
1695 text: `x: {
1696 shape: sql_table
1697 x: {p -> q}
1698 }`,
1699 expErr: `d2/testdata/d2compiler/TestCompile/edge_in_column.d2:3:7: sql_table columns cannot have children
1700 d2/testdata/d2compiler/TestCompile/edge_in_column.d2:3:12: sql_table columns cannot have children`,
1701 },
1702 {
1703 name: "no-nested-columns-sql",
1704
1705 text: `x: {
1706 shape: sql_table
1707 a -- b.b
1708 }`,
1709 expErr: `d2/testdata/d2compiler/TestCompile/no-nested-columns-sql.d2:3:10: sql_table columns cannot have children`,
1710 },
1711 {
1712 name: "no-nested-columns-sql-2",
1713
1714 text: `x: {
1715 shape: sql_table
1716 a
1717 }
1718 x.a.b`,
1719 expErr: `d2/testdata/d2compiler/TestCompile/no-nested-columns-sql-2.d2:5:5: sql_table columns cannot have children`,
1720 },
1721 {
1722 name: "no-nested-columns-class",
1723
1724 text: `x: {
1725 shape: class
1726 a.a
1727 }`,
1728 expErr: `d2/testdata/d2compiler/TestCompile/no-nested-columns-class.d2:3:5: class fields cannot have children`,
1729 },
1730 {
1731 name: "improper-class-ref",
1732
1733 text: `myobj.class.style.stroke-dash: 3`,
1734 expErr: `d2/testdata/d2compiler/TestCompile/improper-class-ref.d2:1:7: "class" must be the last part of the key`,
1735 },
1736 {
1737 name: "tail-style",
1738
1739 text: `myobj.style: 3`,
1740 expErr: `d2/testdata/d2compiler/TestCompile/tail-style.d2:1:7: "style" expected to be set to a map of key-values, or contain an additional keyword like "style.opacity: 0.4"`,
1741 },
1742 {
1743 name: "tail-style-map",
1744
1745 text: `myobj.style: {}`,
1746 expErr: `d2/testdata/d2compiler/TestCompile/tail-style-map.d2:1:7: "style" expected to be set to a map of key-values, or contain an additional keyword like "style.opacity: 0.4"`,
1747 },
1748 {
1749 name: "bad-style-nesting",
1750
1751 text: `myobj.style.style.stroke-dash: 3`,
1752 expErr: `d2/testdata/d2compiler/TestCompile/bad-style-nesting.d2:1:13: invalid style keyword: "style"`,
1753 },
1754 {
1755 name: "edge_to_style",
1756
1757 text: `x: {style.opacity: 0.4}
1758 y -> x.style
1759 `,
1760 expErr: `d2/testdata/d2compiler/TestCompile/edge_to_style.d2:2:8: reserved keywords are prohibited in edges`,
1761 },
1762 {
1763 name: "keyword-container",
1764
1765 text: `a.near.b
1766 `,
1767 expErr: `d2/testdata/d2compiler/TestCompile/keyword-container.d2:1:3: "near" must be the last part of the key`,
1768 },
1769 {
1770 name: "escaped_id",
1771
1772 text: `b\nb`,
1773 assertions: func(t *testing.T, g *d2graph.Graph) {
1774 if len(g.Objects) != 1 {
1775 t.Fatal(g.Objects)
1776 }
1777 assert.String(t, `"b\nb"`, g.Objects[0].ID)
1778 assert.String(t, `b
1779 b`, g.Objects[0].Label.Value)
1780 },
1781 },
1782 {
1783 name: "unescaped_id_cr",
1784
1785 text: `b\rb`,
1786 assertions: func(t *testing.T, g *d2graph.Graph) {
1787 if len(g.Objects) != 1 {
1788 t.Fatal(g.Objects)
1789 }
1790 assert.String(t, "b\rb", g.Objects[0].ID)
1791 assert.String(t, "b\rb", g.Objects[0].Label.Value)
1792 },
1793 },
1794 {
1795 name: "class_style",
1796
1797 text: `IUserProperties: {
1798 shape: "class"
1799 firstName?: "string"
1800 style.opacity: 0.4
1801 }
1802 `,
1803 assertions: func(t *testing.T, g *d2graph.Graph) {
1804 if len(g.Objects) != 1 {
1805 t.Fatal(g.Objects)
1806 }
1807 if len(g.Objects[0].Class.Fields) != 1 {
1808 t.Fatal(len(g.Objects[0].Class.Fields))
1809 }
1810 if len(g.Objects[0].Class.Methods) != 0 {
1811 t.Fatal(len(g.Objects[0].Class.Methods))
1812 }
1813 if g.Objects[0].Style.Opacity.Value != "0.4" {
1814 t.Fatal(g.Objects[0].Style.Opacity.Value)
1815 }
1816 },
1817 },
1818 {
1819 name: "table_style",
1820
1821 text: `IUserProperties: {
1822 shape: sql_table
1823 GetType(): string
1824 style.opacity: 0.4
1825 }
1826 `,
1827 assertions: func(t *testing.T, g *d2graph.Graph) {
1828 if len(g.Objects) != 1 {
1829 t.Fatal(g.Objects)
1830 }
1831 if len(g.Objects[0].SQLTable.Columns) != 1 {
1832 t.Fatal(len(g.Objects[0].SQLTable.Columns))
1833 }
1834 if g.Objects[0].Style.Opacity.Value != "0.4" {
1835 t.Fatal(g.Objects[0].Style.Opacity.Value)
1836 }
1837 },
1838 },
1839 {
1840 name: "table_style_map",
1841
1842 text: `IUserProperties: {
1843 shape: sql_table
1844 GetType(): string
1845 style: {
1846 opacity: 0.4
1847 font-color: blue
1848 }
1849 }
1850 `,
1851 assertions: func(t *testing.T, g *d2graph.Graph) {
1852 if len(g.Objects) != 1 {
1853 t.Fatal(g.Objects)
1854 }
1855 if len(g.Objects[0].SQLTable.Columns) != 1 {
1856 t.Fatal(len(g.Objects[0].SQLTable.Columns))
1857 }
1858 if g.Objects[0].Style.Opacity.Value != "0.4" {
1859 t.Fatal(g.Objects[0].Style.Opacity.Value)
1860 }
1861 },
1862 },
1863 {
1864 name: "table_connection_attr",
1865
1866 text: `x: {
1867 shape: sql_table
1868 y
1869 }
1870 a: {
1871 shape: sql_table
1872 b
1873 }
1874 x.y -> a.b: {
1875 style.animated: true
1876 }
1877 `,
1878 assertions: func(t *testing.T, g *d2graph.Graph) {
1879 tassert.Equal(t, "true", g.Edges[0].Style.Animated.Value)
1880 },
1881 },
1882 {
1883 name: "class_paren",
1884
1885 text: `_shape_: "shape" {
1886 shape: class
1887
1888 field here
1889 GetType(): string
1890 Is(): bool
1891 }`,
1892 assertions: func(t *testing.T, g *d2graph.Graph) {
1893 if len(g.Objects) != 1 {
1894 t.Fatal(g.Objects)
1895 }
1896 assert.String(t, `field here`, g.Objects[0].Class.Fields[0].Name)
1897 assert.String(t, `GetType()`, g.Objects[0].Class.Methods[0].Name)
1898 assert.String(t, `Is()`, g.Objects[0].Class.Methods[1].Name)
1899 },
1900 },
1901 {
1902 name: "sql_paren",
1903
1904 text: `_shape_: "shape" {
1905 shape: sql_table
1906
1907 GetType(): string
1908 Is(): bool
1909 }`,
1910 assertions: func(t *testing.T, g *d2graph.Graph) {
1911 if len(g.Objects) != 1 {
1912 t.Fatal(g.Objects)
1913 }
1914 assert.String(t, `GetType()`, g.Objects[0].SQLTable.Columns[0].Name.Label)
1915 assert.String(t, `Is()`, g.Objects[0].SQLTable.Columns[1].Name.Label)
1916 },
1917 },
1918 {
1919 name: "nested_sql",
1920
1921 text: `outer: {
1922 table: {
1923 shape: sql_table
1924
1925 GetType(): string
1926 Is(): bool
1927 }
1928 }`,
1929 assertions: func(t *testing.T, g *d2graph.Graph) {
1930 if len(g.Objects) != 2 {
1931 t.Fatal(g.Objects)
1932 }
1933 if _, has := g.Objects[0].HasChild([]string{"table"}); !has {
1934 t.Fatal(g.Objects)
1935 }
1936 if len(g.Objects[0].ChildrenArray) != 1 {
1937 t.Fatal(g.Objects)
1938 }
1939 assert.String(t, `GetType()`, g.Objects[1].SQLTable.Columns[0].Name.Label)
1940 assert.String(t, `Is()`, g.Objects[1].SQLTable.Columns[1].Name.Label)
1941 },
1942 },
1943 {
1944 name: "3d_oval",
1945
1946 text: `SVP1.shape: oval
1947 SVP1.style.3d: true`,
1948 expErr: `d2/testdata/d2compiler/TestCompile/3d_oval.d2:2:1: key "3d" can only be applied to squares, rectangles, and hexagons`,
1949 }, {
1950 name: "edge_column_index",
1951 text: `src: {
1952 shape: sql_table
1953 id: int
1954 dst_id: int
1955 }
1956
1957 dst: {
1958 shape: sql_table
1959 id: int
1960 name: string
1961 }
1962
1963 dst.id <-> src.dst_id
1964 `,
1965 assertions: func(t *testing.T, g *d2graph.Graph) {
1966 srcIndex := g.Edges[0].SrcTableColumnIndex
1967 if srcIndex == nil || *srcIndex != 0 {
1968 t.Fatalf("expected SrcTableColumnIndex to be 0, got %v", srcIndex)
1969 }
1970 dstIndex := g.Edges[0].DstTableColumnIndex
1971 if dstIndex == nil || *dstIndex != 1 {
1972 t.Fatalf("expected DstTableColumnIndex to be 1, got %v", dstIndex)
1973 }
1974 },
1975 },
1976 {
1977 name: "basic_sequence",
1978
1979 text: `x: {
1980 shape: sequence_diagram
1981 }
1982 `,
1983 assertions: func(t *testing.T, g *d2graph.Graph) {
1984 assert.String(t, "sequence_diagram", g.Objects[0].Shape.Value)
1985 },
1986 },
1987 {
1988 name: "near_sequence",
1989
1990 text: `x: {
1991 shape: sequence_diagram
1992 a
1993 }
1994 b.near: x.a
1995 `,
1996 expErr: `d2/testdata/d2compiler/TestCompile/near_sequence.d2:5:9: near keys cannot be set to an object within sequence diagrams`,
1997 },
1998 {
1999 name: "sequence-timestamp",
2000
2001 text: `shape: sequence_diagram
2002 a
2003 b
2004
2005 "04:20,11:20": {
2006 "loop through each table": {
2007 a."start_time = datetime.datetime.now"
2008 a -> b
2009 }
2010 }
2011 `,
2012 assertions: func(t *testing.T, g *d2graph.Graph) {
2013 tassert.Equal(t, 1, len(g.Edges))
2014 tassert.Equal(t, 5, len(g.Objects))
2015 tassert.Equal(t, "a", g.Objects[0].ID)
2016 tassert.Equal(t, "b", g.Objects[1].ID)
2017 tassert.Equal(t, `"04:20,11:20"`, g.Objects[2].ID)
2018 tassert.Equal(t, `loop through each table`, g.Objects[3].ID)
2019 tassert.Equal(t, 1, len(g.Objects[0].ChildrenArray))
2020 tassert.Equal(t, 0, len(g.Objects[1].ChildrenArray))
2021 tassert.Equal(t, 1, len(g.Objects[2].ChildrenArray))
2022 tassert.True(t, g.Edges[0].ContainedBy(g.Objects[3]))
2023 },
2024 },
2025 {
2026 name: "root_sequence",
2027
2028 text: `shape: sequence_diagram
2029 `,
2030 assertions: func(t *testing.T, g *d2graph.Graph) {
2031 assert.String(t, "sequence_diagram", g.Root.Shape.Value)
2032 },
2033 },
2034 {
2035 name: "leaky_sequence",
2036
2037 text: `x: {
2038 shape: sequence_diagram
2039 a
2040 }
2041 b -> x.a
2042 `,
2043 expErr: ``,
2044 },
2045 {
2046 name: "sequence_scoping",
2047
2048 text: `x: {
2049 shape: sequence_diagram
2050 a;b
2051 group: {
2052 a -> b
2053 a.t1 -> b.t1
2054 b.t1.t2 -> b.t1
2055 }
2056 }
2057 `,
2058 assertions: func(t *testing.T, g *d2graph.Graph) {
2059 tassert.Equal(t, 7, len(g.Objects))
2060 tassert.Equal(t, 3, len(g.Objects[0].ChildrenArray))
2061 },
2062 },
2063 {
2064 name: "sequence_grouped_note",
2065
2066 text: `shape: sequence_diagram
2067 a;d
2068 choo: {
2069 d."this note"
2070 }
2071 `,
2072 assertions: func(t *testing.T, g *d2graph.Graph) {
2073 tassert.Equal(t, 4, len(g.Objects))
2074 tassert.Equal(t, 3, len(g.Root.ChildrenArray))
2075 },
2076 },
2077 {
2078 name: "sequence_container",
2079
2080 text: `shape: sequence_diagram
2081 x.y.q -> j.y.p
2082 ok: {
2083 x.y.q -> j.y.p
2084 }
2085 `,
2086 assertions: func(t *testing.T, g *d2graph.Graph) {
2087 tassert.Equal(t, 7, len(g.Objects))
2088 tassert.Equal(t, 3, len(g.Root.ChildrenArray))
2089 },
2090 },
2091 {
2092 name: "sequence_container_2",
2093
2094 text: `shape: sequence_diagram
2095 x.y.q
2096 ok: {
2097 x.y.q -> j.y.p
2098 meow
2099 }
2100 `,
2101 assertions: func(t *testing.T, g *d2graph.Graph) {
2102 tassert.Equal(t, 8, len(g.Objects))
2103 tassert.Equal(t, 2, len(g.Root.ChildrenArray))
2104 },
2105 },
2106 {
2107 name: "root_direction",
2108
2109 text: `direction: right`,
2110 assertions: func(t *testing.T, g *d2graph.Graph) {
2111 assert.String(t, "right", g.Root.Direction.Value)
2112 },
2113 },
2114 {
2115 name: "default_direction",
2116
2117 text: `x`,
2118 assertions: func(t *testing.T, g *d2graph.Graph) {
2119 assert.String(t, "", g.Objects[0].Direction.Value)
2120 },
2121 },
2122 {
2123 name: "set_direction",
2124
2125 text: `x: {
2126 direction: left
2127 }`,
2128 assertions: func(t *testing.T, g *d2graph.Graph) {
2129 assert.String(t, "left", g.Objects[0].Direction.Value)
2130 },
2131 },
2132 {
2133 name: "constraint_label",
2134
2135 text: `foo {
2136 label: bar
2137 constraint: BIZ
2138 }`,
2139 expErr: `d2/testdata/d2compiler/TestCompile/constraint_label.d2:3:3: "constraint" keyword can only be used in "sql_table" shapes`,
2140 },
2141 {
2142 name: "invalid_direction",
2143
2144 text: `x: {
2145 direction: diagonal
2146 }`,
2147 expErr: `d2/testdata/d2compiler/TestCompile/invalid_direction.d2:2:14: direction must be one of up, down, right, left, got "diagonal"`,
2148 },
2149 {
2150 name: "self-referencing",
2151
2152 text: `x -> x
2153 `,
2154 assertions: func(t *testing.T, g *d2graph.Graph) {
2155 _, err := diff.Strings(g.Edges[0].Dst.ID, g.Edges[0].Src.ID)
2156 if err != nil {
2157 t.Fatal(err)
2158 }
2159 },
2160 },
2161 {
2162 name: "null",
2163
2164 text: `null
2165 `,
2166 assertions: func(t *testing.T, g *d2graph.Graph) {
2167 tassert.Equal(t, "'null'", g.Objects[0].ID)
2168 tassert.Equal(t, "null", g.Objects[0].IDVal)
2169 },
2170 },
2171 {
2172 name: "sql-regression",
2173
2174 text: `a: {
2175 style: {
2176 fill: lemonchiffon
2177 }
2178 b: {
2179 shape: sql_table
2180 c
2181 }
2182 d
2183 }
2184 `,
2185 assertions: func(t *testing.T, g *d2graph.Graph) {
2186 tassert.Equal(t, 3, len(g.Objects))
2187 },
2188 },
2189 {
2190 name: "sql-constraints",
2191 text: `x: {
2192 shape: sql_table
2193 a: int {constraint: primary_key}
2194 b: int {constraint: [primary_key; foreign_key]}
2195 }`,
2196 assertions: func(t *testing.T, g *d2graph.Graph) {
2197 table := g.Objects[0].SQLTable
2198 tassert.Equal(t, []string{"primary_key"}, table.Columns[0].Constraint)
2199 tassert.Equal(t, []string{"primary_key", "foreign_key"}, table.Columns[1].Constraint)
2200 },
2201 },
2202 {
2203 name: "sql-null-constraint",
2204 text: `x: {
2205 shape: sql_table
2206 a: int {constraint: null}
2207 b: int {constraint: [null]}
2208 }`,
2209 assertions: func(t *testing.T, g *d2graph.Graph) {
2210 table := g.Objects[0].SQLTable
2211 tassert.Nil(t, table.Columns[0].Constraint)
2212 tassert.Equal(t, []string{"null"}, table.Columns[1].Constraint)
2213 },
2214 },
2215 {
2216 name: "wrong_column_index",
2217 text: `Chinchillas: {
2218 shape: sql_table
2219 id: int {constraint: primary_key}
2220 whisker_len: int
2221 fur_color: string
2222 age: int
2223 server: int {constraint: foreign_key}
2224 caretaker: int {constraint: foreign_key}
2225 }
2226
2227 Chinchillas_Collectibles: {
2228 shape: sql_table
2229 id: int
2230 collectible: id {constraint: foreign_key}
2231 chinchilla: id {constraint: foreign_key}
2232 }
2233
2234 Chinchillas_Collectibles.chinchilla -> Chinchillas.id`,
2235 assertions: func(t *testing.T, g *d2graph.Graph) {
2236 tassert.Equal(t, 0, *g.Edges[0].DstTableColumnIndex)
2237 tassert.Equal(t, 2, *g.Edges[0].SrcTableColumnIndex)
2238 },
2239 },
2240 {
2241 name: "link-board-ok",
2242 text: `x.link: layers.x
2243 layers: {
2244 x: {
2245 y
2246 }
2247 }`,
2248 assertions: func(t *testing.T, g *d2graph.Graph) {
2249 tassert.Equal(t, "root.layers.x", g.Objects[0].Link.Value)
2250 },
2251 },
2252 {
2253 name: "link-board-mixed",
2254 text: `question: How does the cat go?
2255 question.link: layers.cat
2256
2257 layers: {
2258 cat: {
2259 the cat -> meeeowwww: goes
2260 }
2261 }
2262
2263 scenarios: {
2264 green: {
2265 question.style.fill: green
2266 }
2267 }`,
2268 assertions: func(t *testing.T, g *d2graph.Graph) {
2269 tassert.Equal(t, "root.layers.cat", g.Objects[0].Link.Value)
2270 tassert.Equal(t, "root.layers.cat", g.Scenarios[0].Objects[0].Link.Value)
2271 },
2272 },
2273 {
2274 name: "link-board-not-found",
2275 text: `x.link: layers.x
2276 `,
2277 expErr: `d2/testdata/d2compiler/TestCompile/link-board-not-found.d2:1:1: linked board not found`,
2278 },
2279 {
2280 name: "link-board-not-board",
2281 text: `zzz
2282 x.link: layers.x.y
2283 layers: {
2284 x: {
2285 y
2286 }
2287 }`,
2288 expErr: `d2/testdata/d2compiler/TestCompile/link-board-not-board.d2:2:1: linked board not found`,
2289 },
2290 {
2291 name: "link-board-nested",
2292 text: `x.link: layers.x.layers.x
2293 layers: {
2294 x: {
2295 layers: {
2296 x: {
2297 hello
2298 }
2299 }
2300 }
2301 }`,
2302 assertions: func(t *testing.T, g *d2graph.Graph) {
2303 tassert.Equal(t, "root.layers.x.layers.x", g.Objects[0].Link.Value)
2304 },
2305 },
2306 {
2307 name: "link-board-key-nested",
2308 text: `x: {
2309 y.link: layers.x
2310 }
2311 layers: {
2312 x: {
2313 yo
2314 }
2315 }`,
2316 assertions: func(t *testing.T, g *d2graph.Graph) {
2317 tassert.Equal(t, "root.layers.x", g.Objects[1].Link.Value)
2318 },
2319 },
2320 {
2321 name: "link-board-underscore",
2322 text: `x
2323 layers: {
2324 x: {
2325 yo
2326 layers: {
2327 x: {
2328 hello.link: _._.layers.x
2329 hey.link: _
2330 }
2331 }
2332 }
2333 }`,
2334 assertions: func(t *testing.T, g *d2graph.Graph) {
2335 tassert.NotNil(t, g.Layers[0].Layers[0].Objects[0].Link.Value)
2336 tassert.Equal(t, "root.layers.x", g.Layers[0].Layers[0].Objects[0].Link.Value)
2337 tassert.Equal(t, "root.layers.x", g.Layers[0].Layers[0].Objects[1].Link.Value)
2338 },
2339 },
2340 {
2341 name: "link-board-underscore-not-found",
2342 text: `x
2343 layers: {
2344 x: {
2345 yo
2346 layers: {
2347 x: {
2348 hello.link: _._._
2349 }
2350 }
2351 }
2352 }`,
2353 expErr: `d2/testdata/d2compiler/TestCompile/link-board-underscore-not-found.d2:7:9: invalid underscore usage`,
2354 },
2355 {
2356 name: "border-radius-negative",
2357 text: `x
2358 x: {
2359 style.border-radius: -1
2360 }`,
2361 expErr: `d2/testdata/d2compiler/TestCompile/border-radius-negative.d2:3:24: expected "border-radius" to be a number greater or equal to 0`,
2362 },
2363 {
2364 name: "text-transform",
2365 text: `direction: right
2366 x -> y: hi {
2367 style: {
2368 text-transform: capitalize
2369 }
2370 }
2371 x.style.text-transform: uppercase
2372 y.style.text-transform: lowercase`,
2373 },
2374 {
2375 name: "near_near_const",
2376 text: `
2377 title: Title {
2378 near: top-center
2379 }
2380
2381 obj {
2382 near: title
2383 }
2384 `,
2385 expErr: `d2/testdata/d2compiler/TestCompile/near_near_const.d2:7:8: near keys cannot be set to an object with a constant near key`,
2386 },
2387 {
2388 name: "label-near-parent",
2389 text: `hey: sushi {
2390 label.near: outside-top-left
2391 }
2392 `,
2393 assertions: func(t *testing.T, g *d2graph.Graph) {
2394 tassert.Equal(t, "sushi", g.Objects[0].Attributes.Label.Value)
2395 tassert.Equal(t, "outside-top-left", g.Objects[0].Attributes.LabelPosition.Value)
2396 },
2397 },
2398 {
2399 name: "label-near-composite-separate",
2400 text: `hey: {
2401 label: sushi
2402 label.near: outside-top-left
2403 }
2404 `,
2405 assertions: func(t *testing.T, g *d2graph.Graph) {
2406 tassert.Equal(t, "sushi", g.Objects[0].Attributes.Label.Value)
2407 tassert.Equal(t, "outside-top-left", g.Objects[0].Attributes.LabelPosition.Value)
2408 },
2409 },
2410 {
2411 name: "label-near-composite-together",
2412 text: `hey: {
2413 label: sushi {
2414 near: outside-top-left
2415 }
2416 }
2417 `,
2418 assertions: func(t *testing.T, g *d2graph.Graph) {
2419 tassert.Equal(t, "sushi", g.Objects[0].Attributes.Label.Value)
2420 tassert.Equal(t, "outside-top-left", g.Objects[0].Attributes.LabelPosition.Value)
2421 },
2422 },
2423 {
2424 name: "icon-near-composite-together",
2425 text: `hey: {
2426 icon: https://asdf.com {
2427 near: outside-top-left
2428 }
2429 }
2430 `,
2431 assertions: func(t *testing.T, g *d2graph.Graph) {
2432 tassert.Equal(t, "asdf.com", g.Objects[0].Attributes.Icon.Host)
2433 tassert.Equal(t, "outside-top-left", g.Objects[0].Attributes.IconPosition.Value)
2434 },
2435 },
2436 {
2437 name: "label-near-invalid-edge",
2438 text: `hey: {
2439 label: sushi {
2440 near: outside-top-left
2441 a -> b
2442 }
2443 }
2444 `,
2445 expErr: `d2/testdata/d2compiler/TestCompile/label-near-invalid-edge.d2:2:3: unexpected edges in map`,
2446 },
2447 {
2448 name: "label-near-invalid-field",
2449 text: `hey: {
2450 label: sushi {
2451 near: outside-top-left
2452 a
2453 }
2454 }
2455 `,
2456 expErr: `d2/testdata/d2compiler/TestCompile/label-near-invalid-field.d2:4:3: unexpected field a`,
2457 },
2458 {
2459 name: "grid",
2460 text: `hey: {
2461 grid-rows: 200
2462 grid-columns: 230
2463 }
2464 `,
2465 assertions: func(t *testing.T, g *d2graph.Graph) {
2466 tassert.Equal(t, "200", g.Objects[0].GridRows.Value)
2467 },
2468 },
2469 {
2470 name: "grid_negative",
2471 text: `hey: {
2472 grid-rows: 200
2473 grid-columns: -200
2474 }
2475 `,
2476 expErr: `d2/testdata/d2compiler/TestCompile/grid_negative.d2:3:16: grid-columns must be a positive integer: "-200"`,
2477 },
2478 {
2479 name: "grid_gap_negative",
2480 text: `hey: {
2481 horizontal-gap: -200
2482 vertical-gap: -30
2483 }
2484 `,
2485 expErr: `d2/testdata/d2compiler/TestCompile/grid_gap_negative.d2:2:18: horizontal-gap must be a non-negative integer: "-200"
2486 d2/testdata/d2compiler/TestCompile/grid_gap_negative.d2:3:16: vertical-gap must be a non-negative integer: "-30"`,
2487 },
2488 {
2489 name: "grid_edge",
2490 text: `hey: {
2491 grid-rows: 1
2492 a -> b: ok
2493 }
2494 c -> hey.b
2495 hey.a -> c
2496 hey -> hey.a
2497
2498 hey -> c: ok
2499 `,
2500 expErr: `d2/testdata/d2compiler/TestCompile/grid_edge.d2:7:1: edge from grid diagram "hey" cannot enter itself`,
2501 },
2502 {
2503 name: "grid_deeper_edge",
2504 text: `hey: {
2505 grid-rows: 1
2506 a -> b: ok
2507 b: {
2508 c -> d: ok now
2509 c.e -> c.f.g: ok
2510 c.e -> d.h: ok
2511 c -> d.h: ok
2512 }
2513 a: {
2514 grid-columns: 1
2515 e -> f: also ok now
2516 e: {
2517 g -> h: ok
2518 g -> h.h: ok
2519 }
2520 e -> f.i: ok now
2521 e.g -> f.i: ok now
2522 }
2523 a -> b.c: ok now
2524 a.e -> b.c: ok now
2525 a -> a.e: not ok
2526 }
2527 `,
2528 expErr: `d2/testdata/d2compiler/TestCompile/grid_deeper_edge.d2:22:2: edge from grid diagram "hey.a" cannot enter itself`,
2529 },
2530 {
2531 name: "parent_graph_edge_to_descendant",
2532 text: `tl: {
2533 near: top-left
2534 a.b
2535 }
2536 grid: {
2537 grid-rows: 1
2538 cell.c.d
2539 }
2540 seq: {
2541 shape: sequence_diagram
2542 e.f
2543 }
2544 tl -> tl.a: no
2545 tl -> tl.a.b: no
2546 grid-> grid.cell: no
2547 grid-> grid.cell.c: no
2548 grid.cell -> grid.cell.c: no
2549 grid.cell -> grid.cell.c.d: no
2550 seq -> seq.e: no
2551 seq -> seq.e.f: no
2552 `,
2553 expErr: `d2/testdata/d2compiler/TestCompile/parent_graph_edge_to_descendant.d2:13:1: edge from constant near "tl" cannot enter itself
2554 d2/testdata/d2compiler/TestCompile/parent_graph_edge_to_descendant.d2:14:1: edge from constant near "tl" cannot enter itself
2555 d2/testdata/d2compiler/TestCompile/parent_graph_edge_to_descendant.d2:17:1: edge from grid cell "grid.cell" cannot enter itself
2556 d2/testdata/d2compiler/TestCompile/parent_graph_edge_to_descendant.d2:18:1: edge from grid cell "grid.cell" cannot enter itself
2557 d2/testdata/d2compiler/TestCompile/parent_graph_edge_to_descendant.d2:15:1: edge from grid diagram "grid" cannot enter itself
2558 d2/testdata/d2compiler/TestCompile/parent_graph_edge_to_descendant.d2:16:1: edge from grid diagram "grid" cannot enter itself
2559 d2/testdata/d2compiler/TestCompile/parent_graph_edge_to_descendant.d2:19:1: edge from sequence diagram "seq" cannot enter itself
2560 d2/testdata/d2compiler/TestCompile/parent_graph_edge_to_descendant.d2:20:1: edge from sequence diagram "seq" cannot enter itself`,
2561 },
2562 {
2563 name: "grid_nested",
2564 text: `hey: {
2565 grid-rows: 200
2566 grid-columns: 200
2567
2568 a
2569 b
2570 c
2571 d.valid descendant
2572 e: {
2573 grid-rows: 1
2574 grid-columns: 2
2575
2576 a
2577 b
2578 }
2579 }
2580 `,
2581 expErr: ``,
2582 },
2583 {
2584 name: "classes",
2585 text: `classes: {
2586 dragon_ball: {
2587 label: ""
2588 shape: circle
2589 style.fill: orange
2590 }
2591 path: {
2592 label: "then"
2593 style.stroke-width: 4
2594 }
2595 }
2596 nostar: { class: dragon_ball }
2597 1star: "*" { class: dragon_ball; style.fill: red }
2598 2star: { label: "**"; class: dragon_ball }
2599
2600 nostar -> 1star: { class: path }
2601 `,
2602 assertions: func(t *testing.T, g *d2graph.Graph) {
2603 tassert.Equal(t, 3, len(g.Objects))
2604 tassert.Equal(t, "dragon_ball", g.Objects[0].Classes[0])
2605 tassert.Equal(t, "", g.Objects[0].Label.Value)
2606
2607 tassert.Equal(t, "", g.Objects[1].Label.Value)
2608 tassert.Equal(t, "**", g.Objects[2].Label.Value)
2609 tassert.Equal(t, "orange", g.Objects[0].Style.Fill.Value)
2610 tassert.Equal(t, "red", g.Objects[1].Style.Fill.Value)
2611
2612 tassert.Equal(t, "4", g.Edges[0].Style.StrokeWidth.Value)
2613 tassert.Equal(t, "then", g.Edges[0].Label.Value)
2614 },
2615 },
2616 {
2617 name: "array-classes",
2618 text: `classes: {
2619 dragon_ball: {
2620 label: ""
2621 shape: circle
2622 style.fill: orange
2623 }
2624 path: {
2625 label: "then"
2626 style.stroke-width: 4
2627 }
2628 path2: {
2629 style.stroke-width: 2
2630 }
2631 }
2632 nostar: { class: [dragon_ball; path] }
2633 1star: { class: [path; dragon_ball] }
2634
2635 nostar -> 1star: { class: [path; path2] }
2636 `,
2637 assertions: func(t *testing.T, g *d2graph.Graph) {
2638 tassert.Equal(t, "then", g.Objects[0].Label.Value)
2639 tassert.Equal(t, "", g.Objects[1].Label.Value)
2640 tassert.Equal(t, "circle", g.Objects[0].Shape.Value)
2641 tassert.Equal(t, "circle", g.Objects[1].Shape.Value)
2642 tassert.Equal(t, "2", g.Edges[0].Style.StrokeWidth.Value)
2643 },
2644 },
2645 {
2646 name: "comma-array-class",
2647
2648 text: `classes: {
2649 dragon_ball: {
2650 label: ""
2651 shape: circle
2652 style.fill: orange
2653 }
2654 path: {
2655 label: "then"
2656 style.stroke-width: 4
2657 }
2658 }
2659 nostar: { class: [dragon_ball, path] }`,
2660 expErr: `d2/testdata/d2compiler/TestCompile/comma-array-class.d2:12:11: class "dragon_ball, path" not found. Did you mean to use ";" to separate array items?`,
2661 },
2662 {
2663 name: "reordered-classes",
2664 text: `classes: {
2665 x: {
2666 shape: circle
2667 }
2668 }
2669 a.class: x
2670 classes.x.shape: diamond
2671 `,
2672 assertions: func(t *testing.T, g *d2graph.Graph) {
2673 tassert.Equal(t, 1, len(g.Objects))
2674 tassert.Equal(t, "diamond", g.Objects[0].Shape.Value)
2675 },
2676 },
2677 {
2678 name: "nested-array-classes",
2679 text: `classes: {
2680 one target: {
2681 target-arrowhead.label: 1
2682 }
2683 association: {
2684 target-arrowhead.shape: arrow
2685 }
2686 }
2687
2688 a -> b: { class: [one target; association] }
2689 a -> b: { class: [association; one target] }
2690 `,
2691 assertions: func(t *testing.T, g *d2graph.Graph) {
2692
2693
2694 tassert.Equal(t, "1", g.Edges[0].DstArrowhead.Label.Value)
2695 tassert.Equal(t, "1", g.Edges[1].DstArrowhead.Label.Value)
2696 tassert.Equal(t, "arrow", g.Edges[0].DstArrowhead.Shape.Value)
2697 tassert.Equal(t, "arrow", g.Edges[1].DstArrowhead.Shape.Value)
2698 },
2699 },
2700 {
2701 name: "var_in_glob",
2702 text: `vars: {
2703 v: {
2704 ok
2705 }
2706 }
2707
2708 x1 -> x2
2709
2710 x*: {
2711 ...${v}
2712 }
2713 `,
2714 assertions: func(t *testing.T, g *d2graph.Graph) {
2715 tassert.Equal(t, 4, len(g.Objects))
2716 tassert.Equal(t, "x1.ok", g.Objects[0].AbsID())
2717 tassert.Equal(t, "x2.ok", g.Objects[1].AbsID())
2718 tassert.Equal(t, "x1", g.Objects[2].AbsID())
2719 tassert.Equal(t, "x2", g.Objects[3].AbsID())
2720 },
2721 },
2722 {
2723 name: "class-shape-class",
2724 text: `classes: {
2725 classClass: {
2726 shape: class
2727 }
2728 }
2729
2730 object: {
2731 class: classClass
2732 length(): int
2733 }
2734 `,
2735 },
2736 {
2737 name: "no-class-primary",
2738 text: `x.class
2739 `,
2740 expErr: `d2/testdata/d2compiler/TestCompile/no-class-primary.d2:1:3: class missing value`,
2741 },
2742 {
2743 name: "no-class-inside-classes",
2744 text: `classes: {
2745 x: {
2746 class: y
2747 }
2748 }
2749 `,
2750 expErr: `d2/testdata/d2compiler/TestCompile/no-class-inside-classes.d2:3:5: "class" cannot appear within "classes"`,
2751 },
2752 {
2753
2754 name: "missing-class",
2755 text: `x.class: yo
2756 `,
2757 },
2758 {
2759 name: "classes-unreserved",
2760 text: `classes: {
2761 mango: {
2762 seed
2763 }
2764 }
2765 `,
2766 expErr: `d2/testdata/d2compiler/TestCompile/classes-unreserved.d2:3:5: seed is an invalid class field, must be reserved keyword`,
2767 },
2768 {
2769 name: "classes-internal-edge",
2770 text: `classes: {
2771 mango: {
2772 width: 100
2773 }
2774 jango: {
2775 height: 100
2776 }
2777 mango -> jango
2778 }
2779 `,
2780 expErr: `d2/testdata/d2compiler/TestCompile/classes-internal-edge.d2:8:3: classes cannot contain an edge`,
2781 },
2782 {
2783 name: "reserved-composite",
2784 text: `shape: sequence_diagram {
2785 alice -> bob: What does it mean\nto be well-adjusted?
2786 bob -> alice: The ability to play bridge or\ngolf as if they were games.
2787 }
2788 `,
2789 expErr: `d2/testdata/d2compiler/TestCompile/reserved-composite.d2:1:1: reserved field shape does not accept composite`,
2790 },
2791 {
2792 name: "text_no_label",
2793 text: `a: "ok" {
2794 shape: text
2795 }
2796 b: " \n " {
2797 shape: text
2798 }
2799 c: "" {
2800 shape: text
2801 }
2802 d: "" {
2803 shape: circle
2804 }
2805 e: " \n "
2806 f: |md |
2807 g: |md
2808
2809 |
2810 `,
2811 expErr: `d2/testdata/d2compiler/TestCompile/text_no_label.d2:14:1: block string cannot be empty
2812 d2/testdata/d2compiler/TestCompile/text_no_label.d2:15:1: block string cannot be empty
2813 d2/testdata/d2compiler/TestCompile/text_no_label.d2:4:1: shape text must have a non-empty label
2814 d2/testdata/d2compiler/TestCompile/text_no_label.d2:7:1: shape text must have a non-empty label`,
2815 },
2816 {
2817 name: "var-not-color",
2818 text: `vars: {
2819 d2-config: {
2820 theme-overrides: {
2821 B1: potato
2822 potato: B1
2823 }
2824 }
2825 }
2826 a
2827 `,
2828 expErr: `d2/testdata/d2compiler/TestCompile/var-not-color.d2:4:7: expected "B1" to be a valid named color ("orange") or a hex code ("#f0ff3a")
2829 d2/testdata/d2compiler/TestCompile/var-not-color.d2:5:4: "potato" is not a valid theme code`,
2830 },
2831 {
2832 name: "no_arrowheads_in_shape",
2833
2834 text: `x.target-arrowhead.shape: cf-one
2835 y.source-arrowhead.shape: cf-one
2836 `,
2837 expErr: `d2/testdata/d2compiler/TestCompile/no_arrowheads_in_shape.d2:1:3: "target-arrowhead" can only be used on connections
2838 d2/testdata/d2compiler/TestCompile/no_arrowheads_in_shape.d2:2:3: "source-arrowhead" can only be used on connections`,
2839 },
2840 }
2841
2842 for _, tc := range testCases {
2843 tc := tc
2844 t.Run(tc.name, func(t *testing.T) {
2845 t.Parallel()
2846
2847 d2Path := fmt.Sprintf("d2/testdata/d2compiler/%v.d2", t.Name())
2848 g, _, err := d2compiler.Compile(d2Path, strings.NewReader(tc.text), nil)
2849 if tc.expErr != "" {
2850 if err == nil {
2851 t.Fatalf("expected error with: %q", tc.expErr)
2852 }
2853 ds, err := diff.Strings(tc.expErr, err.Error())
2854 if err != nil {
2855 t.Fatal(err)
2856 }
2857 if ds != "" {
2858 t.Fatalf("unexpected error: %s", ds)
2859 }
2860 } else if err != nil {
2861 t.Fatal(err)
2862 }
2863
2864 if tc.expErr == "" && tc.assertions != nil {
2865 t.Run("assertions", func(t *testing.T) {
2866 tc.assertions(t, g)
2867 })
2868 }
2869
2870 got := struct {
2871 Graph *d2graph.Graph `json:"graph"`
2872 Err error `json:"err"`
2873 }{
2874 Graph: g,
2875 Err: err,
2876 }
2877
2878 err = diff.TestdataJSON(filepath.Join("..", "testdata", "d2compiler", t.Name()), got)
2879 assert.Success(t, err)
2880 })
2881 }
2882 }
2883
2884 func TestCompile2(t *testing.T) {
2885 t.Parallel()
2886
2887 t.Run("boards", testBoards)
2888 t.Run("seqdiagrams", testSeqDiagrams)
2889 t.Run("nulls", testNulls)
2890 t.Run("vars", testVars)
2891 t.Run("globs", testGlobs)
2892 }
2893
2894 func testBoards(t *testing.T) {
2895 t.Parallel()
2896
2897 tca := []struct {
2898 name string
2899 run func(t *testing.T)
2900 }{
2901 {
2902 name: "root",
2903 run: func(t *testing.T) {
2904 g, _ := assertCompile(t, `base
2905
2906 layers: {
2907 one: {
2908 santa
2909 }
2910 two: {
2911 clause
2912 }
2913 }
2914 `, "")
2915 assert.Equal(t, 2, len(g.Layers))
2916 assert.Equal(t, "one", g.Layers[0].Name)
2917 assert.Equal(t, "two", g.Layers[1].Name)
2918 },
2919 },
2920 {
2921 name: "recursive",
2922 run: func(t *testing.T) {
2923 g, _ := assertCompile(t, `base
2924
2925 layers: {
2926 one: {
2927 santa
2928 }
2929 two: {
2930 clause
2931 steps: {
2932 seinfeld: {
2933 reindeer
2934 }
2935 missoula: {
2936 montana
2937 }
2938 }
2939 }
2940 }
2941 `, "")
2942 assert.Equal(t, 2, len(g.Layers))
2943 assert.Equal(t, "one", g.Layers[0].Name)
2944 assert.Equal(t, "two", g.Layers[1].Name)
2945 assert.Equal(t, 2, len(g.Layers[1].Steps))
2946 },
2947 },
2948 {
2949 name: "isFolderOnly",
2950 run: func(t *testing.T) {
2951 g, _ := assertCompile(t, `
2952 layers: {
2953 one: {
2954 santa
2955 }
2956 two: {
2957 clause
2958 scenarios: {
2959 seinfeld: {
2960 }
2961 missoula: {
2962 steps: {
2963 missus: one two three
2964 }
2965 }
2966 }
2967 }
2968 }
2969 `, "")
2970 assert.True(t, g.IsFolderOnly)
2971 assert.Equal(t, 2, len(g.Layers))
2972 assert.Equal(t, "one", g.Layers[0].Name)
2973 assert.Equal(t, "two", g.Layers[1].Name)
2974 assert.Equal(t, 2, len(g.Layers[1].Scenarios))
2975 assert.False(t, g.Layers[1].Scenarios[0].IsFolderOnly)
2976 assert.False(t, g.Layers[1].Scenarios[1].IsFolderOnly)
2977 },
2978 },
2979 {
2980 name: "isFolderOnly-shapes",
2981 run: func(t *testing.T) {
2982 g, _ := assertCompile(t, `
2983 direction: right
2984
2985 steps: {
2986 1: {
2987 RJ
2988 }
2989 }
2990 `, "")
2991 assert.True(t, g.IsFolderOnly)
2992 },
2993 },
2994 {
2995 name: "scenarios_edge_index",
2996 run: func(t *testing.T) {
2997 assertCompile(t, `a -> x
2998
2999 scenarios: {
3000 1: {
3001 (a -> x)[0].style.opacity: 0.1
3002 }
3003 }
3004 `, "")
3005 },
3006 },
3007 {
3008 name: "errs/duplicate_board",
3009 run: func(t *testing.T) {
3010 assertCompile(t, `base
3011
3012 layers: {
3013 one: {
3014 santa
3015 }
3016 }
3017 steps: {
3018 one: {
3019 clause
3020 }
3021 }
3022 `, `d2/testdata/d2compiler/TestCompile2/boards/errs/duplicate_board.d2:9:2: board name one already used by another board`)
3023 },
3024 },
3025 }
3026
3027 for _, tc := range tca {
3028 tc := tc
3029 t.Run(tc.name, func(t *testing.T) {
3030 t.Parallel()
3031 tc.run(t)
3032 })
3033 }
3034 }
3035
3036 func testSeqDiagrams(t *testing.T) {
3037 t.Parallel()
3038
3039 t.Run("errs", func(t *testing.T) {
3040 t.Parallel()
3041
3042 tca := []struct {
3043 name string
3044 skip bool
3045 run func(t *testing.T)
3046 }{
3047 {
3048 name: "sequence_diagram_edge_between_edge_groups",
3049
3050 skip: true,
3051 run: func(t *testing.T) {
3052 assertCompile(t, `
3053 Office chatter: {
3054 shape: sequence_diagram
3055 alice: Alice
3056 bob: Bobby
3057 awkward small talk: {
3058 alice -> bob: uhm, hi
3059 bob -> alice: oh, hello
3060 icebreaker attempt: {
3061 alice -> bob: what did you have for lunch?
3062 }
3063 unfortunate outcome: {
3064 bob -> alice: that's personal
3065 }
3066 }
3067 awkward small talk.icebreaker attempt.alice -> awkward small talk.unfortunate outcome.bob
3068 }
3069 `, "d2/testdata/d2compiler/TestCompile2/seqdiagrams/errs/sequence_diagram_edge_between_edge_groups.d2:16:3: edges between edge groups are not allowed")
3070 },
3071 },
3072 }
3073
3074 for _, tc := range tca {
3075 tc := tc
3076 t.Run(tc.name, func(t *testing.T) {
3077 t.Parallel()
3078 if tc.skip {
3079 t.SkipNow()
3080 }
3081 tc.run(t)
3082 })
3083 }
3084 })
3085 }
3086
3087 func testNulls(t *testing.T) {
3088 t.Parallel()
3089
3090 t.Run("basic", func(t *testing.T) {
3091 t.Parallel()
3092
3093 tca := []struct {
3094 name string
3095 skip bool
3096 run func(t *testing.T)
3097 }{
3098 {
3099 name: "shape",
3100 run: func(t *testing.T) {
3101 g, _ := assertCompile(t, `
3102 a
3103 a: null
3104 `, "")
3105 assert.Equal(t, 0, len(g.Objects))
3106 },
3107 },
3108 {
3109 name: "edge",
3110 run: func(t *testing.T) {
3111 g, _ := assertCompile(t, `
3112 a -> b
3113 (a -> b)[0]: null
3114 `, "")
3115 assert.Equal(t, 2, len(g.Objects))
3116 assert.Equal(t, 0, len(g.Edges))
3117 },
3118 },
3119 {
3120 name: "attribute",
3121 run: func(t *testing.T) {
3122 g, _ := assertCompile(t, `
3123 a.style.opacity: 0.2
3124 a.style.opacity: null
3125 `, "")
3126 assert.Equal(t, (*d2graph.Scalar)(nil), g.Objects[0].Attributes.Style.Opacity)
3127 },
3128 },
3129 }
3130
3131 for _, tc := range tca {
3132 tc := tc
3133 t.Run(tc.name, func(t *testing.T) {
3134 t.Parallel()
3135 if tc.skip {
3136 t.SkipNow()
3137 }
3138 tc.run(t)
3139 })
3140 }
3141 })
3142
3143 t.Run("reappear", func(t *testing.T) {
3144 t.Parallel()
3145
3146 tca := []struct {
3147 name string
3148 skip bool
3149 run func(t *testing.T)
3150 }{
3151 {
3152 name: "shape",
3153 run: func(t *testing.T) {
3154 g, _ := assertCompile(t, `
3155 a
3156 a: null
3157 a
3158 `, "")
3159 assert.Equal(t, 1, len(g.Objects))
3160 },
3161 },
3162 {
3163 name: "edge",
3164 run: func(t *testing.T) {
3165 g, _ := assertCompile(t, `
3166 a -> b
3167 (a -> b)[0]: null
3168 a -> b
3169 `, "")
3170 assert.Equal(t, 2, len(g.Objects))
3171 assert.Equal(t, 1, len(g.Edges))
3172 },
3173 },
3174 {
3175 name: "attribute-reset",
3176 run: func(t *testing.T) {
3177 g, _ := assertCompile(t, `
3178 a.style.opacity: 0.2
3179 a: null
3180 a
3181 `, "")
3182 assert.Equal(t, 1, len(g.Objects))
3183 assert.Equal(t, (*d2graph.Scalar)(nil), g.Objects[0].Attributes.Style.Opacity)
3184 },
3185 },
3186 {
3187 name: "edge-reset",
3188 run: func(t *testing.T) {
3189 g, _ := assertCompile(t, `
3190 a -> b
3191 a: null
3192 a
3193 `, "")
3194 assert.Equal(t, 2, len(g.Objects))
3195 assert.Equal(t, 0, len(g.Edges))
3196 },
3197 },
3198 {
3199 name: "children-reset",
3200 run: func(t *testing.T) {
3201 g, _ := assertCompile(t, `
3202 a.b.c
3203 a.b: null
3204 a.b
3205 `, "")
3206 assert.Equal(t, 2, len(g.Objects))
3207 },
3208 },
3209 }
3210
3211 for _, tc := range tca {
3212 tc := tc
3213 t.Run(tc.name, func(t *testing.T) {
3214 t.Parallel()
3215 if tc.skip {
3216 t.SkipNow()
3217 }
3218 tc.run(t)
3219 })
3220 }
3221 })
3222
3223 t.Run("implicit", func(t *testing.T) {
3224 t.Parallel()
3225
3226 tca := []struct {
3227 name string
3228 skip bool
3229 run func(t *testing.T)
3230 }{
3231 {
3232 name: "delete-connection",
3233 run: func(t *testing.T) {
3234 g, _ := assertCompile(t, `
3235 x -> y
3236 y: null
3237 `, "")
3238 assert.Equal(t, 1, len(g.Objects))
3239 assert.Equal(t, 0, len(g.Edges))
3240 },
3241 },
3242 {
3243 name: "delete-multiple-connections",
3244 run: func(t *testing.T) {
3245 g, _ := assertCompile(t, `
3246 x -> y
3247 z -> y
3248 y -> a
3249 y: null
3250 `, "")
3251 assert.Equal(t, 3, len(g.Objects))
3252 assert.Equal(t, 0, len(g.Edges))
3253 },
3254 },
3255 {
3256 name: "no-delete-connection",
3257 run: func(t *testing.T) {
3258 g, _ := assertCompile(t, `
3259 y: null
3260 x -> y
3261 `, "")
3262 assert.Equal(t, 2, len(g.Objects))
3263 assert.Equal(t, 1, len(g.Edges))
3264 },
3265 },
3266 {
3267 name: "delete-children",
3268 run: func(t *testing.T) {
3269 g, _ := assertCompile(t, `
3270 x.y.z
3271 a.b.c
3272
3273 x: null
3274 a.b: null
3275 `, "")
3276 assert.Equal(t, 1, len(g.Objects))
3277 },
3278 },
3279 }
3280
3281 for _, tc := range tca {
3282 tc := tc
3283 t.Run(tc.name, func(t *testing.T) {
3284 t.Parallel()
3285 if tc.skip {
3286 t.SkipNow()
3287 }
3288 tc.run(t)
3289 })
3290 }
3291 })
3292
3293 t.Run("multiboard", func(t *testing.T) {
3294 t.Parallel()
3295
3296 tca := []struct {
3297 name string
3298 skip bool
3299 run func(t *testing.T)
3300 }{
3301 {
3302 name: "scenario",
3303 run: func(t *testing.T) {
3304 g, _ := assertCompile(t, `
3305 x
3306
3307 scenarios: {
3308 a: {
3309 x: null
3310 }
3311 }
3312 `, "")
3313 assert.Equal(t, 0, len(g.Scenarios[0].Objects))
3314 },
3315 },
3316 }
3317
3318 for _, tc := range tca {
3319 tc := tc
3320 t.Run(tc.name, func(t *testing.T) {
3321 t.Parallel()
3322 if tc.skip {
3323 t.SkipNow()
3324 }
3325 tc.run(t)
3326 })
3327 }
3328 })
3329 }
3330
3331 func testVars(t *testing.T) {
3332 t.Parallel()
3333
3334 t.Run("basic", func(t *testing.T) {
3335 t.Parallel()
3336
3337 tca := []struct {
3338 name string
3339 skip bool
3340 run func(t *testing.T)
3341 }{
3342 {
3343 name: "shape-label",
3344 run: func(t *testing.T) {
3345 g, _ := assertCompile(t, `
3346 vars: {
3347 x: im a var
3348 }
3349 hi: ${x}
3350 `, "")
3351 assert.Equal(t, 1, len(g.Objects))
3352 assert.Equal(t, "im a var", g.Objects[0].Label.Value)
3353 },
3354 },
3355 {
3356 name: "style",
3357 run: func(t *testing.T) {
3358 g, _ := assertCompile(t, `
3359 vars: {
3360 primary-color: red
3361 }
3362 hi: {
3363 style.fill: ${primary-color}
3364 }
3365 `, "")
3366 assert.Equal(t, 1, len(g.Objects))
3367 assert.Equal(t, "red", g.Objects[0].Style.Fill.Value)
3368 },
3369 },
3370 {
3371 name: "number",
3372 run: func(t *testing.T) {
3373 g, _ := assertCompile(t, `
3374 vars: {
3375 columns: 2
3376 }
3377 hi: {
3378 grid-columns: ${columns}
3379 x
3380 }
3381 `, "")
3382 assert.Equal(t, "2", g.Objects[0].GridColumns.Value)
3383 },
3384 },
3385 {
3386 name: "nested",
3387 run: func(t *testing.T) {
3388 g, _ := assertCompile(t, `
3389 vars: {
3390 colors: {
3391 primary: {
3392 button: red
3393 }
3394 }
3395 }
3396 hi: {
3397 style.fill: ${colors.primary.button}
3398 }
3399 `, "")
3400 assert.Equal(t, "red", g.Objects[0].Style.Fill.Value)
3401 },
3402 },
3403 {
3404 name: "combined",
3405 run: func(t *testing.T) {
3406 g, _ := assertCompile(t, `
3407 vars: {
3408 x: im a var
3409 }
3410 hi: 1 ${x} 2
3411 `, "")
3412 assert.Equal(t, "1 im a var 2", g.Objects[0].Label.Value)
3413 },
3414 },
3415 {
3416 name: "double-quoted",
3417 run: func(t *testing.T) {
3418 g, _ := assertCompile(t, `
3419 vars: {
3420 x: im a var
3421 }
3422 hi: "1 ${x} 2"
3423 `, "")
3424 assert.Equal(t, "1 im a var 2", g.Objects[0].Label.Value)
3425 },
3426 },
3427 {
3428 name: "single-quoted",
3429 run: func(t *testing.T) {
3430 g, _ := assertCompile(t, `
3431 vars: {
3432 x: im a var
3433 }
3434 hi: '1 ${x} 2'
3435 `, "")
3436 assert.Equal(t, "1 ${x} 2", g.Objects[0].Label.Value)
3437 },
3438 },
3439 {
3440 name: "edge-label",
3441 run: func(t *testing.T) {
3442 g, _ := assertCompile(t, `
3443 vars: {
3444 x: im a var
3445 }
3446 a -> b: ${x}
3447 `, "")
3448 assert.Equal(t, 1, len(g.Edges))
3449 assert.Equal(t, "im a var", g.Edges[0].Label.Value)
3450 },
3451 },
3452 {
3453 name: "edge-map",
3454 run: func(t *testing.T) {
3455 g, _ := assertCompile(t, `
3456 vars: {
3457 x: im a var
3458 }
3459 a -> b: {
3460 target-arrowhead.label: ${x}
3461 }
3462 `, "")
3463 assert.Equal(t, 1, len(g.Edges))
3464 assert.Equal(t, "im a var", g.Edges[0].DstArrowhead.Label.Value)
3465 },
3466 },
3467 {
3468 name: "quoted-var",
3469 run: func(t *testing.T) {
3470 g, _ := assertCompile(t, `
3471 vars: {
3472 primaryColors: {
3473 button: {
3474 active: "#4baae5"
3475 }
3476 }
3477 }
3478
3479 button: {
3480 style: {
3481 border-radius: 5
3482 fill: ${primaryColors.button.active}
3483 }
3484 }
3485 `, "")
3486 assert.Equal(t, `#4baae5`, g.Objects[0].Style.Fill.Value)
3487 },
3488 },
3489 {
3490 name: "quoted-var-quoted-sub",
3491 run: func(t *testing.T) {
3492 g, _ := assertCompile(t, `
3493 vars: {
3494 x: "hi"
3495 }
3496
3497 y: "hey ${x}"
3498 `, "")
3499 assert.Equal(t, `hey hi`, g.Objects[0].Label.Value)
3500 },
3501 },
3502 {
3503 name: "parent-scope",
3504 run: func(t *testing.T) {
3505 g, _ := assertCompile(t, `
3506 vars: {
3507 x: im root var
3508 }
3509 a: {
3510 vars: {
3511 b: im nested var
3512 }
3513 hi: ${x}
3514 }
3515 `, "")
3516 assert.Equal(t, "im root var", g.Objects[1].Label.Value)
3517 },
3518 },
3519 {
3520 name: "map",
3521 run: func(t *testing.T) {
3522 g, _ := assertCompile(t, `
3523 vars: {
3524 cool-style: {
3525 fill: red
3526 }
3527 arrows: {
3528 target-arrowhead.label: yay
3529 }
3530 }
3531 hi.style: ${cool-style}
3532 a -> b: ${arrows}
3533 `, "")
3534 assert.Equal(t, "red", g.Objects[0].Style.Fill.Value)
3535 assert.Equal(t, "yay", g.Edges[0].DstArrowhead.Label.Value)
3536 },
3537 },
3538 {
3539 name: "primary-and-composite",
3540 run: func(t *testing.T) {
3541 g, _ := assertCompile(t, `
3542 vars: {
3543 x: all {
3544 a: b
3545 }
3546 }
3547 z: ${x}
3548 `, "")
3549 assert.Equal(t, "z", g.Objects[1].ID)
3550 assert.Equal(t, "all", g.Objects[1].Label.Value)
3551 assert.Equal(t, 1, len(g.Objects[1].Children))
3552 },
3553 },
3554 {
3555 name: "spread",
3556 run: func(t *testing.T) {
3557 g, _ := assertCompile(t, `
3558 vars: {
3559 x: all {
3560 a: b
3561 b: c
3562 }
3563 }
3564 z: {
3565 ...${x}
3566 c
3567 }
3568 `, "")
3569 assert.Equal(t, "z", g.Objects[2].ID)
3570 assert.Equal(t, 4, len(g.Objects))
3571 assert.Equal(t, 3, len(g.Objects[2].Children))
3572 },
3573 },
3574 {
3575 name: "array",
3576 run: func(t *testing.T) {
3577 g, _ := assertCompile(t, `
3578 vars: {
3579 base-constraints: [UNQ; NOT NULL]
3580 }
3581 a: {
3582 shape: sql_table
3583 b: int {constraint: ${base-constraints}}
3584 }
3585 `, "")
3586 assert.Equal(t, "a", g.Objects[0].ID)
3587 assert.Equal(t, 2, len(g.Objects[0].SQLTable.Columns[0].Constraint))
3588 },
3589 },
3590 {
3591 name: "spread-array",
3592 run: func(t *testing.T) {
3593 g, _ := assertCompile(t, `
3594 vars: {
3595 base-constraints: [UNQ; NOT NULL]
3596 }
3597 a: {
3598 shape: sql_table
3599 b: int {constraint: [PK; ...${base-constraints}]}
3600 }
3601 `, "")
3602 assert.Equal(t, "a", g.Objects[0].ID)
3603 assert.Equal(t, 3, len(g.Objects[0].SQLTable.Columns[0].Constraint))
3604 },
3605 },
3606 {
3607 name: "sub-array",
3608 run: func(t *testing.T) {
3609 g, _ := assertCompile(t, `
3610 vars: {
3611 x: all
3612 }
3613 z.class: [a; ${x}]
3614 `, "")
3615 assert.Equal(t, "z", g.Objects[0].ID)
3616 assert.Equal(t, "all", g.Objects[0].Attributes.Classes[1])
3617 },
3618 },
3619 {
3620 name: "multi-part-array",
3621 run: func(t *testing.T) {
3622 g, _ := assertCompile(t, `
3623 vars: {
3624 x: all
3625 }
3626 z.class: [a; ${x}together]
3627 `, "")
3628 assert.Equal(t, "z", g.Objects[0].ID)
3629 assert.Equal(t, "alltogether", g.Objects[0].Attributes.Classes[1])
3630 },
3631 },
3632 {
3633 name: "double-quote-primary",
3634 run: func(t *testing.T) {
3635 g, _ := assertCompile(t, `
3636 vars: {
3637 x: always {
3638 a: b
3639 }
3640 }
3641 z: "${x} be my maybe"
3642 `, "")
3643 assert.Equal(t, "z", g.Objects[0].ID)
3644 assert.Equal(t, "always be my maybe", g.Objects[0].Label.Value)
3645 },
3646 },
3647 {
3648 name: "spread-nested",
3649 run: func(t *testing.T) {
3650 g, _ := assertCompile(t, `
3651 vars: {
3652 disclaimer: {
3653 I am not a lawyer
3654 }
3655 }
3656 custom-disclaimer: DRAFT DISCLAIMER {
3657 ...${disclaimer}
3658 }
3659 `, "")
3660 assert.Equal(t, 2, len(g.Objects))
3661 },
3662 },
3663 {
3664 name: "spread-edge",
3665 run: func(t *testing.T) {
3666 g, _ := assertCompile(t, `
3667 vars: {
3668 connections: {
3669 x -> a
3670 }
3671 }
3672 hi: {
3673 ...${connections}
3674 }
3675 `, "")
3676 assert.Equal(t, 3, len(g.Objects))
3677 assert.Equal(t, 1, len(g.Edges))
3678 },
3679 },
3680 }
3681
3682 for _, tc := range tca {
3683 tc := tc
3684 t.Run(tc.name, func(t *testing.T) {
3685 t.Parallel()
3686 if tc.skip {
3687 t.SkipNow()
3688 }
3689 tc.run(t)
3690 })
3691 }
3692 })
3693
3694 t.Run("override", func(t *testing.T) {
3695 t.Parallel()
3696
3697 tca := []struct {
3698 name string
3699 skip bool
3700 run func(t *testing.T)
3701 }{
3702 {
3703 name: "label",
3704 run: func(t *testing.T) {
3705 g, _ := assertCompile(t, `
3706 vars: {
3707 x: im a var
3708 }
3709 hi: ${x}
3710 hi: not a var
3711 `, "")
3712 assert.Equal(t, 1, len(g.Objects))
3713 assert.Equal(t, "not a var", g.Objects[0].Label.Value)
3714 },
3715 },
3716 {
3717 name: "map",
3718 run: func(t *testing.T) {
3719 g, _ := assertCompile(t, `
3720 vars: {
3721 x: im root var
3722 }
3723 a: {
3724 vars: {
3725 x: im nested var
3726 }
3727 hi: ${x}
3728 }
3729 `, "")
3730 assert.Equal(t, "im nested var", g.Objects[1].Label.Value)
3731 },
3732 },
3733 {
3734 name: "var-in-var",
3735 run: func(t *testing.T) {
3736 g, _ := assertCompile(t, `
3737 vars: {
3738 surname: Smith
3739 }
3740 a: {
3741 vars: {
3742 trade1: Black${surname}
3743 trade2: Metal${surname}
3744 }
3745 hi: ${trade1}
3746 }
3747 `, "")
3748 assert.Equal(t, "BlackSmith", g.Objects[1].Label.Value)
3749 },
3750 },
3751 {
3752 name: "recursive-var",
3753 run: func(t *testing.T) {
3754 g, _ := assertCompile(t, `
3755 vars: {
3756 x: a
3757 }
3758 hi: {
3759 vars: {
3760 x: ${x}-b
3761 }
3762 yo: ${x}
3763 }
3764 `, "")
3765 assert.Equal(t, "a-b", g.Objects[1].Label.Value)
3766 },
3767 },
3768 {
3769 name: "null",
3770 run: func(t *testing.T) {
3771 assertCompile(t, `
3772 vars: {
3773 surname: Smith
3774 }
3775 a: {
3776 vars: {
3777 surname: null
3778 }
3779 hi: John ${surname}
3780 }
3781 `, `d2/testdata/d2compiler/TestCompile2/vars/override/null.d2:9:3: could not resolve variable "surname"`)
3782 },
3783 },
3784 {
3785 name: "nested-null",
3786 run: func(t *testing.T) {
3787 assertCompile(t, `
3788 vars: {
3789 surnames: {
3790 john: smith
3791 }
3792 }
3793 a: {
3794 vars: {
3795 surnames: {
3796 john: null
3797 }
3798 }
3799 hi: John ${surname}
3800 }
3801 `, `d2/testdata/d2compiler/TestCompile2/vars/override/nested-null.d2:13:3: could not resolve variable "surname"`)
3802 },
3803 },
3804 }
3805
3806 for _, tc := range tca {
3807 tc := tc
3808 t.Run(tc.name, func(t *testing.T) {
3809 t.Parallel()
3810 if tc.skip {
3811 t.SkipNow()
3812 }
3813 tc.run(t)
3814 })
3815 }
3816 })
3817
3818 t.Run("boards", func(t *testing.T) {
3819 t.Parallel()
3820
3821 tca := []struct {
3822 name string
3823 skip bool
3824 run func(t *testing.T)
3825 }{
3826 {
3827 name: "layer",
3828 run: func(t *testing.T) {
3829 g, _ := assertCompile(t, `
3830 vars: {
3831 x: im a var
3832 }
3833
3834 layers: {
3835 l: {
3836 hi: ${x}
3837 }
3838 }
3839 `, "")
3840 assert.Equal(t, 1, len(g.Layers[0].Objects))
3841 assert.Equal(t, "im a var", g.Layers[0].Objects[0].Label.Value)
3842 },
3843 },
3844 {
3845 name: "layer-2",
3846 run: func(t *testing.T) {
3847 g, _ := assertCompile(t, `
3848 vars: {
3849 x: root var x
3850 y: root var y
3851 }
3852
3853 layers: {
3854 l: {
3855 vars: {
3856 x: layer var x
3857 }
3858 hi: ${x}
3859 hello: ${y}
3860 }
3861 }
3862 `, "")
3863 assert.Equal(t, "hi", g.Layers[0].Objects[0].ID)
3864 assert.Equal(t, "layer var x", g.Layers[0].Objects[0].Label.Value)
3865 assert.Equal(t, "hello", g.Layers[0].Objects[1].ID)
3866 assert.Equal(t, "root var y", g.Layers[0].Objects[1].Label.Value)
3867 },
3868 },
3869 {
3870 name: "scenario",
3871 run: func(t *testing.T) {
3872 g, _ := assertCompile(t, `
3873 vars: {
3874 x: im a var
3875 }
3876
3877 scenarios: {
3878 l: {
3879 hi: ${x}
3880 }
3881 }
3882 `, "")
3883 assert.Equal(t, 1, len(g.Scenarios[0].Objects))
3884 assert.Equal(t, "im a var", g.Scenarios[0].Objects[0].Label.Value)
3885 },
3886 },
3887 {
3888 name: "overlay",
3889 run: func(t *testing.T) {
3890 g, _ := assertCompile(t, `
3891 vars: {
3892 x: im x var
3893 }
3894
3895 scenarios: {
3896 l: {
3897 vars: {
3898 y: im y var
3899 }
3900 x: ${x}
3901 y: ${y}
3902 }
3903 }
3904 layers: {
3905 l2: {
3906 vars: {
3907 y: im y var
3908 }
3909 x: ${x}
3910 y: ${y}
3911 }
3912 }
3913 `, "")
3914 assert.Equal(t, 2, len(g.Scenarios[0].Objects))
3915 assert.Equal(t, "im x var", g.Scenarios[0].Objects[0].Label.Value)
3916 assert.Equal(t, "im y var", g.Scenarios[0].Objects[1].Label.Value)
3917 assert.Equal(t, 2, len(g.Layers[0].Objects))
3918 assert.Equal(t, "im x var", g.Layers[0].Objects[0].Label.Value)
3919 assert.Equal(t, "im y var", g.Layers[0].Objects[1].Label.Value)
3920 },
3921 },
3922 {
3923 name: "replace",
3924 run: func(t *testing.T) {
3925 g, _ := assertCompile(t, `
3926 vars: {
3927 x: im x var
3928 }
3929
3930 scenarios: {
3931 l: {
3932 vars: {
3933 x: im replaced x var
3934 }
3935 x: ${x}
3936 }
3937 }
3938 `, "")
3939 assert.Equal(t, 1, len(g.Scenarios[0].Objects))
3940 assert.Equal(t, "im replaced x var", g.Scenarios[0].Objects[0].Label.Value)
3941 },
3942 },
3943 }
3944
3945 for _, tc := range tca {
3946 tc := tc
3947 t.Run(tc.name, func(t *testing.T) {
3948 t.Parallel()
3949 if tc.skip {
3950 t.SkipNow()
3951 }
3952 tc.run(t)
3953 })
3954 }
3955 })
3956
3957 t.Run("config", func(t *testing.T) {
3958 t.Parallel()
3959
3960 tca := []struct {
3961 name string
3962 skip bool
3963 run func(t *testing.T)
3964 }{
3965 {
3966 name: "basic",
3967 run: func(t *testing.T) {
3968 _, config := assertCompile(t, `
3969 vars: {
3970 d2-config: {
3971 sketch: true
3972 }
3973 }
3974
3975 x -> y
3976 `, "")
3977 assert.Equal(t, true, *config.Sketch)
3978 },
3979 },
3980 {
3981 name: "invalid",
3982 run: func(t *testing.T) {
3983 assertCompile(t, `
3984 vars: {
3985 d2-config: {
3986 sketch: lol
3987 }
3988 }
3989
3990 x -> y
3991 `, `d2/testdata/d2compiler/TestCompile2/vars/config/invalid.d2:4:5: expected a boolean for "sketch", got "lol"`)
3992 },
3993 },
3994 {
3995 name: "not-root",
3996 run: func(t *testing.T) {
3997 assertCompile(t, `
3998 x: {
3999 vars: {
4000 d2-config: {
4001 sketch: false
4002 }
4003 }
4004 }
4005 `, `d2/testdata/d2compiler/TestCompile2/vars/config/not-root.d2:4:4: "d2-config" can only appear at root vars`)
4006 },
4007 },
4008 }
4009
4010 for _, tc := range tca {
4011 tc := tc
4012 t.Run(tc.name, func(t *testing.T) {
4013 t.Parallel()
4014 if tc.skip {
4015 t.SkipNow()
4016 }
4017 tc.run(t)
4018 })
4019 }
4020 })
4021
4022 t.Run("errors", func(t *testing.T) {
4023 t.Parallel()
4024
4025 tca := []struct {
4026 name string
4027 skip bool
4028 run func(t *testing.T)
4029 }{
4030 {
4031 name: "missing",
4032 run: func(t *testing.T) {
4033 assertCompile(t, `
4034 vars: {
4035 x: hey
4036 }
4037 hi: ${z}
4038 `, `d2/testdata/d2compiler/TestCompile2/vars/errors/missing.d2:5:1: could not resolve variable "z"`)
4039 },
4040 },
4041 {
4042 name: "multi-part-map",
4043 run: func(t *testing.T) {
4044 assertCompile(t, `
4045 vars: {
4046 x: {
4047 a: b
4048 }
4049 }
4050 hi: 1 ${x}
4051 `, `d2/testdata/d2compiler/TestCompile2/vars/errors/multi-part-map.d2:7:1: cannot substitute composite variable "x" as part of a string`)
4052 },
4053 },
4054 {
4055 name: "quoted-map",
4056 run: func(t *testing.T) {
4057 assertCompile(t, `
4058 vars: {
4059 x: {
4060 a: b
4061 }
4062 }
4063 hi: "${x}"
4064 `, `d2/testdata/d2compiler/TestCompile2/vars/errors/quoted-map.d2:7:1: cannot substitute map variable "x" in quotes`)
4065 },
4066 },
4067 {
4068 name: "nested-missing",
4069 run: func(t *testing.T) {
4070 assertCompile(t, `
4071 vars: {
4072 x: {
4073 y: hey
4074 }
4075 }
4076 hi: ${x.z}
4077 `, `d2/testdata/d2compiler/TestCompile2/vars/errors/nested-missing.d2:7:1: could not resolve variable "x.z"`)
4078 },
4079 },
4080 {
4081 name: "out-of-scope",
4082 run: func(t *testing.T) {
4083 assertCompile(t, `
4084 a: {
4085 vars: {
4086 x: hey
4087 }
4088 }
4089 hi: ${x}
4090 `, `d2/testdata/d2compiler/TestCompile2/vars/errors/out-of-scope.d2:7:1: could not resolve variable "x"`)
4091 },
4092 },
4093 {
4094 name: "recursive-var",
4095 run: func(t *testing.T) {
4096 assertCompile(t, `
4097 vars: {
4098 x: ${x}
4099 }
4100 hi: ${x}
4101 `, `d2/testdata/d2compiler/TestCompile2/vars/errors/recursive-var.d2:3:3: could not resolve variable "x"`)
4102 },
4103 },
4104
4105 {
4106 name: "spread-non-map",
4107 run: func(t *testing.T) {
4108 assertCompile(t, `
4109 vars: {
4110 x: all
4111 }
4112 z: {
4113 ...${x}
4114 c
4115 }
4116 `, `d2/testdata/d2compiler/TestCompile2/vars/errors/spread-non-map.d2:6:3: cannot spread non-composite`)
4117 },
4118 },
4119 {
4120 name: "missing-array",
4121 run: func(t *testing.T) {
4122 assertCompile(t, `
4123 vars: {
4124 x: b
4125 }
4126 z: {
4127 class: [...${a}]
4128 }
4129 `, `d2/testdata/d2compiler/TestCompile2/vars/errors/missing-array.d2:6:3: could not resolve variable "a"`)
4130 },
4131 },
4132 {
4133 name: "spread-non-array",
4134 run: func(t *testing.T) {
4135 assertCompile(t, `
4136 vars: {
4137 x: {
4138 a: b
4139 }
4140 }
4141 z: {
4142 class: [...${x}]
4143 }
4144 `, `d2/testdata/d2compiler/TestCompile2/vars/errors/spread-non-array.d2:8:11: cannot spread non-array into array`)
4145 },
4146 },
4147 {
4148 name: "spread-non-solo",
4149
4150 run: func(t *testing.T) {
4151 assertCompile(t, `
4152 vars: {
4153 x: {
4154 a: b
4155 }
4156 }
4157 z: {
4158 d: ...${x}
4159 c
4160 }
4161 `, `d2/testdata/d2compiler/TestCompile2/vars/errors/spread-non-solo.d2:8:2: cannot substitute composite variable "x" as part of a string`)
4162 },
4163 },
4164 {
4165 name: "spread-mid-string",
4166 run: func(t *testing.T) {
4167 assertCompile(t, `
4168 vars: {
4169 test: hello
4170 }
4171
4172 mybox: {
4173 label: prefix${test}suffix
4174 }
4175 `, "")
4176 },
4177 },
4178 {
4179 name: "undeclared-var-usage",
4180 run: func(t *testing.T) {
4181 assertCompile(t, `
4182 x: { ...${v} }
4183 `, `d2/testdata/d2compiler/TestCompile2/vars/errors/undeclared-var-usage.d2:2:4: could not resolve variable "v"`)
4184 },
4185 },
4186 {
4187 name: "split-var-usage",
4188 run: func(t *testing.T) {
4189 assertCompile(t, `
4190 x1
4191
4192 vars: {
4193 v: {
4194 style.fill: green
4195 }
4196 }
4197
4198 x1: { ...${v} }
4199 `, ``)
4200 },
4201 },
4202 }
4203
4204 for _, tc := range tca {
4205 tc := tc
4206 t.Run(tc.name, func(t *testing.T) {
4207 t.Parallel()
4208 if tc.skip {
4209 t.SkipNow()
4210 }
4211 tc.run(t)
4212 })
4213 }
4214 })
4215 }
4216
4217 func testGlobs(t *testing.T) {
4218 t.Parallel()
4219
4220 tca := []struct {
4221 name string
4222 skip bool
4223 run func(t *testing.T)
4224 }{
4225 {
4226 name: "alixander-lazy-globs-review/1",
4227 run: func(t *testing.T) {
4228 assertCompile(t, `
4229 ***.style.fill: yellow
4230 **.shape: circle
4231 *.style.multiple: true
4232
4233 x: {
4234 y
4235 }
4236
4237 layers: {
4238 next: {
4239 a
4240 }
4241 }
4242 `, "")
4243 },
4244 },
4245 {
4246 name: "alixander-lazy-globs-review/2",
4247 run: func(t *testing.T) {
4248 assertCompile(t, `
4249 **.style.fill: yellow
4250
4251 scenarios: {
4252 b: {
4253 a -> b
4254 }
4255 }
4256 `, "")
4257 },
4258 },
4259 {
4260 name: "alixander-lazy-globs-review/3",
4261 run: func(t *testing.T) {
4262 assertCompile(t, `
4263 ***: {
4264 c: d
4265 }
4266
4267 ***: {
4268 style.fill: red
4269 }
4270
4271 table: {
4272 shape: sql_table
4273 a: b
4274 }
4275
4276 class: {
4277 shape: class
4278 a: b
4279 }
4280 `, "")
4281 },
4282 },
4283 {
4284 name: "double-glob-err-val",
4285 run: func(t *testing.T) {
4286 assertCompile(t, `
4287 **: {
4288 label: hi
4289 label.near: center
4290 }
4291
4292 x: {
4293 a -> b
4294 }
4295 `, `d2/testdata/d2compiler/TestCompile2/globs/double-glob-err-val.d2:4:3: invalid "near" field`)
4296 },
4297 },
4298 {
4299 name: "double-glob-override-err-val",
4300 run: func(t *testing.T) {
4301 assertCompile(t, `
4302 (** -> **)[*]: {
4303 label.near: top-center
4304 }
4305 (** -> **)[*]: {
4306 label.near: invalid
4307 }
4308
4309 x: {
4310 a -> b
4311 }
4312 `, `d2/testdata/d2compiler/TestCompile2/globs/double-glob-override-err-val.d2:6:2: invalid "near" field`)
4313 },
4314 },
4315 {
4316 name: "creating-node-bug",
4317 run: func(t *testing.T) {
4318 g, _ := assertCompile(t, `
4319 *.*a -> *.*b
4320
4321 container_1: {
4322 a
4323 }
4324
4325 container_2: {
4326 b
4327 }
4328 `, ``)
4329 assert.Equal(t, 4, len(g.Objects))
4330 },
4331 },
4332 }
4333
4334 for _, tc := range tca {
4335 tc := tc
4336 t.Run(tc.name, func(t *testing.T) {
4337 t.Parallel()
4338 if tc.skip {
4339 t.SkipNow()
4340 }
4341 tc.run(t)
4342 })
4343 }
4344 }
4345
4346 func assertCompile(t *testing.T, text string, expErr string) (*d2graph.Graph, *d2target.Config) {
4347 d2Path := fmt.Sprintf("d2/testdata/d2compiler/%v.d2", t.Name())
4348 g, config, err := d2compiler.Compile(d2Path, strings.NewReader(text), nil)
4349 if expErr != "" {
4350 assert.Error(t, err)
4351 assert.ErrorString(t, err, expErr)
4352 } else {
4353 assert.Success(t, err)
4354 }
4355
4356 got := struct {
4357 Graph *d2graph.Graph `json:"graph"`
4358 Err error `json:"err"`
4359 }{
4360 Graph: g,
4361 Err: err,
4362 }
4363
4364 err = diff.TestdataJSON(filepath.Join("..", "testdata", "d2compiler", t.Name()), got)
4365 assert.Success(t, err)
4366 return g, config
4367 }
4368
View as plain text