1// Copyright 2024 The CUE Authors
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7// http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15package toml_test
16
17import (
18 "bytes"
19 "encoding/json"
20 "io"
21 "path"
22 "reflect"
23 "strings"
24 "testing"
25
26 "github.com/go-quicktest/qt"
27 gotoml "github.com/pelletier/go-toml/v2"
28
29 "cuelang.org/go/cue/ast"
30 "cuelang.org/go/cue/ast/astutil"
31 "cuelang.org/go/cue/cuecontext"
32 "cuelang.org/go/cue/errors"
33 "cuelang.org/go/cue/format"
34 "cuelang.org/go/cue/token"
35 "cuelang.org/go/encoding/toml"
36 "cuelang.org/go/internal/astinternal"
37 "cuelang.org/go/internal/cuetxtar"
38)
39
40func TestDecoder(t *testing.T) {
41 t.Parallel()
42 // Note that we use backquoted Go string literals with indentation for readability.
43 // The whitespace doesn't affect the input TOML, and we cue/format on the "want" CUE source,
44 // so the added newlines and tabs don't change the test behavior.
45 tests := []struct {
46 name string
47 input string
48 wantCUE string
49 wantErr string
50 }{{
51 name: "Empty",
52 input: "",
53 wantCUE: "",
54 }, {
55 name: "LoneComment",
56 input: `
57 # Just a comment
58 `,
59 wantCUE: "",
60 }, {
61 name: "RootKeyMissing",
62 input: `
63 # A comment to verify that parser positions work.
64 = "no key name"
65 `,
66 wantErr: `
67 invalid character at start of key: =:
68 test.toml:2:1
69 `,
70 }, {
71 name: "RootKeysOne",
72 input: `
73 key = "value"
74 `,
75 wantCUE: `
76 key: "value"
77 `,
78 }, {
79 name: "RootMultiple",
80 input: `
81 key1 = "value1"
82 key2 = "value2"
83 key3 = "value3"
84 `,
85 wantCUE: `
86 key1: "value1"
87 key2: "value2"
88 key3: "value3"
89 `,
90 }, {
91 name: "RootKeysDots",
92 input: `
93 a1 = "A"
94 b1.b2 = "B"
95 c1.c2.c3 = "C"
96 `,
97 wantCUE: `
98 a1: "A"
99 b1: b2: "B"
100 c1: c2: c3: "C"
101 `,
102 }, {
103 name: "RootKeysCharacters",
104 input: `
105 a-b = "dashes"
106 a_b = "underscore unquoted"
107 _ = "underscore quoted"
108 _ab = "underscore prefix quoted"
109 123 = "numbers"
110 x._.y._ = "underscores quoted"
111 `,
112 wantCUE: `
113 "a-b": "dashes"
114 a_b: "underscore unquoted"
115 "_": "underscore quoted"
116 "_ab": "underscore prefix quoted"
117 "123": "numbers"
118 x: "_": y: "_": "underscores quoted"
119 `,
120 }, {
121 name: "RootKeysQuoted",
122 input: `
123 "1.2.3" = "quoted dots"
124 "foo bar" = "quoted space"
125 'foo "bar"' = "nested quotes"
126 `,
127 wantCUE: `
128 "1.2.3": "quoted dots"
129 "foo bar": "quoted space"
130 "foo \"bar\"": "nested quotes"
131 `,
132 }, {
133 name: "RootKeysMixed",
134 input: `
135 site."foo.com".title = "foo bar"
136 `,
137 wantCUE: `
138 site: "foo.com": title: "foo bar"
139 `,
140 }, {
141 name: "KeysDuplicateSimple",
142 input: `
143 foo = "same key"
144 foo = "same key"
145 `,
146 wantErr: `
147 duplicate key: foo:
148 test.toml:2:1
149 `,
150 }, {
151 name: "KeysDuplicateQuoted",
152 input: `
153 "foo" = "same key"
154 foo = "same key"
155 `,
156 wantErr: `
157 duplicate key: foo:
158 test.toml:2:1
159 `,
160 }, {
161 name: "KeysDuplicateWhitespace",
162 input: `
163 foo . bar = "same key"
164 foo.bar = "same key"
165 `,
166 wantErr: `
167 duplicate key: foo.bar:
168 test.toml:2:1
169 `,
170 }, {
171 name: "KeysDuplicateDots",
172 input: `
173 foo."bar.baz".zzz = "same key"
174 foo."bar.baz".zzz = "same key"
175 `,
176 wantErr: `
177 duplicate key: foo."bar.baz".zzz:
178 test.toml:2:1
179 `,
180 }, {
181 name: "KeysNotDuplicateDots",
182 input: `
183 foo."bar.baz" = "different key"
184 "foo.bar".baz = "different key"
185 `,
186 wantCUE: `
187 foo: "bar.baz": "different key"
188 "foo.bar": baz: "different key"
189 `,
190 }, {
191 name: "BasicStrings",
192 input: `
193 escapes = "foo \"bar\" \n\t\\ baz"
194 unicode = "foo \u00E9"
195 `,
196 wantCUE: `
197 escapes: "foo \"bar\" \n\t\\ baz"
198 unicode: "foo é"
199 `,
200 }, {
201 // Leading tabs do matter in this test.
202 // TODO: use our own multiline strings where it gives better results.
203 name: "MultilineBasicStrings",
204 input: `
205nested = """ can contain "" quotes """
206four = """"four""""
207double = """
208line one
209line two"""
210double_indented = """
211 line one
212 line two
213 """
214escaped = """\
215line one \
216line two.\
217"""
218 `,
219 wantCUE: `
220 nested: " can contain \"\" quotes "
221 four: "\"four\""
222 double: "line one\nline two"
223 double_indented: "\tline one\n\tline two\n\t"
224 escaped: "line one line two."
225 `,
226 }, {
227 // TODO: we can probably do better in many cases, e.g. #""
228 name: "LiteralStrings",
229 input: `
230 winpath = 'C:\Users\nodejs\templates'
231 winpath2 = '\\ServerX\admin$\system32\'
232 quoted = 'Tom "Dubs" Preston-Werner'
233 regex = '<\i\c*\s*>'
234 `,
235 wantCUE: `
236 winpath: "C:\\Users\\nodejs\\templates"
237 winpath2: "\\\\ServerX\\admin$\\system32\\"
238 quoted: "Tom \"Dubs\" Preston-Werner"
239 regex: "<\\i\\c*\\s*>"
240 `,
241 }, {
242 // Leading tabs do matter in this test.
243 // TODO: use our own multiline strings where it gives better results.
244 name: "MultilineLiteralStrings",
245 input: `
246nested = ''' can contain '' quotes '''
247four = ''''four''''
248double = '''
249line one
250line two'''
251double_indented = '''
252 line one
253 line two
254 '''
255escaped = '''\
256line one \
257line two.\
258'''
259 `,
260 wantCUE: `
261 nested: " can contain '' quotes "
262 four: "'four'"
263 double: "line one\nline two"
264 double_indented: "\tline one\n\tline two\n\t"
265 escaped: "\\\nline one \\\nline two.\\\n"
266 `,
267 }, {
268 name: "Integers",
269 input: `
270 zero = 0
271 positive = 123
272 plus = +40
273 minus = -40
274 underscores = 1_002_003
275 hexadecimal = 0xdeadBEEF
276 octal = 0o755
277 binary = 0b11010110
278 `,
279 wantCUE: `
280 zero: 0
281 positive: 123
282 plus: +40
283 minus: -40
284 underscores: 1_002_003
285 hexadecimal: 0xdeadBEEF
286 octal: 0o755
287 binary: 0b11010110
288 `,
289 }, {
290 name: "Floats",
291 input: `
292 pi = 3.1415
293 plus = +1.23
294 minus = -4.56
295 exponent = 1e067
296 exponent_plus = 5e+20
297 exponent_minus = -2E-4
298 exponent_dot = 6.789e-30
299 `,
300 wantCUE: `
301 pi: 3.1415
302 plus: +1.23
303 minus: -4.56
304 exponent: 1e067
305 exponent_plus: 5e+20
306 exponent_minus: -2E-4
307 exponent_dot: 6.789e-30
308 `,
309 }, {
310 name: "Bools",
311 input: `
312 positive = true
313 negative = false
314 `,
315 wantCUE: `
316 positive: true
317 negative: false
318 `,
319 }, {
320 name: "DateTimes",
321 input: `
322 offsetDateTime1 = 1979-05-27T07:32:00Z
323 offsetDateTime2 = 1979-05-27T00:32:00-07:00
324 offsetDateTime3 = 1979-05-27T00:32:00.999999-07:00
325 localDateTime1 = 1979-05-27T07:32:00
326 localDateTime2 = 1979-05-27T00:32:00.999999
327 localDate1 = 1979-05-27
328 localTime1 = 07:32:00
329 localTime2 = 00:32:00.999999
330
331 inlineArray = [1979-05-27, 07:32:00]
332
333 notActuallyDate = "1979-05-27"
334 notActuallyTime = "07:32:00"
335 inlineArrayNotActually = ["1979-05-27", "07:32:00"]
336 `,
337 wantCUE: `
338 import "time"
339
340 offsetDateTime1: "1979-05-27T07:32:00Z" & time.Format(time.RFC3339)
341 offsetDateTime2: "1979-05-27T00:32:00-07:00" & time.Format(time.RFC3339)
342 offsetDateTime3: "1979-05-27T00:32:00.999999-07:00" & time.Format(time.RFC3339)
343 localDateTime1: "1979-05-27T07:32:00" & time.Format("2006-01-02T15:04:05")
344 localDateTime2: "1979-05-27T00:32:00.999999" & time.Format("2006-01-02T15:04:05")
345 localDate1: "1979-05-27" & time.Format(time.RFC3339Date)
346 localTime1: "07:32:00" & time.Format("15:04:05")
347 localTime2: "00:32:00.999999" & time.Format("15:04:05")
348 inlineArray: ["1979-05-27" & time.Format(time.RFC3339Date), "07:32:00" & time.Format("15:04:05")]
349 notActuallyDate: "1979-05-27"
350 notActuallyTime: "07:32:00"
351 inlineArrayNotActually: ["1979-05-27", "07:32:00"]
352 `,
353 }, {
354 name: "Arrays",
355 input: `
356 integers = [1, 2, 3]
357 colors = ["red", "yellow", "green"]
358 nested_ints = [[1, 2], [3, 4, 5]]
359 nested_mixed = [[1, 2], ["a", "b", "c"], {extra = "keys"}]
360 strings = ["all", 'strings', """are the same""", '''type''']
361 mixed_numbers = [0.1, 0.2, 0.5, 1, 2, 5]
362 `,
363 wantCUE: `
364 integers: [1, 2, 3]
365 colors: ["red", "yellow", "green"]
366 nested_ints: [[1, 2], [3, 4, 5]]
367 nested_mixed: [[1, 2], ["a", "b", "c"], {extra: "keys"}]
368 strings: ["all", "strings", "are the same", "type"]
369 mixed_numbers: [0.1, 0.2, 0.5, 1, 2, 5]
370 `,
371 }, {
372 name: "InlineTables",
373 input: `
374 empty = {}
375 point = {x = 1, y = 2}
376 animal = {type.name = "pug"}
377 deep = {l1 = {l2 = {l3 = "leaf"}}}
378 `,
379 wantCUE: `
380 empty: {}
381 point: {x: 1, y: 2}
382 animal: {type: name: "pug"}
383 deep: {l1: {l2: {l3: "leaf"}}}
384 `,
385 }, {
386 name: "InlineTablesDuplicate",
387 input: `
388 point = {x = "same key", x = "same key"}
389 `,
390 wantErr: `
391 duplicate key: point.x:
392 test.toml:1:26
393 `,
394 }, {
395 name: "ArrayInlineTablesDuplicate",
396 input: `
397 point = [{}, {}, {x = "same key", x = "same key"}]
398 `,
399 wantErr: `
400 duplicate key: point.2.x:
401 test.toml:1:35
402 `,
403 }, {
404 name: "InlineTablesNotDuplicateScoping",
405 input: `
406 repeat = {repeat = {repeat = "leaf"}}
407 struct1 = {sibling = "leaf"}
408 struct2 = {sibling = "leaf"}
409 arrays = [{sibling = "leaf"}, {sibling = "leaf"}]
410 `,
411 wantCUE: `
412 repeat: {repeat: {repeat: "leaf"}}
413 struct1: {sibling: "leaf"}
414 struct2: {sibling: "leaf"}
415 arrays: [{sibling: "leaf"}, {sibling: "leaf"}]
416 `,
417 }, {
418 name: "TablesEmpty",
419 input: `
420 [foo]
421 [bar]
422 `,
423 wantCUE: `
424 foo: {}
425 bar: {}
426 `,
427 }, {
428 name: "TablesOne",
429 input: `
430 [foo]
431 single = "single"
432 `,
433 wantCUE: `
434 foo: {
435 single: "single"
436 }
437 `,
438 }, {
439 name: "TablesMultiple",
440 input: `
441 root1 = "root1 value"
442 root2 = "root2 value"
443 [foo]
444 foo1 = "foo1 value"
445 foo2 = "foo2 value"
446 [bar]
447 bar1 = "bar1 value"
448 bar2 = "bar2 value"
449 `,
450 wantCUE: `
451 root1: "root1 value"
452 root2: "root2 value"
453 foo: {
454 foo1: "foo1 value"
455 foo2: "foo2 value"
456 }
457 bar: {
458 bar1: "bar1 value"
459 bar2: "bar2 value"
460 }
461 `,
462 }, {
463 // A lot of these edge cases are covered by RootKeys tests already.
464 name: "TablesKeysComplex",
465 input: `
466 [foo.bar . "baz.zzz zzz"]
467 one = "1"
468 [123-456]
469 two = "2"
470 `,
471 wantCUE: `
472 foo: bar: "baz.zzz zzz": {
473 one: "1"
474 }
475 "123-456": {
476 two: "2"
477 }
478 `,
479 }, {
480 name: "TableKeysDuplicateSimple",
481 input: `
482 [foo]
483 [foo]
484 `,
485 wantErr: `
486 duplicate key: foo:
487 test.toml:2:2
488 `,
489 }, {
490 name: "TableKeysDuplicateOverlap",
491 input: `
492 [foo]
493 bar = "leaf"
494 [foo.bar]
495 baz = "second leaf"
496 `,
497 wantErr: `
498 duplicate key: foo.bar:
499 test.toml:3:2
500 `,
501 }, {
502 name: "TableInnerKeysDuplicateSimple",
503 input: `
504 [foo]
505 bar = "same key"
506 bar = "same key"
507 `,
508 wantErr: `
509 duplicate key: foo.bar:
510 test.toml:3:1
511 `,
512 }, {
513 name: "TablesNotDuplicateScoping",
514 input: `
515 [repeat]
516 repeat.repeat = "leaf"
517 [struct1]
518 sibling = "leaf"
519 [struct2]
520 sibling = "leaf"
521 `,
522 wantCUE: `
523 repeat: {
524 repeat: repeat: "leaf"
525 }
526 struct1: {
527 sibling: "leaf"
528 }
529 struct2: {
530 sibling: "leaf"
531 }
532 `,
533 }, {
534 name: "ArrayTablesEmpty",
535 input: `
536 [[foo]]
537 `,
538 wantCUE: `
539 foo: [
540 {},
541 ]
542 `,
543 }, {
544 name: "ArrayTablesOne",
545 input: `
546 [[foo]]
547 single = "single"
548 `,
549 wantCUE: `
550 foo: [
551 {
552 single: "single"
553 },
554 ]
555 `,
556 }, {
557 name: "ArrayTablesMultiple",
558 input: `
559 root = "root value"
560 [[foo]]
561 foo1 = "foo1 value"
562 foo2 = "foo2 value"
563 [[foo]]
564 foo3 = "foo3 value"
565 foo4 = "foo4 value"
566 [[foo]]
567 [[foo]]
568 single = "single"
569 `,
570 wantCUE: `
571 root: "root value"
572 foo: [
573 {
574 foo1: "foo1 value"
575 foo2: "foo2 value"
576 },
577 {
578 foo3: "foo3 value"
579 foo4: "foo4 value"
580 },
581 {},
582 {
583 single: "single"
584 },
585 ]
586 `,
587 }, {
588 name: "ArrayTablesSeparate",
589 input: `
590 root = "root value"
591 [[foo]]
592 foo1 = "foo1 value"
593 [[bar]]
594 bar1 = "bar1 value"
595 [[baz]]
596 `,
597 wantCUE: `
598 root: "root value"
599 foo: [
600 {
601 foo1: "foo1 value"
602 },
603 ]
604 bar: [
605 {
606 bar1: "bar1 value"
607 },
608 ]
609 baz: [
610 {},
611 ]
612 `,
613 }, {
614 name: "ArrayTablesSubtable",
615 input: `
616 [[foo]]
617 foo1 = "foo1 value"
618 [foo.subtable1]
619 sub1 = "sub1 value"
620 [foo.subtable2]
621 sub2 = "sub2 value"
622 [foo.subtable2.deeper]
623 sub2d = "sub2d value"
624 [[foo]]
625 foo2 = "foo2 value"
626 `,
627 wantCUE: `
628 foo: [
629 {
630 foo1: "foo1 value"
631 subtable1: {
632 sub1: "sub1 value"
633 }
634 subtable2: {
635 sub2: "sub2 value"
636 }
637 subtable2: deeper: {
638 sub2d: "sub2d value"
639 }
640 },
641 {
642 foo2: "foo2 value"
643 },
644 ]
645 `,
646 }, {
647 name: "ArrayTablesSubtableDuplicateKey",
648 input: `
649 [[foo]]
650 [foo.subtable]
651 name = "bar"
652 [[foo]]
653 [foo.subtable]
654 name = "bar"
655 `,
656 wantCUE: `
657 foo: [
658 {
659 subtable: {
660 name: "bar"
661 }
662 },
663 {
664 subtable: {
665 name: "bar"
666 }
667 }
668 ]
669 `,
670 }, {
671 name: "ArrayTablesSubtableActualDuplicate",
672 input: `
673 [[foo]]
674 [foo.subtable]
675 name = "bar"
676 [foo.subtable]
677 `,
678 wantErr: `
679 duplicate key: foo.subtable:
680 test.toml:4:2
681 `,
682 }, {
683 name: "ArrayTablesNested",
684 input: `
685 [[foo]]
686 foo1 = "foo1 value"
687 [[foo.nested1]]
688 nest1a = "nest1a value"
689 [[foo.nested1]]
690 nest1b = "nest1b value"
691 [[foo.nested2]]
692 nest2 = "nest2 value"
693 [[foo.nested2.deeper]]
694 nest2d = "nest2d value"
695 [[foo.nested3.directly.deeper]]
696 nest3d = "nest3d value"
697 [[foo]]
698 foo2 = "foo2 value"
699 `,
700 wantCUE: `
701 foo: [
702 {
703 foo1: "foo1 value"
704 nested1: [
705 {
706 nest1a: "nest1a value"
707 },
708 {
709 nest1b: "nest1b value"
710 },
711 ]
712 nested2: [
713 {
714 nest2: "nest2 value"
715 deeper: [
716 {
717 nest2d: "nest2d value"
718 }
719 ]
720 },
721 ]
722 nested3: directly: deeper: [
723 {
724 nest3d: "nest3d value"
725 },
726 ]
727 },
728 {
729 foo2: "foo2 value"
730 },
731 ]
732 `,
733 }, {
734 name: "ArrayTablesNestedSiblings",
735 input: `
736 [[foo]]
737 foo1 = "foo1 value"
738 [[foo.nested1]]
739 foo1_n1 = "foo1_n1 value"
740 not_duplicate = "not a duplicate"
741 [[foo.nested2.deeper]]
742 foo1_n2 = "foo1_n2 value"
743 [[foo]]
744 foo2 = "foo2 value"
745 [[foo.nested1]]
746 foo2_n1 = "foo2_n1 value"
747 not_duplicate = "not a duplicate"
748 [[foo.nested2.deeper]]
749 foo2_n2 = "foo2_n2 value"
750 `,
751 wantCUE: `
752 foo: [
753 {
754 foo1: "foo1 value"
755 nested1: [
756 {
757 foo1_n1: "foo1_n1 value"
758 not_duplicate: "not a duplicate"
759 },
760 ]
761 nested2: deeper: [
762 {
763 foo1_n2: "foo1_n2 value"
764 },
765 ]
766 },
767 {
768 foo2: "foo2 value"
769 nested1: [
770 {
771 foo2_n1: "foo2_n1 value"
772 not_duplicate: "not a duplicate"
773 },
774 ]
775 nested2: deeper: [
776 {
777 foo2_n2: "foo2_n2 value"
778 },
779 ]
780 },
781 ]
782 `,
783 }, {
784 name: "RedeclareKeyAsTableArray",
785 input: `
786 foo = "foo value"
787 [middle]
788 middle = "to ensure we don't rely on the last key"
789 [[foo]]
790 baz = "baz value"
791 `,
792 wantErr: `
793 cannot redeclare key "foo" as a table array:
794 test.toml:4:3
795 `,
796 }, {
797 name: "RedeclareTableAsTableArray",
798 input: `
799 [foo]
800 bar = "bar value"
801 [middle]
802 middle = "to ensure we don't rely on the last key"
803 [[foo]]
804 baz = "baz value"
805 `,
806 wantErr: `
807 cannot redeclare key "foo" as a table array:
808 test.toml:5:3
809 `,
810 }, {
811 name: "RedeclareArrayAsTableArray",
812 input: `
813 foo = ["inline array"]
814 [middle]
815 middle = "to ensure we don't rely on the last key"
816 [[foo]]
817 baz = "baz value"
818 `,
819 wantErr: `
820 cannot redeclare key "foo" as a table array:
821 test.toml:4:3
822 `,
823 }, {
824 name: "RedeclareTableArrayAsKey",
825 input: `
826 [[foo.foo2]]
827 bar = "bar value"
828 [middle]
829 middle = "to ensure we don't rely on the last key"
830 [foo]
831 foo2 = "redeclaring"
832 `,
833 wantErr: `
834 cannot redeclare table array "foo.foo2" as a table:
835 test.toml:6:1
836 `,
837 }, {
838 name: "RedeclareTableArrayAsTable",
839 input: `
840 [[foo]]
841 bar = "bar value"
842 [middle]
843 middle = "to ensure we don't rely on the last key"
844 [foo]
845 baz = "baz value"
846 `,
847 wantErr: `
848 cannot redeclare table array "foo" as a table:
849 test.toml:5:2
850 `,
851 }, {
852 name: "KeysNotDuplicateTableArrays",
853 input: `
854 [[foo]]
855 bar = "foo.0.bar"
856 [[foo]]
857 bar = "foo.1.bar"
858 [[foo]]
859 bar = "foo.2.bar"
860 [[foo.nested]]
861 bar = "foo.2.nested.0.bar"
862 [[foo.nested]]
863 bar = "foo.2.nested.1.bar"
864 [[foo.nested]]
865 bar = "foo.2.nested.2.bar"
866 `,
867 wantCUE: `
868 foo: [
869 {
870 bar: "foo.0.bar"
871 },
872 {
873 bar: "foo.1.bar"
874 },
875 {
876 bar: "foo.2.bar"
877 nested: [
878 {
879 bar: "foo.2.nested.0.bar"
880 },
881 {
882 bar: "foo.2.nested.1.bar"
883 },
884 {
885 bar: "foo.2.nested.2.bar"
886 },
887 ]
888 },
889 ]
890 `,
891 }}
892 for _, test := range tests {
893 t.Run(test.name, func(t *testing.T) {
894 t.Parallel()
895
896 input := unindentMultiline(test.input)
897 dec := toml.NewDecoder("test.toml", strings.NewReader(input))
898
899 node, err := dec.Decode()
900 if test.wantErr != "" {
901 gotErr := strings.TrimSuffix(errors.Details(err, nil), "\n")
902 wantErr := unindentMultiline(test.wantErr)
903
904 qt.Assert(t, qt.Equals(gotErr, wantErr))
905 qt.Assert(t, qt.IsNil(node))
906 // We don't continue, so we can't expect any decoded CUE.
907 qt.Assert(t, qt.Equals(test.wantCUE, ""))
908
909 // Validate that go-toml's Unmarshal also rejects this input.
910 err = gotoml.Unmarshal([]byte(input), new(any))
911 qt.Assert(t, qt.IsNotNil(err))
912 return
913 }
914 qt.Assert(t, qt.IsNil(err))
915
916 file, err := astutil.ToFile(node)
917 qt.Assert(t, qt.IsNil(err))
918
919 node2, err := dec.Decode()
920 qt.Assert(t, qt.IsNil(node2))
921 qt.Assert(t, qt.Equals(err, io.EOF))
922
923 wantFormatted, err := format.Source([]byte(test.wantCUE))
924 qt.Assert(t, qt.IsNil(err), qt.Commentf("wantCUE:\n%s", test.wantCUE))
925
926 formatted, err := format.Node(file)
927 qt.Assert(t, qt.IsNil(err))
928 t.Logf("CUE:\n%s", formatted)
929 qt.Assert(t, qt.Equals(string(formatted), string(wantFormatted)))
930
931 // Ensure that the CUE node can be compiled into a cue.Value and validated.
932 ctx := cuecontext.New()
933 val := ctx.BuildFile(file)
934 qt.Assert(t, qt.IsNil(val.Err()))
935 qt.Assert(t, qt.IsNil(val.Validate()))
936
937 // Validate that the decoded CUE value is equivalent
938 // to the Go value that go-toml's Unmarshal produces.
939 // We use JSON equality as some details such as which integer types are used
940 // are not actually relevant to an "equal data" check.
941 var unmarshalTOML any
942 err = gotoml.Unmarshal([]byte(input), &unmarshalTOML)
943 qt.Assert(t, qt.IsNil(err))
944 jsonTOML, err := json.Marshal(unmarshalTOML)
945 qt.Assert(t, qt.IsNil(err))
946 t.Logf("json.Marshal via go-toml:\t%s\n", jsonTOML)
947
948 jsonCUE, err := json.Marshal(val)
949 qt.Assert(t, qt.IsNil(err))
950 t.Logf("json.Marshal via CUE:\t%s\n", jsonCUE)
951 qt.Assert(t, qt.JSONEquals(jsonCUE, unmarshalTOML))
952
953 // Ensure that the decoded CUE can be re-encoded as TOML,
954 // and the resulting TOML is still JSON-equivalent.
955 t.Run("reencode", func(t *testing.T) {
956 switch test.name {
957 case "DateTimes":
958 t.Skip("TODO(mvdan): dates and times always encode as TOML strings today")
959 }
960 sb := new(strings.Builder)
961 enc := toml.NewEncoder(sb)
962
963 err := enc.Encode(val)
964 qt.Assert(t, qt.IsNil(err))
965 cueTOML := sb.String()
966 t.Logf("reencoded TOML:\n%s", cueTOML)
967
968 var unmarshalCueTOML any
969 err = gotoml.Unmarshal([]byte(cueTOML), &unmarshalCueTOML)
970 qt.Assert(t, qt.IsNil(err))
971
972 qt.Assert(t, qt.CmpEquals(unmarshalCueTOML, unmarshalTOML))
973 })
974 })
975 }
976}
977
978// unindentMultiline mimics CUE's behavior with `"""` multi-line strings,
979// where a leading newline is omitted, and any whitespace preceding the trailing newline
980// is removed from the start of all lines.
981func unindentMultiline(s string) string {
982 i := strings.LastIndexByte(s, '\n')
983 if i < 0 {
984 // Not a multi-line string.
985 return s
986 }
987 trim := s[i:]
988 s = strings.ReplaceAll(s, trim, "\n")
989 s = strings.TrimPrefix(s, "\n")
990 s = strings.TrimSuffix(s, "\n")
991 return s
992}
993
994var (
995 typNode = reflect.TypeFor[ast.Node]()
996 typPos = reflect.TypeFor[token.Pos]()
997)
998
999func TestDecoderTxtar(t *testing.T) {
1000 test := cuetxtar.TxTarTest{
1001 Root: "testdata",
1002 Name: "decode",
1003 }
1004
1005 test.Run(t, func(t *cuetxtar.Test) {
1006 for _, file := range t.Archive.Files {
1007 if strings.HasPrefix(file.Name, "out/") {
1008 continue
1009 }
1010 dec := toml.NewDecoder(file.Name, bytes.NewReader(file.Data))
1011 node, err := dec.Decode()
1012 qt.Assert(t, qt.IsNil(err))
1013
1014 // Show all valid node positions.
1015 out := astinternal.AppendDebug(nil, node, astinternal.DebugConfig{
1016 OmitEmpty: true,
1017 Filter: func(v reflect.Value) bool {
1018 t := v.Type()
1019 return t.Implements(typNode) || t.Kind() == reflect.Slice || t == typPos
1020 },
1021 })
1022 t.Writer(path.Join(file.Name, "positions")).Write(out)
1023 }
1024 })
1025}