github.com/tinygo-org/tinygo@v0.31.3-0.20240404173401-90b0bf646c27/main_test.go (about) 1 package main 2 3 // This file tests the compiler by running Go files in testdata/*.go and 4 // comparing their output with the expected output in testdata/*.txt. 5 6 import ( 7 "bufio" 8 "bytes" 9 "errors" 10 "flag" 11 "fmt" 12 "io" 13 "os" 14 "os/exec" 15 "reflect" 16 "regexp" 17 "runtime" 18 "slices" 19 "strings" 20 "sync" 21 "testing" 22 "time" 23 24 "github.com/aykevl/go-wasm" 25 "github.com/tinygo-org/tinygo/builder" 26 "github.com/tinygo-org/tinygo/compileopts" 27 "github.com/tinygo-org/tinygo/goenv" 28 ) 29 30 const TESTDATA = "testdata" 31 32 var testTarget = flag.String("target", "", "override test target") 33 34 var supportedLinuxArches = map[string]string{ 35 "AMD64Linux": "linux/amd64", 36 "X86Linux": "linux/386", 37 "ARMLinux": "linux/arm/6", 38 "ARM64Linux": "linux/arm64", 39 "WASIp1": "wasip1/wasm", 40 } 41 42 func init() { 43 major, _, _ := goenv.GetGorootVersion() 44 if major < 21 { 45 // Go 1.20 backwards compatibility. 46 // Should be removed once we drop support for Go 1.20. 47 delete(supportedLinuxArches, "WASIp1") 48 } 49 } 50 51 var sema = make(chan struct{}, runtime.NumCPU()) 52 53 func TestBuild(t *testing.T) { 54 t.Parallel() 55 56 tests := []string{ 57 "alias.go", 58 "atomic.go", 59 "binop.go", 60 "calls.go", 61 "cgo/", 62 "channel.go", 63 "embed/", 64 "float.go", 65 "gc.go", 66 "generics.go", 67 "goroutines.go", 68 "init.go", 69 "init_multi.go", 70 "interface.go", 71 "json.go", 72 "map.go", 73 "math.go", 74 "oldgo/", 75 "print.go", 76 "reflect.go", 77 "slice.go", 78 "sort.go", 79 "stdlib.go", 80 "string.go", 81 "structs.go", 82 "testing.go", 83 "timers.go", 84 "zeroalloc.go", 85 } 86 87 // Go 1.21 made some changes to the language, which we can only test when 88 // we're actually on Go 1.21. 89 _, minor, err := goenv.GetGorootVersion() 90 if err != nil { 91 t.Fatal("could not get version:", minor) 92 } 93 if minor >= 21 { 94 tests = append(tests, "go1.21.go") 95 } 96 if minor >= 22 { 97 tests = append(tests, "go1.22/") 98 } 99 100 if *testTarget != "" { 101 // This makes it possible to run one specific test (instead of all), 102 // which is especially useful to quickly check whether some changes 103 // affect a particular target architecture. 104 runPlatTests(optionsFromTarget(*testTarget, sema), tests, t) 105 return 106 } 107 108 t.Run("Host", func(t *testing.T) { 109 t.Parallel() 110 runPlatTests(optionsFromTarget("", sema), tests, t) 111 }) 112 113 // Test a few build options. 114 t.Run("build-options", func(t *testing.T) { 115 t.Parallel() 116 117 // Test with few optimizations enabled (no inlining, etc). 118 t.Run("opt=1", func(t *testing.T) { 119 t.Parallel() 120 opts := optionsFromTarget("", sema) 121 opts.Opt = "1" 122 runTestWithConfig("stdlib.go", t, opts, nil, nil) 123 }) 124 125 // Test with only the bare minimum of optimizations enabled. 126 // TODO: fix this for stdlib.go, which currently fails. 127 t.Run("opt=0", func(t *testing.T) { 128 t.Parallel() 129 opts := optionsFromTarget("", sema) 130 opts.Opt = "0" 131 runTestWithConfig("print.go", t, opts, nil, nil) 132 }) 133 134 t.Run("ldflags", func(t *testing.T) { 135 t.Parallel() 136 opts := optionsFromTarget("", sema) 137 opts.GlobalValues = map[string]map[string]string{ 138 "main": { 139 "someGlobal": "foobar", 140 }, 141 } 142 runTestWithConfig("ldflags.go", t, opts, nil, nil) 143 }) 144 }) 145 146 if testing.Short() { 147 // Don't test other targets when the -short flag is used. Only test the 148 // host system. 149 return 150 } 151 152 t.Run("EmulatedCortexM3", func(t *testing.T) { 153 t.Parallel() 154 runPlatTests(optionsFromTarget("cortex-m-qemu", sema), tests, t) 155 }) 156 157 t.Run("EmulatedRISCV", func(t *testing.T) { 158 t.Parallel() 159 runPlatTests(optionsFromTarget("riscv-qemu", sema), tests, t) 160 }) 161 162 t.Run("AVR", func(t *testing.T) { 163 t.Parallel() 164 runPlatTests(optionsFromTarget("simavr", sema), tests, t) 165 }) 166 167 if runtime.GOOS == "linux" { 168 for name, osArch := range supportedLinuxArches { 169 options := optionsFromOSARCH(osArch, sema) 170 if options.GOARCH != runtime.GOARCH { // Native architecture already run above. 171 t.Run(name, func(t *testing.T) { 172 runPlatTests(options, tests, t) 173 }) 174 } 175 } 176 t.Run("WebAssembly", func(t *testing.T) { 177 t.Parallel() 178 runPlatTests(optionsFromTarget("wasm", sema), tests, t) 179 }) 180 t.Run("WASI", func(t *testing.T) { 181 t.Parallel() 182 runPlatTests(optionsFromTarget("wasip1", sema), tests, t) 183 }) 184 } 185 } 186 187 func runPlatTests(options compileopts.Options, tests []string, t *testing.T) { 188 emuCheck(t, options) 189 190 spec, err := compileopts.LoadTarget(&options) 191 if err != nil { 192 t.Fatal("failed to load target spec:", err) 193 } 194 195 // FIXME: this should really be: 196 // isWebAssembly := strings.HasPrefix(spec.Triple, "wasm") 197 isWASI := strings.HasPrefix(options.Target, "wasi") 198 isWebAssembly := isWASI || strings.HasPrefix(options.Target, "wasm") || (options.Target == "" && strings.HasPrefix(options.GOARCH, "wasm")) 199 200 for _, name := range tests { 201 if options.GOOS == "linux" && (options.GOARCH == "arm" || options.GOARCH == "386") { 202 switch name { 203 case "timers.go": 204 // Timer tests do not work because syscall.seek is implemented 205 // as Assembly in mainline Go and causes linker failure 206 continue 207 } 208 } 209 if options.Target == "simavr" { 210 // Not all tests are currently supported on AVR. 211 // Skip the ones that aren't. 212 switch name { 213 case "reflect.go": 214 // Reflect tests do not run correctly, probably because of the 215 // limited amount of memory. 216 continue 217 218 case "gc.go": 219 // Does not pass due to high mark false positive rate. 220 continue 221 222 case "json.go", "stdlib.go", "testing.go": 223 // Too big for AVR. Doesn't fit in flash/RAM. 224 continue 225 226 case "math.go": 227 // Needs newer picolibc version (for sqrt). 228 continue 229 230 case "cgo/": 231 // CGo function pointers don't work on AVR (needs LLVM 16 and 232 // some compiler changes). 233 continue 234 235 default: 236 } 237 } 238 name := name // redefine to avoid race condition 239 t.Run(name, func(t *testing.T) { 240 t.Parallel() 241 runTest(name, options, t, nil, nil) 242 }) 243 } 244 if !strings.HasPrefix(spec.Emulator, "simavr ") { 245 t.Run("env.go", func(t *testing.T) { 246 t.Parallel() 247 runTest("env.go", options, t, []string{"first", "second"}, []string{"ENV1=VALUE1", "ENV2=VALUE2"}) 248 }) 249 } 250 if isWebAssembly { 251 t.Run("alias.go-scheduler-none", func(t *testing.T) { 252 t.Parallel() 253 options := compileopts.Options(options) 254 options.Scheduler = "none" 255 runTest("alias.go", options, t, nil, nil) 256 }) 257 } 258 if options.Target == "" || isWASI { 259 t.Run("filesystem.go", func(t *testing.T) { 260 t.Parallel() 261 runTest("filesystem.go", options, t, nil, nil) 262 }) 263 } 264 if options.Target == "" || options.Target == "wasm" || isWASI { 265 t.Run("rand.go", func(t *testing.T) { 266 t.Parallel() 267 runTest("rand.go", options, t, nil, nil) 268 }) 269 } 270 if !isWebAssembly { 271 // The recover() builtin isn't supported yet on WebAssembly and Windows. 272 t.Run("recover.go", func(t *testing.T) { 273 t.Parallel() 274 runTest("recover.go", options, t, nil, nil) 275 }) 276 } 277 } 278 279 func emuCheck(t *testing.T, options compileopts.Options) { 280 // Check if the emulator is installed. 281 spec, err := compileopts.LoadTarget(&options) 282 if err != nil { 283 t.Fatal("failed to load target spec:", err) 284 } 285 if spec.Emulator != "" { 286 emulatorCommand := strings.SplitN(spec.Emulator, " ", 2)[0] 287 _, err := exec.LookPath(emulatorCommand) 288 if err != nil { 289 if errors.Is(err, exec.ErrNotFound) { 290 t.Skipf("emulator not installed: %q", emulatorCommand) 291 } 292 293 t.Errorf("searching for emulator: %v", err) 294 return 295 } 296 } 297 } 298 299 func optionsFromTarget(target string, sema chan struct{}) compileopts.Options { 300 return compileopts.Options{ 301 // GOOS/GOARCH are only used if target == "" 302 GOOS: goenv.Get("GOOS"), 303 GOARCH: goenv.Get("GOARCH"), 304 GOARM: goenv.Get("GOARM"), 305 Target: target, 306 Semaphore: sema, 307 InterpTimeout: 180 * time.Second, 308 Debug: true, 309 VerifyIR: true, 310 Opt: "z", 311 } 312 } 313 314 // optionsFromOSARCH returns a set of options based on the "osarch" string. This 315 // string is in the form of "os/arch/subarch", with the subarch only sometimes 316 // being necessary. Examples are "darwin/amd64" or "linux/arm/7". 317 func optionsFromOSARCH(osarch string, sema chan struct{}) compileopts.Options { 318 parts := strings.Split(osarch, "/") 319 options := compileopts.Options{ 320 GOOS: parts[0], 321 GOARCH: parts[1], 322 Semaphore: sema, 323 InterpTimeout: 180 * time.Second, 324 Debug: true, 325 VerifyIR: true, 326 Opt: "z", 327 } 328 if options.GOARCH == "arm" { 329 options.GOARM = parts[2] 330 } 331 return options 332 } 333 334 func runTest(name string, options compileopts.Options, t *testing.T, cmdArgs, environmentVars []string) { 335 runTestWithConfig(name, t, options, cmdArgs, environmentVars) 336 } 337 338 func runTestWithConfig(name string, t *testing.T, options compileopts.Options, cmdArgs, environmentVars []string) { 339 // Get the expected output for this test. 340 // Note: not using filepath.Join as it strips the path separator at the end 341 // of the path. 342 path := TESTDATA + "/" + name 343 // Get the expected output for this test. 344 txtpath := path[:len(path)-3] + ".txt" 345 pkgName := "./" + path 346 if path[len(path)-1] == '/' { 347 txtpath = path + "out.txt" 348 options.Directory = path 349 pkgName = "." 350 } 351 expected, err := os.ReadFile(txtpath) 352 if err != nil { 353 t.Fatal("could not read expected output file:", err) 354 } 355 356 config, err := builder.NewConfig(&options) 357 if err != nil { 358 t.Fatal(err) 359 } 360 361 // Build the test binary. 362 stdout := &bytes.Buffer{} 363 _, err = buildAndRun(pkgName, config, stdout, cmdArgs, environmentVars, time.Minute, func(cmd *exec.Cmd, result builder.BuildResult) error { 364 return cmd.Run() 365 }) 366 if err != nil { 367 printCompilerError(t.Log, err) 368 t.Fail() 369 return 370 } 371 372 // putchar() prints CRLF, convert it to LF. 373 actual := bytes.Replace(stdout.Bytes(), []byte{'\r', '\n'}, []byte{'\n'}, -1) 374 expected = bytes.Replace(expected, []byte{'\r', '\n'}, []byte{'\n'}, -1) // for Windows 375 376 if config.EmulatorName() == "simavr" { 377 // Strip simavr log formatting. 378 actual = bytes.Replace(actual, []byte{0x1b, '[', '3', '2', 'm'}, nil, -1) 379 actual = bytes.Replace(actual, []byte{0x1b, '[', '0', 'm'}, nil, -1) 380 actual = bytes.Replace(actual, []byte{'.', '.', '\n'}, []byte{'\n'}, -1) 381 actual = bytes.Replace(actual, []byte{'\n', '.', '\n'}, []byte{'\n', '\n'}, -1) 382 } 383 if name == "testing.go" { 384 // Strip actual time. 385 re := regexp.MustCompile(`\([0-9]\.[0-9][0-9]s\)`) 386 actual = re.ReplaceAllLiteral(actual, []byte{'(', '0', '.', '0', '0', 's', ')'}) 387 } 388 389 // Check whether the command ran successfully. 390 fail := false 391 if err != nil { 392 t.Log("failed to run:", err) 393 fail = true 394 } else if !bytes.Equal(expected, actual) { 395 t.Logf("output did not match (expected %d bytes, got %d bytes):", len(expected), len(actual)) 396 fail = true 397 } 398 399 if fail { 400 r := bufio.NewReader(bytes.NewReader(actual)) 401 for { 402 line, err := r.ReadString('\n') 403 if err != nil { 404 break 405 } 406 t.Log("stdout:", line[:len(line)-1]) 407 } 408 t.Fail() 409 } 410 } 411 412 // Test WebAssembly files for certain properties. 413 func TestWebAssembly(t *testing.T) { 414 t.Parallel() 415 type testCase struct { 416 name string 417 panicStrategy string 418 imports []string 419 } 420 for _, tc := range []testCase{ 421 // Test whether there really are no imports when using -panic=trap. This 422 // tests the bugfix for https://github.com/tinygo-org/tinygo/issues/4161. 423 {name: "panic-default", imports: []string{"wasi_snapshot_preview1.fd_write"}}, 424 {name: "panic-trap", panicStrategy: "trap", imports: []string{}}, 425 } { 426 tc := tc 427 t.Run(tc.name, func(t *testing.T) { 428 t.Parallel() 429 tmpdir := t.TempDir() 430 options := optionsFromTarget("wasi", sema) 431 options.PanicStrategy = tc.panicStrategy 432 config, err := builder.NewConfig(&options) 433 if err != nil { 434 t.Fatal(err) 435 } 436 437 result, err := builder.Build("testdata/trivialpanic.go", ".wasm", tmpdir, config) 438 if err != nil { 439 t.Fatal("failed to build binary:", err) 440 } 441 f, err := os.Open(result.Binary) 442 if err != nil { 443 t.Fatal("could not open output binary:", err) 444 } 445 defer f.Close() 446 module, err := wasm.Parse(f) 447 if err != nil { 448 t.Fatal("could not parse output binary:", err) 449 } 450 451 // Test the list of imports. 452 if tc.imports != nil { 453 var imports []string 454 for _, section := range module.Sections { 455 switch section := section.(type) { 456 case *wasm.SectionImport: 457 for _, symbol := range section.Entries { 458 imports = append(imports, symbol.Module+"."+symbol.Field) 459 } 460 } 461 } 462 if !slices.Equal(imports, tc.imports) { 463 t.Errorf("import list not as expected!\nexpected: %v\nactual: %v", tc.imports, imports) 464 } 465 } 466 }) 467 } 468 } 469 470 func TestTest(t *testing.T) { 471 t.Parallel() 472 473 type targ struct { 474 name string 475 opts compileopts.Options 476 } 477 targs := []targ{ 478 // Host 479 {"Host", optionsFromTarget("", sema)}, 480 } 481 if !testing.Short() { 482 if runtime.GOOS == "linux" { 483 for name, osArch := range supportedLinuxArches { 484 options := optionsFromOSARCH(osArch, sema) 485 if options.GOARCH != runtime.GOARCH { // Native architecture already run above. 486 targs = append(targs, targ{name, options}) 487 } 488 } 489 } 490 491 targs = append(targs, 492 // QEMU microcontrollers 493 targ{"EmulatedCortexM3", optionsFromTarget("cortex-m-qemu", sema)}, 494 targ{"EmulatedRISCV", optionsFromTarget("riscv-qemu", sema)}, 495 496 // Node/Wasmtime 497 targ{"WASM", optionsFromTarget("wasm", sema)}, 498 targ{"WASI", optionsFromTarget("wasip1", sema)}, 499 ) 500 } 501 for _, targ := range targs { 502 targ := targ 503 t.Run(targ.name, func(t *testing.T) { 504 t.Parallel() 505 506 emuCheck(t, targ.opts) 507 508 t.Run("Pass", func(t *testing.T) { 509 t.Parallel() 510 511 // Test a package which builds and passes normally. 512 513 var wg sync.WaitGroup 514 defer wg.Wait() 515 516 out := ioLogger(t, &wg) 517 defer out.Close() 518 519 opts := targ.opts 520 passed, err := Test("github.com/tinygo-org/tinygo/tests/testing/pass", out, out, &opts, "") 521 if err != nil { 522 t.Errorf("test error: %v", err) 523 } 524 if !passed { 525 t.Error("test failed") 526 } 527 }) 528 529 t.Run("Fail", func(t *testing.T) { 530 t.Parallel() 531 532 // Test a package which builds fine but fails. 533 534 var wg sync.WaitGroup 535 defer wg.Wait() 536 537 out := ioLogger(t, &wg) 538 defer out.Close() 539 540 opts := targ.opts 541 passed, err := Test("github.com/tinygo-org/tinygo/tests/testing/fail", out, out, &opts, "") 542 if err != nil { 543 t.Errorf("test error: %v", err) 544 } 545 if passed { 546 t.Error("test passed") 547 } 548 }) 549 550 if targ.name != "Host" { 551 // Emulated tests are somewhat slow, and these do not need to be run across every platform. 552 return 553 } 554 555 t.Run("Nothing", func(t *testing.T) { 556 t.Parallel() 557 558 // Test a package with no test files. 559 560 var wg sync.WaitGroup 561 defer wg.Wait() 562 563 out := ioLogger(t, &wg) 564 defer out.Close() 565 566 var output bytes.Buffer 567 opts := targ.opts 568 passed, err := Test("github.com/tinygo-org/tinygo/tests/testing/nothing", io.MultiWriter(&output, out), out, &opts, "") 569 if err != nil { 570 t.Errorf("test error: %v", err) 571 } 572 if !passed { 573 t.Error("test failed") 574 } 575 if !strings.Contains(output.String(), "[no test files]") { 576 t.Error("missing [no test files] in output") 577 } 578 }) 579 580 t.Run("BuildErr", func(t *testing.T) { 581 t.Parallel() 582 583 // Test a package which fails to build. 584 585 var wg sync.WaitGroup 586 defer wg.Wait() 587 588 out := ioLogger(t, &wg) 589 defer out.Close() 590 591 opts := targ.opts 592 passed, err := Test("github.com/tinygo-org/tinygo/tests/testing/builderr", out, out, &opts, "") 593 if err == nil { 594 t.Error("test did not error") 595 } 596 if passed { 597 t.Error("test passed") 598 } 599 }) 600 }) 601 } 602 } 603 604 func ioLogger(t *testing.T, wg *sync.WaitGroup) io.WriteCloser { 605 r, w := io.Pipe() 606 wg.Add(1) 607 go func() { 608 defer wg.Done() 609 defer r.Close() 610 611 scanner := bufio.NewScanner(r) 612 for scanner.Scan() { 613 t.Log(scanner.Text()) 614 } 615 }() 616 617 return w 618 } 619 620 func TestGetListOfPackages(t *testing.T) { 621 opts := optionsFromTarget("", sema) 622 tests := []struct { 623 pkgs []string 624 expectedPkgs []string 625 expectesError bool 626 }{ 627 { 628 pkgs: []string{"./tests/testing/recurse/..."}, 629 expectedPkgs: []string{ 630 "github.com/tinygo-org/tinygo/tests/testing/recurse", 631 "github.com/tinygo-org/tinygo/tests/testing/recurse/subdir", 632 }, 633 }, 634 { 635 pkgs: []string{"./tests/testing/pass"}, 636 expectedPkgs: []string{ 637 "github.com/tinygo-org/tinygo/tests/testing/pass", 638 }, 639 }, 640 { 641 pkgs: []string{"./tests/testing"}, 642 expectesError: true, 643 }, 644 } 645 646 for _, test := range tests { 647 actualPkgs, err := getListOfPackages(test.pkgs, &opts) 648 if err != nil && !test.expectesError { 649 t.Errorf("unexpected error: %v", err) 650 } else if err == nil && test.expectesError { 651 t.Error("expected error, but got none") 652 } 653 654 if !reflect.DeepEqual(test.expectedPkgs, actualPkgs) { 655 t.Errorf("expected two slices to be equal, expected %v got %v", test.expectedPkgs, actualPkgs) 656 } 657 } 658 } 659 660 // This TestMain is necessary because TinyGo may also be invoked to run certain 661 // LLVM tools in a separate process. Not capturing these invocations would lead 662 // to recursive tests. 663 func TestMain(m *testing.M) { 664 if len(os.Args) >= 2 { 665 switch os.Args[1] { 666 case "clang", "ld.lld", "wasm-ld": 667 // Invoke a specific tool. 668 err := builder.RunTool(os.Args[1], os.Args[2:]...) 669 if err != nil { 670 fmt.Fprintln(os.Stderr, err) 671 os.Exit(1) 672 } 673 os.Exit(0) 674 } 675 } 676 677 // Run normal tests. 678 os.Exit(m.Run()) 679 }