git.sr.ht/~pingoo/stdx@v0.0.0-20240218134121-094174641f6e/toml/toml_test.go (about) 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 "git.sr.ht/~pingoo/stdx/toml" 17 "git.sr.ht/~pingoo/stdx/toml/internal/tag" 18 tomltest "git.sr.ht/~pingoo/stdx/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 func TestToml(t *testing.T) { 243 for k := range errorTests { // Make sure patterns are valid. 244 _, err := filepath.Match(k, "") 245 if err != nil { 246 t.Fatal(err) 247 } 248 } 249 250 // TODO: bit of a hack to make sure not all test run; without this "-run=.." 251 // will still run alll tests, but just report the errors for the -run value. 252 // This is annoying in cases where you have some debug printf. 253 // 254 // Need to update toml-test a bit to make this easier, but this good enough 255 // for now. 256 var runTests []string 257 for _, a := range os.Args { 258 if strings.HasPrefix(a, "-test.run=TestToml/") { 259 a = strings.TrimPrefix(a, "-test.run=TestToml/encode/") 260 a = strings.TrimPrefix(a, "-test.run=TestToml/decode/") 261 runTests = []string{a, a + "/*"} 262 break 263 } 264 } 265 266 // Make sure the keys in metaTests and errorTests actually exist; easy to 267 // make a typo and nothing will get tested. 268 var ( 269 shouldExistValid = make(map[string]struct{}) 270 shouldExistInvalid = make(map[string]struct{}) 271 ) 272 if len(runTests) == 0 { 273 for k := range metaTests { 274 shouldExistValid["valid/"+k] = struct{}{} 275 } 276 for k := range errorTests { 277 shouldExistInvalid["invalid/"+k] = struct{}{} 278 } 279 } 280 281 run := func(t *testing.T, enc bool) { 282 r := tomltest.Runner{ 283 Files: tomltest.EmbeddedTests(), 284 Encoder: enc, 285 Parser: parser{}, 286 RunTests: runTests, 287 SkipTests: []string{ 288 // "15" in time.Parse() accepts both "1" and "01". The TOML 289 // specification says that times *must* start with a leading 290 // zero, but this requires writing out own datetime parser. 291 // I think it's actually okay to just accept both really. 292 // https://git.sr.ht/~pingoo/stdx/toml/issues/320 293 "invalid/datetime/time-no-leads", 294 295 // This test is fine, just doesn't deal well with empty output. 296 "valid/comment/noeol", 297 298 // TODO: fix this. 299 "invalid/table/append-with-dotted*", 300 "invalid/inline-table/add", 301 "invalid/table/duplicate-key-dotted-table", 302 "invalid/table/duplicate-key-dotted-table2", 303 }, 304 } 305 306 tests, err := r.Run() 307 if err != nil { 308 t.Fatal(err) 309 } 310 311 for _, test := range tests.Tests { 312 t.Run(test.Path, func(t *testing.T) { 313 if test.Failed() { 314 t.Fatalf("\nError:\n%s\n\nInput:\n%s\nOutput:\n%s\nWant:\n%s\n", 315 test.Failure, test.Input, test.Output, test.Want) 316 return 317 } 318 319 // Test error message. 320 if test.Type() == tomltest.TypeInvalid { 321 testError(t, test, shouldExistInvalid) 322 } 323 // Test metadata 324 if !enc && test.Type() == tomltest.TypeValid { 325 delete(shouldExistValid, test.Path) 326 testMeta(t, test) 327 } 328 }) 329 } 330 t.Logf("passed: %d; failed: %d; skipped: %d", tests.Passed, tests.Failed, tests.Skipped) 331 } 332 333 t.Run("decode", func(t *testing.T) { run(t, false) }) 334 t.Run("encode", func(t *testing.T) { run(t, true) }) 335 336 if len(shouldExistValid) > 0 { 337 var s []string 338 for k := range shouldExistValid { 339 s = append(s, k) 340 } 341 t.Errorf("the following meta tests didn't match any files: %s", strings.Join(s, ", ")) 342 } 343 if len(shouldExistInvalid) > 0 { 344 var s []string 345 for k := range shouldExistInvalid { 346 s = append(s, k) 347 } 348 t.Errorf("the following meta tests didn't match any files: %s", strings.Join(s, ", ")) 349 } 350 } 351 352 var reCollapseSpace = regexp.MustCompile(` +`) 353 354 func testMeta(t *testing.T, test tomltest.Test) { 355 want, ok := metaTests[strings.TrimPrefix(test.Path, "valid/")] 356 if !ok { 357 return 358 } 359 var s interface{} 360 meta, err := toml.Decode(test.Input, &s) 361 if err != nil { 362 t.Fatal(err) 363 } 364 365 b := new(strings.Builder) 366 for i, k := range meta.Keys() { 367 if i > 0 { 368 b.WriteByte('\n') 369 } 370 fmt.Fprintf(b, "%s: %s", k, meta.Type(k...)) 371 } 372 have := b.String() 373 374 want = reCollapseSpace.ReplaceAllString(strings.ReplaceAll(strings.TrimSpace(want), "\t", ""), " ") 375 if have != want { 376 t.Errorf("MetaData wrong\nhave:\n%s\nwant:\n%s", have, want) 377 } 378 } 379 380 func testError(t *testing.T, test tomltest.Test, shouldExist map[string]struct{}) { 381 path := strings.TrimPrefix(test.Path, "invalid/") 382 383 errs, ok := errorTests[path] 384 if ok { 385 delete(shouldExist, "invalid/"+path) 386 } 387 if !ok { 388 for k := range errorTests { 389 ok, _ = filepath.Match(k, path) 390 if ok { 391 delete(shouldExist, "invalid/"+k) 392 errs = errorTests[k] 393 break 394 } 395 } 396 } 397 if !ok { 398 return 399 } 400 401 for _, e := range errs { 402 if !strings.Contains(test.Output, e) { 403 t.Errorf("\nwrong error message\nhave: %s\nwant: %s", test.Output, e) 404 } 405 } 406 } 407 408 type parser struct{} 409 410 func (p parser) Encode(input string) (output string, outputIsError bool, retErr error) { 411 defer func() { 412 if r := recover(); r != nil { 413 switch rr := r.(type) { 414 case error: 415 retErr = rr 416 default: 417 retErr = fmt.Errorf("%s", rr) 418 } 419 } 420 }() 421 422 var tmp interface{} 423 err := json.Unmarshal([]byte(input), &tmp) 424 if err != nil { 425 return "", false, err 426 } 427 428 rm, err := tag.Remove(tmp) 429 if err != nil { 430 return err.Error(), true, retErr 431 } 432 433 buf := new(bytes.Buffer) 434 err = toml.NewEncoder(buf).Encode(rm) 435 if err != nil { 436 return err.Error(), true, retErr 437 } 438 439 return buf.String(), false, retErr 440 } 441 442 func (p parser) Decode(input string) (output string, outputIsError bool, retErr error) { 443 defer func() { 444 if r := recover(); r != nil { 445 switch rr := r.(type) { 446 case error: 447 retErr = rr 448 default: 449 retErr = fmt.Errorf("%s", rr) 450 } 451 } 452 }() 453 454 var d interface{} 455 if _, err := toml.Decode(input, &d); err != nil { 456 return err.Error(), true, retErr 457 } 458 459 j, err := json.MarshalIndent(tag.Add("", d), "", " ") 460 if err != nil { 461 return "", false, err 462 } 463 return string(j), false, retErr 464 }