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  }