...

Source file src/github.com/BurntSushi/toml/toml_test.go

Documentation: github.com/BurntSushi/toml

     1  //go:build go1.16
     2  // +build go1.16
     3  
     4  package toml_test
     5  
     6  import (
     7  	"bytes"
     8  	"encoding/json"
     9  	"fmt"
    10  	"os"
    11  	"path/filepath"
    12  	"regexp"
    13  	"strings"
    14  	"testing"
    15  
    16  	"github.com/BurntSushi/toml"
    17  	"github.com/BurntSushi/toml/internal/tag"
    18  	tomltest "github.com/BurntSushi/toml/internal/toml-test"
    19  )
    20  
    21  // Test if the error message matches what we want for invalid tests. Every slice
    22  // entry is tested with strings.Contains.
    23  //
    24  // Filepaths are glob'd
    25  var errorTests = map[string][]string{
    26  	"encoding/bad-utf8*":            {"invalid UTF-8 byte"},
    27  	"encoding/utf16*":               {"files cannot contain NULL bytes; probably using UTF-16"},
    28  	"string/multiline-escape-space": {`invalid escape: '\ '`},
    29  }
    30  
    31  // Test metadata; all keys listed as "keyname: type".
    32  var metaTests = map[string]string{
    33  	"implicit-and-explicit-after": `
    34  		a.b.c:         Hash
    35  		a.b.c.answer:  Integer
    36  		a:             Hash
    37  		a.better:      Integer
    38  	`,
    39  	"implicit-and-explicit-before": `
    40  		a:             Hash
    41  		a.better:      Integer
    42  		a.b.c:         Hash
    43  		a.b.c.answer:  Integer
    44  	`,
    45  	"key/case-sensitive": `
    46  		sectioN:       String
    47  		section:       Hash
    48  		section.name:  String
    49  		section.NAME:  String
    50  		section.Name:  String
    51  		Section:       Hash
    52  		Section.name:  String
    53  		Section."μ":   String
    54  		Section."Μ":   String
    55  		Section.M:     String
    56  	`,
    57  	"key/dotted": `
    58  		name.first:                   String
    59  		name.last:                    String
    60  		many.dots.here.dot.dot.dot:   Integer
    61  		count.a:                      Integer
    62  		count.b:                      Integer
    63  		count.c:                      Integer
    64  		count.d:                      Integer
    65  		count.e:                      Integer
    66  		count.f:                      Integer
    67  		count.g:                      Integer
    68  		count.h:                      Integer
    69  		count.i:                      Integer
    70  		count.j:                      Integer
    71  		count.k:                      Integer
    72  		count.l:                      Integer
    73  		tbl:                          Hash
    74  		tbl.a.b.c:                    Float
    75  		a.few.dots:                   Hash
    76  		a.few.dots.polka.dot:         String
    77  		a.few.dots.polka.dance-with:  String
    78  		arr:                          ArrayHash
    79  		arr.a.b.c:                    Integer
    80  		arr.a.b.d:                    Integer
    81  		arr:                          ArrayHash
    82  		arr.a.b.c:                    Integer
    83  		arr.a.b.d:                    Integer
    84  	 `,
    85  	"key/empty": `
    86  		"": String
    87  	`,
    88  	"key/quoted-dots": `
    89  		plain:                          Integer
    90  		"with.dot":                     Integer
    91  		plain_table:                    Hash
    92  		plain_table.plain:              Integer
    93  		plain_table."with.dot":         Integer
    94  		table.withdot:                  Hash
    95  		table.withdot.plain:            Integer
    96  		table.withdot."key.with.dots":  Integer
    97  	`,
    98  	"key/space": `
    99  		"a b": Integer
   100  		" c d ": Integer
   101  		" tbl ": Hash
   102  		" tbl "."\ttab\ttab\t": String
   103  	`,
   104  	"key/special-chars": "\n" +
   105  		"\"=~!@$^&*()_+-`1234567890[]|/?><.,;:'=\": Integer\n",
   106  
   107  	// TODO: "(albums): Hash" is missing; the problem is that this is an
   108  	// "implied key", which is recorded in the parser in implicits, rather than
   109  	// in keys. This is to allow "redefining" tables, for example:
   110  	//
   111  	//    [a.b.c]
   112  	//    answer = 42
   113  	//    [a]
   114  	//    better = 43
   115  	//
   116  	// However, we need to actually pass on this information to the MetaData so
   117  	// we can use it.
   118  	//
   119  	// Keys are supposed to be in order, for the above right now that's:
   120  	//
   121  	//     (a).(b).(c):           Hash
   122  	//     (a).(b).(c).(answer):  Integer
   123  	//     (a):                   Hash
   124  	//     (a).(better):          Integer
   125  	//
   126  	// So if we want to add "(a).(b): Hash", where should this be in the order?
   127  	"table/array-implicit": `
   128  		albums.songs:       ArrayHash
   129  		albums.songs.name:  String
   130  	`,
   131  
   132  	// TODO: people and people.* listed many times; not entirely sure if that's
   133  	// what we want?
   134  	//
   135  	// It certainly causes problems, because keys is a slice, and types a map.
   136  	// So if array entry 1 differs in type from array entry 2 then that won't be
   137  	// recorded right. This related to the problem in the above comment.
   138  	//
   139  	// people:                ArrayHash
   140  	//
   141  	// people[0]:             Hash
   142  	// people[0].first_name:  String
   143  	// people[0].last_name:   String
   144  	//
   145  	// people[1]:             Hash
   146  	// people[1].first_name:  String
   147  	// people[1].last_name:   String
   148  	//
   149  	// people[2]:             Hash
   150  	// people[2].first_name:  String
   151  	// people[2].last_name:   String
   152  	"table/array-many": `
   153  		people:             ArrayHash
   154  		people.first_name:  String
   155  		people.last_name:   String
   156  		people:             ArrayHash
   157  		people.first_name:  String
   158  		people.last_name:   String
   159  		people:             ArrayHash
   160  		people.first_name:  String
   161  		people.last_name:   String
   162  	`,
   163  	"table/array-nest": `
   164  		albums:             ArrayHash
   165  		albums.name:        String
   166  		albums.songs:       ArrayHash
   167  		albums.songs.name:  String
   168  		albums.songs:       ArrayHash
   169  		albums.songs.name:  String
   170  		albums:             ArrayHash
   171  		albums.name:        String
   172  		albums.songs:       ArrayHash
   173  		albums.songs.name:  String
   174  		albums.songs:       ArrayHash
   175  		albums.songs.name:  String
   176  	`,
   177  	"table/array-one": `
   178  		people:             ArrayHash
   179  		people.first_name:  String
   180  		people.last_name:   String
   181  	`,
   182  	"table/array-table-array": `
   183  		a:        ArrayHash
   184  		a.b:      ArrayHash
   185  		a.b.c:    Hash
   186  		a.b.c.d:  String
   187  		a.b:      ArrayHash
   188  		a.b.c:    Hash
   189  		a.b.c.d:  String
   190  	`,
   191  	"table/empty": `
   192  		a: Hash
   193  	`,
   194  	"table/keyword": `
   195  		true:   Hash
   196  		false:  Hash
   197  		inf:    Hash
   198  		nan:    Hash
   199  	`,
   200  	"table/names": `
   201  		a.b.c:    Hash
   202  		a."b.c":  Hash
   203  		a."d.e":  Hash
   204  		a." x ":  Hash
   205  		d.e.f:    Hash
   206  		g.h.i:    Hash
   207  		j."ʞ".l:  Hash
   208  		x.1.2:    Hash
   209  	`,
   210  	"table/no-eol": `
   211  		table: Hash
   212  	`,
   213  	"table/sub-empty": `
   214  		a:    Hash
   215  		a.b:  Hash
   216  	`,
   217  	"table/whitespace": `
   218  		"valid key": Hash
   219  	`,
   220  	"table/with-literal-string": `
   221  		a:                   Hash
   222  		a."\"b\"":           Hash
   223  		a."\"b\"".c:         Hash
   224  		a."\"b\"".c.answer:  Integer
   225  	`,
   226  	"table/with-pound": `
   227  		"key#group":         Hash
   228  		"key#group".answer:  Integer
   229  	`,
   230  	"table/with-single-quotes": `
   231  		a:             Hash
   232  		a.b:           Hash
   233  		a.b.c:         Hash
   234  		a.b.c.answer:  Integer
   235  	`,
   236  	"table/without-super": `
   237  		x.y.z.w:  Hash
   238  		x:        Hash
   239  	`,
   240  }
   241  
   242  // TOML 1.0
   243  func TestToml(t *testing.T) {
   244  	runTomlTest(t, false)
   245  }
   246  
   247  // TOML 1.1
   248  func TestTomlNext(t *testing.T) {
   249  	toml.WithTomlNext(func() {
   250  		runTomlTest(t, true)
   251  	})
   252  }
   253  
   254  // Make sure TOML 1.1 fails by default for now.
   255  func TestTomlNextFails(t *testing.T) {
   256  	runTomlTest(t, true,
   257  		"valid/string/escape-esc",
   258  		"valid/datetime/no-seconds",
   259  		"valid/string/hex-escape",
   260  		"valid/inline-table/newline",
   261  		"valid/key/unicode")
   262  }
   263  
   264  func runTomlTest(t *testing.T, includeNext bool, wantFail ...string) {
   265  	for k := range errorTests { // Make sure patterns are valid.
   266  		_, err := filepath.Match(k, "")
   267  		if err != nil {
   268  			t.Fatal(err)
   269  		}
   270  	}
   271  
   272  	// TODO: bit of a hack to make sure not all test run; without this "-run=.."
   273  	// will still run alll tests, but just report the errors for the -run value.
   274  	// This is annoying in cases where you have some debug printf.
   275  	//
   276  	// Need to update toml-test a bit to make this easier, but this good enough
   277  	// for now.
   278  	var runTests []string
   279  	for _, a := range os.Args {
   280  		if strings.HasPrefix(a, "-test.run=TestToml/") {
   281  			a = strings.TrimPrefix(a, "-test.run=TestToml/encode/")
   282  			a = strings.TrimPrefix(a, "-test.run=TestToml/decode/")
   283  			runTests = []string{a, a + "/*"}
   284  			break
   285  		}
   286  	}
   287  
   288  	// Make sure the keys in metaTests and errorTests actually exist; easy to
   289  	// make a typo and nothing will get tested.
   290  	var (
   291  		shouldExistValid   = make(map[string]struct{})
   292  		shouldExistInvalid = make(map[string]struct{})
   293  	)
   294  	if len(runTests) == 0 {
   295  		for k := range metaTests {
   296  			shouldExistValid["valid/"+k] = struct{}{}
   297  		}
   298  		for k := range errorTests {
   299  			shouldExistInvalid["invalid/"+k] = struct{}{}
   300  		}
   301  	}
   302  
   303  	run := func(t *testing.T, enc bool) {
   304  		r := tomltest.Runner{
   305  			Files:    tomltest.EmbeddedTests(),
   306  			Encoder:  enc,
   307  			Parser:   parser{},
   308  			RunTests: runTests,
   309  			SkipTests: []string{
   310  				// "15" in time.Parse() accepts both "1" and "01". The TOML
   311  				// specification says that times *must* start with a leading
   312  				// zero, but this requires writing out own datetime parser.
   313  				// I think it's actually okay to just accept both really.
   314  				// https://github.com/BurntSushi/toml/issues/320
   315  				"invalid/datetime/time-no-leads",
   316  
   317  				// These tests are fine, just doesn't deal well with empty output.
   318  				"valid/comment/noeol",
   319  				"valid/comment/nonascii",
   320  
   321  				// TODO: fix this; we allow appending to tables, but shouldn't.
   322  				"invalid/table/append-with-dotted*",
   323  				"invalid/inline-table/add",
   324  				"invalid/table/duplicate-key-dotted-table",
   325  				"invalid/table/duplicate-key-dotted-table2",
   326  				"invalid/spec/inline-table-2-0",
   327  				"invalid/spec/table-9-1",
   328  				"invalid/inline-table/nested_key_conflict",
   329  				"invalid/table/append-to-array-with-dotted-keys",
   330  			},
   331  		}
   332  		if includeNext {
   333  			r.Version = "next"
   334  		}
   335  
   336  		tests, err := r.Run()
   337  		if err != nil {
   338  			t.Fatal(err)
   339  		}
   340  
   341  		failed := make(map[string]struct{})
   342  		for _, test := range tests.Tests {
   343  			t.Run(test.Path, func(t *testing.T) {
   344  				if test.Failed() {
   345  					for _, f := range wantFail {
   346  						if f == test.Path {
   347  							failed[test.Path] = struct{}{}
   348  							return
   349  						}
   350  					}
   351  
   352  					t.Fatalf("\nError:\n%s\n\nInput:\n%s\nOutput:\n%s\nWant:\n%s\n",
   353  						test.Failure, test.Input, test.Output, test.Want)
   354  					return
   355  				}
   356  
   357  				// Test error message.
   358  				if test.Type() == tomltest.TypeInvalid {
   359  					testError(t, test, shouldExistInvalid)
   360  				}
   361  				// Test metadata
   362  				if !enc && test.Type() == tomltest.TypeValid {
   363  					delete(shouldExistValid, test.Path)
   364  					testMeta(t, test, includeNext)
   365  				}
   366  			})
   367  		}
   368  		for _, f := range wantFail {
   369  			if _, ok := failed[f]; !ok {
   370  				t.Errorf("expected test %q to fail but it didn't", f)
   371  			}
   372  		}
   373  
   374  		t.Logf("passed: %d; failed: %d; skipped: %d", tests.Passed, tests.Failed, tests.Skipped)
   375  	}
   376  
   377  	t.Run("decode", func(t *testing.T) { run(t, false) })
   378  	t.Run("encode", func(t *testing.T) { run(t, true) })
   379  
   380  	if len(shouldExistValid) > 0 {
   381  		var s []string
   382  		for k := range shouldExistValid {
   383  			s = append(s, k)
   384  		}
   385  		t.Errorf("the following meta tests didn't match any files: %s", strings.Join(s, ", "))
   386  	}
   387  	if len(shouldExistInvalid) > 0 {
   388  		var s []string
   389  		for k := range shouldExistInvalid {
   390  			s = append(s, k)
   391  		}
   392  		t.Errorf("the following meta tests didn't match any files: %s", strings.Join(s, ", "))
   393  	}
   394  }
   395  
   396  var reCollapseSpace = regexp.MustCompile(` +`)
   397  
   398  func testMeta(t *testing.T, test tomltest.Test, includeNext bool) {
   399  	want, ok := metaTests[strings.TrimPrefix(test.Path, "valid/")]
   400  	if !ok {
   401  		return
   402  	}
   403  
   404  	// Output is slightly different due to different quoting; just skip for now.
   405  	if includeNext && (test.Path == "valid/table/names" || test.Path == "valid/key/case-sensitive") {
   406  		return
   407  	}
   408  
   409  	var s interface{}
   410  	meta, err := toml.Decode(test.Input, &s)
   411  	if err != nil {
   412  		t.Fatal(err)
   413  	}
   414  
   415  	b := new(strings.Builder)
   416  	for i, k := range meta.Keys() {
   417  		if i > 0 {
   418  			b.WriteByte('\n')
   419  		}
   420  		fmt.Fprintf(b, "%s: %s", k, meta.Type(k...))
   421  	}
   422  	have := b.String()
   423  
   424  	want = reCollapseSpace.ReplaceAllString(strings.ReplaceAll(strings.TrimSpace(want), "\t", ""), " ")
   425  	if have != want {
   426  		t.Errorf("MetaData wrong\nhave:\n%s\nwant:\n%s", have, want)
   427  	}
   428  }
   429  
   430  func testError(t *testing.T, test tomltest.Test, shouldExist map[string]struct{}) {
   431  	path := strings.TrimPrefix(test.Path, "invalid/")
   432  
   433  	errs, ok := errorTests[path]
   434  	if ok {
   435  		delete(shouldExist, "invalid/"+path)
   436  	}
   437  	if !ok {
   438  		for k := range errorTests {
   439  			ok, _ = filepath.Match(k, path)
   440  			if ok {
   441  				delete(shouldExist, "invalid/"+k)
   442  				errs = errorTests[k]
   443  				break
   444  			}
   445  		}
   446  	}
   447  	if !ok {
   448  		return
   449  	}
   450  
   451  	for _, e := range errs {
   452  		if !strings.Contains(test.Output, e) {
   453  			t.Errorf("\nwrong error message\nhave: %s\nwant: %s", test.Output, e)
   454  		}
   455  	}
   456  }
   457  
   458  type parser struct{}
   459  
   460  func (p parser) Encode(input string) (output string, outputIsError bool, retErr error) {
   461  	defer func() {
   462  		if r := recover(); r != nil {
   463  			switch rr := r.(type) {
   464  			case error:
   465  				retErr = rr
   466  			default:
   467  				retErr = fmt.Errorf("%s", rr)
   468  			}
   469  		}
   470  	}()
   471  
   472  	var tmp interface{}
   473  	err := json.Unmarshal([]byte(input), &tmp)
   474  	if err != nil {
   475  		return "", false, err
   476  	}
   477  
   478  	rm, err := tag.Remove(tmp)
   479  	if err != nil {
   480  		return err.Error(), true, retErr
   481  	}
   482  
   483  	buf := new(bytes.Buffer)
   484  	err = toml.NewEncoder(buf).Encode(rm)
   485  	if err != nil {
   486  		return err.Error(), true, retErr
   487  	}
   488  
   489  	return buf.String(), false, retErr
   490  }
   491  
   492  func (p parser) Decode(input string) (output string, outputIsError bool, retErr error) {
   493  	defer func() {
   494  		if r := recover(); r != nil {
   495  			switch rr := r.(type) {
   496  			case error:
   497  				retErr = rr
   498  			default:
   499  				retErr = fmt.Errorf("%s", rr)
   500  			}
   501  		}
   502  	}()
   503  
   504  	var d interface{}
   505  	if _, err := toml.Decode(input, &d); err != nil {
   506  		return err.Error(), true, retErr
   507  	}
   508  
   509  	j, err := json.MarshalIndent(tag.Add("", d), "", "  ")
   510  	if err != nil {
   511  		return "", false, err
   512  	}
   513  	return string(j), false, retErr
   514  }
   515  

View as plain text