cuelang.org/go@v0.13.0/encoding/toml/decode_test.go (about) 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 15 package toml_test 16 17 import ( 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 40 func 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: ` 205 nested = """ can contain "" quotes """ 206 four = """"four"""" 207 double = """ 208 line one 209 line two""" 210 double_indented = """ 211 line one 212 line two 213 """ 214 escaped = """\ 215 line one \ 216 line 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: ` 246 nested = ''' can contain '' quotes ''' 247 four = ''''four'''' 248 double = ''' 249 line one 250 line two''' 251 double_indented = ''' 252 line one 253 line two 254 ''' 255 escaped = '''\ 256 line one \ 257 line 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: "ArrayTablesNested", 648 input: ` 649 [[foo]] 650 foo1 = "foo1 value" 651 [[foo.nested1]] 652 nest1a = "nest1a value" 653 [[foo.nested1]] 654 nest1b = "nest1b value" 655 [[foo.nested2]] 656 nest2 = "nest2 value" 657 [[foo.nested2.deeper]] 658 nest2d = "nest2d value" 659 [[foo.nested3.directly.deeper]] 660 nest3d = "nest3d value" 661 [[foo]] 662 foo2 = "foo2 value" 663 `, 664 wantCUE: ` 665 foo: [ 666 { 667 foo1: "foo1 value" 668 nested1: [ 669 { 670 nest1a: "nest1a value" 671 }, 672 { 673 nest1b: "nest1b value" 674 }, 675 ] 676 nested2: [ 677 { 678 nest2: "nest2 value" 679 deeper: [ 680 { 681 nest2d: "nest2d value" 682 } 683 ] 684 }, 685 ] 686 nested3: directly: deeper: [ 687 { 688 nest3d: "nest3d value" 689 }, 690 ] 691 }, 692 { 693 foo2: "foo2 value" 694 }, 695 ] 696 `, 697 }, { 698 name: "RedeclareKeyAsTableArray", 699 input: ` 700 foo = "foo value" 701 [middle] 702 middle = "to ensure we don't rely on the last key" 703 [[foo]] 704 baz = "baz value" 705 `, 706 wantErr: ` 707 cannot redeclare key "foo" as a table array: 708 test.toml:4:3 709 `, 710 }, { 711 name: "RedeclareTableAsTableArray", 712 input: ` 713 [foo] 714 bar = "bar value" 715 [middle] 716 middle = "to ensure we don't rely on the last key" 717 [[foo]] 718 baz = "baz value" 719 `, 720 wantErr: ` 721 cannot redeclare key "foo" as a table array: 722 test.toml:5:3 723 `, 724 }, { 725 name: "RedeclareArrayAsTableArray", 726 input: ` 727 foo = ["inline array"] 728 [middle] 729 middle = "to ensure we don't rely on the last key" 730 [[foo]] 731 baz = "baz value" 732 `, 733 wantErr: ` 734 cannot redeclare key "foo" as a table array: 735 test.toml:4:3 736 `, 737 }, { 738 name: "RedeclareTableArrayAsKey", 739 input: ` 740 [[foo.foo2]] 741 bar = "bar value" 742 [middle] 743 middle = "to ensure we don't rely on the last key" 744 [foo] 745 foo2 = "redeclaring" 746 `, 747 wantErr: ` 748 cannot redeclare table array "foo.foo2" as a table: 749 test.toml:6:1 750 `, 751 }, { 752 name: "RedeclareTableArrayAsTable", 753 input: ` 754 [[foo]] 755 bar = "bar value" 756 [middle] 757 middle = "to ensure we don't rely on the last key" 758 [foo] 759 baz = "baz value" 760 `, 761 wantErr: ` 762 cannot redeclare table array "foo" as a table: 763 test.toml:5:2 764 `, 765 }, { 766 name: "KeysNotDuplicateTableArrays", 767 input: ` 768 [[foo]] 769 bar = "foo.0.bar" 770 [[foo]] 771 bar = "foo.1.bar" 772 [[foo]] 773 bar = "foo.2.bar" 774 [[foo.nested]] 775 bar = "foo.2.nested.0.bar" 776 [[foo.nested]] 777 bar = "foo.2.nested.1.bar" 778 [[foo.nested]] 779 bar = "foo.2.nested.2.bar" 780 `, 781 wantCUE: ` 782 foo: [ 783 { 784 bar: "foo.0.bar" 785 }, 786 { 787 bar: "foo.1.bar" 788 }, 789 { 790 bar: "foo.2.bar" 791 nested: [ 792 { 793 bar: "foo.2.nested.0.bar" 794 }, 795 { 796 bar: "foo.2.nested.1.bar" 797 }, 798 { 799 bar: "foo.2.nested.2.bar" 800 }, 801 ] 802 }, 803 ] 804 `, 805 }} 806 for _, test := range tests { 807 t.Run(test.name, func(t *testing.T) { 808 t.Parallel() 809 810 input := unindentMultiline(test.input) 811 dec := toml.NewDecoder("test.toml", strings.NewReader(input)) 812 813 node, err := dec.Decode() 814 if test.wantErr != "" { 815 gotErr := strings.TrimSuffix(errors.Details(err, nil), "\n") 816 wantErr := unindentMultiline(test.wantErr) 817 818 qt.Assert(t, qt.Equals(gotErr, wantErr)) 819 qt.Assert(t, qt.IsNil(node)) 820 // We don't continue, so we can't expect any decoded CUE. 821 qt.Assert(t, qt.Equals(test.wantCUE, "")) 822 823 // Validate that go-toml's Unmarshal also rejects this input. 824 err = gotoml.Unmarshal([]byte(input), new(any)) 825 qt.Assert(t, qt.IsNotNil(err)) 826 return 827 } 828 qt.Assert(t, qt.IsNil(err)) 829 830 file, err := astutil.ToFile(node) 831 qt.Assert(t, qt.IsNil(err)) 832 833 node2, err := dec.Decode() 834 qt.Assert(t, qt.IsNil(node2)) 835 qt.Assert(t, qt.Equals(err, io.EOF)) 836 837 wantFormatted, err := format.Source([]byte(test.wantCUE)) 838 qt.Assert(t, qt.IsNil(err), qt.Commentf("wantCUE:\n%s", test.wantCUE)) 839 840 formatted, err := format.Node(file) 841 qt.Assert(t, qt.IsNil(err)) 842 t.Logf("CUE:\n%s", formatted) 843 qt.Assert(t, qt.Equals(string(formatted), string(wantFormatted))) 844 845 // Ensure that the CUE node can be compiled into a cue.Value and validated. 846 ctx := cuecontext.New() 847 val := ctx.BuildFile(file) 848 qt.Assert(t, qt.IsNil(val.Err())) 849 qt.Assert(t, qt.IsNil(val.Validate())) 850 851 // Validate that the decoded CUE value is equivalent 852 // to the Go value that go-toml's Unmarshal produces. 853 // We use JSON equality as some details such as which integer types are used 854 // are not actually relevant to an "equal data" check. 855 var unmarshalTOML any 856 err = gotoml.Unmarshal([]byte(input), &unmarshalTOML) 857 qt.Assert(t, qt.IsNil(err)) 858 jsonTOML, err := json.Marshal(unmarshalTOML) 859 qt.Assert(t, qt.IsNil(err)) 860 t.Logf("json.Marshal via go-toml:\t%s\n", jsonTOML) 861 862 jsonCUE, err := json.Marshal(val) 863 qt.Assert(t, qt.IsNil(err)) 864 t.Logf("json.Marshal via CUE:\t%s\n", jsonCUE) 865 qt.Assert(t, qt.JSONEquals(jsonCUE, unmarshalTOML)) 866 867 // Ensure that the decoded CUE can be re-encoded as TOML, 868 // and the resulting TOML is still JSON-equivalent. 869 t.Run("reencode", func(t *testing.T) { 870 switch test.name { 871 case "DateTimes": 872 t.Skip("TODO(mvdan): dates and times always encode as TOML strings today") 873 } 874 sb := new(strings.Builder) 875 enc := toml.NewEncoder(sb) 876 877 err := enc.Encode(val) 878 qt.Assert(t, qt.IsNil(err)) 879 cueTOML := sb.String() 880 t.Logf("reencoded TOML:\n%s", cueTOML) 881 882 var unmarshalCueTOML any 883 err = gotoml.Unmarshal([]byte(cueTOML), &unmarshalCueTOML) 884 qt.Assert(t, qt.IsNil(err)) 885 886 qt.Assert(t, qt.CmpEquals(unmarshalCueTOML, unmarshalTOML)) 887 }) 888 }) 889 } 890 } 891 892 // unindentMultiline mimics CUE's behavior with `"""` multi-line strings, 893 // where a leading newline is omitted, and any whitespace preceding the trailing newline 894 // is removed from the start of all lines. 895 func unindentMultiline(s string) string { 896 i := strings.LastIndexByte(s, '\n') 897 if i < 0 { 898 // Not a multi-line string. 899 return s 900 } 901 trim := s[i:] 902 s = strings.ReplaceAll(s, trim, "\n") 903 s = strings.TrimPrefix(s, "\n") 904 s = strings.TrimSuffix(s, "\n") 905 return s 906 } 907 908 var ( 909 typNode = reflect.TypeFor[ast.Node]() 910 typPos = reflect.TypeFor[token.Pos]() 911 ) 912 913 func TestDecoderTxtar(t *testing.T) { 914 test := cuetxtar.TxTarTest{ 915 Root: "testdata", 916 Name: "decode", 917 } 918 919 test.Run(t, func(t *cuetxtar.Test) { 920 for _, file := range t.Archive.Files { 921 if strings.HasPrefix(file.Name, "out/") { 922 continue 923 } 924 dec := toml.NewDecoder(file.Name, bytes.NewReader(file.Data)) 925 node, err := dec.Decode() 926 qt.Assert(t, qt.IsNil(err)) 927 928 // Show all valid node positions. 929 out := astinternal.AppendDebug(nil, node, astinternal.DebugConfig{ 930 OmitEmpty: true, 931 Filter: func(v reflect.Value) bool { 932 t := v.Type() 933 return t.Implements(typNode) || t.Kind() == reflect.Slice || t == typPos 934 }, 935 }) 936 t.Writer(path.Join(file.Name, "positions")).Write(out) 937 } 938 }) 939 }