cuelang.org/go@v0.10.1/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  	"encoding/json"
    19  	"io"
    20  	"strings"
    21  	"testing"
    22  
    23  	"github.com/go-quicktest/qt"
    24  	gotoml "github.com/pelletier/go-toml/v2"
    25  
    26  	"cuelang.org/go/cue/ast/astutil"
    27  	"cuelang.org/go/cue/cuecontext"
    28  	"cuelang.org/go/cue/errors"
    29  	"cuelang.org/go/cue/format"
    30  	"cuelang.org/go/encoding/toml"
    31  )
    32  
    33  func TestDecoder(t *testing.T) {
    34  	t.Parallel()
    35  	// Note that we use backquoted Go string literals with indentation for readability.
    36  	// The whitespace doesn't affect the input TOML, and we cue/format on the "want" CUE source,
    37  	// so the added newlines and tabs don't change the test behavior.
    38  	tests := []struct {
    39  		name    string
    40  		input   string
    41  		wantCUE string
    42  		wantErr string
    43  	}{{
    44  		name:    "Empty",
    45  		input:   "",
    46  		wantCUE: "",
    47  	}, {
    48  		name: "LoneComment",
    49  		input: `
    50  			# Just a comment
    51  			`,
    52  		wantCUE: "",
    53  	}, {
    54  		name: "RootKeyMissing",
    55  		input: `
    56  			# A comment to verify that parser positions work.
    57  			= "no key name"
    58  			`,
    59  		wantErr: `
    60  			invalid character at start of key: =:
    61  			    test.toml:2:1
    62  			`,
    63  	}, {
    64  		name: "RootKeysOne",
    65  		input: `
    66  			key = "value"
    67  			`,
    68  		wantCUE: `
    69  			key: "value"
    70  			`,
    71  	}, {
    72  		name: "RootMultiple",
    73  		input: `
    74  			key1 = "value1"
    75  			key2 = "value2"
    76  			key3 = "value3"
    77  			`,
    78  		wantCUE: `
    79  			key1: "value1"
    80  			key2: "value2"
    81  			key3: "value3"
    82  			`,
    83  	}, {
    84  		name: "RootKeysDots",
    85  		input: `
    86  			a1       = "A"
    87  			b1.b2    = "B"
    88  			c1.c2.c3 = "C"
    89  			`,
    90  		wantCUE: `
    91  			a1: "A"
    92  			b1: b2: "B"
    93  			c1: c2: c3: "C"
    94  			`,
    95  	}, {
    96  		name: "RootKeysCharacters",
    97  		input: `
    98  			a-b = "dashes"
    99  			a_b = "underscore unquoted"
   100  			_   = "underscore quoted"
   101  			_ab = "underscore prefix quoted"
   102  			123 = "numbers"
   103  			x._.y._ = "underscores quoted"
   104  			`,
   105  		wantCUE: `
   106  			"a-b": "dashes"
   107  			a_b:   "underscore unquoted"
   108  			"_":   "underscore quoted"
   109  			"_ab": "underscore prefix quoted"
   110  			"123": "numbers"
   111  			x: "_": y: "_": "underscores quoted"
   112  			`,
   113  	}, {
   114  		name: "RootKeysQuoted",
   115  		input: `
   116  			"1.2.3" = "quoted dots"
   117  			"foo bar" = "quoted space"
   118  			'foo "bar"' = "nested quotes"
   119  			`,
   120  		wantCUE: `
   121  			"1.2.3":       "quoted dots"
   122  			"foo bar":     "quoted space"
   123  			"foo \"bar\"": "nested quotes"
   124  			`,
   125  	}, {
   126  		name: "RootKeysMixed",
   127  		input: `
   128  			site."foo.com".title = "foo bar"
   129  			`,
   130  		wantCUE: `
   131  			site: "foo.com": title: "foo bar"
   132  			`,
   133  	}, {
   134  		name: "KeysDuplicateSimple",
   135  		input: `
   136  			foo = "same key"
   137  			foo = "same key"
   138  			`,
   139  		wantErr: `
   140  			duplicate key: foo:
   141  			    test.toml:2:1
   142  			`,
   143  	}, {
   144  		name: "KeysDuplicateQuoted",
   145  		input: `
   146  			"foo" = "same key"
   147  			foo = "same key"
   148  			`,
   149  		wantErr: `
   150  			duplicate key: foo:
   151  			    test.toml:2:1
   152  			`,
   153  	}, {
   154  		name: "KeysDuplicateWhitespace",
   155  		input: `
   156  			foo . bar = "same key"
   157  			foo.bar = "same key"
   158  			`,
   159  		wantErr: `
   160  			duplicate key: foo.bar:
   161  			    test.toml:2:1
   162  			`,
   163  	}, {
   164  		name: "KeysDuplicateDots",
   165  		input: `
   166  			foo."bar.baz".zzz = "same key"
   167  			foo."bar.baz".zzz = "same key"
   168  			`,
   169  		wantErr: `
   170  			duplicate key: foo."bar.baz".zzz:
   171  			    test.toml:2:1
   172  			`,
   173  	}, {
   174  		name: "KeysNotDuplicateDots",
   175  		input: `
   176  			foo."bar.baz" = "different key"
   177  			"foo.bar".baz = "different key"
   178  			`,
   179  		wantCUE: `
   180  			foo: "bar.baz": "different key"
   181  			"foo.bar": baz: "different key"
   182  			`,
   183  	}, {
   184  		name: "BasicStrings",
   185  		input: `
   186  			escapes = "foo \"bar\" \n\t\\ baz"
   187  			unicode = "foo \u00E9"
   188  			`,
   189  		wantCUE: `
   190  			escapes: "foo \"bar\" \n\t\\ baz"
   191  			unicode: "foo é"
   192  			`,
   193  	}, {
   194  		// Leading tabs do matter in this test.
   195  		// TODO: use our own multiline strings where it gives better results.
   196  		name: "MultilineBasicStrings",
   197  		input: `
   198  nested = """ can contain "" quotes """
   199  four   = """"four""""
   200  double = """
   201  line one
   202  line two"""
   203  double_indented = """
   204  	line one
   205  	line two
   206  	"""
   207  escaped = """\
   208  line one \
   209  line two.\
   210  """
   211  			`,
   212  		wantCUE: `
   213  			nested:           " can contain \"\" quotes "
   214  			four:             "\"four\""
   215  			double:           "line one\nline two"
   216  			double_indented:  "\tline one\n\tline two\n\t"
   217  			escaped:          "line one line two."
   218  			`,
   219  	}, {
   220  		// TODO: we can probably do better in many cases, e.g. #""
   221  		name: "LiteralStrings",
   222  		input: `
   223  			winpath  = 'C:\Users\nodejs\templates'
   224  			winpath2 = '\\ServerX\admin$\system32\'
   225  			quoted   = 'Tom "Dubs" Preston-Werner'
   226  			regex    = '<\i\c*\s*>'
   227  			`,
   228  		wantCUE: `
   229  			winpath:  "C:\\Users\\nodejs\\templates"
   230  			winpath2: "\\\\ServerX\\admin$\\system32\\"
   231  			quoted:   "Tom \"Dubs\" Preston-Werner"
   232  			regex:    "<\\i\\c*\\s*>"
   233  			`,
   234  	}, {
   235  		// Leading tabs do matter in this test.
   236  		// TODO: use our own multiline strings where it gives better results.
   237  		name: "MultilineLiteralStrings",
   238  		input: `
   239  nested = ''' can contain '' quotes '''
   240  four   = ''''four''''
   241  double = '''
   242  line one
   243  line two'''
   244  double_indented = '''
   245  	line one
   246  	line two
   247  	'''
   248  escaped = '''\
   249  line one \
   250  line two.\
   251  '''
   252  			`,
   253  		wantCUE: `
   254  			nested:           " can contain '' quotes "
   255  			four:             "'four'"
   256  			double:           "line one\nline two"
   257  			double_indented:  "\tline one\n\tline two\n\t"
   258  			escaped:          "\\\nline one \\\nline two.\\\n"
   259  			`,
   260  	}, {
   261  		name: "Integers",
   262  		input: `
   263  			zero        = 0
   264  			positive    = 123
   265  			plus        = +40
   266  			minus       = -40
   267  			underscores = 1_002_003
   268  			hexadecimal = 0xdeadBEEF
   269  			octal       = 0o755
   270  			binary      = 0b11010110
   271  			`,
   272  		wantCUE: `
   273  			zero:        0
   274  			positive:    123
   275  			plus:        +40
   276  			minus:       -40
   277  			underscores: 1_002_003
   278  			hexadecimal: 0xdeadBEEF
   279  			octal:       0o755
   280  			binary:      0b11010110
   281  			`,
   282  	}, {
   283  		name: "Floats",
   284  		input: `
   285  			pi             = 3.1415
   286  			plus           = +1.23
   287  			minus          = -4.56
   288  			exponent       = 1e067
   289  			exponent_plus  = 5e+20
   290  			exponent_minus = -2E-4
   291  			exponent_dot   = 6.789e-30
   292  			`,
   293  		wantCUE: `
   294  			pi:             3.1415
   295  			plus:           +1.23
   296  			minus:          -4.56
   297  			exponent:       1e067
   298  			exponent_plus:  5e+20
   299  			exponent_minus: -2E-4
   300  			exponent_dot:   6.789e-30
   301  			`,
   302  	}, {
   303  		name: "Bools",
   304  		input: `
   305  			positive = true
   306  			negative = false
   307  			`,
   308  		wantCUE: `
   309  			positive: true
   310  			negative: false
   311  			`,
   312  	}, {
   313  		name: "Arrays",
   314  		input: `
   315  			integers      = [1, 2, 3]
   316  			colors        = ["red", "yellow", "green"]
   317  			nested_ints   = [[1, 2], [3, 4, 5]]
   318  			nested_mixed  = [[1, 2], ["a", "b", "c"], {extra = "keys"}]
   319  			strings       = ["all", 'strings', """are the same""", '''type''']
   320  			mixed_numbers = [0.1, 0.2, 0.5, 1, 2, 5]
   321  			`,
   322  		wantCUE: `
   323  			integers:      [1, 2, 3]
   324  			colors:        ["red", "yellow", "green"]
   325  			nested_ints:   [[1, 2], [3, 4, 5]]
   326  			nested_mixed:  [[1, 2], ["a", "b", "c"], {extra: "keys"}]
   327  			strings:       ["all", "strings", "are the same", "type"]
   328  			mixed_numbers: [0.1, 0.2, 0.5, 1, 2, 5]
   329  			`,
   330  	}, {
   331  		name: "InlineTables",
   332  		input: `
   333  			empty  = {}
   334  			point  = {x = 1, y = 2}
   335  			animal = {type.name = "pug"}
   336  			deep   = {l1 = {l2 = {l3 = "leaf"}}}
   337  			`,
   338  		wantCUE: `
   339  			empty:  {}
   340  			point:  {x: 1, y: 2}
   341  			animal: {type: name: "pug"}
   342  			deep:   {l1: {l2: {l3: "leaf"}}}
   343  			`,
   344  	}, {
   345  		name: "InlineTablesDuplicate",
   346  		input: `
   347  			point = {x = "same key", x = "same key"}
   348  			`,
   349  		wantErr: `
   350  			duplicate key: point.x:
   351  			    test.toml:1:26
   352  			`,
   353  	}, {
   354  		name: "ArrayInlineTablesDuplicate",
   355  		input: `
   356  			point = [{}, {}, {x = "same key", x = "same key"}]
   357  			`,
   358  		wantErr: `
   359  			duplicate key: point.2.x:
   360  			    test.toml:1:35
   361  			`,
   362  	}, {
   363  		name: "InlineTablesNotDuplicateScoping",
   364  		input: `
   365  			repeat = {repeat = {repeat = "leaf"}}
   366  			struct1 = {sibling = "leaf"}
   367  			struct2 = {sibling = "leaf"}
   368  			arrays = [{sibling = "leaf"}, {sibling = "leaf"}]
   369  			`,
   370  		wantCUE: `
   371  			repeat: {repeat: {repeat: "leaf"}}
   372  			struct1: {sibling: "leaf"}
   373  			struct2: {sibling: "leaf"}
   374  			arrays: [{sibling: "leaf"}, {sibling: "leaf"}]
   375  			`,
   376  	}, {
   377  		name: "TablesEmpty",
   378  		input: `
   379  			[foo]
   380  			[bar]
   381  			`,
   382  		wantCUE: `
   383  			foo: {}
   384  			bar: {}
   385  			`,
   386  	}, {
   387  		name: "TablesOne",
   388  		input: `
   389  			[foo]
   390  			single = "single"
   391  			`,
   392  		wantCUE: `
   393  			foo: {
   394  				single: "single"
   395  			}
   396  			`,
   397  	}, {
   398  		name: "TablesMultiple",
   399  		input: `
   400  			root1 = "root1 value"
   401  			root2 = "root2 value"
   402  			[foo]
   403  			foo1 = "foo1 value"
   404  			foo2 = "foo2 value"
   405  			[bar]
   406  			bar1 = "bar1 value"
   407  			bar2 = "bar2 value"
   408  			`,
   409  		wantCUE: `
   410  			root1: "root1 value"
   411  			root2: "root2 value"
   412  			foo: {
   413  				foo1: "foo1 value"
   414  				foo2: "foo2 value"
   415  			}
   416  			bar: {
   417  				bar1: "bar1 value"
   418  				bar2: "bar2 value"
   419  			}
   420  			`,
   421  	}, {
   422  		// A lot of these edge cases are covered by RootKeys tests already.
   423  		name: "TablesKeysComplex",
   424  		input: `
   425  			[foo.bar . "baz.zzz zzz"]
   426  			one = "1"
   427  			[123-456]
   428  			two = "2"
   429  			`,
   430  		wantCUE: `
   431  			foo: bar: "baz.zzz zzz": {
   432  				one: "1"
   433  			}
   434  			"123-456": {
   435  				two: "2"
   436  			}
   437  			`,
   438  	}, {
   439  		name: "TableKeysDuplicateSimple",
   440  		input: `
   441  			[foo]
   442  			[foo]
   443  			`,
   444  		wantErr: `
   445  			duplicate key: foo:
   446  			    test.toml:2:2
   447  			`,
   448  	}, {
   449  		name: "TableKeysDuplicateOverlap",
   450  		input: `
   451  			[foo]
   452  			bar = "leaf"
   453  			[foo.bar]
   454  			baz = "second leaf"
   455  			`,
   456  		wantErr: `
   457  			duplicate key: foo.bar:
   458  			    test.toml:3:2
   459  			`,
   460  	}, {
   461  		name: "TableInnerKeysDuplicateSimple",
   462  		input: `
   463  			[foo]
   464  			bar = "same key"
   465  			bar = "same key"
   466  			`,
   467  		wantErr: `
   468  			duplicate key: foo.bar:
   469  			    test.toml:3:1
   470  			`,
   471  	}, {
   472  		name: "TablesNotDuplicateScoping",
   473  		input: `
   474  			[repeat]
   475  			repeat.repeat = "leaf"
   476  			[struct1]
   477  			sibling = "leaf"
   478  			[struct2]
   479  			sibling = "leaf"
   480  			`,
   481  		wantCUE: `
   482  			repeat: {
   483  				repeat: repeat: "leaf"
   484  			}
   485  			struct1: {
   486  				sibling: "leaf"
   487  			}
   488  			struct2: {
   489  				sibling: "leaf"
   490  			}
   491  			`,
   492  	}, {
   493  		name: "ArrayTablesEmpty",
   494  		input: `
   495  			[[foo]]
   496  			`,
   497  		wantCUE: `
   498  			foo: [
   499  				{},
   500  			]
   501  			`,
   502  	}, {
   503  		name: "ArrayTablesOne",
   504  		input: `
   505  			[[foo]]
   506  			single = "single"
   507  			`,
   508  		wantCUE: `
   509  			foo: [
   510  				{
   511  					single: "single"
   512  				},
   513  			]
   514  			`,
   515  	}, {
   516  		name: "ArrayTablesMultiple",
   517  		input: `
   518  			root = "root value"
   519  			[[foo]]
   520  			foo1 = "foo1 value"
   521  			foo2 = "foo2 value"
   522  			[[foo]]
   523  			foo3 = "foo3 value"
   524  			foo4 = "foo4 value"
   525  			[[foo]]
   526  			[[foo]]
   527  			single = "single"
   528  			`,
   529  		wantCUE: `
   530  			root: "root value"
   531  			foo: [
   532  				{
   533  					foo1: "foo1 value"
   534  					foo2: "foo2 value"
   535  				},
   536  				{
   537  					foo3: "foo3 value"
   538  					foo4: "foo4 value"
   539  				},
   540  				{},
   541  				{
   542  					single: "single"
   543  				},
   544  			]
   545  			`,
   546  	}, {
   547  		name: "ArrayTablesSeparate",
   548  		input: `
   549  			root = "root value"
   550  			[[foo]]
   551  			foo1 = "foo1 value"
   552  			[[bar]]
   553  			bar1 = "bar1 value"
   554  			[[baz]]
   555  			`,
   556  		wantCUE: `
   557  			root: "root value"
   558  			foo: [
   559  				{
   560  					foo1: "foo1 value"
   561  				},
   562  			]
   563  			bar: [
   564  				{
   565  					bar1: "bar1 value"
   566  				},
   567  			]
   568  			baz: [
   569  				{},
   570  			]
   571  			`,
   572  	}, {
   573  		name: "ArrayTablesSubtable",
   574  		input: `
   575  			[[foo]]
   576  			foo1 = "foo1 value"
   577  			[foo.subtable1]
   578  			sub1 = "sub1 value"
   579  			[foo.subtable2]
   580  			sub2 = "sub2 value"
   581  			[foo.subtable2.deeper]
   582  			sub2d = "sub2d value"
   583  			[[foo]]
   584  			foo2 = "foo2 value"
   585  			`,
   586  		wantCUE: `
   587  			foo: [
   588  				{
   589  					foo1: "foo1 value"
   590  					subtable1: {
   591  						sub1: "sub1 value"
   592  					}
   593  					subtable2: {
   594  						sub2: "sub2 value"
   595  					}
   596  					subtable2: deeper: {
   597  						sub2d: "sub2d value"
   598  					}
   599  				},
   600  				{
   601  					foo2: "foo2 value"
   602  				},
   603  			]
   604  			`,
   605  	}, {
   606  		name: "ArrayTablesNested",
   607  		input: `
   608  			[[foo]]
   609  			foo1 = "foo1 value"
   610  			[[foo.nested1]]
   611  			nest1a = "nest1a value"
   612  			[[foo.nested1]]
   613  			nest1b = "nest1b value"
   614  			[[foo.nested2]]
   615  			nest2 = "nest2 value"
   616  			[[foo.nested2.deeper]]
   617  			nest2d = "nest2d value"
   618  			[[foo.nested3.directly.deeper]]
   619  			nest3d = "nest3d value"
   620  			[[foo]]
   621  			foo2 = "foo2 value"
   622  			`,
   623  		wantCUE: `
   624  			foo: [
   625  				{
   626  					foo1: "foo1 value"
   627  					nested1: [
   628  						{
   629  							nest1a: "nest1a value"
   630  						},
   631  						{
   632  							nest1b: "nest1b value"
   633  						},
   634  					]
   635  					nested2: [
   636  						{
   637  							nest2: "nest2 value"
   638  							deeper: [
   639  								{
   640  									nest2d: "nest2d value"
   641  								}
   642  							]
   643  						},
   644  					]
   645  					nested3: directly: deeper: [
   646  						{
   647  							nest3d: "nest3d value"
   648  						},
   649  					]
   650  				},
   651  				{
   652  					foo2: "foo2 value"
   653  				},
   654  			]
   655  			`,
   656  	}, {
   657  		name: "RedeclareKeyAsTableArray",
   658  		input: `
   659  			foo = "foo value"
   660  			[middle]
   661  			middle = "to ensure we don't rely on the last key"
   662  			[[foo]]
   663  			baz = "baz value"
   664  			`,
   665  		wantErr: `
   666  			cannot redeclare key "foo" as a table array:
   667  			    test.toml:4:3
   668  			`,
   669  	}, {
   670  		name: "RedeclareTableAsTableArray",
   671  		input: `
   672  			[foo]
   673  			bar = "bar value"
   674  			[middle]
   675  			middle = "to ensure we don't rely on the last key"
   676  			[[foo]]
   677  			baz = "baz value"
   678  			`,
   679  		wantErr: `
   680  			cannot redeclare key "foo" as a table array:
   681  			    test.toml:5:3
   682  			`,
   683  	}, {
   684  		name: "RedeclareArrayAsTableArray",
   685  		input: `
   686  			foo = ["inline array"]
   687  			[middle]
   688  			middle = "to ensure we don't rely on the last key"
   689  			[[foo]]
   690  			baz = "baz value"
   691  			`,
   692  		wantErr: `
   693  			cannot redeclare key "foo" as a table array:
   694  			    test.toml:4:3
   695  			`,
   696  	}, {
   697  		name: "RedeclareTableArrayAsKey",
   698  		input: `
   699  			[[foo.foo2]]
   700  			bar = "bar value"
   701  			[middle]
   702  			middle = "to ensure we don't rely on the last key"
   703  			[foo]
   704  			foo2 = "redeclaring"
   705  			`,
   706  		wantErr: `
   707  			cannot redeclare table array "foo.foo2" as a table:
   708  			    test.toml:6:1
   709  			`,
   710  	}, {
   711  		name: "RedeclareTableArrayAsTable",
   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 table array "foo" as a table:
   722  			    test.toml:5:2
   723  			`,
   724  	}, {
   725  		name: "KeysNotDuplicateTableArrays",
   726  		input: `
   727  			[[foo]]
   728  			bar = "foo.0.bar"
   729  			[[foo]]
   730  			bar = "foo.1.bar"
   731  			[[foo]]
   732  			bar = "foo.2.bar"
   733  			[[foo.nested]]
   734  			bar = "foo.2.nested.0.bar"
   735  			[[foo.nested]]
   736  			bar = "foo.2.nested.1.bar"
   737  			[[foo.nested]]
   738  			bar = "foo.2.nested.2.bar"
   739  			`,
   740  		wantCUE: `
   741  			foo: [
   742  				{
   743  					bar: "foo.0.bar"
   744  				},
   745  				{
   746  					bar: "foo.1.bar"
   747  				},
   748  				{
   749  					bar: "foo.2.bar"
   750  					nested: [
   751  						{
   752  							bar: "foo.2.nested.0.bar"
   753  						},
   754  						{
   755  							bar: "foo.2.nested.1.bar"
   756  						},
   757  						{
   758  							bar: "foo.2.nested.2.bar"
   759  						},
   760  					]
   761  				},
   762  			]
   763  			`,
   764  	}}
   765  	for _, test := range tests {
   766  		t.Run(test.name, func(t *testing.T) {
   767  			t.Parallel()
   768  
   769  			input := unindentMultiline(test.input)
   770  			dec := toml.NewDecoder("test.toml", strings.NewReader(input))
   771  
   772  			node, err := dec.Decode()
   773  			if test.wantErr != "" {
   774  				gotErr := strings.TrimSuffix(errors.Details(err, nil), "\n")
   775  				wantErr := unindentMultiline(test.wantErr)
   776  
   777  				qt.Assert(t, qt.Equals(gotErr, wantErr))
   778  				qt.Assert(t, qt.IsNil(node))
   779  				// We don't continue, so we can't expect any decoded CUE.
   780  				qt.Assert(t, qt.Equals(test.wantCUE, ""))
   781  
   782  				// Validate that go-toml's Unmarshal also rejects this input.
   783  				err = gotoml.Unmarshal([]byte(input), new(any))
   784  				qt.Assert(t, qt.IsNotNil(err))
   785  				return
   786  			}
   787  			qt.Assert(t, qt.IsNil(err))
   788  
   789  			file, err := astutil.ToFile(node)
   790  			qt.Assert(t, qt.IsNil(err))
   791  
   792  			node2, err := dec.Decode()
   793  			qt.Assert(t, qt.IsNil(node2))
   794  			qt.Assert(t, qt.Equals(err, io.EOF))
   795  
   796  			wantFormatted, err := format.Source([]byte(test.wantCUE))
   797  			qt.Assert(t, qt.IsNil(err), qt.Commentf("wantCUE:\n%s", test.wantCUE))
   798  
   799  			formatted, err := format.Node(file)
   800  			qt.Assert(t, qt.IsNil(err))
   801  			t.Logf("CUE:\n%s", formatted)
   802  			qt.Assert(t, qt.Equals(string(formatted), string(wantFormatted)))
   803  
   804  			// Ensure that the CUE node can be compiled into a cue.Value and validated.
   805  			ctx := cuecontext.New()
   806  			// TODO(mvdan): cue.Context can only build ast.Expr or ast.File, not ast.Node;
   807  			// it's then likely not the right choice for the interface to return ast.Node.
   808  			val := ctx.BuildFile(file)
   809  			qt.Assert(t, qt.IsNil(val.Err()))
   810  			qt.Assert(t, qt.IsNil(val.Validate()))
   811  
   812  			// Validate that the decoded CUE value is equivalent
   813  			// to the Go value that go-toml's Unmarshal produces.
   814  			// We use JSON equality as some details such as which integer types are used
   815  			// are not actually relevant to an "equal data" check.
   816  			var unmarshalTOML any
   817  			err = gotoml.Unmarshal([]byte(input), &unmarshalTOML)
   818  			qt.Assert(t, qt.IsNil(err))
   819  			jsonTOML, err := json.Marshal(unmarshalTOML)
   820  			qt.Assert(t, qt.IsNil(err))
   821  			t.Logf("json.Marshal via go-toml:\t%s\n", jsonTOML)
   822  
   823  			jsonCUE, err := json.Marshal(val)
   824  			qt.Assert(t, qt.IsNil(err))
   825  			t.Logf("json.Marshal via CUE:\t%s\n", jsonCUE)
   826  			qt.Assert(t, qt.JSONEquals(jsonCUE, unmarshalTOML))
   827  
   828  			// Ensure that the decoded CUE can be re-encoded as TOML,
   829  			// and the resulting TOML is still JSON-equivalent.
   830  			t.Run("reencode", func(t *testing.T) {
   831  				sb := new(strings.Builder)
   832  				enc := toml.NewEncoder(sb)
   833  
   834  				err := enc.Encode(val)
   835  				qt.Assert(t, qt.IsNil(err))
   836  				cueTOML := sb.String()
   837  				t.Logf("reencoded TOML:\n%s", cueTOML)
   838  
   839  				var unmarshalCueTOML any
   840  				err = gotoml.Unmarshal([]byte(cueTOML), &unmarshalCueTOML)
   841  				qt.Assert(t, qt.IsNil(err))
   842  				qt.Assert(t, qt.JSONEquals(jsonCUE, unmarshalCueTOML))
   843  			})
   844  		})
   845  	}
   846  }
   847  
   848  // unindentMultiline mimics CUE's behavior with `"""` multi-line strings,
   849  // where a leading newline is omitted, and any whitespace preceding the trailing newline
   850  // is removed from the start of all lines.
   851  func unindentMultiline(s string) string {
   852  	i := strings.LastIndexByte(s, '\n')
   853  	if i < 0 {
   854  		// Not a multi-line string.
   855  		return s
   856  	}
   857  	trim := s[i:]
   858  	s = strings.ReplaceAll(s, trim, "\n")
   859  	s = strings.TrimPrefix(s, "\n")
   860  	s = strings.TrimSuffix(s, "\n")
   861  	return s
   862  }