gojs: drops HTTP support to be compatible with Go 1.21 (#1557)

Signed-off-by: Adrian Cole <adrian@tetrate.io>
This commit is contained in:
Crypt Keeper
2023-07-05 16:56:18 +08:00
committed by GitHub
parent c2ee2242f6
commit 7498ad335f
40 changed files with 303 additions and 636 deletions

View File

@@ -1,5 +1,5 @@
// Package fstest defines filesystem test cases that help validate host
// functions implementing WASI and `GOARCH=wasm GOOS=js`. Tests are defined
// functions implementing WASI and `GOOS=js GOARCH=wasm`. Tests are defined
// here to reduce duplication and drift.
//
// Here's an example using this inside code that compiles to wasm.
@@ -15,7 +15,7 @@
// for example, gojs, sysfs or wasi_snapshot_preview1.
//
// This package must have no dependencies. Otherwise, compiling this with
// TinyGo or `GOARCH=wasm GOOS=js` can become bloated or complicated.
// TinyGo or `GOOS=js GOARCH=wasm` can become bloated or complicated.
package fstest
import (

View File

@@ -15,8 +15,8 @@ func Test_argsAndEnv(t *testing.T) {
return moduleConfig.WithEnv("c", "d").WithEnv("a", "b"), config.NewConfig()
})
require.EqualError(t, err, `module closed with exit_code(0)`)
require.Zero(t, stderr)
require.NoError(t, err)
require.Equal(t, `
args 0 = test
args 1 = argsenv

View File

@@ -14,26 +14,18 @@ func newJsGlobal(config *config.Config) *jsVal {
cwd: config.Workdir,
umask: config.Umask,
}
rt := config.Rt
if config.Rt != nil {
fetchProperty = goos.RefHttpFetch
}
return newJsVal(goos.RefValueGlobal, "global").
addProperties(map[string]interface{}{
"Object": objectConstructor,
"Array": arrayConstructor,
"crypto": jsCrypto,
"Uint8Array": uint8ArrayConstructor,
"fetch": fetchProperty,
"AbortController": goos.Undefined,
"Headers": headersConstructor,
"process": newJsProcess(uid, gid, euid, groups, proc),
"fs": newJsFs(proc),
"Date": jsDateConstructor,
}).
addFunction("fetch", &httpFetch{rt})
"Object": objectConstructor,
"Array": arrayConstructor,
"crypto": jsCrypto,
"Uint8Array": uint8ArrayConstructor,
"fetch": fetchProperty,
"process": newJsProcess(uid, gid, euid, groups, proc),
"fs": newJsFs(proc),
"Date": jsDateConstructor,
})
}
var (
@@ -51,7 +43,7 @@ var (
arrayConstructor = newJsVal(goos.RefArrayConstructor, "Array")
// uint8ArrayConstructor = js.Global().Get("Uint8Array")
// // fs_js.go, rand_js.go, roundtrip_js.go init
// // fs_js.go, rand_js.go init
//
// It has only one invocation pattern: `buf := uint8Array.New(len(b))`
uint8ArrayConstructor = newJsVal(goos.RefUint8ArrayConstructor, "Uint8Array")

View File

@@ -12,6 +12,7 @@ import (
"path/filepath"
"reflect"
"runtime"
"strings"
"testing"
"time"
@@ -41,30 +42,27 @@ func compileAndRun(ctx context.Context, arg string, config newConfig) (stdout, s
}
func compileAndRunWithRuntime(ctx context.Context, r wazero.Runtime, arg string, config newConfig) (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 {
var guest wazero.CompiledModule
if guest, err = r.CompileModule(testCtx, testBin); err != nil {
log.Panicln(err)
}
if _, err = gojs.Instantiate(ctx, r, guest); err != nil {
return
}
var stdoutBuf, stderrBuf bytes.Buffer
mc, c := config(wazero.NewModuleConfig().
WithStdout(&stdoutBuf).
WithStderr(&stderrBuf).
WithArgs("test", arg))
var s *internalgojs.State
s, err = run.RunAndReturnState(ctx, r, compiled, mc, c)
s, err = run.RunAndReturnState(ctx, r, guest, mc, c)
if err == nil {
if !reflect.DeepEqual(s, internalgojs.NewState(c)) {
log.Panicf("unexpected state: %v\n", s)
if want, have := internalgojs.NewState(c), s; !reflect.DeepEqual(want, have) {
log.Panicf("unexpected state: want %#v, have %#v", want, have)
}
}
@@ -169,3 +167,8 @@ func findGoBin() (string, error) {
// Now, search the path
return exec.LookPath(binName)
}
// logString handles the "go" -> "gojs" module rename in Go 1.21
func logString(log bytes.Buffer) string {
return strings.ReplaceAll(log.String(), "==> gojs", "==> go")
}

View File

@@ -4,7 +4,6 @@ package config
import (
"fmt"
"net/http"
"os"
"path/filepath"
"runtime"
@@ -23,7 +22,6 @@ type Config struct {
// Workdir is the actual working directory value.
Workdir string
Umask uint32
Rt http.RoundTripper
}
func NewConfig() *Config {
@@ -36,7 +34,6 @@ func NewConfig() *Config {
Groups: []int{0},
Workdir: "/",
Umask: uint32(0o0022),
Rt: nil,
}
}

View File

@@ -20,7 +20,7 @@ func Test_crypto(t *testing.T) {
stdout, stderr, err := compileAndRun(loggingCtx, "crypto", defaultConfig)
require.Zero(t, stderr)
require.EqualError(t, err, `module closed with exit_code(0)`)
require.NoError(t, err)
require.Equal(t, `7a0c9f9f0d
`, stdout)
require.Equal(t, `==> go.runtime.getRandomData(r_len=32)
@@ -29,5 +29,5 @@ func Test_crypto(t *testing.T) {
<==
==> go.syscall/js.valueCall(crypto.getRandomValues(r_len=5))
<== (n=5)
`, log.String())
`, logString(log))
}

View File

@@ -1,5 +1,5 @@
// Package custom is similar to the WebAssembly Custom Sections. These are
// needed because `GOARCH=wasm GOOS=js` functions aren't defined naturally
// needed because `GOOS=js GOARCH=wasm` functions aren't defined naturally
// in WebAssembly. For example, every function has a single parameter "sp",
// which implicitly maps to stack parameters in this package.
package custom

View File

@@ -2,6 +2,7 @@ package custom
const (
NameProcess = "process"
NameProcessArgv0 = "argv0"
NameProcessCwd = "cwd"
NameProcessChdir = "chdir"
NameProcessGetuid = "getuid"
@@ -15,6 +16,11 @@ const (
// Results here are those set to the current event object, but effectively are
// results of the host function.
var ProcessNameSection = map[string]*Names{
NameProcessArgv0: {
Name: NameProcessArgv0,
ParamNames: []string{},
ResultNames: []string{"argv0"},
},
NameProcessCwd: {
Name: NameProcessCwd,
ParamNames: []string{},

View File

@@ -20,7 +20,7 @@ func Test_fs(t *testing.T) {
})
require.Zero(t, stderr)
require.EqualError(t, err, `module closed with exit_code(0)`)
require.NoError(t, err)
require.Equal(t, `sub mode drwxr-xr-x
/animals.txt mode -rw-r--r--
animals.txt mode -rw-r--r--
@@ -50,7 +50,7 @@ func Test_testfs(t *testing.T) {
})
require.Zero(t, stderr)
require.EqualError(t, err, `module closed with exit_code(0)`)
require.NoError(t, err)
require.Zero(t, stdout)
}
@@ -67,7 +67,7 @@ func Test_writefs(t *testing.T) {
})
require.Zero(t, stderr)
require.EqualError(t, err, `module closed with exit_code(0)`)
require.NoError(t, err)
if platform.CompilerSupported() {
// Note: as of Go 1.19, only the Sec field is set on update in fs_js.go.

View File

@@ -48,8 +48,6 @@ const (
IdJsCrypto
IdJsDateConstructor
IdJsDate
IdHttpFetch
IdHttpHeaders
NextID
)
@@ -63,17 +61,15 @@ const (
RefValueGlobal = (NanHead|Ref(TypeFlagObject))<<32 | Ref(IdValueGlobal)
RefJsGo = (NanHead|Ref(TypeFlagObject))<<32 | Ref(IdJsGo)
RefObjectConstructor = (NanHead|Ref(TypeFlagFunction))<<32 | Ref(IdObjectConstructor)
RefArrayConstructor = (NanHead|Ref(TypeFlagFunction))<<32 | Ref(IdArrayConstructor)
RefJsProcess = (NanHead|Ref(TypeFlagObject))<<32 | Ref(IdJsProcess)
RefJsfs = (NanHead|Ref(TypeFlagObject))<<32 | Ref(IdJsfs)
RefJsfsConstants = (NanHead|Ref(TypeFlagObject))<<32 | Ref(IdJsfsConstants)
RefUint8ArrayConstructor = (NanHead|Ref(TypeFlagFunction))<<32 | Ref(IdUint8ArrayConstructor)
RefJsCrypto = (NanHead|Ref(TypeFlagFunction))<<32 | Ref(IdJsCrypto)
RefJsDateConstructor = (NanHead|Ref(TypeFlagFunction))<<32 | Ref(IdJsDateConstructor)
RefJsDate = (NanHead|Ref(TypeFlagObject))<<32 | Ref(IdJsDate)
RefHttpFetch = (NanHead|Ref(TypeFlagFunction))<<32 | Ref(IdHttpFetch)
RefHttpHeadersConstructor = (NanHead|Ref(TypeFlagFunction))<<32 | Ref(IdHttpHeaders)
RefObjectConstructor = (NanHead|Ref(TypeFlagFunction))<<32 | Ref(IdObjectConstructor)
RefArrayConstructor = (NanHead|Ref(TypeFlagFunction))<<32 | Ref(IdArrayConstructor)
RefJsProcess = (NanHead|Ref(TypeFlagObject))<<32 | Ref(IdJsProcess)
RefJsfs = (NanHead|Ref(TypeFlagObject))<<32 | Ref(IdJsfs)
RefJsfsConstants = (NanHead|Ref(TypeFlagObject))<<32 | Ref(IdJsfsConstants)
RefUint8ArrayConstructor = (NanHead|Ref(TypeFlagFunction))<<32 | Ref(IdUint8ArrayConstructor)
RefJsCrypto = (NanHead|Ref(TypeFlagFunction))<<32 | Ref(IdJsCrypto)
RefJsDateConstructor = (NanHead|Ref(TypeFlagFunction))<<32 | Ref(IdJsDateConstructor)
RefJsDate = (NanHead|Ref(TypeFlagObject))<<32 | Ref(IdJsDate)
)
type TypeFlag byte

View File

@@ -1,156 +0,0 @@
package gojs
import (
"context"
"fmt"
"io"
"net/http"
"net/textproto"
"sort"
"github.com/tetratelabs/wazero/api"
"github.com/tetratelabs/wazero/internal/gojs/goos"
)
// headersConstructor = Get("Headers").New() // http.Roundtrip && "fetch"
var headersConstructor = newJsVal(goos.RefHttpHeadersConstructor, "Headers")
// httpFetch implements jsFn for http.RoundTripper
//
// Reference in roundtrip_js.go init
//
// jsFetchMissing = js.Global().Get("fetch").IsUndefined()
//
// In http.Transport RoundTrip, this returns a promise
//
// fetchPromise := js.Global().Call("fetch", req.URL.String(), opt)
type httpFetch struct{ rt http.RoundTripper }
func (h *httpFetch) invoke(ctx context.Context, _ api.Module, args ...interface{}) (interface{}, error) {
rt := h.rt
if rt == nil {
panic("unexpected to reach here without roundtripper as property is nil checked")
}
url := args[0].(string)
properties := args[1].(*object).properties
req, err := http.NewRequestWithContext(ctx, properties["method"].(string), url, nil)
if err != nil {
return nil, err
}
// TODO: headers properties[headers]
v := &fetchPromise{rt: rt, req: req}
return v, nil
}
type fetchPromise struct {
rt http.RoundTripper
req *http.Request
}
// call implements jsCall.call
func (p *fetchPromise) call(ctx context.Context, mod api.Module, this goos.Ref, method string, args ...interface{}) (interface{}, error) {
if method == "then" {
if res, err := p.rt.RoundTrip(p.req); err != nil {
failure := args[1].(funcWrapper)
// HTTP is at the GOOS=js abstraction, so we can return any error.
return failure.invoke(ctx, mod, this, err)
} else {
success := args[0].(funcWrapper)
return success.invoke(ctx, mod, this, &fetchResult{res: res})
}
}
panic(fmt.Sprintf("TODO: fetchPromise.%s", method))
}
type fetchResult struct {
res *http.Response
}
// Get implements the same method as documented on goos.GetFunction
func (s *fetchResult) Get(propertyKey string) interface{} {
switch propertyKey {
case "headers":
names := make([]string, 0, len(s.res.Header))
for k := range s.res.Header {
names = append(names, k)
}
// Sort names for consistent iteration
sort.Strings(names)
h := &headers{names: names, headers: s.res.Header}
return h
case "body":
// return undefined as arrayPromise is more complicated than an array.
return goos.Undefined
case "status":
return uint32(s.res.StatusCode)
}
panic(fmt.Sprintf("TODO: get fetchResult.%s", propertyKey))
}
// call implements jsCall.call
func (s *fetchResult) call(_ context.Context, _ api.Module, _ goos.Ref, method string, _ ...interface{}) (interface{}, error) {
switch method {
case "arrayBuffer":
v := &arrayPromise{reader: s.res.Body}
return v, nil
}
panic(fmt.Sprintf("TODO: call fetchResult.%s", method))
}
type headers struct {
headers http.Header
names []string
i int
}
// Get implements the same method as documented on goos.GetFunction
func (h *headers) Get(propertyKey string) interface{} {
switch propertyKey {
case "done":
return h.i == len(h.names)
case "value":
name := h.names[h.i]
value := h.headers.Get(name)
h.i++
return &objectArray{[]interface{}{name, value}}
}
panic(fmt.Sprintf("TODO: get headers.%s", propertyKey))
}
// call implements jsCall.call
func (h *headers) call(_ context.Context, _ api.Module, _ goos.Ref, method string, args ...interface{}) (interface{}, error) {
switch method {
case "entries":
// Sort names for consistent iteration
sort.Strings(h.names)
return h, nil
case "next":
return h, nil
case "append":
name := textproto.CanonicalMIMEHeaderKey(args[0].(string))
value := args[1].(string)
h.names = append(h.names, name)
h.headers.Add(name, value)
return nil, nil
}
panic(fmt.Sprintf("TODO: call headers.%s", method))
}
type arrayPromise struct {
reader io.ReadCloser
}
// call implements jsCall.call
func (p *arrayPromise) call(ctx context.Context, mod api.Module, this goos.Ref, method string, args ...interface{}) (interface{}, error) {
switch method {
case "then":
defer p.reader.Close()
if b, err := io.ReadAll(p.reader); err != nil {
// HTTP is at the GOOS=js abstraction, so we can return any error.
return args[1].(funcWrapper).invoke(ctx, mod, this, err)
} else {
return args[0].(funcWrapper).invoke(ctx, mod, this, goos.WrapByteArray(b))
}
}
panic(fmt.Sprintf("TODO: call arrayPromise.%s", method))
}

View File

@@ -1,55 +0,0 @@
package gojs_test
import (
"errors"
"io"
"net/http"
"strings"
"testing"
"github.com/tetratelabs/wazero"
"github.com/tetratelabs/wazero/internal/gojs/config"
"github.com/tetratelabs/wazero/internal/testing/require"
)
type roundTripperFunc func(r *http.Request) (*http.Response, error)
func (f roundTripperFunc) RoundTrip(r *http.Request) (*http.Response, error) {
return f(r)
}
func Test_http(t *testing.T) {
t.Parallel()
rt := roundTripperFunc(func(req *http.Request) (*http.Response, error) {
if req.URL.Path == "/error" {
return nil, errors.New("error")
}
if req.Body != nil {
require.Equal(t, http.MethodPost, req.Method)
bytes, err := io.ReadAll(req.Body)
require.NoError(t, err)
require.Equal(t, "ice cream", string(bytes))
}
return &http.Response{
StatusCode: http.StatusOK,
Status: http.StatusText(http.StatusOK),
Header: http.Header{"Custom": {"1"}},
Body: io.NopCloser(strings.NewReader("abcdef")),
ContentLength: 6,
}, nil
})
stdout, stderr, err := compileAndRun(testCtx, "http", func(moduleConfig wazero.ModuleConfig) (wazero.ModuleConfig, *config.Config) {
config := config.NewConfig()
config.Rt = rt
return moduleConfig.WithEnv("BASE_URL", "http://host"), config
})
require.EqualError(t, err, `module closed with exit_code(0)`)
require.Zero(t, stderr)
require.Equal(t, `Get "http://host/error": net/http: fetch() failed: error
1
abcdef
`, stdout)
}

View File

@@ -23,12 +23,12 @@ func Test_exit(t *testing.T) {
stdout, stderr, err := compileAndRun(loggingCtx, "exit", defaultConfig)
require.EqualError(t, err, `module closed with exit_code(255)`)
require.Zero(t, stderr)
require.EqualError(t, err, `module closed with exit_code(255)`)
require.Zero(t, stdout)
require.Equal(t, `==> go.runtime.wasmExit(code=255)
<==
`, log.String()) // Note: gojs doesn't panic on exit, so you see "<=="
`, logString(log)) // Note: gojs doesn't panic on exit, so you see "<=="
}
func Test_goroutine(t *testing.T) {
@@ -36,8 +36,8 @@ func Test_goroutine(t *testing.T) {
stdout, stderr, err := compileAndRun(testCtx, "goroutine", defaultConfig)
require.EqualError(t, err, `module closed with exit_code(0)`)
require.Zero(t, stderr)
require.NoError(t, err)
require.Equal(t, `producer
consumer
`, stdout)
@@ -52,12 +52,12 @@ func Test_mem(t *testing.T) {
stdout, stderr, err := compileAndRun(loggingCtx, "mem", defaultConfig)
require.EqualError(t, err, `module closed with exit_code(0)`)
require.Zero(t, stderr)
require.NoError(t, err)
require.Zero(t, stdout)
// The memory view is reset at least once.
require.Contains(t, log.String(), `==> go.runtime.resetMemoryDataView()
require.Contains(t, logString(log), `==> go.runtime.resetMemoryDataView()
<==
`)
}
@@ -71,7 +71,7 @@ func Test_stdio(t *testing.T) {
})
require.Equal(t, "stderr 6\n", stderr)
require.EqualError(t, err, `module closed with exit_code(0)`)
require.NoError(t, err)
require.Equal(t, "stdout 6\n", stdout)
}
@@ -89,17 +89,13 @@ func Test_stdio_large(t *testing.T) {
return defaultConfig(moduleConfig.WithStdin(bytes.NewReader(input)))
})
require.EqualError(t, err, `module closed with exit_code(0)`)
require.NoError(t, err)
require.Equal(t, fmt.Sprintf("stderr %d\n", size), stderr)
require.Equal(t, fmt.Sprintf("stdout %d\n", size), stdout)
// We can't predict the precise ms the timeout event will be, so we partial match.
require.Contains(t, log.String(), `==> go.runtime.scheduleTimeoutEvent(ms=`)
require.Contains(t, log.String(), `<== (id=1)`)
// There may be another timeout event between the first and its clear.
require.Contains(t, log.String(), `==> go.runtime.clearTimeoutEvent(id=1)
<==
`)
// There's no guarantee of a timeout event (in Go 1.21 there isn't), so we
// don't verify this. gojs is in maintenance mode until it is removed after
// Go 1.22 is out.
}
func Test_gc(t *testing.T) {
@@ -107,7 +103,7 @@ func Test_gc(t *testing.T) {
stdout, stderr, err := compileAndRun(testCtx, "gc", defaultConfig)
require.EqualError(t, err, `module closed with exit_code(0)`)
require.NoError(t, err)
require.Equal(t, "", stderr)
require.Equal(t, "before gc\nafter gc\n", stdout)
}

View File

@@ -18,7 +18,7 @@ func Test_process(t *testing.T) {
})
require.Zero(t, stderr)
require.EqualError(t, err, `module closed with exit_code(0)`)
require.NoError(t, err)
require.Equal(t, `syscall.Getpid()=1
syscall.Getppid()=0
syscall.Getuid()=0

View File

@@ -8,6 +8,7 @@ import (
"github.com/tetratelabs/wazero"
"github.com/tetratelabs/wazero/internal/gojs"
"github.com/tetratelabs/wazero/internal/gojs/config"
"github.com/tetratelabs/wazero/sys"
)
func RunAndReturnState(
@@ -40,5 +41,10 @@ func RunAndReturnState(
// Invoke the run function.
_, err = mod.ExportedFunction("run").Call(ctx, uint64(argc), uint64(argv))
if se, ok := err.(*sys.ExitError); ok {
if se.ExitCode() == 0 { // Don't err on success.
err = nil
}
}
return s, err
}

View File

@@ -13,6 +13,7 @@ import (
func NewState(config *config.Config) *State {
return &State{
config: config,
values: values.NewValues(),
valueGlobal: newJsGlobal(config),
_nextCallbackTimeoutID: 1,
@@ -100,8 +101,6 @@ func LoadValue(ctx context.Context, ref goos.Ref) interface{} { //nolint
return jsDateConstructor
case goos.RefJsDate:
return jsDate
case goos.RefHttpHeadersConstructor:
return headersConstructor
default:
if f, ok := ref.ParseFloat(); ok { // numbers are passed through as a Ref
return f
@@ -169,6 +168,7 @@ func toFloatRef(f float64) goos.Ref {
// State holds state used by the "go" imports used by gojs.
// Note: This is module-scoped.
type State struct {
config *config.Config
values *values.Values
_pendingEvent *event
// _lastEvent was the last _pendingEvent value
@@ -208,11 +208,12 @@ func (s *State) close() {
}
// Reset all state recursively to their initial values. This allows our
// unit tests to check we closed everything.
s._scheduledTimeouts = map[uint32]chan bool{}
s.values.Reset()
s._pendingEvent = nil
s._lastEvent = nil
s.valueGlobal = newJsGlobal(s.config)
s._nextCallbackTimeoutID = 1
s._scheduledTimeouts = map[uint32]chan bool{}
}
func toInt64(arg interface{}) int64 {

View File

@@ -3,7 +3,6 @@ package gojs
import (
"context"
"fmt"
"net/http"
"github.com/tetratelabs/wazero/api"
"github.com/tetratelabs/wazero/internal/gojs/custom"
@@ -215,10 +214,6 @@ func valueNew(ctx context.Context, mod api.Module, stack goos.Stack) {
result := &object{properties: map[string]interface{}{}}
res = storeValue(ctx, result)
ok = true
case goos.RefHttpHeadersConstructor:
result := &headers{headers: http.Header{}}
res = storeValue(ctx, result)
ok = true
case goos.RefJsDateConstructor:
res = goos.RefJsDate
ok = true

View File

@@ -1,32 +0,0 @@
package http
import (
"fmt"
"io"
"log"
"net/http"
"os"
"strings"
)
func Main() {
url := os.Getenv("BASE_URL")
res, err := http.Get(url + "/error")
if err == nil {
log.Panicln(err)
}
fmt.Println(err)
res, err = http.Post(url, "text/plain", io.NopCloser(strings.NewReader("ice cream")))
if err != nil {
log.Panicln(err)
}
body, err := io.ReadAll(res.Body)
if err != nil {
log.Panicln(err)
}
res.Body.Close()
fmt.Println(res.Header.Get("Custom"))
fmt.Println(string(body))
}

View File

@@ -9,7 +9,6 @@ import (
"github.com/tetratelabs/wazero/internal/gojs/testdata/fs"
"github.com/tetratelabs/wazero/internal/gojs/testdata/gc"
"github.com/tetratelabs/wazero/internal/gojs/testdata/goroutine"
"github.com/tetratelabs/wazero/internal/gojs/testdata/http"
"github.com/tetratelabs/wazero/internal/gojs/testdata/mem"
"github.com/tetratelabs/wazero/internal/gojs/testdata/process"
"github.com/tetratelabs/wazero/internal/gojs/testdata/stdio"
@@ -31,8 +30,6 @@ func main() {
fs.Main()
case "gc":
gc.Main()
case "http":
http.Main()
case "goroutine":
goroutine.Main()
case "mem":

View File

@@ -19,20 +19,21 @@ func Test_time(t *testing.T) {
stdout, stderr, err := compileAndRun(loggingCtx, "time", defaultConfig)
require.EqualError(t, err, `module closed with exit_code(0)`)
require.Zero(t, stderr)
require.NoError(t, err)
require.Equal(t, `Local
1ms
`, stdout)
// To avoid multiple similar assertions, just check three functions we
// expect were called.
require.Contains(t, log.String(), `==> go.runtime.nanotime1()
logString := logString(log)
require.Contains(t, logString, `==> go.runtime.nanotime1()
<== (nsec=0)`)
require.Contains(t, log.String(), `==> go.runtime.walltime()
require.Contains(t, logString, `==> go.runtime.walltime()
<== (sec=1640995200,nsec=0)
`)
require.Contains(t, log.String(), `==> go.syscall/js.valueCall(Date.getTimezoneOffset())
require.Contains(t, logString, `==> go.syscall/js.valueCall(Date.getTimezoneOffset())
<== (tz=0)
`)
}

View File

@@ -15,7 +15,7 @@ func Test_Values(t *testing.T) {
err := require.CapturePanic(func() {
_ = vs.Get(goos.NextID)
})
require.EqualError(t, err, "id 18 is out of range 0")
require.Contains(t, err.Error(), "is out of range 0")
v1 := "foo"
id1 := vs.Increment(v1)
@@ -43,7 +43,7 @@ func Test_Values(t *testing.T) {
err = require.CapturePanic(func() {
_ = vs.Get(id1)
})
require.EqualError(t, err, "value for 18 was nil")
require.Contains(t, err.Error(), "was nil")
// Since the ID is no longer in use, we should be able to revive it.
require.Equal(t, id1, vs.Increment(v1))