Skip to content

Commit bba38b6

Browse files
authored
Add test coverage support (#41)
* 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"
1 parent 8753b3a commit bba38b6

File tree

4 files changed

+139
-77
lines changed

4 files changed

+139
-77
lines changed

go.mod

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
module github.com/agnivade/wasmbrowsertest
22

3-
go 1.12
3+
go 1.16
44

55
require (
66
github.com/chromedp/cdproto v0.0.0-20221108233440-fad8339618ab

handler.go

+25-74
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package main
22

33
import (
4+
_ "embed"
45
"html/template"
56
"io/ioutil"
67
"log"
@@ -14,22 +15,27 @@ import (
1415
"time"
1516
)
1617

18+
//go:embed index.html
19+
var indexHTML string
20+
1721
type wasmServer struct {
18-
indexTmpl *template.Template
19-
wasmFile string
20-
wasmExecJS []byte
21-
args []string
22-
envMap map[string]string
23-
logger *log.Logger
22+
indexTmpl *template.Template
23+
wasmFile string
24+
wasmExecJS []byte
25+
args []string
26+
coverageFile string
27+
envMap map[string]string
28+
logger *log.Logger
2429
}
2530

26-
func NewWASMServer(wasmFile string, args []string, l *log.Logger) (http.Handler, error) {
31+
func NewWASMServer(wasmFile string, args []string, coverageFile string, l *log.Logger) (http.Handler, error) {
2732
var err error
2833
srv := &wasmServer{
29-
wasmFile: wasmFile,
30-
args: args,
31-
logger: l,
32-
envMap: make(map[string]string),
34+
wasmFile: wasmFile,
35+
args: args,
36+
coverageFile: coverageFile,
37+
logger: l,
38+
envMap: make(map[string]string),
3339
}
3440

3541
for _, env := range os.Environ() {
@@ -56,13 +62,15 @@ func (ws *wasmServer) ServeHTTP(w http.ResponseWriter, r *http.Request) {
5662
case "/", "/index.html":
5763
w.Header().Set("Content-Type", "text/html; charset=UTF-8")
5864
data := struct {
59-
WASMFile string
60-
Args []string
61-
EnvMap map[string]string
65+
WASMFile string
66+
Args []string
67+
CoverageFile string
68+
EnvMap map[string]string
6269
}{
63-
WASMFile: filepath.Base(ws.wasmFile),
64-
Args: ws.args,
65-
EnvMap: ws.envMap,
70+
WASMFile: filepath.Base(ws.wasmFile),
71+
Args: ws.args,
72+
CoverageFile: ws.coverageFile,
73+
EnvMap: ws.envMap,
6674
}
6775
err := ws.indexTmpl.Execute(w, data)
6876
if err != nil {
@@ -89,60 +97,3 @@ func (ws *wasmServer) ServeHTTP(w http.ResponseWriter, r *http.Request) {
8997
}
9098
}
9199
}
92-
93-
const indexHTML = `<!doctype html>
94-
<!--
95-
Copyright 2018 The Go Authors. All rights reserved.
96-
Use of this source code is governed by a BSD-style
97-
license that can be found in the LICENSE file.
98-
-->
99-
<html>
100-
101-
<head>
102-
<meta charset="utf-8">
103-
<title>Go wasm</title>
104-
</head>
105-
106-
<body>
107-
<!--
108-
Add the following polyfill for Microsoft Edge 17/18 support:
109-
<script src="https://cdn.jsdelivr.net/npm/[email protected]/lib/encoding.min.js"></script>
110-
(see https://caniuse.com/#feat=textencoder)
111-
-->
112-
<script src="wasm_exec.js"></script>
113-
<script>
114-
if (!WebAssembly.instantiateStreaming) { // polyfill
115-
WebAssembly.instantiateStreaming = async (resp, importObject) => {
116-
const source = await (await resp).arrayBuffer();
117-
return await WebAssembly.instantiate(source, importObject);
118-
};
119-
}
120-
121-
let exitCode = 0;
122-
function goExit(code) {
123-
exitCode = code;
124-
}
125-
126-
(async() => {
127-
const go = new Go();
128-
go.argv = [{{range $i, $item := .Args}} {{if $i}}, {{end}} "{{$item}}" {{end}}];
129-
// The notFirst variable sets itself to true after first iteration. This is to put commas in between.
130-
go.env = { {{ $notFirst := false }}
131-
{{range $key, $val := .EnvMap}} {{if $notFirst}}, {{end}} {{$key}}: "{{$val}}" {{ $notFirst = true }}
132-
{{end}} };
133-
go.exit = goExit;
134-
let mod, inst;
135-
await WebAssembly.instantiateStreaming(fetch("{{.WASMFile}}"), go.importObject).then((result) => {
136-
mod = result.module;
137-
inst = result.instance;
138-
}).catch((err) => {
139-
console.error(err);
140-
});
141-
await go.run(inst);
142-
document.getElementById("doneButton").disabled = false;
143-
})();
144-
</script>
145-
146-
<button id="doneButton" style="display: none;" disabled>Done</button>
147-
</body>
148-
</html>`

index.html

+99
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
<!doctype html>
2+
<!--
3+
Copyright 2018 The Go Authors. All rights reserved.
4+
Use of this source code is governed by a BSD-style
5+
license that can be found in the LICENSE file.
6+
-->
7+
<html>
8+
9+
<head>
10+
<meta charset="utf-8">
11+
<title>Go wasm</title>
12+
</head>
13+
14+
<body>
15+
<!--
16+
Add the following polyfill for Microsoft Edge 17/18 support:
17+
<script src="https://cdn.jsdelivr.net/npm/[email protected]/lib/encoding.min.js"></script>
18+
(see https://caniuse.com/#feat=textencoder)
19+
-->
20+
<script src="wasm_exec.js"></script>
21+
<script>
22+
if (!WebAssembly.instantiateStreaming) { // polyfill
23+
WebAssembly.instantiateStreaming = async (resp, importObject) => {
24+
const source = await (await resp).arrayBuffer();
25+
return await WebAssembly.instantiate(source, importObject);
26+
};
27+
}
28+
29+
let exitCode = 0;
30+
function goExit(code) {
31+
exitCode = code;
32+
}
33+
function enosys() {
34+
const err = new Error("not implemented");
35+
err.code = "ENOSYS";
36+
return err;
37+
}
38+
let coverageProfileContents = "";
39+
function overrideFS(fs) {
40+
// A typical runtime opens fd's in sequence above the standard descriptors (0-2).
41+
// Choose an arbitrarily high fd for the custom coverage file to avoid conflict with the actual runtime fd's.
42+
const coverFileDescriptor = Number.MAX_SAFE_INTEGER;
43+
const coverFilePath = {{.CoverageFile}};
44+
// Wraps the default operations with bind() to ensure internal usage of 'this' continues to work.
45+
const defaultOpen = fs.open.bind(fs);
46+
fs.open = (path, flags, mode, callback) => {
47+
if (path === coverFilePath) {
48+
callback(null, coverFileDescriptor);
49+
return;
50+
}
51+
defaultOpen(path, flags, mode, callback);
52+
};
53+
const defaultClose = fs.close.bind(fs);
54+
fs.close = (fd, callback) => {
55+
if (fd === coverFileDescriptor) {
56+
callback(null);
57+
return;
58+
}
59+
defaultClose(fd, callback);
60+
};
61+
if (!globalThis.TextDecoder) {
62+
throw new Error("globalThis.TextDecoder is not available, polyfill required");
63+
}
64+
const decoder = new TextDecoder("utf-8");
65+
const defaultWrite = fs.write.bind(fs);
66+
fs.write = (fd, buf, offset, length, position, callback) => {
67+
if (fd === coverFileDescriptor) {
68+
coverageProfileContents += decoder.decode(buf);
69+
callback(null, buf.length);
70+
return;
71+
}
72+
defaultWrite(fd, buf, offset, length, position, callback);
73+
};
74+
}
75+
76+
(async() => {
77+
const go = new Go();
78+
overrideFS(globalThis.fs)
79+
go.argv = [{{range $i, $item := .Args}} {{if $i}}, {{end}} "{{$item}}" {{end}}];
80+
// The notFirst variable sets itself to true after first iteration. This is to put commas in between.
81+
go.env = { {{ $notFirst := false }}
82+
{{range $key, $val := .EnvMap}} {{if $notFirst}}, {{end}} {{$key}}: "{{$val}}" {{ $notFirst = true }}
83+
{{end}} };
84+
go.exit = goExit;
85+
let mod, inst;
86+
await WebAssembly.instantiateStreaming(fetch("{{.WASMFile}}"), go.importObject).then((result) => {
87+
mod = result.module;
88+
inst = result.instance;
89+
}).catch((err) => {
90+
console.error(err);
91+
});
92+
await go.run(inst);
93+
document.getElementById("doneButton").disabled = false;
94+
})();
95+
</script>
96+
97+
<button id="doneButton" style="display: none;" disabled>Done</button>
98+
</body>
99+
</html>

main.go

+14-2
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,8 @@ import (
2525
)
2626

2727
var (
28-
cpuProfile *string
28+
cpuProfile *string
29+
coverageProfile *string
2930
)
3031

3132
func main() {
@@ -45,6 +46,7 @@ func main() {
4546
}
4647

4748
cpuProfile = flag.String("test.cpuprofile", "", "")
49+
coverageProfile = flag.String("test.coverprofile", "", "")
4850

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

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

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

7984
// Setup web server.
80-
handler, err := NewWASMServer(wasmFile, passon, logger)
85+
handler, err := NewWASMServer(wasmFile, passon, *coverageProfile, logger)
8186
if err != nil {
8287
logger.Fatal(err)
8388
}
@@ -117,10 +122,12 @@ func main() {
117122
}
118123
done <- struct{}{}
119124
}()
125+
var coverageProfileContents string
120126
tasks := []chromedp.Action{
121127
chromedp.Navigate(`http://localhost:` + port),
122128
chromedp.WaitEnabled(`#doneButton`),
123129
chromedp.Evaluate(`exitCode;`, &exitCode),
130+
chromedp.Evaluate(`coverageProfileContents;`, &coverageProfileContents),
124131
}
125132
if *cpuProfile != "" {
126133
// Prepend and append profiling tasks
@@ -152,6 +159,11 @@ func main() {
152159
return WriteProfile(profile, outF, funcMap)
153160
}))
154161
}
162+
if *coverageProfile != "" {
163+
tasks = append(tasks, chromedp.ActionFunc(func(ctx context.Context) error {
164+
return os.WriteFile(*coverageProfile, []byte(coverageProfileContents), 0644)
165+
}))
166+
}
155167

156168
err = chromedp.Run(ctx, tasks...)
157169
if err != nil {

0 commit comments

Comments
 (0)