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  }