Files
wazero/internal/gojs/compiler_test.go
Crypt Keeper 7e953d7483 gojs: introduces --experimental-workdir CLI arg (#1226)
When compiled to `GOOS=js`, wasm does not maintain the working
directory: it is defined by the host. While not explicitly documented,
`os.TestDirFSRootDir` in Go suggests the working directory must be valid
to pass (literally the directory holding the file).

This adds an experimental CLI flag that gives the initial working
directory. This is experimental because while GOOS=js uses this, current
WASI compilers will not, as they maintain working directory in code
managed by wasi-libc, or as a convention (e.g. in Zig).

It is not yet known if wasi-cli will maintain working directory
externally or not.

Signed-off-by: Adrian Cole <adrian@tetrate.io>
2023-03-13 15:43:45 +08:00

178 lines
5.1 KiB
Go

package gojs_test
import (
"bytes"
"context"
_ "embed"
"fmt"
"log"
"os"
"os/exec"
"path"
"path/filepath"
"reflect"
"runtime"
"testing"
"time"
"github.com/tetratelabs/wazero"
"github.com/tetratelabs/wazero/api"
"github.com/tetratelabs/wazero/experimental/gojs"
"github.com/tetratelabs/wazero/internal/fstest"
internalgojs "github.com/tetratelabs/wazero/internal/gojs"
"github.com/tetratelabs/wazero/internal/gojs/run"
"github.com/tetratelabs/wazero/internal/testing/binaryencoding"
"github.com/tetratelabs/wazero/internal/wasm"
binaryformat "github.com/tetratelabs/wazero/internal/wasm/binary"
)
func compileAndRun(ctx context.Context, arg string, config wazero.ModuleConfig) (stdout, stderr string, err error) {
rt := wazero.NewRuntimeWithConfig(ctx, wazero.NewRuntimeConfig().
// https://github.com/tetratelabs/wazero/issues/992
WithMemoryCapacityFromMax(true).
WithCompilationCache(cache))
return compileAndRunWithRuntime(ctx, rt, arg, config) // use global runtime
}
func compileAndRunWithRuntime(ctx context.Context, r wazero.Runtime, arg string, config wazero.ModuleConfig) (stdout, stderr string, err error) {
var stdoutBuf, stderrBuf bytes.Buffer
builder := r.NewHostModuleBuilder("go")
gojs.NewFunctionExporter().ExportFunctions(builder)
if _, err = builder.Instantiate(ctx); err != nil {
return
}
// Note: this hits the file cache.
compiled, err := r.CompileModule(testCtx, testBin)
if err != nil {
log.Panicln(err)
}
var s *internalgojs.State
s, err = run.RunAndReturnState(ctx, r, compiled, config.
WithStdout(&stdoutBuf).
WithStderr(&stderrBuf).
WithArgs("test", arg))
if err == nil {
if !reflect.DeepEqual(s, internalgojs.NewState(ctx)) {
log.Panicf("unexpected state: %v\n", s)
}
}
stdout = stdoutBuf.String()
stderr = stderrBuf.String()
return
}
// testBin is not checked in as it is >7.5MB
var testBin []byte
// testCtx is configured in TestMain to re-use wazero's compilation cache.
var (
testCtx = context.Background()
testFS = fstest.FS
cache = wazero.NewCompilationCache()
)
func TestMain(m *testing.M) {
// For some reason, windows and freebsd fail to compile with exit status 1.
if o := runtime.GOOS; o != "darwin" && o != "linux" {
log.Println("gojs: skipping due to not yet supported OS:", o)
os.Exit(0)
}
// Find the go binary (if present), and compile the Wasm binary.
goBin, err := findGoBin()
if err != nil {
log.Println("gojs: skipping due missing Go binary:", err)
os.Exit(0)
}
if err = compileJsWasm(goBin); err != nil {
log.Panicln(err)
}
// Define a compilation cache so that tests run faster. This works because
// all tests use the same binary.
compilationCacheDir, err := os.MkdirTemp("", "gojs")
if err != nil {
log.Panicln(err)
}
defer os.RemoveAll(compilationCacheDir)
cache, err := wazero.NewCompilationCacheWithDir(compilationCacheDir)
if err != nil {
log.Panicln(err)
}
// In order to avoid race condition on scheduleTimeoutEvent, we need to set the memory max
// and WithMemoryCapacityFromMax(true) above.
// https://github.com/tetratelabs/wazero/issues/992
//
// TODO: Maybe add WithMemoryMax API?
parsed, err := binaryformat.DecodeModule(testBin, api.CoreFeaturesV2, wasm.MemoryLimitPages, false, false, false)
if err != nil {
log.Panicln(err)
}
// Set max to a high value, e.g. so that Test_stdio_large can pass
parsed.MemorySection.Max = 1024 // 64MB
parsed.MemorySection.IsMaxEncoded = true
testBin = binaryencoding.EncodeModule(parsed)
// Seed wazero's compilation cache to see any error up-front and to prevent
// one test from a cache-miss performance penalty.
r := wazero.NewRuntimeWithConfig(testCtx, wazero.NewRuntimeConfig().WithCompilationCache(cache))
_, err = r.CompileModule(testCtx, testBin)
if err != nil {
log.Panicln(err)
}
var exit int
defer func() {
cache.Close(testCtx)
r.Close(testCtx)
os.Exit(exit)
}()
exit = m.Run()
}
// compileJsWasm allows us to generate a binary with runtime.GOOS=js and
// runtime.GOARCH=wasm. This intentionally does so on-demand, as it allows us
// to test the user's current version of Go, as opposed to a specific one.
// For example, this allows testing both Go 1.18 and 1.19 in CI.
func compileJsWasm(goBin string) error {
// Prepare the working directory.
workdir, err := os.MkdirTemp("", "example")
if err != nil {
return err
}
defer os.RemoveAll(workdir)
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
bin := path.Join(workdir, "out.wasm")
cmd := exec.CommandContext(ctx, goBin, "build", "-o", bin, ".") //nolint:gosec
cmd.Env = append(os.Environ(), "GOOS=js", "GOARCH=wasm", "GOWASM=satconv,signext")
cmd.Dir = "testdata"
out, err := cmd.CombinedOutput()
if err != nil {
return fmt.Errorf("couldn't compile %s: %w", string(out), err)
}
testBin, err = os.ReadFile(bin) //nolint:gosec
return err
}
func findGoBin() (string, error) {
binName := "go"
if runtime.GOOS == "windows" {
binName += ".exe"
}
goBin := filepath.Join(runtime.GOROOT(), "bin", binName)
if _, err := os.Stat(goBin); err == nil {
return goBin, nil
}
// Now, search the path
return exec.LookPath(binName)
}