github.com/bananabytelabs/wazero@v0.0.0-20240105073314-54b22a776da8/internal/gojs/compiler_test.go (about) 1 package gojs_test 2 3 import ( 4 "bytes" 5 "context" 6 _ "embed" 7 "fmt" 8 "log" 9 "os" 10 "os/exec" 11 "path" 12 "path/filepath" 13 "reflect" 14 "runtime" 15 "strings" 16 "testing" 17 "time" 18 19 "github.com/bananabytelabs/wazero" 20 "github.com/bananabytelabs/wazero/experimental" 21 "github.com/bananabytelabs/wazero/experimental/gojs" 22 "github.com/bananabytelabs/wazero/internal/fstest" 23 internalgojs "github.com/bananabytelabs/wazero/internal/gojs" 24 "github.com/bananabytelabs/wazero/internal/gojs/config" 25 "github.com/bananabytelabs/wazero/internal/gojs/run" 26 ) 27 28 type newConfig func(moduleConfig wazero.ModuleConfig) (wazero.ModuleConfig, *config.Config) 29 30 func defaultConfig(moduleConfig wazero.ModuleConfig) (wazero.ModuleConfig, *config.Config) { 31 return moduleConfig, config.NewConfig() 32 } 33 34 func compileAndRun(ctx context.Context, arg string, config newConfig) (stdout, stderr string, err error) { 35 rt := wazero.NewRuntimeWithConfig(ctx, wazero.NewRuntimeConfig(). 36 // In order to avoid race condition on scheduleTimeoutEvent, we need to set the memory max 37 // and WithMemoryCapacityFromMax(true) above. See #992. 38 WithMemoryCapacityFromMax(true). 39 // Set max to a high value, e.g. so that Test_stdio_large can pass. 40 WithMemoryLimitPages(1024). // 64MB 41 WithCompilationCache(cache)) 42 return compileAndRunWithRuntime(ctx, rt, arg, config) // use global runtime 43 } 44 45 func compileAndRunWithRuntime(ctx context.Context, r wazero.Runtime, arg string, config newConfig) (stdout, stderr string, err error) { 46 // Note: this hits the file cache. 47 var guest wazero.CompiledModule 48 if guest, err = r.CompileModule(testCtx, testBin); err != nil { 49 log.Panicln(err) 50 } 51 52 if _, err = gojs.Instantiate(ctx, r, guest); err != nil { 53 return 54 } 55 56 var stdoutBuf, stderrBuf bytes.Buffer 57 mc, c := config(wazero.NewModuleConfig(). 58 WithStdout(&stdoutBuf). 59 WithStderr(&stderrBuf). 60 WithArgs("test", arg)) 61 62 ctx = experimental.WithCloseNotifier(ctx, experimental.CloseNotifyFunc(func(ctx context.Context, exitCode uint32) { 63 s := ctx.Value(internalgojs.StateKey{}) 64 if want, have := internalgojs.NewState(c), s; !reflect.DeepEqual(want, have) { 65 log.Panicf("unexpected state: want %#v, have %#v", want, have) 66 } 67 })) 68 err = run.Run(ctx, r, guest, mc, c) 69 stdout = stdoutBuf.String() 70 stderr = stderrBuf.String() 71 return 72 } 73 74 // testBin is not checked in as it is >7.5MB 75 var testBin []byte 76 77 // testCtx is configured in TestMain to re-use wazero's compilation cache. 78 var ( 79 testCtx = context.Background() 80 testFS = fstest.FS 81 cache = wazero.NewCompilationCache() 82 ) 83 84 func TestMain(m *testing.M) { 85 // For some reason, windows and freebsd fail to compile with exit status 1. 86 if o := runtime.GOOS; o != "darwin" && o != "linux" { 87 log.Println("gojs: skipping due to not yet supported OS:", o) 88 os.Exit(0) 89 } 90 91 // Find the go binary (if present), and compile the Wasm binary. 92 goBin, err := findGoBin() 93 if err != nil { 94 log.Println("gojs: skipping due missing Go binary:", err) 95 os.Exit(0) 96 } 97 if err = compileJsWasm(goBin); err != nil { 98 log.Panicln(err) 99 } 100 101 // Define a compilation cache so that tests run faster. This works because 102 // all tests use the same binary. 103 compilationCacheDir, err := os.MkdirTemp("", "gojs") 104 if err != nil { 105 log.Panicln(err) 106 } 107 defer os.RemoveAll(compilationCacheDir) 108 cache, err := wazero.NewCompilationCacheWithDir(compilationCacheDir) 109 if err != nil { 110 log.Panicln(err) 111 } 112 113 // Seed wazero's compilation cache to see any error up-front and to prevent 114 // one test from a cache-miss performance penalty. 115 r := wazero.NewRuntimeWithConfig(testCtx, wazero.NewRuntimeConfig().WithCompilationCache(cache)) 116 _, err = r.CompileModule(testCtx, testBin) 117 if err != nil { 118 log.Panicln(err) 119 } 120 121 var exit int 122 defer func() { 123 cache.Close(testCtx) 124 r.Close(testCtx) 125 os.Exit(exit) 126 }() 127 exit = m.Run() 128 } 129 130 // compileJsWasm allows us to generate a binary with runtime.GOOS=js and 131 // runtime.GOARCH=wasm. This intentionally does so on-demand, as it allows us 132 // to test the user's current version of Go, as opposed to a specific one. 133 // For example, this allows testing both Go 1.19 and 1.20 in CI. 134 func compileJsWasm(goBin string) error { 135 // Prepare the working directory. 136 workdir, err := os.MkdirTemp("", "example") 137 if err != nil { 138 return err 139 } 140 defer os.RemoveAll(workdir) 141 142 ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) 143 defer cancel() 144 145 bin := path.Join(workdir, "out.wasm") 146 cmd := exec.CommandContext(ctx, goBin, "build", "-o", bin, ".") //nolint:gosec 147 cmd.Env = append(os.Environ(), "GOOS=js", "GOARCH=wasm", "GOWASM=satconv,signext") 148 cmd.Dir = "testdata" 149 out, err := cmd.CombinedOutput() 150 if err != nil { 151 return fmt.Errorf("couldn't compile %s: %w", string(out), err) 152 } 153 154 testBin, err = os.ReadFile(bin) //nolint:gosec 155 return err 156 } 157 158 func findGoBin() (string, error) { 159 binName := "go" 160 if runtime.GOOS == "windows" { 161 binName += ".exe" 162 } 163 goBin := filepath.Join(runtime.GOROOT(), "bin", binName) 164 if _, err := os.Stat(goBin); err == nil { 165 return goBin, nil 166 } 167 // Now, search the path 168 return exec.LookPath(binName) 169 } 170 171 // logString handles the "go" -> "gojs" module rename in Go 1.21 172 func logString(log bytes.Buffer) string { 173 return strings.ReplaceAll(log.String(), "==> gojs", "==> go") 174 }