mirror of
https://github.com/tinygo-org/tinygo.git
synced 2026-09-11 06:59:31 +00:00
02021b5853
C malloc storage has explicit lifetime: it must remain allocated until free even when no GC-visible pointer references it. Treating it as an ordinary NoPtrs allocation breaks bare-metal C object graphs, while conservatively scanning arbitrary C bytes creates false Go roots. Add allocManual/freeManual so collectors can represent pointer-free, explicitly managed storage. Block GC keeps these objects permanently marked and releases their blocks on free; Boehm uses atomic uncollectable allocations; leaking and custom collectors provide equivalent behavior. Wasm keeps its allocation map only for validation and sizes, and WASIp2 realloc now copies min(oldSize, newSize). Bump the Boehm library cache version because enabling atomic uncollectable allocations changes its compiled flags and exported API. Also handle zero-size and overflowing allocations, serialize allocation registries, reject Go finalizers on manual storage, and add CGo regressions for C pointer graphs, hidden until-free allocations, repeated free/reuse, and allocation edge cases.
1370 lines
38 KiB
Go
1370 lines
38 KiB
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"
|
|
"io"
|
|
"os"
|
|
"os/exec"
|
|
"reflect"
|
|
"regexp"
|
|
"runtime"
|
|
"strings"
|
|
"sync"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/aykevl/go-wasm"
|
|
"github.com/tetratelabs/wazero"
|
|
"github.com/tetratelabs/wazero/api"
|
|
"github.com/tetratelabs/wazero/imports/wasi_snapshot_preview1"
|
|
"github.com/tetratelabs/wazero/sys"
|
|
"github.com/tinygo-org/tinygo/builder"
|
|
"github.com/tinygo-org/tinygo/compileopts"
|
|
"github.com/tinygo-org/tinygo/diagnostics"
|
|
"github.com/tinygo-org/tinygo/goenv"
|
|
)
|
|
|
|
const TESTDATA = "testdata"
|
|
|
|
var testTarget = flag.String("target", "", "override test target")
|
|
|
|
var testOnlyCurrentOS = flag.Bool("only-current-os", false, "")
|
|
|
|
var supportedLinuxArches = map[string]string{
|
|
"AMD64Linux": "linux/amd64",
|
|
"X86Linux": "linux/386",
|
|
"ARMLinux": "linux/arm/6",
|
|
"ARM64Linux": "linux/arm64",
|
|
"MIPSLinux": "linux/mips/hardfloat",
|
|
"WASIp1": "wasip1/wasm",
|
|
}
|
|
|
|
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",
|
|
"embed/",
|
|
"finalizer.go",
|
|
"finalizerbits.go",
|
|
"finalizeridle.go",
|
|
"finalizerinvariants.go",
|
|
"finalizerlarge.go",
|
|
"float.go",
|
|
"gc.go",
|
|
"generics.go",
|
|
"goroutines.go",
|
|
"init.go",
|
|
"init_multi.go",
|
|
"interface.go",
|
|
"json.go",
|
|
"localtypes/",
|
|
"localtypes.go",
|
|
"map.go",
|
|
"map_bigkey.go",
|
|
"math.go",
|
|
"oldgo/",
|
|
"print.go",
|
|
"reflect.go",
|
|
"signal.go",
|
|
"slice.go",
|
|
"sort.go",
|
|
"stdlib.go",
|
|
"string.go",
|
|
"structs.go",
|
|
"testing.go",
|
|
"timers.go",
|
|
"zeroalloc.go",
|
|
}
|
|
|
|
// Go 1.21 made some changes to the language, which we can only test when
|
|
// we're actually on Go 1.21.
|
|
_, minor, err := goenv.GetGorootVersion()
|
|
if err != nil {
|
|
t.Fatal("could not get version:", minor)
|
|
}
|
|
if minor >= 21 {
|
|
tests = append(tests, "go1.21.go")
|
|
}
|
|
if minor >= 22 {
|
|
tests = append(tests, "go1.22/")
|
|
}
|
|
if minor >= 23 {
|
|
tests = append(tests, "go1.23/")
|
|
}
|
|
if minor >= 24 {
|
|
tests = append(tests, "typealias.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.
|
|
options := optionsFromTarget(*testTarget, sema)
|
|
runPlatTests(options, tests, t)
|
|
if *testTarget == "wasip1" {
|
|
t.Run("cgo-realloc", func(t *testing.T) {
|
|
runTest("cgo-realloc/", options, t, nil, nil)
|
|
})
|
|
}
|
|
return
|
|
}
|
|
|
|
t.Run("Host", func(t *testing.T) {
|
|
t.Parallel()
|
|
hostOptions := optionsFromTarget("", sema)
|
|
runPlatTests(hostOptions, tests, t)
|
|
|
|
// scheduler.threads needs threadID, which exists only on Linux and Darwin.
|
|
// scheduler.none does not link on Windows.
|
|
switch runtime.GOOS {
|
|
case "darwin", "linux":
|
|
for _, scheduler := range []string{"threads", "none"} {
|
|
scheduler := scheduler
|
|
t.Run("finalizerinvariants.go-gc-conservative-scheduler-"+scheduler, func(t *testing.T) {
|
|
t.Parallel()
|
|
options := compileopts.Options(hostOptions)
|
|
options.GC = "conservative"
|
|
options.Scheduler = scheduler
|
|
options.Tags = append(append([]string(nil), hostOptions.Tags...), "runtime_asserts")
|
|
runTest("finalizerinvariants.go", options, t, nil, nil)
|
|
})
|
|
}
|
|
}
|
|
})
|
|
|
|
// 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)
|
|
})
|
|
|
|
// Same as ldflags, but the global has a default value in source.
|
|
// -ldflags -X must override it, matching standard Go behaviour.
|
|
t.Run("ldflags-initialized", func(t *testing.T) {
|
|
t.Parallel()
|
|
opts := optionsFromTarget("", sema)
|
|
opts.GlobalValues = map[string]map[string]string{
|
|
"main": {
|
|
"someGlobal": "foobar",
|
|
},
|
|
}
|
|
runTestWithConfig("ldflags-initialized.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
|
|
}
|
|
|
|
if !*testOnlyCurrentOS {
|
|
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) {
|
|
t.Parallel()
|
|
runPlatTests(optionsFromTarget("simavr", sema), tests, t)
|
|
})
|
|
|
|
t.Run("WebAssembly", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
runPlatTests(optionsFromTarget("wasm", sema), tests, t)
|
|
// Test with -gc=boehm.
|
|
t.Run("gc.go-boehm", func(t *testing.T) {
|
|
t.Parallel()
|
|
optionsBoehm := optionsFromTarget("wasm", sema)
|
|
optionsBoehm.GC = "boehm"
|
|
runTest("gc.go", optionsBoehm, t, nil, nil)
|
|
})
|
|
})
|
|
t.Run("WASIp1", func(t *testing.T) {
|
|
t.Parallel()
|
|
options := optionsFromTarget("wasip1", sema)
|
|
runPlatTests(options, tests, t)
|
|
t.Run("cgo-realloc", func(t *testing.T) {
|
|
runTest("cgo-realloc/", options, t, nil, nil)
|
|
})
|
|
|
|
// Test with -gc=boehm.
|
|
t.Run("gc.go-boehm", func(t *testing.T) {
|
|
t.Parallel()
|
|
optionsBoehm := optionsFromTarget("wasip1", sema)
|
|
optionsBoehm.GC = "boehm"
|
|
runTest("gc.go", optionsBoehm, t, nil, nil)
|
|
})
|
|
})
|
|
t.Run("WASIp2", func(t *testing.T) {
|
|
t.Parallel()
|
|
runPlatTests(optionsFromTarget("wasip2", sema), tests, t)
|
|
})
|
|
}
|
|
|
|
if runtime.GOOS == "linux" {
|
|
for name, osArch := range supportedLinuxArches {
|
|
options := optionsFromOSARCH(osArch, sema)
|
|
if options.GOARCH != runtime.GOARCH { // Native architecture already run above.
|
|
t.Run(name, func(t *testing.T) {
|
|
runPlatTests(options, tests, t)
|
|
})
|
|
}
|
|
}
|
|
t.Run("MIPS little-endian", func(t *testing.T) {
|
|
// Run a single test for GOARCH=mipsle to see whether it works at
|
|
// all. It is already mostly tested because GOARCH=mips and
|
|
// GOARCH=mipsle are so similar, but it's good to have an extra test
|
|
// to be sure.
|
|
t.Parallel()
|
|
options := optionsFromOSARCH("linux/mipsle/softfloat", sema)
|
|
emuCheck(t, options)
|
|
runTest("cgo/", options, t, nil, nil)
|
|
})
|
|
} else if runtime.GOOS == "windows" {
|
|
if runtime.GOARCH != "386" {
|
|
t.Run("Windows386", func(t *testing.T) {
|
|
t.Parallel()
|
|
runPlatTests(optionsFromOSARCH("windows/386", sema), tests, t)
|
|
})
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestTimerStopResetRace checks that stopping or resetting a timer while its
|
|
// callback is running behaves correctly. It reaches into the runtime timer
|
|
// hooks via //go:linkname and relies on the threads scheduler, which is only
|
|
// used on these hosts.
|
|
func TestTimerStopResetRace(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
switch runtime.GOOS {
|
|
case "darwin", "linux":
|
|
default:
|
|
t.Skipf("host GOOS %s does not use the threads scheduler", runtime.GOOS)
|
|
}
|
|
runTest("timer_stop_reset_race.go", optionsFromTarget("", sema), t, nil, nil)
|
|
}
|
|
|
|
func TestESP32QEMU(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
options := optionsFromTarget("esp32-qemu", sema)
|
|
emuCheck(t, options)
|
|
machines, err := exec.Command("qemu-system-xtensa", "-machine", "help").Output()
|
|
if err != nil {
|
|
t.Fatal("failed to list qemu-system-xtensa machines:", err)
|
|
}
|
|
if !regexp.MustCompile(`(?m)^esp32\s`).Match(machines) {
|
|
t.Skip("qemu-system-xtensa does not support the ESP32 machine")
|
|
}
|
|
for _, name := range []string{"print.go", "recover.go"} {
|
|
t.Run(name, func(t *testing.T) {
|
|
t.Parallel()
|
|
runTest(name, options, t, nil, nil)
|
|
})
|
|
}
|
|
}
|
|
|
|
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)
|
|
}
|
|
|
|
// FIXME: this should really be:
|
|
// isWebAssembly := strings.HasPrefix(spec.Triple, "wasm")
|
|
isWASI := strings.HasPrefix(options.Target, "wasi")
|
|
isWebAssembly := isWASI || strings.HasPrefix(options.Target, "wasm") || (options.Target == "" && strings.HasPrefix(options.GOARCH, "wasm"))
|
|
isBaremetal := options.Target == "simavr" || options.Target == "cortex-m-qemu" || options.Target == "riscv-qemu"
|
|
_, goMinor, err := goenv.GetGorootVersion()
|
|
if err != nil {
|
|
t.Fatal("could not get version:", goMinor)
|
|
}
|
|
|
|
for _, name := range tests {
|
|
if name == "goroutines.go" && (spec.Scheduler == "threads" || spec.Scheduler == "cores") {
|
|
// This test intentionally checks concurrent scheduling by comparing
|
|
// output order, so only run it with non-threaded schedulers.
|
|
continue
|
|
}
|
|
if options.GOOS == "linux" && (options.GOARCH == "arm" || options.GOARCH == "386") {
|
|
switch name {
|
|
case "timers.go":
|
|
// Timer tests do not work because syscall.seek is implemented
|
|
// as Assembly in mainline Go and causes linker failure
|
|
continue
|
|
}
|
|
}
|
|
if options.GOOS == "linux" && (options.GOARCH == "mips" || options.GOARCH == "mipsle") {
|
|
if name == "atomic.go" || name == "timers.go" {
|
|
// 64-bit atomic operations aren't currently supported on MIPS.
|
|
continue
|
|
}
|
|
}
|
|
if options.GOOS == "linux" && options.GOARCH == "mips" {
|
|
if name == "cgo/" {
|
|
// CGo isn't supported yet on big-endian systems (needs updates
|
|
// to bitfield access methods).
|
|
continue
|
|
}
|
|
}
|
|
if options.Target == "cortex-m-qemu" && goMinor >= 27 && name == "json.go" {
|
|
// Go 1.27 jsonv2 exceeds the LM3S6965's 256KiB flash. json.go
|
|
// is still covered by larger targets such as riscv-qemu.
|
|
continue
|
|
}
|
|
if options.Target == "simavr" {
|
|
// Not all tests are currently supported on AVR.
|
|
// Skip the ones that aren't.
|
|
switch name {
|
|
case "reflect.go":
|
|
// Reflect tests do not run correctly, probably because of the
|
|
// limited amount of memory.
|
|
continue
|
|
|
|
case "map_bigkey.go":
|
|
// Compiler generates many large stack temporaries for [256]byte
|
|
// map keys, overflowing the goroutine stack (384 bytes).
|
|
continue
|
|
|
|
case "gc.go":
|
|
// Does not pass due to high mark false positive rate.
|
|
continue
|
|
|
|
case "json.go", "stdlib.go", "testing.go":
|
|
// Too big for AVR. Doesn't fit in flash/RAM.
|
|
continue
|
|
|
|
case "cgo/":
|
|
// CGo function pointers don't work on AVR (needs LLVM 16 and
|
|
// some compiler changes).
|
|
continue
|
|
|
|
case "timers.go":
|
|
// Crashes starting with Go 1.23.
|
|
// Bug: https://github.com/llvm/llvm-project/issues/104032
|
|
continue
|
|
|
|
default:
|
|
}
|
|
}
|
|
if options.Target == "wasip2" {
|
|
switch name {
|
|
case "cgo/":
|
|
// waisp2 use our own libc; cgo tests fail
|
|
continue
|
|
}
|
|
}
|
|
if isWebAssembly || isBaremetal || options.GOOS == "windows" {
|
|
switch name {
|
|
case "signal.go":
|
|
// Signals only work on POSIX-like systems.
|
|
continue
|
|
}
|
|
}
|
|
if options.Target != "wasm" {
|
|
switch name {
|
|
case "finalizer.go", "finalizerbits.go", "finalizeridle.go", "finalizerlarge.go":
|
|
// These tests require deterministic finalization on target wasm.
|
|
// finalizerinvariants.go covers other block GC targets.
|
|
continue
|
|
}
|
|
}
|
|
if options.Target == "" && options.GC == "" {
|
|
switch name {
|
|
case "finalizerinvariants.go":
|
|
// Skip the default host GC because it does not implement finalizers.
|
|
// Explicit conservative GC variants cover this test.
|
|
continue
|
|
}
|
|
}
|
|
if options.Target == "simavr" {
|
|
switch name {
|
|
case "finalizerinvariants.go":
|
|
// Skip because runtime.GC does not return. See the gc.go exclusion above.
|
|
continue
|
|
}
|
|
}
|
|
|
|
name := name // redefine to avoid race condition
|
|
t.Run(name, func(t *testing.T) {
|
|
t.Parallel()
|
|
testOptions := compileopts.Options(options)
|
|
if name == "finalizerinvariants.go" || name == "finalizerlarge.go" {
|
|
testOptions.Tags = append(append([]string(nil), options.Tags...), "runtime_asserts")
|
|
}
|
|
if testOptions.Target == "simavr" && name == "math.go" {
|
|
// This test exceeds simavr's default 384-byte goroutine stack.
|
|
testOptions.StackSize = 512
|
|
}
|
|
runTest(name, testOptions, t, nil, nil)
|
|
})
|
|
}
|
|
if !strings.HasPrefix(spec.Emulator, "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 isWebAssembly {
|
|
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 == "" || isWASI {
|
|
t.Run("filesystem.go", func(t *testing.T) {
|
|
t.Parallel()
|
|
runTest("filesystem.go", options, t, nil, nil)
|
|
})
|
|
}
|
|
if options.Target == "" || options.Target == "wasm" || isWASI {
|
|
t.Run("rand.go", func(t *testing.T) {
|
|
t.Parallel()
|
|
runTest("rand.go", options, t, nil, nil)
|
|
})
|
|
}
|
|
if !isWebAssembly {
|
|
// The recover() builtin isn't supported yet on WebAssembly and Windows.
|
|
t.Run("recover.go", func(t *testing.T) {
|
|
t.Parallel()
|
|
runTest("recover.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 spec.Emulator != "" {
|
|
emulatorCommand, _, _ := strings.Cut(spec.Emulator, " ")
|
|
_, err := exec.LookPath(emulatorCommand)
|
|
if err != nil {
|
|
if errors.Is(err, exec.ErrNotFound) {
|
|
t.Skipf("emulator not installed: %q", emulatorCommand)
|
|
}
|
|
|
|
t.Errorf("searching for emulator: %v", err)
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
func optionsFromTarget(target string, sema chan struct{}) compileopts.Options {
|
|
separators := strings.Count(target, "/")
|
|
if (separators == 1 || separators == 2) && !strings.HasSuffix(target, ".json") {
|
|
return optionsFromOSARCH(target, sema)
|
|
}
|
|
return compileopts.Options{
|
|
// GOOS/GOARCH are only used if target == ""
|
|
GOOS: goenv.Get("GOOS"),
|
|
GOARCH: goenv.Get("GOARCH"),
|
|
GOARM: goenv.Get("GOARM"),
|
|
GOMIPS: goenv.Get("GOMIPS"),
|
|
Target: target,
|
|
Semaphore: sema,
|
|
InterpTimeout: 180 * time.Second,
|
|
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,
|
|
InterpTimeout: 180 * time.Second,
|
|
Debug: true,
|
|
VerifyIR: true,
|
|
Opt: "z",
|
|
}
|
|
switch options.GOARCH {
|
|
case "arm":
|
|
options.GOARM = parts[2]
|
|
case "mips", "mipsle":
|
|
options.GOMIPS = parts[2]
|
|
}
|
|
return options
|
|
}
|
|
|
|
func runTest(name string, options compileopts.Options, t *testing.T, cmdArgs, environmentVars []string) {
|
|
t.Helper()
|
|
runTestWithConfig(name, t, options, cmdArgs, environmentVars)
|
|
}
|
|
|
|
func runTestWithConfig(name string, t *testing.T, options compileopts.Options, cmdArgs, environmentVars []string) {
|
|
t.Helper()
|
|
// 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.
|
|
expectedOutputPath := path[:len(path)-3] + ".txt"
|
|
pkgName := "./" + path
|
|
if path[len(path)-1] == '/' {
|
|
expectedOutputPath = path + "out.txt"
|
|
options.Directory = path
|
|
pkgName = "."
|
|
}
|
|
isWebAssembly := strings.HasPrefix(options.Target, "wasi") ||
|
|
strings.HasPrefix(options.Target, "wasm") ||
|
|
strings.HasPrefix(options.GOARCH, "wasm")
|
|
if name == "testing.go" && isWebAssembly {
|
|
expectedOutputPath = TESTDATA + "/testing-wasm.txt"
|
|
}
|
|
|
|
config, err := builder.NewConfig(&options)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
// Build the test binary.
|
|
stdout := &bytes.Buffer{}
|
|
_, err = buildAndRun(pkgName, config, stdout, cmdArgs, environmentVars, 2*time.Minute, func(cmd *exec.Cmd, result builder.BuildResult) error {
|
|
if config.EmulatorName() == "simavr" {
|
|
// simavr before v1.8 wrote firmware output to stderr and loader logs
|
|
// to stdout, but PR #490 swapped these streams:
|
|
// https://github.com/buserror/simavr/pull/490
|
|
cmd.Stdout = stdout
|
|
}
|
|
return cmd.Run()
|
|
})
|
|
if err != nil {
|
|
w := &bytes.Buffer{}
|
|
diagnostics.CreateDiagnostics(err).WriteTo(w, "")
|
|
for line := range strings.SplitSeq(strings.TrimRight(w.String(), "\n"), "\n") {
|
|
t.Log(line)
|
|
}
|
|
if stdout.Len() != 0 {
|
|
t.Logf("output:\n%s", stdout.String())
|
|
}
|
|
t.Fail()
|
|
return
|
|
}
|
|
|
|
actual := stdout.Bytes()
|
|
if config.EmulatorName() == "simavr" {
|
|
actual = cleanSimAVRTestOutput(actual)
|
|
}
|
|
if config.EmulatorName() == "qemu-system-xtensa" {
|
|
actual = cleanESP32QEMUOutput(actual)
|
|
}
|
|
if name == "testing.go" {
|
|
// Strip actual time.
|
|
re := regexp.MustCompile(`\([0-9]\.[0-9][0-9]s\)`)
|
|
actual = re.ReplaceAllLiteral(actual, []byte{'(', '0', '.', '0', '0', 's', ')'})
|
|
}
|
|
|
|
// Check whether the command ran successfully.
|
|
if err != nil {
|
|
t.Error("failed to run:", err)
|
|
}
|
|
checkOutput(t, expectedOutputPath, actual)
|
|
|
|
if t.Failed() {
|
|
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 cleanESP32QEMUOutput(output []byte) []byte {
|
|
entryLine := bytes.Index(output, []byte("\nentry "))
|
|
if entryLine < 0 {
|
|
return output
|
|
}
|
|
entryLineEnd := bytes.IndexByte(output[entryLine+1:], '\n')
|
|
if entryLineEnd < 0 {
|
|
return output
|
|
}
|
|
return output[entryLine+1+entryLineEnd+1:]
|
|
}
|
|
|
|
func cleanSimAVRTestOutput(output []byte) []byte {
|
|
output = bytes.ReplaceAll(output, []byte{0x1b, '[', '3', '2', 'm'}, nil)
|
|
output = bytes.ReplaceAll(output, []byte{0x1b, '[', '0', 'm'}, nil)
|
|
output = bytes.ReplaceAll(output, []byte{'.', '.', '\n'}, []byte{'\n'})
|
|
output = bytes.ReplaceAll(output, []byte{'\n', '.', '\n'}, []byte{'\n', '\n'})
|
|
|
|
var cleaned []byte
|
|
for _, line := range bytes.SplitAfter(output, []byte{'\n'}) {
|
|
trimmedLine := bytes.TrimRight(line, "\r\n")
|
|
if simavrLoadTextLogPattern.Match(trimmedLine) || simavrLoadBytesLogPattern.Match(trimmedLine) {
|
|
continue
|
|
}
|
|
cleaned = append(cleaned, line...)
|
|
}
|
|
return cleaned
|
|
}
|
|
|
|
var (
|
|
simavrLoadTextLogPattern = regexp.MustCompile(`^Loaded [0-9]+ \.[A-Za-z0-9_]+( at address 0x[0-9a-fA-F]+)?$`)
|
|
simavrLoadBytesLogPattern = regexp.MustCompile(`^Loaded [0-9]+ bytes of [A-Za-z]+ data at (0x)?[0-9a-fA-F]+$`)
|
|
)
|
|
|
|
// Test WebAssembly files for certain properties.
|
|
func TestWebAssembly(t *testing.T) {
|
|
t.Parallel()
|
|
type testCase struct {
|
|
name string
|
|
target string
|
|
panicStrategy string
|
|
imports []string
|
|
}
|
|
for _, tc := range []testCase{
|
|
// Test whether there really are no imports when using -panic=trap. This
|
|
// tests the bugfix for https://github.com/tinygo-org/tinygo/issues/4161.
|
|
{name: "panic-default", target: "wasip1", imports: []string{"wasi_snapshot_preview1.fd_write", "wasi_snapshot_preview1.random_get"}},
|
|
{name: "panic-trap", target: "wasm-unknown", panicStrategy: "trap", imports: []string{}},
|
|
} {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
t.Parallel()
|
|
tmpdir := t.TempDir()
|
|
options := optionsFromTarget(tc.target, sema)
|
|
options.PanicStrategy = tc.panicStrategy
|
|
config, err := builder.NewConfig(&options)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
result, err := builder.Build("testdata/trivialpanic.go", ".wasm", tmpdir, config)
|
|
if err != nil {
|
|
t.Fatal("failed to build binary:", err)
|
|
}
|
|
f, err := os.Open(result.Binary)
|
|
if err != nil {
|
|
t.Fatal("could not open output binary:", err)
|
|
}
|
|
defer f.Close()
|
|
module, err := wasm.Parse(f)
|
|
if err != nil {
|
|
t.Fatal("could not parse output binary:", err)
|
|
}
|
|
|
|
// Test the list of imports.
|
|
if tc.imports != nil {
|
|
var imports []string
|
|
for _, section := range module.Sections {
|
|
switch section := section.(type) {
|
|
case *wasm.SectionImport:
|
|
for _, symbol := range section.Entries {
|
|
imports = append(imports, symbol.Module+"."+symbol.Field)
|
|
}
|
|
}
|
|
}
|
|
if !stringSlicesEqual(imports, tc.imports) {
|
|
t.Errorf("import list not as expected!\nexpected: %v\nactual: %v", tc.imports, imports)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func stringSlicesEqual(s1, s2 []string) bool {
|
|
// We can use slices.Equal once we drop support for Go 1.20 (it was added in
|
|
// Go 1.21).
|
|
if len(s1) != len(s2) {
|
|
return false
|
|
}
|
|
for i, s := range s1 {
|
|
if s != s2[i] {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
func TestWasmExport(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
type testCase struct {
|
|
name string
|
|
target string
|
|
buildMode string
|
|
scheduler string
|
|
file string
|
|
noOutput bool
|
|
command bool // call _start (command mode) instead of _initialize
|
|
}
|
|
|
|
tests := []testCase{
|
|
// "command mode" WASI
|
|
{
|
|
name: "WASIp1-command",
|
|
target: "wasip1",
|
|
command: true,
|
|
},
|
|
// "reactor mode" WASI (with -buildmode=c-shared)
|
|
{
|
|
name: "WASIp1-reactor",
|
|
target: "wasip1",
|
|
buildMode: "c-shared",
|
|
},
|
|
// Make sure reactor mode also works without a scheduler.
|
|
{
|
|
name: "WASIp1-reactor-noscheduler",
|
|
target: "wasip1",
|
|
buildMode: "c-shared",
|
|
scheduler: "none",
|
|
file: "wasmexport-noscheduler.go",
|
|
},
|
|
// Test -target=wasm-unknown with the default build mode (which is
|
|
// c-shared).
|
|
{
|
|
name: "wasm-unknown-reactor",
|
|
target: "wasm-unknown",
|
|
file: "wasmexport-noscheduler.go",
|
|
noOutput: true, // wasm-unknown cannot produce output
|
|
},
|
|
// Test -target=wasm-unknown with -buildmode=default, which makes it run
|
|
// in command mode.
|
|
{
|
|
name: "wasm-unknown-command",
|
|
target: "wasm-unknown",
|
|
buildMode: "default",
|
|
file: "wasmexport-noscheduler.go",
|
|
noOutput: true, // wasm-unknown cannot produce output
|
|
command: true,
|
|
},
|
|
// Test buildmode=wasi-legacy with WASI.
|
|
{
|
|
name: "WASIp1-legacy",
|
|
target: "wasip1",
|
|
buildMode: "wasi-legacy",
|
|
scheduler: "none",
|
|
file: "wasmexport-noscheduler.go",
|
|
command: true,
|
|
},
|
|
}
|
|
|
|
for _, tc := range tests {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
// Build the wasm binary.
|
|
tmpdir := t.TempDir()
|
|
options := optionsFromTarget(tc.target, sema)
|
|
options.BuildMode = tc.buildMode
|
|
options.Scheduler = tc.scheduler
|
|
buildConfig, err := builder.NewConfig(&options)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
filename := "wasmexport.go"
|
|
if tc.file != "" {
|
|
filename = tc.file
|
|
}
|
|
result, err := builder.Build("testdata/"+filename, ".wasm", tmpdir, buildConfig)
|
|
if err != nil {
|
|
t.Fatal("failed to build binary:", err)
|
|
}
|
|
|
|
// Read the wasm binary back into memory.
|
|
data, err := os.ReadFile(result.Binary)
|
|
if err != nil {
|
|
t.Fatal("could not read wasm binary: ", err)
|
|
}
|
|
|
|
// Set up the wazero runtime.
|
|
output := &bytes.Buffer{}
|
|
ctx := context.Background()
|
|
r := wazero.NewRuntimeWithConfig(ctx, wazero.NewRuntimeConfigInterpreter())
|
|
defer r.Close(ctx)
|
|
config := wazero.NewModuleConfig().
|
|
WithStdout(output).WithStderr(output).
|
|
WithStartFunctions()
|
|
|
|
// Prepare for testing.
|
|
var mod api.Module
|
|
mustCall := func(results []uint64, err error) []uint64 {
|
|
if err != nil {
|
|
t.Error("failed to run function:", err)
|
|
}
|
|
return results
|
|
}
|
|
checkResult := func(name string, results []uint64, expected []uint64) {
|
|
if len(results) != len(expected) {
|
|
t.Errorf("%s: expected %v but got %v", name, expected, results)
|
|
}
|
|
for i, result := range results {
|
|
if result != expected[i] {
|
|
t.Errorf("%s: expected %v but got %v", name, expected, results)
|
|
break
|
|
}
|
|
}
|
|
}
|
|
runTests := func() {
|
|
// Test an exported function without params or return value.
|
|
checkResult("hello()", mustCall(mod.ExportedFunction("hello").Call(ctx)), nil)
|
|
|
|
// Test that we can call an exported function more than once.
|
|
checkResult("add(3, 5)", mustCall(mod.ExportedFunction("add").Call(ctx, 3, 5)), []uint64{8})
|
|
checkResult("add(7, 9)", mustCall(mod.ExportedFunction("add").Call(ctx, 7, 9)), []uint64{16})
|
|
checkResult("add(6, 1)", mustCall(mod.ExportedFunction("add").Call(ctx, 6, 1)), []uint64{7})
|
|
|
|
// Test that imported functions can call exported functions
|
|
// again.
|
|
checkResult("reentrantCall(2, 3)", mustCall(mod.ExportedFunction("reentrantCall").Call(ctx, 2, 3)), []uint64{5})
|
|
checkResult("reentrantCall(1, 8)", mustCall(mod.ExportedFunction("reentrantCall").Call(ctx, 1, 8)), []uint64{9})
|
|
|
|
// Check that goroutines started inside //go:wasmexport don't
|
|
// block the called function from returning.
|
|
checkResult("goroutineExit()", mustCall(mod.ExportedFunction("goroutineExit").Call(ctx)), nil)
|
|
}
|
|
|
|
// Add wasip1 module.
|
|
wasi_snapshot_preview1.MustInstantiate(ctx, r)
|
|
|
|
// Add custom "tester" module.
|
|
callOutside := func(a, b int32) int32 {
|
|
results, err := mod.ExportedFunction("add").Call(ctx, uint64(a), uint64(b))
|
|
if err != nil {
|
|
t.Error("could not call exported add function:", err)
|
|
}
|
|
return int32(results[0])
|
|
}
|
|
callTestMain := func() {
|
|
runTests()
|
|
}
|
|
builder := r.NewHostModuleBuilder("tester")
|
|
builder.NewFunctionBuilder().WithFunc(callOutside).Export("callOutside")
|
|
builder.NewFunctionBuilder().WithFunc(callTestMain).Export("callTestMain")
|
|
_, err = builder.Instantiate(ctx)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
// Parse and instantiate the wasm.
|
|
mod, err = r.InstantiateWithConfig(ctx, data, config)
|
|
if err != nil {
|
|
t.Fatal("could not instantiate wasm module:", err)
|
|
}
|
|
|
|
// Initialize the module and run the tests.
|
|
if tc.command {
|
|
// Call _start (the entry point), which calls
|
|
// tester.callTestMain, which then runs all the tests.
|
|
_, err := mod.ExportedFunction("_start").Call(ctx)
|
|
if err != nil {
|
|
if exitErr, ok := err.(*sys.ExitError); ok && exitErr.ExitCode() == 0 {
|
|
// Exited with code 0. Nothing to worry about.
|
|
} else {
|
|
t.Error("failed to run _start:", err)
|
|
}
|
|
}
|
|
} else {
|
|
// Run the _initialize call, because this is reactor mode wasm.
|
|
mustCall(mod.ExportedFunction("_initialize").Call(ctx))
|
|
runTests()
|
|
}
|
|
|
|
// Check that the output matches the expected output.
|
|
// (Skip this for wasm-unknown because it can't produce output).
|
|
if !tc.noOutput {
|
|
checkOutput(t, "testdata/wasmexport.txt", output.Bytes())
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// Test js.FuncOf (for syscall/js).
|
|
// This test might be extended in the future to cover more cases in syscall/js.
|
|
func TestWasmFuncOf(t *testing.T) {
|
|
// Build the wasm binary.
|
|
tmpdir := t.TempDir()
|
|
options := optionsFromTarget("wasm", sema)
|
|
buildConfig, err := builder.NewConfig(&options)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
result, err := builder.Build("testdata/wasmfunc.go", ".wasm", tmpdir, buildConfig)
|
|
if err != nil {
|
|
t.Fatal("failed to build binary:", err)
|
|
}
|
|
|
|
// Test the resulting binary using NodeJS.
|
|
output := &bytes.Buffer{}
|
|
cmd := exec.Command("node", "testdata/wasmfunc.js", result.Binary, buildConfig.BuildMode())
|
|
cmd.Stdout = output
|
|
cmd.Stderr = output
|
|
err = cmd.Run()
|
|
if err != nil {
|
|
t.Error("failed to run node:", err)
|
|
}
|
|
checkOutput(t, "testdata/wasmfunc.txt", output.Bytes())
|
|
}
|
|
|
|
// Test //go:wasmexport in JavaScript (using NodeJS).
|
|
func TestWasmExportJS(t *testing.T) {
|
|
t.Parallel()
|
|
type testCase struct {
|
|
name string
|
|
buildMode string
|
|
}
|
|
|
|
tests := []testCase{
|
|
{name: "default"},
|
|
{name: "c-shared", buildMode: "c-shared"},
|
|
}
|
|
for _, tc := range tests {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
t.Parallel()
|
|
// Build the wasm binary.
|
|
tmpdir := t.TempDir()
|
|
options := optionsFromTarget("wasm", sema)
|
|
options.BuildMode = tc.buildMode
|
|
buildConfig, err := builder.NewConfig(&options)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
result, err := builder.Build("testdata/wasmexport-noscheduler.go", ".wasm", tmpdir, buildConfig)
|
|
if err != nil {
|
|
t.Fatal("failed to build binary:", err)
|
|
}
|
|
|
|
// Test the resulting binary using NodeJS.
|
|
output := &bytes.Buffer{}
|
|
cmd := exec.Command("node", "testdata/wasmexport.js", result.Binary, buildConfig.BuildMode())
|
|
cmd.Stdout = output
|
|
cmd.Stderr = output
|
|
err = cmd.Run()
|
|
if err != nil {
|
|
t.Error("failed to run node:", err)
|
|
}
|
|
checkOutput(t, "testdata/wasmexport.txt", output.Bytes())
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestWasmExportFinalizersJS(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
tmpdir := t.TempDir()
|
|
options := optionsFromTarget("wasm", sema)
|
|
options.BuildMode = "c-shared"
|
|
buildConfig, err := builder.NewConfig(&options)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
result, err := builder.Build("testdata/wasmexport-finalizer.go", ".wasm", tmpdir, buildConfig)
|
|
if err != nil {
|
|
t.Fatal("failed to build binary:", err)
|
|
}
|
|
|
|
output := &bytes.Buffer{}
|
|
cmd := exec.Command("node", "testdata/wasmexport-finalizer.js", result.Binary)
|
|
cmd.Stdout = output
|
|
cmd.Stderr = output
|
|
if err := cmd.Run(); err != nil {
|
|
t.Fatalf("failed to run node: %v\n%s", err, output)
|
|
}
|
|
}
|
|
|
|
// Test whether Go.run() (in wasm_exec.js) normally returns and returns the
|
|
// right exit code.
|
|
func TestWasmExit(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
type testCase struct {
|
|
name string
|
|
output string
|
|
}
|
|
|
|
tests := []testCase{
|
|
{name: "normal", output: "exit code: 0\n"},
|
|
{name: "exit-0", output: "exit code: 0\n"},
|
|
{name: "exit-0-sleep", output: "slept\nexit code: 0\n"},
|
|
{name: "exit-1", output: "exit code: 1\n"},
|
|
{name: "exit-1-sleep", output: "slept\nexit code: 1\n"},
|
|
}
|
|
for _, tc := range tests {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
t.Parallel()
|
|
options := optionsFromTarget("wasm", sema)
|
|
buildConfig, err := builder.NewConfig(&options)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
buildConfig.Target.Emulator = "node testdata/wasmexit.js {}"
|
|
output := &bytes.Buffer{}
|
|
_, err = buildAndRun("testdata/wasmexit.go", buildConfig, output, []string{tc.name}, nil, time.Minute, func(cmd *exec.Cmd, result builder.BuildResult) error {
|
|
return cmd.Run()
|
|
})
|
|
if err != nil {
|
|
t.Error(err)
|
|
}
|
|
expected := "wasmexit test: " + tc.name + "\n" + tc.output
|
|
checkOutputData(t, []byte(expected), output.Bytes())
|
|
})
|
|
}
|
|
}
|
|
|
|
// Check whether the output of a test equals the expected output.
|
|
func checkOutput(t *testing.T, filename string, actual []byte) {
|
|
t.Helper()
|
|
expectedOutput, err := os.ReadFile(filename)
|
|
if err != nil {
|
|
t.Fatal("could not read output file:", err)
|
|
}
|
|
checkOutputData(t, expectedOutput, actual)
|
|
}
|
|
|
|
func checkOutputData(t *testing.T, expectedOutput, actual []byte) {
|
|
t.Helper()
|
|
expectedOutput = bytes.ReplaceAll(expectedOutput, []byte("\r\n"), []byte("\n"))
|
|
actual = bytes.ReplaceAll(actual, []byte("\r\n"), []byte("\n"))
|
|
|
|
if !bytes.Equal(actual, expectedOutput) {
|
|
t.Errorf("output did not match (expected %d bytes, got %d bytes):", len(expectedOutput), len(actual))
|
|
t.Error(string(Diff("expected", expectedOutput, "actual", actual)))
|
|
}
|
|
}
|
|
|
|
func TestGoexitCrash(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
options := optionsFromTarget("", sema)
|
|
config, err := builder.NewConfig(&options)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
for _, tc := range []struct {
|
|
name string
|
|
want string
|
|
}{
|
|
{"main", "fatal error: all goroutines are asleep - deadlock!"},
|
|
{"deadlock", "fatal error: all goroutines are asleep - deadlock!"},
|
|
{"exit", "fatal error: all goroutines are asleep - deadlock!"},
|
|
{"main-other", "fatal error: all goroutines are asleep - deadlock!"},
|
|
{"in-panic", "fatal error: all goroutines are asleep - deadlock!"},
|
|
{"panic", "panic: panic after Goexit"},
|
|
{"recovered-panic", "fatal error: all goroutines are asleep - deadlock!"},
|
|
{"recover-before-panic", "fatal error: all goroutines are asleep - deadlock!"},
|
|
{"recover-before-panic-loop", "fatal error: all goroutines are asleep - deadlock!"},
|
|
} {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
output := &bytes.Buffer{}
|
|
_, err = buildAndRun("testdata/goexit.go", config, output, []string{tc.name}, nil, time.Minute, func(cmd *exec.Cmd, result builder.BuildResult) error {
|
|
cmd.Stdout = nil
|
|
cmd.Stderr = nil
|
|
data, err := cmd.CombinedOutput()
|
|
output.Write(data)
|
|
return err
|
|
})
|
|
if err == nil {
|
|
t.Fatal("program unexpectedly exited successfully")
|
|
}
|
|
if !strings.Contains(output.String(), tc.want) {
|
|
t.Fatalf("output does not contain %q:\n%s", tc.want, output.String())
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestRuntimeFatal(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
options := optionsFromTarget("", sema)
|
|
config, err := builder.NewConfig(&options)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
output := &bytes.Buffer{}
|
|
_, err = buildAndRun("testdata/runtimefatal.go", config, output, nil, nil, time.Minute, func(cmd *exec.Cmd, result builder.BuildResult) error {
|
|
cmd.Stdout = nil
|
|
cmd.Stderr = nil
|
|
data, err := cmd.CombinedOutput()
|
|
output.Write(data)
|
|
return err
|
|
})
|
|
if err == nil {
|
|
t.Fatal("program unexpectedly exited successfully")
|
|
}
|
|
if want := "fatal error: sync: unlock of unlocked Mutex"; !strings.Contains(output.String(), want) {
|
|
t.Fatalf("output does not contain %q:\n%s", want, output.String())
|
|
}
|
|
if strings.Contains(output.String(), "recovered:") {
|
|
t.Fatalf("fatal runtime error was recovered:\n%s", output.String())
|
|
}
|
|
}
|
|
|
|
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" {
|
|
for name, osArch := range supportedLinuxArches {
|
|
options := optionsFromOSARCH(osArch, sema)
|
|
if options.GOARCH != runtime.GOARCH { // Native architecture already run above.
|
|
targs = append(targs, targ{name, options})
|
|
}
|
|
}
|
|
}
|
|
|
|
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("wasip1", sema)},
|
|
)
|
|
}
|
|
for _, targ := range targs {
|
|
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, "")
|
|
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, "")
|
|
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, "")
|
|
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, "")
|
|
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
|
|
}
|
|
|
|
func TestGetListOfPackages(t *testing.T) {
|
|
opts := optionsFromTarget("", sema)
|
|
tests := []struct {
|
|
pkgs []string
|
|
expectedPkgs []string
|
|
expectesError bool
|
|
}{
|
|
{
|
|
pkgs: []string{"./tests/testing/recurse/..."},
|
|
expectedPkgs: []string{
|
|
"github.com/tinygo-org/tinygo/tests/testing/recurse",
|
|
"github.com/tinygo-org/tinygo/tests/testing/recurse/subdir",
|
|
},
|
|
},
|
|
{
|
|
pkgs: []string{"./tests/testing/pass"},
|
|
expectedPkgs: []string{
|
|
"github.com/tinygo-org/tinygo/tests/testing/pass",
|
|
},
|
|
},
|
|
{
|
|
pkgs: []string{"./tests/testing"},
|
|
expectesError: true,
|
|
},
|
|
}
|
|
|
|
for _, test := range tests {
|
|
actualPkgs, err := getListOfPackages(test.pkgs, &opts)
|
|
if err != nil && !test.expectesError {
|
|
t.Errorf("unexpected error: %v", err)
|
|
} else if err == nil && test.expectesError {
|
|
t.Error("expected error, but got none")
|
|
}
|
|
|
|
if !reflect.DeepEqual(test.expectedPkgs, actualPkgs) {
|
|
t.Errorf("expected two slices to be equal, expected %v got %v", test.expectedPkgs, actualPkgs)
|
|
}
|
|
}
|
|
}
|
|
|
|
// 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 {
|
|
// The tool should have printed an error message already.
|
|
// Don't print another error message here.
|
|
os.Exit(1)
|
|
}
|
|
os.Exit(0)
|
|
}
|
|
}
|
|
|
|
// Run normal tests.
|
|
os.Exit(m.Run())
|
|
}
|