1 package d2ast_test
2
3 import (
4 "encoding/json"
5 "math/big"
6 math_rand "math/rand"
7 "reflect"
8 "strconv"
9 "strings"
10 "testing"
11
12 "oss.terrastruct.com/util-go/assert"
13 "oss.terrastruct.com/util-go/xrand"
14
15 "oss.terrastruct.com/util-go/diff"
16
17 "oss.terrastruct.com/util-go/go2"
18
19 "oss.terrastruct.com/d2/d2ast"
20 "oss.terrastruct.com/d2/d2format"
21 "oss.terrastruct.com/d2/d2parser"
22 )
23
24 func TestRange(t *testing.T) {
25 t.Parallel()
26
27 t.Run("String", func(t *testing.T) {
28 t.Parallel()
29
30 testCases := []struct {
31 name string
32 r d2ast.Range
33 exp string
34 }{
35 {
36 name: "one_byte",
37 r: d2ast.Range{
38 Path: "/src/example.go",
39 Start: d2ast.Position{
40 Line: 10,
41 Column: 5,
42 Byte: 100,
43 },
44 End: d2ast.Position{
45 Line: 10,
46 Column: 6,
47 Byte: 100,
48 },
49 },
50 exp: "/src/example.go:11:6",
51 },
52 {
53 name: "more_than_one_byte",
54 r: d2ast.Range{
55 Path: "/src/example.go",
56 Start: d2ast.Position{
57 Line: 10,
58 Column: 5,
59 Byte: 100,
60 },
61 End: d2ast.Position{
62 Line: 10,
63 Column: 7,
64 Byte: 101,
65 },
66 },
67 exp: "/src/example.go:11:6",
68 },
69 {
70 name: "empty_path",
71 r: d2ast.Range{
72 Start: d2ast.Position{
73 Line: 10,
74 Column: 5,
75 Byte: 100,
76 },
77 End: d2ast.Position{
78 Line: 10,
79 Column: 7,
80 Byte: 101,
81 },
82 },
83 exp: "11:6",
84 },
85 {
86 name: "start_equal_end",
87 r: d2ast.Range{
88 Start: d2ast.Position{
89 Line: 10,
90 Column: 5,
91 Byte: 100,
92 },
93 End: d2ast.Position{
94 Line: 10,
95 Column: 5,
96 Byte: 100,
97 },
98 },
99 exp: "11:6",
100 },
101 }
102
103 for _, tc := range testCases {
104 tc := tc
105 t.Run(tc.name, func(t *testing.T) {
106 t.Parallel()
107
108 if tc.exp != tc.r.String() {
109 t.Fatalf("expected %q but got %q", tc.exp, tc.r.String())
110 }
111 })
112 }
113 })
114
115 t.Run("UnmarshalText", func(t *testing.T) {
116 t.Parallel()
117
118 testCases := []struct {
119 name string
120 in string
121
122 exp d2ast.Range
123
124 errmsg string
125 }{
126 {
127 name: "success",
128 in: `"json_test.d2,1:1:0-5:1:50"`,
129 exp: d2ast.Range{Path: "json_test.d2", Start: d2ast.Position{Line: 1, Column: 1, Byte: 0}, End: d2ast.Position{Line: 5, Column: 1, Byte: 50}},
130 },
131 {
132 name: "err1",
133 in: `"json_test.d2-5:1:50"`,
134 errmsg: "missing Start field",
135 },
136 {
137 name: "err2",
138 in: `"json_test.d2"`,
139 errmsg: "missing End field",
140 },
141 {
142 name: "err3",
143 in: `"json_test.d2,1:1:0-5:150"`,
144 errmsg: "expected three fields",
145 },
146 {
147 name: "err4",
148 in: `"json_test.d2,1:10-5:1:50"`,
149 errmsg: "expected three fields",
150 },
151 {
152 name: "err5",
153 in: `"json_test.d2,a:1:0-5:1:50"`,
154 errmsg: `strconv.Atoi: parsing "a": invalid syntax`,
155 },
156 {
157 name: "err6",
158 in: `"json_test.d2,1:c:0-5:1:50"`,
159 errmsg: `strconv.Atoi: parsing "c": invalid syntax`,
160 },
161 }
162
163 for _, tc := range testCases {
164 tc := tc
165 t.Run(tc.name, func(t *testing.T) {
166 t.Parallel()
167
168 var r d2ast.Range
169 err := json.Unmarshal([]byte(tc.in), &r)
170
171 if tc.errmsg != "" {
172 if err == nil {
173 t.Fatalf("expected error: %#v", err)
174 }
175 if !strings.Contains(err.Error(), tc.errmsg) {
176 t.Fatalf("error message does not contain %q: %q", tc.errmsg, err.Error())
177 }
178 } else {
179 if err != nil {
180 t.Fatal(err)
181 }
182 if !reflect.DeepEqual(tc.exp, r) {
183 t.Fatalf("expected %#v but got %#v", tc.exp, r)
184 }
185 }
186 })
187 }
188 })
189
190 t.Run("Advance", func(t *testing.T) {
191 t.Parallel()
192
193 t.Run("UTF-8", func(t *testing.T) {
194 t.Parallel()
195
196 var p d2ast.Position
197 p = p.Advance('a', false)
198 assert.StringJSON(t, `"0:1:1"`, p)
199 p = p.Advance('\n', false)
200 assert.StringJSON(t, `"1:0:2"`, p)
201 p = p.Advance('è', false)
202 assert.StringJSON(t, `"1:2:4"`, p)
203 p = p.Advance('𐀀', false)
204 assert.StringJSON(t, `"1:6:8"`, p)
205
206 p = p.Subtract('𐀀', false)
207 assert.StringJSON(t, `"1:2:4"`, p)
208 p = p.Subtract('è', false)
209 assert.StringJSON(t, `"1:0:2"`, p)
210 })
211
212 t.Run("UTF-16", func(t *testing.T) {
213 t.Parallel()
214
215 var p d2ast.Position
216 p = p.Advance('a', true)
217 assert.StringJSON(t, `"0:1:1"`, p)
218 p = p.Advance('\n', true)
219 assert.StringJSON(t, `"1:0:2"`, p)
220 p = p.Advance('è', true)
221 assert.StringJSON(t, `"1:1:3"`, p)
222 p = p.Advance('𐀀', true)
223 assert.StringJSON(t, `"1:3:5"`, p)
224
225 p = p.Subtract('𐀀', true)
226 assert.StringJSON(t, `"1:1:3"`, p)
227 p = p.Subtract('è', true)
228 assert.StringJSON(t, `"1:0:2"`, p)
229 })
230 })
231 }
232
233 func TestJSON(t *testing.T) {
234 t.Parallel()
235
236 m := &d2ast.Map{
237 Range: d2ast.Range{Path: "json_test.d2", Start: d2ast.Position{Line: 0, Column: 0, Byte: 0}, End: d2ast.Position{Line: 5, Column: 1, Byte: 50}},
238
239 Nodes: []d2ast.MapNodeBox{
240 {
241 Comment: &d2ast.Comment{
242 Value: `America was discovered by Amerigo Vespucci and was named after him, until
243 people got tired of living in a place called "Vespuccia" and changed its
244 name to "America".
245 -- Mike Harding, "The Armchair Anarchist's Almanac"`,
246 },
247 },
248 {
249 BlockComment: &d2ast.BlockComment{
250 Value: `America was discovered by Amerigo Vespucci and was named after him, until
251 people got tired of living in a place called "Vespuccia" and changed its
252 name to "America".
253 -- Mike Harding, "The Armchair Anarchist's Almanac"`,
254 },
255 },
256 {
257 Substitution: &d2ast.Substitution{
258 Spread: true,
259 Path: []*d2ast.StringBox{
260 {
261 BlockString: &d2ast.BlockString{
262 Quote: "|",
263 Tag: "text",
264 Value: `America was discovered by Amerigo Vespucci and was named after him, until
265 people got tired of living in a place called "Vespuccia" and changed its
266 name to "America".
267 -- Mike Harding, "The Armchair Anarchist's Almanac"`,
268 },
269 },
270 },
271 },
272 },
273 {
274 MapKey: &d2ast.Key{
275 Ampersand: true,
276
277 Key: &d2ast.KeyPath{
278 Path: []*d2ast.StringBox{
279 {
280 SingleQuotedString: &d2ast.SingleQuotedString{
281 Value: "before edges",
282 },
283 },
284 },
285 },
286
287 Edges: []*d2ast.Edge{
288 {
289 Src: &d2ast.KeyPath{
290 Path: []*d2ast.StringBox{
291 {
292 SingleQuotedString: &d2ast.SingleQuotedString{
293 Value: "src",
294 },
295 },
296 },
297 },
298 SrcArrow: "*",
299
300 Dst: &d2ast.KeyPath{
301 Path: []*d2ast.StringBox{
302 {
303 SingleQuotedString: &d2ast.SingleQuotedString{
304 Value: "dst",
305 },
306 },
307 },
308 },
309 DstArrow: ">",
310 },
311 {
312 Src: &d2ast.KeyPath{
313 Path: []*d2ast.StringBox{
314 {
315 SingleQuotedString: &d2ast.SingleQuotedString{
316 Value: "dst",
317 },
318 },
319 },
320 },
321
322 Dst: &d2ast.KeyPath{
323 Path: []*d2ast.StringBox{
324 {
325 SingleQuotedString: &d2ast.SingleQuotedString{
326 Value: "dst2",
327 },
328 },
329 },
330 },
331 },
332 },
333
334 EdgeIndex: &d2ast.EdgeIndex{
335 Glob: true,
336 },
337
338 EdgeKey: &d2ast.KeyPath{
339 Path: []*d2ast.StringBox{
340 {
341 SingleQuotedString: &d2ast.SingleQuotedString{
342 Value: "after edges",
343 },
344 },
345 },
346 },
347
348 Primary: d2ast.ScalarBox{
349 Null: &d2ast.Null{},
350 },
351
352 Value: d2ast.ValueBox{
353 Array: &d2ast.Array{
354 Nodes: []d2ast.ArrayNodeBox{
355 {
356 Boolean: &d2ast.Boolean{
357 Value: true,
358 },
359 },
360 {
361 Number: &d2ast.Number{
362 Raw: "0xFF",
363 Value: big.NewRat(15, 1),
364 },
365 },
366 {
367 UnquotedString: &d2ast.UnquotedString{
368 Value: []d2ast.InterpolationBox{
369 {
370 String: go2.Pointer("no quotes needed"),
371 },
372 },
373 },
374 },
375 {
376 UnquotedString: &d2ast.UnquotedString{
377 Value: []d2ast.InterpolationBox{
378 {
379 Substitution: &d2ast.Substitution{},
380 },
381 },
382 },
383 },
384 {
385 DoubleQuotedString: &d2ast.DoubleQuotedString{
386 Value: []d2ast.InterpolationBox{
387 {
388 String: go2.Pointer("no quotes needed"),
389 },
390 },
391 },
392 },
393 {
394 SingleQuotedString: &d2ast.SingleQuotedString{
395 Value: "rawr",
396 },
397 },
398 {
399 BlockString: &d2ast.BlockString{
400 Quote: "|",
401 Tag: "text",
402 Value: `America was discovered by Amerigo Vespucci and was named after him, until
403 people got tired of living in a place called "Vespuccia" and changed its
404 name to "America".
405 -- Mike Harding, "The Armchair Anarchist's Almanac"`,
406 },
407 },
408 },
409 },
410 },
411 },
412 },
413 },
414 }
415
416 assert.StringJSON(t, `{
417 "range": "json_test.d2,0:0:0-5:1:50",
418 "nodes": [
419 {
420 "comment": {
421 "range": ",0:0:0-0:0:0",
422 "value": "America was discovered by Amerigo Vespucci and was named after him, until\npeople got tired of living in a place called \"Vespuccia\" and changed its\nname to \"America\".\n\t\t-- Mike Harding, \"The Armchair Anarchist's Almanac\""
423 }
424 },
425 {
426 "block_comment": {
427 "range": ",0:0:0-0:0:0",
428 "value": "America was discovered by Amerigo Vespucci and was named after him, until\npeople got tired of living in a place called \"Vespuccia\" and changed its\nname to \"America\".\n\t\t-- Mike Harding, \"The Armchair Anarchist's Almanac\""
429 }
430 },
431 {
432 "substitution": {
433 "range": ",0:0:0-0:0:0",
434 "spread": true,
435 "path": [
436 {
437 "block_string": {
438 "range": ",0:0:0-0:0:0",
439 "quote": "|",
440 "tag": "text",
441 "value": "America was discovered by Amerigo Vespucci and was named after him, until\n\tpeople got tired of living in a place called \"Vespuccia\" and changed its\n\tname to \"America\".\n\t-- Mike Harding, \"The Armchair Anarchist's Almanac\""
442 }
443 }
444 ]
445 }
446 },
447 {
448 "map_key": {
449 "range": ",0:0:0-0:0:0",
450 "ampersand": true,
451 "key": {
452 "range": ",0:0:0-0:0:0",
453 "path": [
454 {
455 "single_quoted_string": {
456 "range": ",0:0:0-0:0:0",
457 "raw": "",
458 "value": "before edges"
459 }
460 }
461 ]
462 },
463 "edges": [
464 {
465 "range": ",0:0:0-0:0:0",
466 "src": {
467 "range": ",0:0:0-0:0:0",
468 "path": [
469 {
470 "single_quoted_string": {
471 "range": ",0:0:0-0:0:0",
472 "raw": "",
473 "value": "src"
474 }
475 }
476 ]
477 },
478 "src_arrow": "*",
479 "dst": {
480 "range": ",0:0:0-0:0:0",
481 "path": [
482 {
483 "single_quoted_string": {
484 "range": ",0:0:0-0:0:0",
485 "raw": "",
486 "value": "dst"
487 }
488 }
489 ]
490 },
491 "dst_arrow": ">"
492 },
493 {
494 "range": ",0:0:0-0:0:0",
495 "src": {
496 "range": ",0:0:0-0:0:0",
497 "path": [
498 {
499 "single_quoted_string": {
500 "range": ",0:0:0-0:0:0",
501 "raw": "",
502 "value": "dst"
503 }
504 }
505 ]
506 },
507 "src_arrow": "",
508 "dst": {
509 "range": ",0:0:0-0:0:0",
510 "path": [
511 {
512 "single_quoted_string": {
513 "range": ",0:0:0-0:0:0",
514 "raw": "",
515 "value": "dst2"
516 }
517 }
518 ]
519 },
520 "dst_arrow": ""
521 }
522 ],
523 "edge_index": {
524 "range": ",0:0:0-0:0:0",
525 "int": null,
526 "glob": true
527 },
528 "edge_key": {
529 "range": ",0:0:0-0:0:0",
530 "path": [
531 {
532 "single_quoted_string": {
533 "range": ",0:0:0-0:0:0",
534 "raw": "",
535 "value": "after edges"
536 }
537 }
538 ]
539 },
540 "primary": {
541 "null": {
542 "range": ",0:0:0-0:0:0"
543 }
544 },
545 "value": {
546 "array": {
547 "range": ",0:0:0-0:0:0",
548 "nodes": [
549 {
550 "boolean": {
551 "range": ",0:0:0-0:0:0",
552 "value": true
553 }
554 },
555 {
556 "number": {
557 "range": ",0:0:0-0:0:0",
558 "raw": "0xFF",
559 "value": "15"
560 }
561 },
562 {
563 "unquoted_string": {
564 "range": ",0:0:0-0:0:0",
565 "value": [
566 {
567 "string": "no quotes needed"
568 }
569 ]
570 }
571 },
572 {
573 "unquoted_string": {
574 "range": ",0:0:0-0:0:0",
575 "value": [
576 {
577 "substitution": {
578 "range": ",0:0:0-0:0:0",
579 "spread": false,
580 "path": null
581 }
582 }
583 ]
584 }
585 },
586 {
587 "double_quoted_string": {
588 "range": ",0:0:0-0:0:0",
589 "value": [
590 {
591 "string": "no quotes needed"
592 }
593 ]
594 }
595 },
596 {
597 "single_quoted_string": {
598 "range": ",0:0:0-0:0:0",
599 "raw": "",
600 "value": "rawr"
601 }
602 },
603 {
604 "block_string": {
605 "range": ",0:0:0-0:0:0",
606 "quote": "|",
607 "tag": "text",
608 "value": "America was discovered by Amerigo Vespucci and was named after him, until\n\t\t\tpeople got tired of living in a place called \"Vespuccia\" and changed its\n\t\t\tname to \"America\".\n\t\t\t-- Mike Harding, \"The Armchair Anarchist's Almanac\""
609 }
610 }
611 ]
612 }
613 }
614 }
615 }
616 ]
617 }`, m)
618 }
619
620 func testRawStringKey(t *testing.T, key string) {
621 ast := d2ast.RawString(key, true)
622 enc := d2format.Format(ast)
623 k, err := d2parser.ParseKey(enc)
624 if err != nil {
625 t.Fatal(err)
626 }
627 if len(k.Path) != 1 {
628 t.Fatalf("unexpected key length: %#v", k.Path)
629 }
630 err = diff.Runes(key, k.Path[0].Unbox().ScalarString())
631 if err != nil {
632 t.Fatal(err)
633 }
634 }
635
636 func testRawStringValue(t *testing.T, value string) {
637 ast := d2ast.RawString(value, false)
638 enc := d2format.Format(ast)
639 v, err := d2parser.ParseValue(enc)
640 if err != nil {
641 t.Fatal(err)
642 }
643 ps, ok := v.(d2ast.Scalar)
644 if !ok {
645 t.Fatalf("unexpected value type: %#v", v)
646 }
647 err = diff.Runes(value, ps.ScalarString())
648 if err != nil {
649 t.Fatal(err)
650 }
651 }
652
653 func TestRawString(t *testing.T) {
654 t.Parallel()
655
656 t.Run("chaos", func(t *testing.T) {
657 t.Parallel()
658
659 t.Run("pinned", func(t *testing.T) {
660 t.Parallel()
661
662 pinnedTestCases := []struct {
663 name string
664 str string
665 }{
666 {
667 name: "1",
668 str: "\U000b64cd\U0008b732\U0009632c\U000983f8\U000f42d4\U000c4749\U00041723\uf584蝉\U00100cd5\U0003325d\U0003e4d2\U0007ff0e\U000e03d8\U000b0431\U00042053\U0001b3ea𠒹\U0006d9cf\U000c5b1c\U00019a3c\U000f3c3d\U0004acedଶ\U0009da18\U0001a0bb\U000b6bfd\U00015ebd\U00088c5a녈\U00078277\U000eaa58\U0009266b\U000d85ae\U000d6ce8譊𣱡\U0008ac84\U000a722f\U000d3d35\U00072581\U000c3423\U000a1753\U00082014\U0001bde6\U0010bf47炏\U000423fa\U0007df70\U00088aaf\U00074e5e\U000ee80b\U000e3d53\U0003f542\U0001ad9f\U00031408\U000cce7e\U00082172\u202f",
669 },
670 {
671 name: "2",
672 str: "'\"Tc\U000d148d\U000dd61a\U0007cf68OO\U000b87a9\U000c073a\U000e7828n\U00068a9fc\U0004fbf5\x041\\'''",
673 },
674 {
675 name: "3",
676 str: "\r\U00057d53\x01'\U00042e5a\U0007be73T\U000fb916\x01\U000e0e4afL]\U000474d1\x15\U00083bc0\fbT\ue09bs{vP\U000b3d33\x0f\U0007ad13\x10\U00098b38\x1d\U000cf9da\n ",
677 },
678 }
679 for _, tc := range pinnedTestCases {
680 tc := tc
681 t.Run(tc.name, func(t *testing.T) {
682 t.Parallel()
683
684 t.Run("key", func(t *testing.T) {
685 t.Parallel()
686 testRawStringKey(t, tc.str)
687 })
688
689 t.Run("value", func(t *testing.T) {
690 t.Parallel()
691 testRawStringValue(t, tc.str)
692 })
693 })
694 }
695 })
696
697 for i := 0; i < 1000; i++ {
698 i := i
699 t.Run(strconv.Itoa(i), func(t *testing.T) {
700 t.Parallel()
701
702 s := xrand.String(math_rand.Intn(99), nil)
703 t.Logf("testing: %q", s)
704
705 t.Run("key", func(t *testing.T) {
706 t.Parallel()
707 testRawStringKey(t, s)
708 })
709
710 t.Run("value", func(t *testing.T) {
711 t.Parallel()
712 testRawStringValue(t, s)
713 })
714 })
715 }
716 })
717
718 testCases := []struct {
719 name string
720 str string
721 exp string
722 inKey bool
723 }{
724 {
725 name: "empty",
726 str: ``,
727 exp: `""`,
728 },
729 {
730 name: "null",
731 str: `null`,
732 exp: `"null"`,
733 },
734 {
735 name: "simple",
736 str: `wearisome_condition_of_humanity`,
737 exp: `wearisome_condition_of_humanity`,
738 },
739 {
740 name: "specials_double",
741 str: `'#;#;#'`,
742 exp: `"'#;#;#'"`,
743 },
744 {
745 name: "specials_single_quote",
746 str: `"cambridge"`,
747 exp: `'"cambridge"'`,
748 },
749 {
750 name: "specials_single_dollar",
751 str: `$bingo`,
752 exp: `'$bingo'`,
753 },
754 {
755 name: "not_key_specials",
756 str: `------`,
757 exp: `------`,
758 },
759 {
760 name: "key_specials_double",
761 str: `-----`,
762 exp: `"-----"`,
763 inKey: true,
764 },
765 {
766 name: "key_specials_single",
767 str: `"cambridge"`,
768 exp: `'"cambridge"'`,
769 inKey: true,
770 },
771 {
772 name: "key_specials_unquoted",
773 str: `square-2`,
774 exp: `square-2`,
775 inKey: true,
776 },
777 {
778 name: "multiline",
779 str: `||||yes
780 yes
781 yes
782 yes
783 ||||`,
784 exp: `"||||yes\nyes\nyes\nyes\n||||"`,
785 inKey: true,
786 },
787 {
788 name: "leading_whitespace",
789 str: ` yoho_park `,
790 exp: `" yoho_park "`,
791 },
792 {
793 name: "leading_whitespace_newlines",
794 str: ` yoho
795 _park `,
796 exp: `" yoho\n_park "`,
797 },
798 {
799 name: "leading_space_double_quotes_and_newlines",
800 str: ` "yoho"
801 _park `,
802 exp: `" \"yoho\"\n_park "`,
803 },
804 }
805
806 for _, tc := range testCases {
807 tc := tc
808 t.Run(tc.name, func(t *testing.T) {
809 t.Parallel()
810
811 ast := d2ast.RawString(tc.str, tc.inKey)
812 assert.String(t, tc.exp, d2format.Format(ast))
813 })
814 }
815 }
816
View as plain text