
This change updates the test runner to use exec.CommandContext for timeout handling. The timeout has been raised to 1 minute to handle slow machines and (hopefully) Windows. The test run also now acquires the semaphore to reserve CPU time for the test and (hopefully?????) reduce the number of timeouts in Windows CI.
622 строки
16 КиБ
Go
622 строки
16 КиБ
Go
package main
|
|
|
|
// This file tests the compiler by running Go files in testdata/*.go and
|
|
// comparing their output with the expected output in testdata/*.txt.
|
|
|
|
import (
|
|
"bufio"
|
|
"bytes"
|
|
"context"
|
|
"errors"
|
|
"flag"
|
|
"fmt"
|
|
"io"
|
|
"io/ioutil"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"runtime"
|
|
"strings"
|
|
"sync"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/tinygo-org/tinygo/builder"
|
|
"github.com/tinygo-org/tinygo/compileopts"
|
|
"github.com/tinygo-org/tinygo/goenv"
|
|
)
|
|
|
|
const TESTDATA = "testdata"
|
|
|
|
var testTarget = flag.String("target", "", "override test target")
|
|
|
|
var sema = make(chan struct{}, runtime.NumCPU())
|
|
|
|
func TestBuild(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
tests := []string{
|
|
"alias.go",
|
|
"atomic.go",
|
|
"binop.go",
|
|
"calls.go",
|
|
"cgo/",
|
|
"channel.go",
|
|
"float.go",
|
|
"gc.go",
|
|
"goroutines.go",
|
|
"init.go",
|
|
"init_multi.go",
|
|
"interface.go",
|
|
"json.go",
|
|
"map.go",
|
|
"math.go",
|
|
"print.go",
|
|
"reflect.go",
|
|
"slice.go",
|
|
"sort.go",
|
|
"stdlib.go",
|
|
"string.go",
|
|
"structs.go",
|
|
"testing.go",
|
|
"zeroalloc.go",
|
|
}
|
|
_, minor, err := goenv.GetGorootVersion(goenv.Get("GOROOT"))
|
|
if err != nil {
|
|
t.Fatal("could not read version from GOROOT:", err)
|
|
}
|
|
if minor >= 17 {
|
|
tests = append(tests, "go1.17.go")
|
|
}
|
|
|
|
if *testTarget != "" {
|
|
// This makes it possible to run one specific test (instead of all),
|
|
// which is especially useful to quickly check whether some changes
|
|
// affect a particular target architecture.
|
|
runPlatTests(optionsFromTarget(*testTarget, sema), tests, t)
|
|
return
|
|
}
|
|
|
|
t.Run("Host", func(t *testing.T) {
|
|
t.Parallel()
|
|
runPlatTests(optionsFromTarget("", sema), tests, t)
|
|
})
|
|
|
|
// Test a few build options.
|
|
t.Run("build-options", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
// Test with few optimizations enabled (no inlining, etc).
|
|
t.Run("opt=1", func(t *testing.T) {
|
|
t.Parallel()
|
|
opts := optionsFromTarget("", sema)
|
|
opts.Opt = "1"
|
|
runTestWithConfig("stdlib.go", t, opts, nil, nil)
|
|
})
|
|
|
|
// Test with only the bare minimum of optimizations enabled.
|
|
// TODO: fix this for stdlib.go, which currently fails.
|
|
t.Run("opt=0", func(t *testing.T) {
|
|
t.Parallel()
|
|
opts := optionsFromTarget("", sema)
|
|
opts.Opt = "0"
|
|
runTestWithConfig("print.go", t, opts, nil, nil)
|
|
})
|
|
|
|
t.Run("ldflags", func(t *testing.T) {
|
|
t.Parallel()
|
|
opts := optionsFromTarget("", sema)
|
|
opts.GlobalValues = map[string]map[string]string{
|
|
"main": {
|
|
"someGlobal": "foobar",
|
|
},
|
|
}
|
|
runTestWithConfig("ldflags.go", t, opts, nil, nil)
|
|
})
|
|
})
|
|
|
|
if testing.Short() {
|
|
// Don't test other targets when the -short flag is used. Only test the
|
|
// host system.
|
|
return
|
|
}
|
|
|
|
t.Run("EmulatedCortexM3", func(t *testing.T) {
|
|
t.Parallel()
|
|
runPlatTests(optionsFromTarget("cortex-m-qemu", sema), tests, t)
|
|
})
|
|
|
|
t.Run("EmulatedRISCV", func(t *testing.T) {
|
|
t.Parallel()
|
|
runPlatTests(optionsFromTarget("riscv-qemu", sema), tests, t)
|
|
})
|
|
|
|
t.Run("AVR", func(t *testing.T) {
|
|
// LLVM backend crash:
|
|
// LIBCLANG FATAL ERROR: Cannot select: t3: i16 = JumpTable<0>
|
|
// This bug is non-deterministic, and only happens when run concurrently with non-AVR tests.
|
|
// For this reason, we do not t.Parallel() here.
|
|
|
|
var avrTests []string
|
|
for _, t := range tests {
|
|
switch t {
|
|
case "atomic.go":
|
|
// Requires GCC 11.2.0 or above for interface comparison.
|
|
// https://github.com/gcc-mirror/gcc/commit/f30dd607669212de135dec1f1d8a93b8954c327c
|
|
|
|
case "reflect.go":
|
|
// Reflect tests do not work due to type code issues.
|
|
|
|
case "gc.go":
|
|
// Does not pass due to high mark false positive rate.
|
|
|
|
case "json.go", "stdlib.go", "testing.go":
|
|
// Breaks interp.
|
|
|
|
case "map.go":
|
|
// Reflect size calculation crashes.
|
|
|
|
case "binop.go":
|
|
// Interface comparison results are inverted.
|
|
|
|
case "channel.go":
|
|
// Freezes after recv from closed channel.
|
|
|
|
case "float.go", "math.go", "print.go":
|
|
// Stuck in runtime.printfloat64.
|
|
|
|
case "interface.go":
|
|
// Several comparison tests fail.
|
|
|
|
case "cgo/":
|
|
// CGo does not work on AVR.
|
|
|
|
default:
|
|
avrTests = append(avrTests, t)
|
|
}
|
|
}
|
|
runPlatTests(optionsFromTarget("simavr", sema), avrTests, t)
|
|
})
|
|
|
|
if runtime.GOOS == "linux" {
|
|
t.Run("X86Linux", func(t *testing.T) {
|
|
t.Parallel()
|
|
runPlatTests(optionsFromOSARCH("linux/386", sema), tests, t)
|
|
})
|
|
t.Run("ARMLinux", func(t *testing.T) {
|
|
t.Parallel()
|
|
runPlatTests(optionsFromOSARCH("linux/arm/6", sema), tests, t)
|
|
})
|
|
t.Run("ARM64Linux", func(t *testing.T) {
|
|
t.Parallel()
|
|
runPlatTests(optionsFromOSARCH("linux/arm64", sema), tests, t)
|
|
})
|
|
t.Run("WebAssembly", func(t *testing.T) {
|
|
t.Parallel()
|
|
runPlatTests(optionsFromTarget("wasm", sema), tests, t)
|
|
})
|
|
t.Run("WASI", func(t *testing.T) {
|
|
t.Parallel()
|
|
runPlatTests(optionsFromTarget("wasi", sema), tests, t)
|
|
})
|
|
}
|
|
}
|
|
|
|
func runPlatTests(options compileopts.Options, tests []string, t *testing.T) {
|
|
emuCheck(t, options)
|
|
|
|
spec, err := compileopts.LoadTarget(&options)
|
|
if err != nil {
|
|
t.Fatal("failed to load target spec:", err)
|
|
}
|
|
|
|
for _, name := range tests {
|
|
name := name // redefine to avoid race condition
|
|
t.Run(name, func(t *testing.T) {
|
|
t.Parallel()
|
|
runTest(name, options, t, nil, nil)
|
|
})
|
|
}
|
|
if len(spec.Emulator) == 0 || spec.Emulator[0] != "simavr" {
|
|
t.Run("env.go", func(t *testing.T) {
|
|
t.Parallel()
|
|
runTest("env.go", options, t, []string{"first", "second"}, []string{"ENV1=VALUE1", "ENV2=VALUE2"})
|
|
})
|
|
}
|
|
if options.Target == "wasi" || options.Target == "wasm" {
|
|
t.Run("alias.go-scheduler-none", func(t *testing.T) {
|
|
t.Parallel()
|
|
options := compileopts.Options(options)
|
|
options.Scheduler = "none"
|
|
runTest("alias.go", options, t, nil, nil)
|
|
})
|
|
}
|
|
if options.Target == "" || options.Target == "wasi" {
|
|
t.Run("filesystem.go", func(t *testing.T) {
|
|
t.Parallel()
|
|
runTest("filesystem.go", options, t, nil, nil)
|
|
})
|
|
}
|
|
if options.Target == "" || options.Target == "wasi" || options.Target == "wasm" {
|
|
t.Run("rand.go", func(t *testing.T) {
|
|
t.Parallel()
|
|
runTest("rand.go", options, t, nil, nil)
|
|
})
|
|
}
|
|
}
|
|
|
|
func emuCheck(t *testing.T, options compileopts.Options) {
|
|
// Check if the emulator is installed.
|
|
spec, err := compileopts.LoadTarget(&options)
|
|
if err != nil {
|
|
t.Fatal("failed to load target spec:", err)
|
|
}
|
|
if len(spec.Emulator) != 0 {
|
|
_, err := exec.LookPath(spec.Emulator[0])
|
|
if err != nil {
|
|
if errors.Is(err, exec.ErrNotFound) {
|
|
t.Skipf("emulator not installed: %q", spec.Emulator[0])
|
|
}
|
|
|
|
t.Errorf("searching for emulator: %v", err)
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
func optionsFromTarget(target string, sema chan struct{}) compileopts.Options {
|
|
return compileopts.Options{
|
|
// GOOS/GOARCH are only used if target == ""
|
|
GOOS: goenv.Get("GOOS"),
|
|
GOARCH: goenv.Get("GOARCH"),
|
|
GOARM: goenv.Get("GOARM"),
|
|
Target: target,
|
|
Semaphore: sema,
|
|
Debug: true,
|
|
VerifyIR: true,
|
|
Opt: "z",
|
|
}
|
|
}
|
|
|
|
// optionsFromOSARCH returns a set of options based on the "osarch" string. This
|
|
// string is in the form of "os/arch/subarch", with the subarch only sometimes
|
|
// being necessary. Examples are "darwin/amd64" or "linux/arm/7".
|
|
func optionsFromOSARCH(osarch string, sema chan struct{}) compileopts.Options {
|
|
parts := strings.Split(osarch, "/")
|
|
options := compileopts.Options{
|
|
GOOS: parts[0],
|
|
GOARCH: parts[1],
|
|
Semaphore: sema,
|
|
Debug: true,
|
|
VerifyIR: true,
|
|
Opt: "z",
|
|
}
|
|
if options.GOARCH == "arm" {
|
|
options.GOARM = parts[2]
|
|
}
|
|
return options
|
|
}
|
|
|
|
func runTest(name string, options compileopts.Options, t *testing.T, cmdArgs, environmentVars []string) {
|
|
runTestWithConfig(name, t, options, cmdArgs, environmentVars)
|
|
}
|
|
|
|
func runTestWithConfig(name string, t *testing.T, options compileopts.Options, cmdArgs, environmentVars []string) {
|
|
// Get the expected output for this test.
|
|
// Note: not using filepath.Join as it strips the path separator at the end
|
|
// of the path.
|
|
path := TESTDATA + "/" + name
|
|
// Get the expected output for this test.
|
|
txtpath := path[:len(path)-3] + ".txt"
|
|
if path[len(path)-1] == '/' {
|
|
txtpath = path + "out.txt"
|
|
}
|
|
expected, err := ioutil.ReadFile(txtpath)
|
|
if err != nil {
|
|
t.Fatal("could not read expected output file:", err)
|
|
}
|
|
|
|
// Create a temporary directory for test output files.
|
|
tmpdir := t.TempDir()
|
|
|
|
// Determine whether we're on a system that supports environment variables
|
|
// and command line parameters (operating systems, WASI) or not (baremetal,
|
|
// WebAssembly in the browser). If we're on a system without an environment,
|
|
// we need to pass command line arguments and environment variables through
|
|
// global variables (built into the binary directly) instead of the
|
|
// conventional way.
|
|
spec, err := compileopts.LoadTarget(&options)
|
|
if err != nil {
|
|
t.Fatal("failed to load target spec:", err)
|
|
}
|
|
needsEnvInVars := spec.GOOS == "js"
|
|
for _, tag := range spec.BuildTags {
|
|
if tag == "baremetal" {
|
|
needsEnvInVars = true
|
|
}
|
|
}
|
|
if needsEnvInVars {
|
|
runtimeGlobals := make(map[string]string)
|
|
if len(cmdArgs) != 0 {
|
|
runtimeGlobals["osArgs"] = strings.Join(cmdArgs, "\x00")
|
|
}
|
|
if len(environmentVars) != 0 {
|
|
runtimeGlobals["osEnv"] = strings.Join(environmentVars, "\x00")
|
|
}
|
|
if len(runtimeGlobals) != 0 {
|
|
// This sets the global variables like they would be set with
|
|
// `-ldflags="-X=runtime.osArgs=first\x00second`.
|
|
// The runtime package has two variables (osArgs and osEnv) that are
|
|
// both strings, from which the parameters and environment variables
|
|
// are read.
|
|
options.GlobalValues = map[string]map[string]string{
|
|
"runtime": runtimeGlobals,
|
|
}
|
|
}
|
|
}
|
|
|
|
// Build the test binary.
|
|
binary := filepath.Join(tmpdir, "test")
|
|
if spec.GOOS == "windows" {
|
|
binary += ".exe"
|
|
}
|
|
err = Build("./"+path, binary, &options)
|
|
if err != nil {
|
|
printCompilerError(t.Log, err)
|
|
t.Fail()
|
|
return
|
|
}
|
|
|
|
// Reserve CPU time for the test to run.
|
|
// This attempts to ensure that the test is not CPU-starved.
|
|
options.Semaphore <- struct{}{}
|
|
defer func() { <-options.Semaphore }()
|
|
|
|
// Create the test command, taking care of emulators etc.
|
|
ctx, cancel := context.WithTimeout(context.Background(), time.Minute)
|
|
defer cancel()
|
|
var cmd *exec.Cmd
|
|
if len(spec.Emulator) == 0 {
|
|
cmd = exec.CommandContext(ctx, binary)
|
|
} else {
|
|
args := append(spec.Emulator[1:], binary)
|
|
cmd = exec.CommandContext(ctx, spec.Emulator[0], args...)
|
|
}
|
|
if len(spec.Emulator) != 0 && spec.Emulator[0] == "wasmtime" {
|
|
// Allow reading from the current directory.
|
|
cmd.Args = append(cmd.Args, "--dir=.")
|
|
for _, v := range environmentVars {
|
|
cmd.Args = append(cmd.Args, "--env", v)
|
|
}
|
|
cmd.Args = append(cmd.Args, cmdArgs...)
|
|
} else {
|
|
if !needsEnvInVars {
|
|
cmd.Args = append(cmd.Args, cmdArgs...) // works on qemu-aarch64 etc
|
|
cmd.Env = append(cmd.Env, environmentVars...)
|
|
}
|
|
}
|
|
|
|
// Run the test.
|
|
stdout := &bytes.Buffer{}
|
|
if len(spec.Emulator) != 0 && spec.Emulator[0] == "simavr" {
|
|
cmd.Stdout = os.Stderr
|
|
cmd.Stderr = stdout
|
|
} else {
|
|
cmd.Stdout = stdout
|
|
cmd.Stderr = os.Stderr
|
|
}
|
|
err = cmd.Start()
|
|
if err != nil {
|
|
t.Fatal("failed to start:", err)
|
|
}
|
|
err = cmd.Wait()
|
|
|
|
if cerr := ctx.Err(); cerr == context.DeadlineExceeded {
|
|
stdout.WriteString("--- test ran too long, terminating...\n")
|
|
err = cerr
|
|
}
|
|
|
|
// putchar() prints CRLF, convert it to LF.
|
|
actual := bytes.Replace(stdout.Bytes(), []byte{'\r', '\n'}, []byte{'\n'}, -1)
|
|
expected = bytes.Replace(expected, []byte{'\r', '\n'}, []byte{'\n'}, -1) // for Windows
|
|
|
|
if len(spec.Emulator) != 0 && spec.Emulator[0] == "simavr" {
|
|
// Strip simavr log formatting.
|
|
actual = bytes.Replace(actual, []byte{0x1b, '[', '3', '2', 'm'}, nil, -1)
|
|
actual = bytes.Replace(actual, []byte{0x1b, '[', '0', 'm'}, nil, -1)
|
|
actual = bytes.Replace(actual, []byte{'.', '.', '\n'}, []byte{'\n'}, -1)
|
|
}
|
|
|
|
// Check whether the command ran successfully.
|
|
fail := false
|
|
if err != nil {
|
|
t.Log("failed to run:", err)
|
|
fail = true
|
|
} else if !bytes.Equal(expected, actual) {
|
|
t.Logf("output did not match (expected %d bytes, got %d bytes):", len(expected), len(actual))
|
|
fail = true
|
|
}
|
|
|
|
if fail {
|
|
r := bufio.NewReader(bytes.NewReader(actual))
|
|
for {
|
|
line, err := r.ReadString('\n')
|
|
if err != nil {
|
|
break
|
|
}
|
|
t.Log("stdout:", line[:len(line)-1])
|
|
}
|
|
t.Fail()
|
|
}
|
|
}
|
|
|
|
func TestTest(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
type targ struct {
|
|
name string
|
|
opts compileopts.Options
|
|
}
|
|
targs := []targ{
|
|
// Host
|
|
{"Host", optionsFromTarget("", sema)},
|
|
}
|
|
if !testing.Short() {
|
|
if runtime.GOOS == "linux" {
|
|
targs = append(targs,
|
|
// Linux
|
|
targ{"X86Linux", optionsFromOSARCH("linux/386", sema)},
|
|
targ{"ARMLinux", optionsFromOSARCH("linux/arm/6", sema)},
|
|
targ{"ARM64Linux", optionsFromOSARCH("linux/arm64", sema)},
|
|
)
|
|
}
|
|
|
|
targs = append(targs,
|
|
// QEMU microcontrollers
|
|
targ{"EmulatedCortexM3", optionsFromTarget("cortex-m-qemu", sema)},
|
|
targ{"EmulatedRISCV", optionsFromTarget("riscv-qemu", sema)},
|
|
|
|
// Node/Wasmtime
|
|
targ{"WASM", optionsFromTarget("wasm", sema)},
|
|
targ{"WASI", optionsFromTarget("wasi", sema)},
|
|
)
|
|
}
|
|
for _, targ := range targs {
|
|
targ := targ
|
|
t.Run(targ.name, func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
emuCheck(t, targ.opts)
|
|
|
|
t.Run("Pass", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
// Test a package which builds and passes normally.
|
|
|
|
var wg sync.WaitGroup
|
|
defer wg.Wait()
|
|
|
|
out := ioLogger(t, &wg)
|
|
defer out.Close()
|
|
|
|
opts := targ.opts
|
|
passed, err := Test("github.com/tinygo-org/tinygo/tests/testing/pass", out, out, &opts, false, false, false, "", "", "", "")
|
|
if err != nil {
|
|
t.Errorf("test error: %v", err)
|
|
}
|
|
if !passed {
|
|
t.Error("test failed")
|
|
}
|
|
})
|
|
|
|
t.Run("Fail", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
// Test a package which builds fine but fails.
|
|
|
|
var wg sync.WaitGroup
|
|
defer wg.Wait()
|
|
|
|
out := ioLogger(t, &wg)
|
|
defer out.Close()
|
|
|
|
opts := targ.opts
|
|
passed, err := Test("github.com/tinygo-org/tinygo/tests/testing/fail", out, out, &opts, false, false, false, "", "", "", "")
|
|
if err != nil {
|
|
t.Errorf("test error: %v", err)
|
|
}
|
|
if passed {
|
|
t.Error("test passed")
|
|
}
|
|
})
|
|
|
|
if targ.name != "Host" {
|
|
// Emulated tests are somewhat slow, and these do not need to be run across every platform.
|
|
return
|
|
}
|
|
|
|
t.Run("Nothing", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
// Test a package with no test files.
|
|
|
|
var wg sync.WaitGroup
|
|
defer wg.Wait()
|
|
|
|
out := ioLogger(t, &wg)
|
|
defer out.Close()
|
|
|
|
var output bytes.Buffer
|
|
opts := targ.opts
|
|
passed, err := Test("github.com/tinygo-org/tinygo/tests/testing/nothing", io.MultiWriter(&output, out), out, &opts, false, false, false, "", "", "", "")
|
|
if err != nil {
|
|
t.Errorf("test error: %v", err)
|
|
}
|
|
if !passed {
|
|
t.Error("test failed")
|
|
}
|
|
if !strings.Contains(output.String(), "[no test files]") {
|
|
t.Error("missing [no test files] in output")
|
|
}
|
|
})
|
|
|
|
t.Run("BuildErr", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
// Test a package which fails to build.
|
|
|
|
var wg sync.WaitGroup
|
|
defer wg.Wait()
|
|
|
|
out := ioLogger(t, &wg)
|
|
defer out.Close()
|
|
|
|
opts := targ.opts
|
|
passed, err := Test("github.com/tinygo-org/tinygo/tests/testing/builderr", out, out, &opts, false, false, false, "", "", "", "")
|
|
if err == nil {
|
|
t.Error("test did not error")
|
|
}
|
|
if passed {
|
|
t.Error("test passed")
|
|
}
|
|
})
|
|
})
|
|
}
|
|
}
|
|
|
|
func ioLogger(t *testing.T, wg *sync.WaitGroup) io.WriteCloser {
|
|
r, w := io.Pipe()
|
|
wg.Add(1)
|
|
go func() {
|
|
defer wg.Done()
|
|
defer r.Close()
|
|
|
|
scanner := bufio.NewScanner(r)
|
|
for scanner.Scan() {
|
|
t.Log(scanner.Text())
|
|
}
|
|
}()
|
|
|
|
return w
|
|
}
|
|
|
|
// This TestMain is necessary because TinyGo may also be invoked to run certain
|
|
// LLVM tools in a separate process. Not capturing these invocations would lead
|
|
// to recursive tests.
|
|
func TestMain(m *testing.M) {
|
|
if len(os.Args) >= 2 {
|
|
switch os.Args[1] {
|
|
case "clang", "ld.lld", "wasm-ld":
|
|
// Invoke a specific tool.
|
|
err := builder.RunTool(os.Args[1], os.Args[2:]...)
|
|
if err != nil {
|
|
fmt.Fprintln(os.Stderr, err)
|
|
os.Exit(1)
|
|
}
|
|
os.Exit(0)
|
|
}
|
|
}
|
|
|
|
// Run normal tests.
|
|
os.Exit(m.Run())
|
|
}
|