diff --git a/interp/scan.go b/interp/scan.go index da06169ac8..4d272835ea 100644 --- a/interp/scan.go +++ b/interp/scan.go @@ -60,6 +60,10 @@ func (e *evalPackage) hasSideEffects(fn llvm.Value) (*sideEffectResult, *Error) return &sideEffectResult{severity: sideEffectNone}, nil case name == "runtime.trackPointer": return &sideEffectResult{severity: sideEffectNone}, nil + case name == "os.runtime_args": + // Special-casing this one because the (only) global it accesses changes + // before runtime.initAll is called. + return &sideEffectResult{severity: sideEffectLimited}, nil case name == "llvm.dbg.value": return &sideEffectResult{severity: sideEffectNone}, nil case name == "(*sync/atomic.Value).Load" || name == "(*sync/atomic.Value).Store": diff --git a/main_test.go b/main_test.go index a7eca14f03..00dbc9833a 100644 --- a/main_test.go +++ b/main_test.go @@ -13,7 +13,6 @@ import ( "os/exec" "path/filepath" "runtime" - "sort" "strings" "sync" "testing" @@ -24,40 +23,44 @@ import ( "github.com/tinygo-org/tinygo/goenv" ) -const TESTDATA = "testdata" - var testTarget = flag.String("target", "", "override test target") func TestCompiler(t *testing.T) { - matches, err := filepath.Glob(filepath.Join(TESTDATA, "*.go")) - if err != nil { - t.Fatal("could not read test files:", err) + tests := []string{ + "alias.go", + "atomic.go", + "binop.go", + "calls.go", + "cgo/", + "channel.go", + "coroutines.go", + "float.go", + "gc.go", + "init.go", + "init_multi.go", + "interface.go", + "map.go", + "math.go", + "print.go", + "reflect.go", + "slice.go", + "stdlib.go", + "string.go", + "structs.go", + "zeroalloc.go", } - dirMatches, err := filepath.Glob(filepath.Join(TESTDATA, "*", "main.go")) - if err != nil { - t.Fatal("could not read test packages:", err) - } - if len(matches) == 0 || len(dirMatches) == 0 { - t.Fatal("no test files found") - } - for _, m := range dirMatches { - matches = append(matches, filepath.Dir(m)+string(filepath.Separator)) - } - - sort.Strings(matches) - 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(*testTarget, matches, t) + runPlatTests(*testTarget, tests, t) return } if runtime.GOOS != "windows" { t.Run("Host", func(t *testing.T) { - runPlatTests("", matches, t) + runPlatTests("", tests, t) }) } @@ -66,26 +69,26 @@ func TestCompiler(t *testing.T) { } t.Run("EmulatedCortexM3", func(t *testing.T) { - runPlatTests("cortex-m-qemu", matches, t) + runPlatTests("cortex-m-qemu", tests, t) }) if runtime.GOOS == "windows" || runtime.GOOS == "darwin" { // Note: running only on Windows and macOS because Linux (as of 2020) // usually has an outdated QEMU version that doesn't support RISC-V yet. t.Run("EmulatedRISCV", func(t *testing.T) { - runPlatTests("riscv-qemu", matches, t) + runPlatTests("riscv-qemu", tests, t) }) } if runtime.GOOS == "linux" { t.Run("X86Linux", func(t *testing.T) { - runPlatTests("i386--linux-gnu", matches, t) + runPlatTests("i386--linux-gnu", tests, t) }) t.Run("ARMLinux", func(t *testing.T) { - runPlatTests("arm--linux-gnueabihf", matches, t) + runPlatTests("arm--linux-gnueabihf", tests, t) }) t.Run("ARM64Linux", func(t *testing.T) { - runPlatTests("aarch64--linux-gnu", matches, t) + runPlatTests("aarch64--linux-gnu", tests, t) }) goVersion, err := goenv.GorootVersionString(goenv.Get("GOROOT")) if err != nil { @@ -98,20 +101,20 @@ func TestCompiler(t *testing.T) { // below that are also not supported but still seem to pass, so // include them in the tests for now. t.Run("WebAssembly", func(t *testing.T) { - runPlatTests("wasm", matches, t) + runPlatTests("wasm", tests, t) }) } t.Run("WASI", func(t *testing.T) { - runPlatTests("wasi", matches, t) + runPlatTests("wasi", tests, t) }) } } -func runPlatTests(target string, matches []string, t *testing.T) { +func runPlatTests(target string, tests []string, t *testing.T) { t.Parallel() - for _, path := range matches { + for _, path := range tests { path := path // redefine to avoid race condition t.Run(filepath.Base(path), func(t *testing.T) { @@ -133,13 +136,13 @@ func runBuild(src, out string, opts *compileopts.Options) error { return Build(src, out, opts) } -func runTest(path, target string, t *testing.T) { +func runTest(name, target string, t *testing.T) { // Get the expected output for this test. - txtpath := path[:len(path)-3] + ".txt" - if path[len(path)-1] == os.PathSeparator { - txtpath = path + "out.txt" + txtfile := name[:len(name)-3] + ".txt" + if name[len(name)-1] == '/' { + txtfile = name + "out.txt" } - expected, err := ioutil.ReadFile(txtpath) + expected, err := ioutil.ReadFile(filepath.Join("testdata", txtfile)) if err != nil { t.Fatal("could not read expected output file:", err) } @@ -169,7 +172,7 @@ func runTest(path, target string, t *testing.T) { } binary := filepath.Join(tmpdir, "test") - err = runBuild("./"+path, binary, config) + err = runBuild("./testdata/"+name, binary, config) if err != nil { printCompilerError(t.Log, err) t.Fail() @@ -253,6 +256,81 @@ func runTest(path, target string, t *testing.T) { } } +// TestHostEnvironment tests command line arguments. In the future it may also +// test other things, such as environment variables. +func TestHostEnvironment(t *testing.T) { + // Create a temporary directory for test output files. + tmpdir, err := ioutil.TempDir("", "tinygo-test") + if err != nil { + t.Fatal("could not create temporary directory:", err) + } + defer func() { + rerr := os.RemoveAll(tmpdir) + if rerr != nil { + t.Errorf("failed to remove temporary directory %q: %s", tmpdir, rerr.Error()) + } + }() + + // Build the test binary. + config := &compileopts.Options{ + Target: "", + Opt: "z", + PrintIR: false, + DumpSSA: false, + VerifyIR: true, + Debug: true, + PrintSizes: "", + WasmAbi: "", + } + binary := filepath.Join(tmpdir, "test") + err = runBuild("./testdata/environment.go", binary, config) + if err != nil { + printCompilerError(t.Log, err) + t.Fail() + return + } + + // Run the test. + cmd := exec.Command(binary, "arg1", "\targ2 \n") + stdout := &bytes.Buffer{} + cmd.Stdout = stdout + cmd.Stderr = stdout + err = cmd.Start() + if err != nil { + t.Fatal("failed to start:", err) + } + err = cmd.Wait() + + expected := fmt.Sprintf("args: 3\narg: %s\narg: arg1\narg: \targ2 \n", binary) + + // Munge the output a bit to make it easier to work with: putchar() prints + // CRLF, convert it to LF. + actual := strings.Replace(string(stdout.Bytes()), "\r\n", "\n", -1) + actual = actual[:len(actual)-1] // remove trailing '\n' char + + // Check whether the command ran successfully, and print the actual output + // if it differs. + fail := false + if err != nil { + t.Log("failed to run:", err) + fail = true + } else if expected != actual { + t.Log("output did not match") + fail = true + } + if fail { + r := bufio.NewReader(strings.NewReader(actual)) + for { + line, err := r.ReadString('\n') + if err != nil { + break + } + t.Log("stdout:", line[:len(line)-1]) + } + t.Fail() + } +} + // 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. diff --git a/src/runtime/runtime.go b/src/runtime/runtime.go index 3a4a8cd8d6..6f47ea12f3 100644 --- a/src/runtime/runtime.go +++ b/src/runtime/runtime.go @@ -27,6 +27,9 @@ func GOROOT() string { // TODO: fill with real args. var args = []string{"/proc/self/exe"} +// This function is called from the os package. It is special-cased by the +// interp package to not run at init time, otherwise it would find the +// uninitialized args slice. //go:linkname os_runtime_args os.runtime_args func os_runtime_args() []string { return args diff --git a/src/runtime/runtime_unix.go b/src/runtime/runtime_unix.go index 17940f69d3..2f29a05950 100644 --- a/src/runtime/runtime_unix.go +++ b/src/runtime/runtime_unix.go @@ -10,6 +10,9 @@ import ( //export putchar func _putchar(c int) int +//export strlen +func strlen(s *byte) uintptr + //export usleep func usleep(usec uint) int @@ -43,9 +46,30 @@ func postinit() {} // Entry point for Go. Initialize all packages and call main.main(). //export main -func main() int { +func main(argc int32, argv **byte) int { preinit() + // Make args global big enough so that it can store all command line + // arguments. Unfortunately this has to be done with some magic as the heap + // is not yet initialized. + argsSlice := (*struct { + ptr unsafe.Pointer + len uintptr + cap uintptr + })(unsafe.Pointer(&args)) + argsSlice.ptr = malloc(uintptr(argc) * (unsafe.Sizeof(uintptr(0))) * 3) + argsSlice.len = uintptr(argc) + argsSlice.cap = uintptr(argc) + + // Initialize command line parameters. Again, using some magic, this time to + // convert (argc, argv) to a Go slice without doing any memory allocations. + argvSlice := (*[1 << 16]*byte)(unsafe.Pointer(argv))[:argc] + for i, ptr := range argvSlice { + argString := (*_string)(unsafe.Pointer(&args[i])) + argString.length = strlen(ptr) + argString.ptr = ptr + } + // Obtain the initial stack pointer right before calling the run() function. // The run function has been moved to a separate (non-inlined) function so // that the correct stack pointer is read. diff --git a/testdata/environment.go b/testdata/environment.go new file mode 100644 index 0000000000..d1c5e2786e --- /dev/null +++ b/testdata/environment.go @@ -0,0 +1,10 @@ +package main + +import "os" + +func main() { + println("args:", len(os.Args)) + for _, arg := range os.Args { + println("arg:", arg) + } +}