Skip to content

Commit

Permalink
Add test coverage support (#41)
Browse files Browse the repository at this point in the history
* Move index.html to separate file with Go 1.16 embedded files

* Add test coverage support

Overrides three file system operations for the purposes of reading and
writing the coverage profile file.

The contents are copied out of the JS runtime and written again to the
real file.

* Extract FS wrappers to 'overrideFS' function

Also guard use of global TextDecoder if not supported.

* Wrap each FS operation, switch from writeSync() to write(), and bind "fs" as "this"
  • Loading branch information
JohnStarich committed Dec 20, 2022
1 parent 8753b3a commit bba38b6
Show file tree
Hide file tree
Showing 4 changed files with 139 additions and 77 deletions.
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
module github.com/agnivade/wasmbrowsertest

go 1.12
go 1.16

require (
github.com/chromedp/cdproto v0.0.0-20221108233440-fad8339618ab
Expand Down
99 changes: 25 additions & 74 deletions handler.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package main

import (
_ "embed"
"html/template"
"io/ioutil"
"log"
Expand All @@ -14,22 +15,27 @@ import (
"time"
)

//go:embed index.html
var indexHTML string

type wasmServer struct {
indexTmpl *template.Template
wasmFile string
wasmExecJS []byte
args []string
envMap map[string]string
logger *log.Logger
indexTmpl *template.Template
wasmFile string
wasmExecJS []byte
args []string
coverageFile string
envMap map[string]string
logger *log.Logger
}

func NewWASMServer(wasmFile string, args []string, l *log.Logger) (http.Handler, error) {
func NewWASMServer(wasmFile string, args []string, coverageFile string, l *log.Logger) (http.Handler, error) {
var err error
srv := &wasmServer{
wasmFile: wasmFile,
args: args,
logger: l,
envMap: make(map[string]string),
wasmFile: wasmFile,
args: args,
coverageFile: coverageFile,
logger: l,
envMap: make(map[string]string),
}

for _, env := range os.Environ() {
Expand All @@ -56,13 +62,15 @@ func (ws *wasmServer) ServeHTTP(w http.ResponseWriter, r *http.Request) {
case "/", "/index.html":
w.Header().Set("Content-Type", "text/html; charset=UTF-8")
data := struct {
WASMFile string
Args []string
EnvMap map[string]string
WASMFile string
Args []string
CoverageFile string
EnvMap map[string]string
}{
WASMFile: filepath.Base(ws.wasmFile),
Args: ws.args,
EnvMap: ws.envMap,
WASMFile: filepath.Base(ws.wasmFile),
Args: ws.args,
CoverageFile: ws.coverageFile,
EnvMap: ws.envMap,
}
err := ws.indexTmpl.Execute(w, data)
if err != nil {
Expand All @@ -89,60 +97,3 @@ func (ws *wasmServer) ServeHTTP(w http.ResponseWriter, r *http.Request) {
}
}
}

const indexHTML = `<!doctype html>
<!--
Copyright 2018 The Go Authors. All rights reserved.
Use of this source code is governed by a BSD-style
license that can be found in the LICENSE file.
-->
<html>
<head>
<meta charset="utf-8">
<title>Go wasm</title>
</head>
<body>
<!--
Add the following polyfill for Microsoft Edge 17/18 support:
<script src="https://cdn.jsdelivr.net/npm/[email protected]/lib/encoding.min.js"></script>
(see https://caniuse.com/#feat=textencoder)
-->
<script src="wasm_exec.js"></script>
<script>
if (!WebAssembly.instantiateStreaming) { // polyfill
WebAssembly.instantiateStreaming = async (resp, importObject) => {
const source = await (await resp).arrayBuffer();
return await WebAssembly.instantiate(source, importObject);
};
}
let exitCode = 0;
function goExit(code) {
exitCode = code;
}
(async() => {
const go = new Go();
go.argv = [{{range $i, $item := .Args}} {{if $i}}, {{end}} "{{$item}}" {{end}}];
// The notFirst variable sets itself to true after first iteration. This is to put commas in between.
go.env = { {{ $notFirst := false }}
{{range $key, $val := .EnvMap}} {{if $notFirst}}, {{end}} {{$key}}: "{{$val}}" {{ $notFirst = true }}
{{end}} };
go.exit = goExit;
let mod, inst;
await WebAssembly.instantiateStreaming(fetch("{{.WASMFile}}"), go.importObject).then((result) => {
mod = result.module;
inst = result.instance;
}).catch((err) => {
console.error(err);
});
await go.run(inst);
document.getElementById("doneButton").disabled = false;
})();
</script>
<button id="doneButton" style="display: none;" disabled>Done</button>
</body>
</html>`
99 changes: 99 additions & 0 deletions index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
<!doctype html>
<!--
Copyright 2018 The Go Authors. All rights reserved.
Use of this source code is governed by a BSD-style
license that can be found in the LICENSE file.
-->
<html>

<head>
<meta charset="utf-8">
<title>Go wasm</title>
</head>

<body>
<!--
Add the following polyfill for Microsoft Edge 17/18 support:
<script src="https://cdn.jsdelivr.net/npm/[email protected]/lib/encoding.min.js"></script>
(see https://caniuse.com/#feat=textencoder)
-->
<script src="wasm_exec.js"></script>
<script>
if (!WebAssembly.instantiateStreaming) { // polyfill
WebAssembly.instantiateStreaming = async (resp, importObject) => {
const source = await (await resp).arrayBuffer();
return await WebAssembly.instantiate(source, importObject);
};
}

let exitCode = 0;
function goExit(code) {
exitCode = code;
}
function enosys() {
const err = new Error("not implemented");
err.code = "ENOSYS";
return err;
}
let coverageProfileContents = "";
function overrideFS(fs) {
// A typical runtime opens fd's in sequence above the standard descriptors (0-2).
// Choose an arbitrarily high fd for the custom coverage file to avoid conflict with the actual runtime fd's.
const coverFileDescriptor = Number.MAX_SAFE_INTEGER;
const coverFilePath = {{.CoverageFile}};
// Wraps the default operations with bind() to ensure internal usage of 'this' continues to work.
const defaultOpen = fs.open.bind(fs);
fs.open = (path, flags, mode, callback) => {
if (path === coverFilePath) {
callback(null, coverFileDescriptor);
return;
}
defaultOpen(path, flags, mode, callback);
};
const defaultClose = fs.close.bind(fs);
fs.close = (fd, callback) => {
if (fd === coverFileDescriptor) {
callback(null);
return;
}
defaultClose(fd, callback);
};
if (!globalThis.TextDecoder) {
throw new Error("globalThis.TextDecoder is not available, polyfill required");
}
const decoder = new TextDecoder("utf-8");
const defaultWrite = fs.write.bind(fs);
fs.write = (fd, buf, offset, length, position, callback) => {
if (fd === coverFileDescriptor) {
coverageProfileContents += decoder.decode(buf);
callback(null, buf.length);
return;
}
defaultWrite(fd, buf, offset, length, position, callback);
};
}

(async() => {
const go = new Go();
overrideFS(globalThis.fs)
go.argv = [{{range $i, $item := .Args}} {{if $i}}, {{end}} "{{$item}}" {{end}}];
// The notFirst variable sets itself to true after first iteration. This is to put commas in between.
go.env = { {{ $notFirst := false }}
{{range $key, $val := .EnvMap}} {{if $notFirst}}, {{end}} {{$key}}: "{{$val}}" {{ $notFirst = true }}
{{end}} };
go.exit = goExit;
let mod, inst;
await WebAssembly.instantiateStreaming(fetch("{{.WASMFile}}"), go.importObject).then((result) => {
mod = result.module;
inst = result.instance;
}).catch((err) => {
console.error(err);
});
await go.run(inst);
document.getElementById("doneButton").disabled = false;
})();
</script>

<button id="doneButton" style="display: none;" disabled>Done</button>
</body>
</html>
16 changes: 14 additions & 2 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,8 @@ import (
)

var (
cpuProfile *string
cpuProfile *string
coverageProfile *string
)

func main() {
Expand All @@ -45,6 +46,7 @@ func main() {
}

cpuProfile = flag.String("test.cpuprofile", "", "")
coverageProfile = flag.String("test.coverprofile", "", "")

wasmFile := os.Args[1]
ext := path.Ext(wasmFile)
Expand All @@ -61,6 +63,9 @@ func main() {

passon := gentleParse(flag.CommandLine, os.Args[2:])
passon = append([]string{wasmFile}, passon...)
if *coverageProfile != "" {
passon = append(passon, "-test.coverprofile="+*coverageProfile)
}

// Need to generate a random port every time for tests in parallel to run.
l, err := net.Listen("tcp", "localhost:")
Expand All @@ -77,7 +82,7 @@ func main() {
}

// Setup web server.
handler, err := NewWASMServer(wasmFile, passon, logger)
handler, err := NewWASMServer(wasmFile, passon, *coverageProfile, logger)
if err != nil {
logger.Fatal(err)
}
Expand Down Expand Up @@ -117,10 +122,12 @@ func main() {
}
done <- struct{}{}
}()
var coverageProfileContents string
tasks := []chromedp.Action{
chromedp.Navigate(`http://localhost:` + port),
chromedp.WaitEnabled(`#doneButton`),
chromedp.Evaluate(`exitCode;`, &exitCode),
chromedp.Evaluate(`coverageProfileContents;`, &coverageProfileContents),
}
if *cpuProfile != "" {
// Prepend and append profiling tasks
Expand Down Expand Up @@ -152,6 +159,11 @@ func main() {
return WriteProfile(profile, outF, funcMap)
}))
}
if *coverageProfile != "" {
tasks = append(tasks, chromedp.ActionFunc(func(ctx context.Context) error {
return os.WriteFile(*coverageProfile, []byte(coverageProfileContents), 0644)
}))
}

err = chromedp.Run(ctx, tasks...)
if err != nil {
Expand Down

0 comments on commit bba38b6

Please sign in to comment.