1 package d2sketch_test
2
3 import (
4 "context"
5 "encoding/xml"
6 "os"
7 "path/filepath"
8 "strings"
9 "testing"
10
11 "cdr.dev/slog"
12
13 tassert "github.com/stretchr/testify/assert"
14
15 "oss.terrastruct.com/util-go/assert"
16 "oss.terrastruct.com/util-go/diff"
17 "oss.terrastruct.com/util-go/go2"
18
19 "oss.terrastruct.com/d2/d2graph"
20 "oss.terrastruct.com/d2/d2layouts/d2dagrelayout"
21 "oss.terrastruct.com/d2/d2layouts/d2elklayout"
22 "oss.terrastruct.com/d2/d2lib"
23 "oss.terrastruct.com/d2/d2renderers/d2fonts"
24 "oss.terrastruct.com/d2/d2renderers/d2svg"
25 "oss.terrastruct.com/d2/d2themes/d2themescatalog"
26 "oss.terrastruct.com/d2/lib/log"
27 "oss.terrastruct.com/d2/lib/textmeasure"
28 )
29
30 func TestSketch(t *testing.T) {
31 t.Parallel()
32
33 tcs := []testCase{
34 {
35 name: "basic",
36 script: `a -> b
37 `,
38 },
39 {
40 name: "child to child",
41 script: `winter.snow -> summer.sun
42 `,
43 },
44 {
45 name: "elk corners",
46 engine: "elk",
47 script: `a -> b
48 b -> c
49 a -> c
50 c -> a
51 `,
52 },
53 {
54 name: "animated",
55 script: `winter.snow -> summer.sun -> trees -> winter.snow: { style.animated: true }
56 `,
57 },
58 {
59 name: "connection label",
60 script: `a -> b: hello
61 `,
62 },
63 {
64 name: "crows feet",
65 script: `a1 <-> b1: {
66 style.stroke-width: 1
67 source-arrowhead: {
68 shape: cf-many
69 }
70 target-arrowhead: {
71 shape: cf-many
72 }
73 }
74 a2 <-> b2: {
75 style.stroke-width: 3
76 source-arrowhead: {
77 shape: cf-many
78 }
79 target-arrowhead: {
80 shape: cf-many
81 }
82 }
83 a3 <-> b3: {
84 style.stroke-width: 6
85 source-arrowhead: {
86 shape: cf-many
87 }
88 target-arrowhead: {
89 shape: cf-many
90 }
91 }
92
93 c1 <-> d1: {
94 style.stroke-width: 1
95 source-arrowhead: {
96 shape: cf-many-required
97 }
98 target-arrowhead: {
99 shape: cf-many-required
100 }
101 }
102 c2 <-> d2: {
103 style.stroke-width: 3
104 source-arrowhead: {
105 shape: cf-many-required
106 }
107 target-arrowhead: {
108 shape: cf-many-required
109 }
110 }
111 c3 <-> d3: {
112 style.stroke-width: 6
113 source-arrowhead: {
114 shape: cf-many-required
115 }
116 target-arrowhead: {
117 shape: cf-many-required
118 }
119 }
120
121 e1 <-> f1: {
122 style.stroke-width: 1
123 source-arrowhead: {
124 shape: cf-one
125 }
126 target-arrowhead: {
127 shape: cf-one
128 }
129 }
130 e2 <-> f2: {
131 style.stroke-width: 3
132 source-arrowhead: {
133 shape: cf-one
134 }
135 target-arrowhead: {
136 shape: cf-one
137 }
138 }
139 e3 <-> f3: {
140 style.stroke-width: 6
141 source-arrowhead: {
142 shape: cf-one
143 }
144 target-arrowhead: {
145 shape: cf-one
146 }
147 }
148
149 g1 <-> h1: {
150 style.stroke-width: 1
151 source-arrowhead: {
152 shape: cf-one-required
153 }
154 target-arrowhead: {
155 shape: cf-one-required
156 }
157 }
158 g2 <-> h2: {
159 style.stroke-width: 3
160 source-arrowhead: {
161 shape: cf-one-required
162 }
163 target-arrowhead: {
164 shape: cf-one-required
165 }
166 }
167 g3 <-> h3: {
168 style.stroke-width: 6
169 source-arrowhead: {
170 shape: cf-one-required
171 }
172 target-arrowhead: {
173 shape: cf-one-required
174 }
175 }
176
177 c <-> d <-> f: {
178 style.stroke-width: 1
179 style.stroke: "orange"
180 source-arrowhead: {
181 shape: cf-many-required
182 }
183 target-arrowhead: {
184 shape: cf-one
185 }
186 }
187 `,
188 },
189 {
190 name: "twitter",
191 script: `timeline mixer: "" {
192 explanation: |md
193 ## **Timeline mixer**
194 - Inject ads, who-to-follow, onboarding
195 - Conversation module
196 - Cursoring,pagination
197 - Tweat deduplication
198 - Served data logging
199 |
200 }
201 People discovery: "People discovery \nservice"
202 admixer: Ad mixer {
203 style.fill: "#c1a2f3"
204 }
205
206 onboarding service: "Onboarding \nservice"
207 timeline mixer -> People discovery
208 timeline mixer -> onboarding service
209 timeline mixer -> admixer
210 container0: "" {
211 graphql
212 comment
213 tlsapi
214 }
215 container0.graphql: GraphQL\nFederated Strato Column {
216 shape: image
217 icon: https://upload.wikimedia.org/wikipedia/commons/thumb/1/17/GraphQL_Logo.svg/1200px-GraphQL_Logo.svg.png
218 }
219 container0.comment: |md
220 ## Tweet/user content hydration, visibility filtering
221 |
222 container0.tlsapi: TLS-API (being deprecated)
223 container0.graphql -> timeline mixer
224 timeline mixer <- container0.tlsapi
225 twitter fe: "Twitter Frontend " {
226 icon: https://icons.terrastruct.com/social/013-twitter-1.svg
227 shape: image
228 }
229 twitter fe -> container0.graphql: iPhone web
230 twitter fe -> container0.tlsapi: HTTP Android
231 web: Web {
232 icon: https://icons.terrastruct.com/azure/Web%20Service%20Color/App%20Service%20Domains.svg
233 shape: image
234 }
235
236 Iphone: {
237 icon: 'https://ss7.vzw.com/is/image/VerizonWireless/apple-iphone-12-64gb-purple-53017-mjn13ll-a?$device-lg$'
238 shape: image
239 }
240 Android: {
241 icon: https://cdn4.iconfinder.com/data/icons/smart-phones-technologies/512/android-phone.png
242 shape: image
243 }
244
245 web -> twitter fe
246 timeline scorer: "Timeline\nScorer" {
247 style.fill: "#ffdef1"
248 }
249 home ranker: Home Ranker
250
251 timeline service: Timeline Service
252 timeline mixer -> timeline scorer: Thrift RPC
253 timeline mixer -> home ranker: {
254 style.stroke-dash: 4
255 style.stroke: "#000E3D"
256 }
257 timeline mixer -> timeline service
258 home mixer: Home mixer {
259 # style.fill "#c1a2f3"
260 }
261 container0.graphql -> home mixer: {
262 style.stroke-dash: 4
263 style.stroke: "#000E3D"
264 }
265 home mixer -> timeline scorer
266 home mixer -> home ranker: {
267 style.stroke-dash: 4
268 style.stroke: "#000E3D"
269 }
270 home mixer -> timeline service
271 manhattan 2: Manhattan
272 gizmoduck: Gizmoduck
273 socialgraph: Social graph
274 tweetypie: Tweety Pie
275 home mixer -> manhattan 2
276 home mixer -> gizmoduck
277 home mixer -> socialgraph
278 home mixer -> tweetypie
279 Iphone -> twitter fe
280 Android -> twitter fe
281 prediction service2: Prediction Service {
282 shape: image
283 icon: https://cdn-icons-png.flaticon.com/512/6461/6461819.png
284 }
285 home scorer: Home Scorer {
286 style.fill: "#ffdef1"
287 }
288 manhattan: Manhattan
289 memcache: Memcache {
290 icon: https://d1q6f0aelx0por.cloudfront.net/product-logos/de041504-0ddb-43f6-b89e-fe04403cca8d-memcached.png
291 }
292
293 fetch: Fetch {
294 style.multiple: true
295 shape: step
296 }
297 feature: Feature {
298 style.multiple: true
299 shape: step
300 }
301 scoring: Scoring {
302 style.multiple: true
303 shape: step
304 }
305 fetch -> feature
306 feature -> scoring
307
308 prediction service: Prediction Service {
309 shape: image
310 icon: https://cdn-icons-png.flaticon.com/512/6461/6461819.png
311 }
312 scoring -> prediction service
313 fetch -> container2.crmixer
314
315 home scorer -> manhattan: ""
316
317 home scorer -> memcache: ""
318 home scorer -> prediction service2
319 home ranker -> home scorer
320 home ranker -> container2.crmixer: Candidate Fetch
321 container2: "" {
322 style.stroke: "#000E3D"
323 style.fill: "#ffffff"
324 crmixer: CrMixer {
325 style.fill: "#F7F8FE"
326 }
327 earlybird: EarlyBird
328 utag: Utag
329 space: Space
330 communities: Communities
331 }
332 etc: ...etc
333
334 home scorer -> etc: Feature Hydration
335
336 feature -> manhattan
337 feature -> memcache
338 feature -> etc: Candidate sources
339 `,
340 },
341 {
342 name: "all_shapes",
343 script: `
344 rectangle: {shape: "rectangle"}
345 square: {shape: "square"}
346 page: {shape: "page"}
347 parallelogram: {shape: "parallelogram"}
348 document: {shape: "document"}
349 cylinder: {shape: "cylinder"}
350 queue: {shape: "queue"}
351 package: {shape: "package"}
352 step: {shape: "step"}
353 callout: {shape: "callout"}
354 stored_data: {shape: "stored_data"}
355 person: {shape: "person"}
356 diamond: {shape: "diamond"}
357 oval: {shape: "oval"}
358 circle: {shape: "circle"}
359 hexagon: {shape: "hexagon"}
360 cloud: {shape: "cloud"}
361
362 rectangle -> square -> page
363 parallelogram -> document -> cylinder
364 queue -> package -> step
365 callout -> stored_data -> person
366 diamond -> oval -> circle
367 hexagon -> cloud
368 `,
369 },
370 {
371 name: "sql_tables",
372 script: `users: {
373 shape: sql_table
374 id: int
375 name: string
376 email: string
377 password: string
378 last_login: datetime
379 }
380
381 products: {
382 shape: sql_table
383 id: int
384 price: decimal
385 sku: string
386 name: string
387 }
388
389 orders: {
390 shape: sql_table
391 id: int
392 user_id: int
393 product_id: int
394 }
395
396 shipments: {
397 shape: sql_table
398 id: int
399 order_id: int
400 tracking_number: string {constraint: primary_key}
401 status: string
402 }
403
404 users.id <-> orders.user_id
405 products.id <-> orders.product_id
406 shipments.order_id <-> orders.id`,
407 },
408 {
409 name: "class",
410 script: `manager: BatchManager {
411 shape: class
412 -num: int
413 -timeout: int
414 -pid
415
416 +getStatus(): Enum
417 +getJobs(): "Job[]"
418 +setTimeout(seconds int)
419 }
420 `,
421 },
422 {
423 name: "arrowheads",
424 script: `
425 a: ""
426 b: ""
427 a.1 -- b.1: none
428 a.2 <-> b.2: arrow {
429 source-arrowhead.shape: arrow
430 target-arrowhead.shape: arrow
431 }
432 a.3 <-> b.3: triangle {
433 source-arrowhead.shape: triangle
434 target-arrowhead.shape: triangle
435 }
436 a.4 <-> b.4: diamond {
437 source-arrowhead.shape: diamond
438 target-arrowhead.shape: diamond
439 }
440 a.5 <-> b.5: diamond filled {
441 source-arrowhead: {
442 shape: diamond
443 style.filled: true
444 }
445 target-arrowhead: {
446 shape: diamond
447 style.filled: true
448 }
449 }
450 a.6 <-> b.6: cf-many {
451 source-arrowhead.shape: cf-many
452 target-arrowhead.shape: cf-many
453 }
454 a.7 <-> b.7: cf-many-required {
455 source-arrowhead.shape: cf-many-required
456 target-arrowhead.shape: cf-many-required
457 }
458 a.8 <-> b.8: cf-one {
459 source-arrowhead.shape: cf-one
460 target-arrowhead.shape: cf-one
461 }
462 a.9 <-> b.9: cf-one-required {
463 source-arrowhead.shape: cf-one-required
464 target-arrowhead.shape: cf-one-required
465 }
466 `,
467 },
468 {
469 name: "opacity",
470 script: `x.style.opacity: 0.4
471 y: |md
472 linux: because a PC is a terrible thing to waste
473 | {
474 style.opacity: 0.4
475 }
476 x -> a: {
477 label: You don't have to know how the computer works,\njust how to work the computer.
478 style.opacity: 0.4
479 }
480 users: {
481 shape: sql_table
482 last_login: datetime
483 style.opacity: 0.4
484 }
485 `,
486 },
487 {
488 name: "overlay",
489 script: `bright: {
490 style.stroke: "#000"
491 style.font-color: "#000"
492 style.fill: "#fff"
493 }
494 normal: {
495 style.stroke: "#000"
496 style.font-color: "#000"
497 style.fill: "#ccc"
498 }
499 dark: {
500 style.stroke: "#000"
501 style.font-color: "#fff"
502 style.fill: "#555"
503 }
504 darker: {
505 style.stroke: "#000"
506 style.font-color: "#fff"
507 style.fill: "#000"
508 }
509 `,
510 },
511 {
512 name: "terminal",
513 themeID: d2themescatalog.Terminal.ID,
514 script: `network: {
515 cell tower: {
516 satellites: {
517 shape: stored_data
518 style.multiple: true
519 }
520
521 transmitter
522
523 satellites -> transmitter: send
524 satellites -> transmitter: send
525 satellites -> transmitter: send
526 }
527
528 online portal: {
529 ui: { shape: hexagon }
530 }
531
532 data processor: {
533 storage: {
534 shape: cylinder
535 style.multiple: true
536 }
537 }
538
539 cell tower.transmitter -> data processor.storage: phone logs
540 }
541
542 user: {
543 shape: person
544 width: 130
545 }
546
547 user -> network.cell tower: make call
548 user -> network.online portal.ui: access {
549 style.stroke-dash: 3
550 }
551
552 api server -> network.online portal.ui: display
553 api server -> logs: persist
554 logs: { shape: page; style.multiple: true }
555
556 network.data processor -> api server
557 `,
558 },
559 {
560 name: "basic dark",
561 themeID: 200,
562 script: `a -> b
563 `,
564 },
565 {
566 name: "child to child dark",
567 themeID: 200,
568 script: `winter.snow -> summer.sun
569 `,
570 },
571 {
572 name: "animated dark",
573 themeID: 200,
574 script: `winter.snow -> summer.sun -> trees -> winter.snow: { style.animated: true }
575 `,
576 },
577 {
578 name: "connection label dark",
579 themeID: 200,
580 script: `a -> b: hello
581 `,
582 },
583 {
584 name: "crows feet dark",
585 themeID: 200,
586 script: `a1 <-> b1: {
587 style.stroke-width: 1
588 source-arrowhead: {
589 shape: cf-many
590 }
591 target-arrowhead: {
592 shape: cf-many
593 }
594 }
595 a2 <-> b2: {
596 style.stroke-width: 3
597 source-arrowhead: {
598 shape: cf-many
599 }
600 target-arrowhead: {
601 shape: cf-many
602 }
603 }
604 a3 <-> b3: {
605 style.stroke-width: 6
606 source-arrowhead: {
607 shape: cf-many
608 }
609 target-arrowhead: {
610 shape: cf-many
611 }
612 }
613
614 c1 <-> d1: {
615 style.stroke-width: 1
616 source-arrowhead: {
617 shape: cf-many-required
618 }
619 target-arrowhead: {
620 shape: cf-many-required
621 }
622 }
623 c2 <-> d2: {
624 style.stroke-width: 3
625 source-arrowhead: {
626 shape: cf-many-required
627 }
628 target-arrowhead: {
629 shape: cf-many-required
630 }
631 }
632 c3 <-> d3: {
633 style.stroke-width: 6
634 source-arrowhead: {
635 shape: cf-many-required
636 }
637 target-arrowhead: {
638 shape: cf-many-required
639 }
640 }
641
642 e1 <-> f1: {
643 style.stroke-width: 1
644 source-arrowhead: {
645 shape: cf-one
646 }
647 target-arrowhead: {
648 shape: cf-one
649 }
650 }
651 e2 <-> f2: {
652 style.stroke-width: 3
653 source-arrowhead: {
654 shape: cf-one
655 }
656 target-arrowhead: {
657 shape: cf-one
658 }
659 }
660 e3 <-> f3: {
661 style.stroke-width: 6
662 source-arrowhead: {
663 shape: cf-one
664 }
665 target-arrowhead: {
666 shape: cf-one
667 }
668 }
669
670 g1 <-> h1: {
671 style.stroke-width: 1
672 source-arrowhead: {
673 shape: cf-one-required
674 }
675 target-arrowhead: {
676 shape: cf-one-required
677 }
678 }
679 g2 <-> h2: {
680 style.stroke-width: 3
681 source-arrowhead: {
682 shape: cf-one-required
683 }
684 target-arrowhead: {
685 shape: cf-one-required
686 }
687 }
688 g3 <-> h3: {
689 style.stroke-width: 6
690 source-arrowhead: {
691 shape: cf-one-required
692 }
693 target-arrowhead: {
694 shape: cf-one-required
695 }
696 }
697
698 c <-> d <-> f: {
699 style.stroke-width: 1
700 style.stroke: "orange"
701 source-arrowhead: {
702 shape: cf-many-required
703 }
704 target-arrowhead: {
705 shape: cf-one
706 }
707 }
708 `,
709 },
710 {
711 name: "twitter dark",
712 themeID: 200,
713 script: `timeline mixer: "" {
714 explanation: |md
715 ## **Timeline mixer**
716 - Inject ads, who-to-follow, onboarding
717 - Conversation module
718 - Cursoring,pagination
719 - Tweat deduplication
720 - Served data logging
721 |
722 }
723 People discovery: "People discovery \nservice"
724 admixer: Ad mixer {
725 style.fill: "#c1a2f3"
726 }
727
728 onboarding service: "Onboarding \nservice"
729 timeline mixer -> People discovery
730 timeline mixer -> onboarding service
731 timeline mixer -> admixer
732 container0: "" {
733 graphql
734 comment
735 tlsapi
736 }
737 container0.graphql: GraphQL\nFederated Strato Column {
738 shape: image
739 icon: https://upload.wikimedia.org/wikipedia/commons/thumb/1/17/GraphQL_Logo.svg/1200px-GraphQL_Logo.svg.png
740 }
741 container0.comment: |md
742 ## Tweet/user content hydration, visibility filtering
743 |
744 container0.tlsapi: TLS-API (being deprecated)
745 container0.graphql -> timeline mixer
746 timeline mixer <- container0.tlsapi
747 twitter fe: "Twitter Frontend " {
748 icon: https://icons.terrastruct.com/social/013-twitter-1.svg
749 shape: image
750 }
751 twitter fe -> container0.graphql: iPhone web
752 twitter fe -> container0.tlsapi: HTTP Android
753 web: Web {
754 icon: https://icons.terrastruct.com/azure/Web%20Service%20Color/App%20Service%20Domains.svg
755 shape: image
756 }
757
758 Iphone: {
759 icon: 'https://ss7.vzw.com/is/image/VerizonWireless/apple-iphone-12-64gb-purple-53017-mjn13ll-a?$device-lg$'
760 shape: image
761 }
762 Android: {
763 icon: https://cdn4.iconfinder.com/data/icons/smart-phones-technologies/512/android-phone.png
764 shape: image
765 }
766
767 web -> twitter fe
768 timeline scorer: "Timeline\nScorer" {
769 style.fill: "#ffdef1"
770 }
771 home ranker: Home Ranker
772
773 timeline service: Timeline Service
774 timeline mixer -> timeline scorer: Thrift RPC
775 timeline mixer -> home ranker: {
776 style.stroke-dash: 4
777 style.stroke: "#000E3D"
778 }
779 timeline mixer -> timeline service
780 home mixer: Home mixer {
781 # style.fill "#c1a2f3"
782 }
783 container0.graphql -> home mixer: {
784 style.stroke-dash: 4
785 style.stroke: "#000E3D"
786 }
787 home mixer -> timeline scorer
788 home mixer -> home ranker: {
789 style.stroke-dash: 4
790 style.stroke: "#000E3D"
791 }
792 home mixer -> timeline service
793 manhattan 2: Manhattan
794 gizmoduck: Gizmoduck
795 socialgraph: Social graph
796 tweetypie: Tweety Pie
797 home mixer -> manhattan 2
798 home mixer -> gizmoduck
799 home mixer -> socialgraph
800 home mixer -> tweetypie
801 Iphone -> twitter fe
802 Android -> twitter fe
803 prediction service2: Prediction Service {
804 shape: image
805 icon: https://cdn-icons-png.flaticon.com/512/6461/6461819.png
806 }
807 home scorer: Home Scorer {
808 style.fill: "#ffdef1"
809 }
810 manhattan: Manhattan
811 memcache: Memcache {
812 icon: https://d1q6f0aelx0por.cloudfront.net/product-logos/de041504-0ddb-43f6-b89e-fe04403cca8d-memcached.png
813 }
814
815 fetch: Fetch {
816 style.multiple: true
817 shape: step
818 }
819 feature: Feature {
820 style.multiple: true
821 shape: step
822 }
823 scoring: Scoring {
824 style.multiple: true
825 shape: step
826 }
827 fetch -> feature
828 feature -> scoring
829
830 prediction service: Prediction Service {
831 shape: image
832 icon: https://cdn-icons-png.flaticon.com/512/6461/6461819.png
833 }
834 scoring -> prediction service
835 fetch -> container2.crmixer
836
837 home scorer -> manhattan: ""
838
839 home scorer -> memcache: ""
840 home scorer -> prediction service2
841 home ranker -> home scorer
842 home ranker -> container2.crmixer: Candidate Fetch
843 container2: "" {
844 style.stroke: "#000E3D"
845 style.fill: "#ffffff"
846 crmixer: CrMixer {
847 style.fill: "#F7F8FE"
848 }
849 earlybird: EarlyBird
850 utag: Utag
851 space: Space
852 communities: Communities
853 }
854 etc: ...etc
855
856 home scorer -> etc: Feature Hydration
857
858 feature -> manhattan
859 feature -> memcache
860 feature -> etc: Candidate sources
861 `,
862 },
863 {
864 name: "all_shapes dark",
865 themeID: 200,
866 script: `
867 rectangle: {shape: "rectangle"}
868 square: {shape: "square"}
869 page: {shape: "page"}
870 parallelogram: {shape: "parallelogram"}
871 document: {shape: "document"}
872 cylinder: {shape: "cylinder"}
873 queue: {shape: "queue"}
874 package: {shape: "package"}
875 step: {shape: "step"}
876 callout: {shape: "callout"}
877 stored_data: {shape: "stored_data"}
878 person: {shape: "person"}
879 diamond: {shape: "diamond"}
880 oval: {shape: "oval"}
881 circle: {shape: "circle"}
882 hexagon: {shape: "hexagon"}
883 cloud: {shape: "cloud"}
884
885 rectangle -> square -> page
886 parallelogram -> document -> cylinder
887 queue -> package -> step
888 callout -> stored_data -> person
889 diamond -> oval -> circle
890 hexagon -> cloud
891 `,
892 },
893 {
894 name: "sql_tables dark",
895 themeID: 200,
896 script: `users: {
897 shape: sql_table
898 id: int
899 name: string
900 email: string
901 password: string
902 last_login: datetime
903 }
904
905 products: {
906 shape: sql_table
907 id: int
908 price: decimal
909 sku: string
910 name: string
911 }
912
913 orders: {
914 shape: sql_table
915 id: int
916 user_id: int
917 product_id: int
918 }
919
920 shipments: {
921 shape: sql_table
922 id: int
923 order_id: int
924 tracking_number: string {constraint: primary_key}
925 status: string
926 }
927
928 users.id <-> orders.user_id
929 products.id <-> orders.product_id
930 shipments.order_id <-> orders.id`,
931 },
932 {
933 name: "class dark",
934 themeID: 200,
935 script: `manager: BatchManager {
936 shape: class
937 -num: int
938 -timeout: int
939 -pid
940
941 +getStatus(): Enum
942 +getJobs(): "Job[]"
943 +setTimeout(seconds int)
944 }
945 `,
946 },
947 {
948 name: "arrowheads dark",
949 themeID: 200,
950 script: `
951 a: ""
952 b: ""
953 a.1 -- b.1: none
954 a.2 <-> b.2: arrow {
955 source-arrowhead.shape: arrow
956 target-arrowhead.shape: arrow
957 }
958 a.3 <-> b.3: triangle {
959 source-arrowhead.shape: triangle
960 target-arrowhead.shape: triangle
961 }
962 a.4 <-> b.4: diamond {
963 source-arrowhead.shape: diamond
964 target-arrowhead.shape: diamond
965 }
966 a.5 <-> b.5: diamond filled {
967 source-arrowhead: {
968 shape: diamond
969 style.filled: true
970 }
971 target-arrowhead: {
972 shape: diamond
973 style.filled: true
974 }
975 }
976 a.6 <-> b.6: cf-many {
977 source-arrowhead.shape: cf-many
978 target-arrowhead.shape: cf-many
979 }
980 a.7 <-> b.7: cf-many-required {
981 source-arrowhead.shape: cf-many-required
982 target-arrowhead.shape: cf-many-required
983 }
984 a.8 <-> b.8: cf-one {
985 source-arrowhead.shape: cf-one
986 target-arrowhead.shape: cf-one
987 }
988 a.9 <-> b.9: cf-one-required {
989 source-arrowhead.shape: cf-one-required
990 target-arrowhead.shape: cf-one-required
991 }
992 `,
993 },
994 {
995 name: "opacity dark",
996 themeID: 200,
997 script: `x.style.opacity: 0.4
998 y: |md
999 linux: because a PC is a terrible thing to waste
1000 | {
1001 style.opacity: 0.4
1002 }
1003 x -> a: {
1004 label: You don't have to know how the computer works,\njust how to work the computer.
1005 style.opacity: 0.4
1006 }
1007 users: {
1008 shape: sql_table
1009 last_login: datetime
1010 style.opacity: 0.4
1011 }
1012 `,
1013 },
1014 {
1015 name: "root-fill",
1016 script: `style.fill: honeydew
1017 style.stroke: LightSteelBlue
1018 style.double-border: true
1019
1020 title: Flow-I (Warehousing, Installation) {
1021 near: top-center
1022 shape: text
1023 style: {
1024 font-size: 24
1025 bold: false
1026 underline: false
1027 }
1028 }
1029 OEM Factory
1030 OEM Factory -> OEM Warehouse
1031 OEM Factory -> Distributor Warehouse
1032 OEM Factory -> company Warehouse
1033
1034 company Warehouse.Master -> company Warehouse.Regional-1
1035 company Warehouse.Master -> company Warehouse.Regional-2
1036 company Warehouse.Master -> company Warehouse.Regional-N
1037 company Warehouse.Regional-1 -> company Warehouse.Regional-2
1038 company Warehouse.Regional-2 -> company Warehouse.Regional-N
1039 company Warehouse.Regional-N -> company Warehouse.Regional-1
1040
1041 company Warehouse.explanation: |md
1042 ### company Warehouse
1043 - Asset Tagging
1044 - Inventory
1045 - Staging
1046 - Dispatch to Site
1047 |
1048 `,
1049 },
1050 {
1051 name: "double-border",
1052 script: `a: {
1053 style.double-border: true
1054 b
1055 }
1056 c: {
1057 shape: oval
1058 style.double-border: true
1059 d
1060 }
1061 normal: {
1062 nested normal
1063 }
1064 something
1065 `,
1066 },
1067 {
1068 name: "class_and_sqlTable_border_radius",
1069 script: `
1070 a: {
1071 shape: sql_table
1072 id: int {constraint: primary_key}
1073 disk: int {constraint: foreign_key}
1074
1075 json: jsonb {constraint: unique}
1076 last_updated: timestamp with time zone
1077
1078 style: {
1079 fill: red
1080 border-radius: 0
1081 }
1082 }
1083
1084 b: {
1085 shape: class
1086
1087 field: "[]string"
1088 method(a uint64): (x, y int)
1089
1090 style: {
1091 border-radius: 0
1092 }
1093 }
1094
1095 c: {
1096 shape: class
1097 style: {
1098 border-radius: 0
1099 }
1100 }
1101
1102 d: {
1103 shape: sql_table
1104 style: {
1105 border-radius: 0
1106 }
1107 }
1108 `,
1109 },
1110 {
1111 name: "paper-real",
1112 script: `style.fill-pattern: paper
1113 style.fill: "#947A6D"
1114 NETWORK: {
1115 style: {
1116 stroke: black
1117 fill-pattern: dots
1118 double-border: true
1119 fill: "#E7E9EE"
1120 font: mono
1121 }
1122 CELL TOWER: {
1123 style: {
1124 stroke: black
1125 fill-pattern: dots
1126 fill: "#F5F6F9"
1127 font: mono
1128 }
1129 satellites: SATELLITES {
1130 shape: stored_data
1131 style: {
1132 font: mono
1133 fill: white
1134 stroke: black
1135 multiple: true
1136 }
1137 }
1138
1139 transmitter: TRANSMITTER {
1140 style: {
1141 font: mono
1142 fill: white
1143 stroke: black
1144 }
1145 }
1146
1147 satellites -> transmitter: SEND {
1148 style.stroke: black
1149 style.font: mono
1150 }
1151 satellites -> transmitter: SEND {
1152 style.stroke: black
1153 style.font: mono
1154 }
1155 satellites -> transmitter: SEND {
1156 style.stroke: black
1157 style.font: mono
1158 }
1159 }
1160 }
1161 `},
1162 {
1163 name: "dots-real",
1164 script: `
1165 NETWORK: {
1166 style: {
1167 stroke: black
1168 fill-pattern: dots
1169 double-border: true
1170 fill: "#E7E9EE"
1171 font: mono
1172 }
1173 CELL TOWER: {
1174 style: {
1175 stroke: black
1176 fill-pattern: dots
1177 fill: "#F5F6F9"
1178 font: mono
1179 }
1180 satellites: SATELLITES {
1181 shape: stored_data
1182 style: {
1183 font: mono
1184 fill: white
1185 stroke: black
1186 multiple: true
1187 }
1188 }
1189
1190 transmitter: TRANSMITTER {
1191 style: {
1192 font: mono
1193 fill: white
1194 stroke: black
1195 }
1196 }
1197
1198 satellites -> transmitter: SEND {
1199 style.stroke: black
1200 style.font: mono
1201 }
1202 satellites -> transmitter: SEND {
1203 style.stroke: black
1204 style.font: mono
1205 }
1206 satellites -> transmitter: SEND {
1207 style.stroke: black
1208 style.font: mono
1209 }
1210 }
1211 }
1212 D2 Parser: {
1213 style.fill-pattern: grain
1214 shape: class
1215
1216 +reader: io.RuneReader
1217 # Default visibility is + so no need to specify.
1218 readerPos: d2ast.Position
1219
1220 # Private field.
1221 -lookahead: "[]rune"
1222
1223 # Escape the # to prevent being parsed as comment
1224 #lookaheadPos: d2ast.Position
1225 # Or just wrap in quotes
1226 "#peekn(n int)": (s string, eof bool)
1227
1228 +peek(): (r rune, eof bool)
1229 rewind()
1230 commit()
1231 }
1232 `,
1233 },
1234 {
1235 name: "dots-3d",
1236 script: `x: {style.3d: true; style.fill-pattern: dots}
1237 y: {shape: hexagon; style.3d: true; style.fill-pattern: dots}
1238 `,
1239 },
1240 {
1241 name: "dots-multiple",
1242 script: `
1243 rectangle: {shape: "rectangle"; style.fill-pattern: dots; style.multiple: true}
1244 square: {shape: "square"; style.fill-pattern: dots; style.multiple: true}
1245 page: {shape: "page"; style.fill-pattern: dots; style.multiple: true}
1246 parallelogram: {shape: "parallelogram"; style.fill-pattern: dots; style.multiple: true}
1247 document: {shape: "document"; style.fill-pattern: dots; style.multiple: true}
1248 cylinder: {shape: "cylinder"; style.fill-pattern: dots; style.multiple: true}
1249 queue: {shape: "queue"; style.fill-pattern: dots; style.multiple: true}
1250 package: {shape: "package"; style.fill-pattern: dots; style.multiple: true}
1251 step: {shape: "step"; style.fill-pattern: dots; style.multiple: true}
1252 callout: {shape: "callout"; style.fill-pattern: dots; style.multiple: true}
1253 stored_data: {shape: "stored_data"; style.fill-pattern: dots; style.multiple: true}
1254 person: {shape: "person"; style.fill-pattern: dots; style.multiple: true}
1255 diamond: {shape: "diamond"; style.fill-pattern: dots; style.multiple: true}
1256 oval: {shape: "oval"; style.fill-pattern: dots; style.multiple: true}
1257 circle: {shape: "circle"; style.fill-pattern: dots; style.multiple: true}
1258 hexagon: {shape: "hexagon"; style.fill-pattern: dots; style.multiple: true}
1259 cloud: {shape: "cloud"; style.fill-pattern: dots; style.multiple: true}
1260
1261 rectangle -> square -> page
1262 parallelogram -> document -> cylinder
1263 queue -> package -> step
1264 callout -> stored_data -> person
1265 diamond -> oval -> circle
1266 hexagon -> cloud
1267 `,
1268 },
1269 {
1270 name: "dots-all",
1271 script: `
1272 rectangle: {shape: "rectangle"; style.fill-pattern: dots}
1273 square: {shape: "square"; style.fill-pattern: dots}
1274 page: {shape: "page"; style.fill-pattern: dots}
1275 parallelogram: {shape: "parallelogram"; style.fill-pattern: dots}
1276 document: {shape: "document"; style.fill-pattern: dots}
1277 cylinder: {shape: "cylinder"; style.fill-pattern: dots}
1278 queue: {shape: "queue"; style.fill-pattern: dots}
1279 package: {shape: "package"; style.fill-pattern: dots}
1280 step: {shape: "step"; style.fill-pattern: dots}
1281 callout: {shape: "callout"; style.fill-pattern: dots}
1282 stored_data: {shape: "stored_data"; style.fill-pattern: dots}
1283 person: {shape: "person"; style.fill-pattern: dots}
1284 diamond: {shape: "diamond"; style.fill-pattern: dots}
1285 oval: {shape: "oval"; style.fill-pattern: dots}
1286 circle: {shape: "circle"; style.fill-pattern: dots}
1287 hexagon: {shape: "hexagon"; style.fill-pattern: dots}
1288 cloud: {shape: "cloud"; style.fill-pattern: dots}
1289
1290 rectangle -> square -> page
1291 parallelogram -> document -> cylinder
1292 queue -> package -> step
1293 callout -> stored_data -> person
1294 diamond -> oval -> circle
1295 hexagon -> cloud
1296 `,
1297 },
1298 {
1299 name: "long_arrowhead_label",
1300 script: `
1301 a -> b: {
1302 target-arrowhead: "a to b with unexpectedly long target arrowhead label"
1303 }
1304 `,
1305 },
1306 {
1307 name: "unfilled_triangle",
1308 script: `
1309 direction: right
1310
1311 A <-> B: default {
1312 source-arrowhead.style.filled: false
1313 target-arrowhead.style.filled: false
1314 }
1315 C <-> D: triangle {
1316 source-arrowhead: {
1317 shape: triangle
1318 style.filled: false
1319 }
1320 target-arrowhead: {
1321 shape: triangle
1322 style.filled: false
1323 }
1324 }`,
1325 },
1326 }
1327 runa(t, tcs)
1328 }
1329
1330 type testCase struct {
1331 name string
1332 themeID int64
1333 script string
1334 skip bool
1335 engine string
1336 }
1337
1338 func runa(t *testing.T, tcs []testCase) {
1339 for _, tc := range tcs {
1340 tc := tc
1341 t.Run(tc.name, func(t *testing.T) {
1342 if tc.skip {
1343 t.Skip()
1344 }
1345 t.Parallel()
1346
1347 run(t, tc)
1348 })
1349 }
1350 }
1351
1352 func run(t *testing.T, tc testCase) {
1353 ctx := context.Background()
1354 ctx = log.WithTB(ctx, t, nil)
1355 ctx = log.Leveled(ctx, slog.LevelDebug)
1356
1357 ruler, err := textmeasure.NewRuler()
1358 if !tassert.Nil(t, err) {
1359 return
1360 }
1361
1362 layoutResolver := func(engine string) (d2graph.LayoutGraph, error) {
1363 if strings.EqualFold(engine, "elk") {
1364 return d2elklayout.DefaultLayout, nil
1365 }
1366 return d2dagrelayout.DefaultLayout, nil
1367 }
1368 renderOpts := &d2svg.RenderOpts{
1369 Sketch: go2.Pointer(true),
1370 ThemeID: go2.Pointer(tc.themeID),
1371 }
1372 diagram, _, err := d2lib.Compile(ctx, tc.script, &d2lib.CompileOptions{
1373 Ruler: ruler,
1374 Layout: &tc.engine,
1375 LayoutResolver: layoutResolver,
1376 FontFamily: go2.Pointer(d2fonts.HandDrawn),
1377 }, renderOpts)
1378 if !tassert.Nil(t, err) {
1379 return
1380 }
1381
1382 dataPath := filepath.Join("testdata", strings.TrimPrefix(t.Name(), "TestSketch/"))
1383 pathGotSVG := filepath.Join(dataPath, "sketch.got.svg")
1384
1385 svgBytes, err := d2svg.Render(diagram, renderOpts)
1386 assert.Success(t, err)
1387 err = os.MkdirAll(dataPath, 0755)
1388 assert.Success(t, err)
1389 err = os.WriteFile(pathGotSVG, svgBytes, 0600)
1390 assert.Success(t, err)
1391 defer os.Remove(pathGotSVG)
1392
1393 var xmlParsed interface{}
1394 err = xml.Unmarshal(svgBytes, &xmlParsed)
1395 assert.Success(t, err)
1396
1397
1398 err = diff.Testdata(filepath.Join(dataPath, "sketch"), ".svg", svgBytes)
1399 assert.Success(t, err)
1400 }
1401
View as plain text