Skip to content

Commit

Permalink
test: Add browser test adapter
Browse files Browse the repository at this point in the history
  • Loading branch information
kateinoigakukun committed Dec 16, 2023
1 parent 802ca6b commit 50bc435
Show file tree
Hide file tree
Showing 10 changed files with 306 additions and 51 deletions.
4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@
"scripts": {
"build": "swc src -d dist && tsc --emitDeclarationOnly",
"prepare": "swc src -d dist && tsc --emitDeclarationOnly",
"test": "./test/run-testsuite.sh",
"test:node": "./test/run-testsuite.sh node",
"test:browser": "playwright-core install && ./test/run-testsuite.sh browser",
"test": "npm run test:node && npm run test:browser",
"check": "tsc --noEmit && prettier src -c && eslint src/"
},
"repository": {
Expand Down
1 change: 1 addition & 0 deletions test/adapters/browser/adapter.py
88 changes: 88 additions & 0 deletions test/adapters/browser/run-test.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
<html>
<script type="module">
import { WASI, OpenFile, File, Fd, Directory, PreopenDirectory, wasi } from "/dist/index.js";
class ConsoleStdout extends Fd {
constructor(write) {
super();
this.write = write;
}

fd_filestat_get() {
const filestat = new wasi.Filestat(
wasi.FILETYPE_CHARACTER_DEVICE,
BigInt(0),
);
return { ret: 0, filestat };
}

fd_fdstat_get() {
const fdstat = new wasi.Fdstat(wasi.FILETYPE_CHARACTER_DEVICE, 0);
fdstat.fs_rights_base = BigInt(wasi.RIGHTS_FD_WRITE);
return { ret: 0, fdstat };
}

fd_write(view8, iovs) {
let nwritten = 0;
for (let iovec of iovs) {
let buffer = view8.slice(iovec.buf, iovec.buf + iovec.buf_len);
this.write(buffer);
nwritten += iovec.buf_len;
}
return { ret: 0, nwritten };
}
}

async function derivePreopens(dirs) {
const rawPreopens = await window.bindingDerivePreopens(dirs)
function transform(entry) {
if (entry.kind === "dir") {
const contents = {};
for (const [name, child] of Object.entries(entry.contents)) {
contents[name] = transform(child);
}
return new Directory(contents);
} else if (entry.kind === "file") {
return new File(Uint8Array.from(entry.buffer))
} else {
throw new Error("Unknown kind: ", entry.kind, entry);
}
}
const preopens = []
for (const preopen of rawPreopens) {
const { dir, contents } = preopen;
const newContents = {};
for (const [name, child] of Object.entries(contents)) {
newContents[name] = transform(child);
}
preopens.push(new PreopenDirectory(dir, newContents));
}
return preopens;
}

window.runWASI = async (options) => {
const testFile = options["test-file"];
const args = [testFile].concat(options.arg);
const fds = [
new OpenFile(new File([])),
// Uint8Array is not [Serializable](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify#description)
// so we need to convert it to an array before passing it to Playwright.
new ConsoleStdout(bytes => window.bindingWriteIO(Array.from(bytes), "stdout")),
new ConsoleStdout(bytes => window.bindingWriteIO(Array.from(bytes), "stderr")),
];
const preopens = await derivePreopens(options.dir)
fds.push(...preopens);

const wasi = new WASI(args, options.env, fds, { debug: false });

const moduleBytes = await fetch(testFile).then(r => r.arrayBuffer());
const module = await WebAssembly.compile(moduleBytes);
const instance = await WebAssembly.instantiate(module, {
wasi_snapshot_preview1: wasi.wasiImport
});

const exitCode = wasi.start(instance);
return exitCode;
}
</script>

</html>
131 changes: 131 additions & 0 deletions test/adapters/browser/run-wasi.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
#!/usr/bin/env node

import fs from 'fs/promises';
import path from 'path';
import { chromium } from "playwright"
import { parseArgs } from "../shared/parseArgs.mjs"
import { walkFs } from "../shared/walkFs.mjs"

async function derivePreopens(dirs) {
const preopens = [];
for (let dir of dirs) {
const contents = await walkFs(dir, (name, entry, out) => {
if (entry.kind === "file") {
// Convert buffer to array to make it serializable.
entry.buffer = Array.from(entry.buffer);
}
return { ...out, [name]: entry };
}, {});
preopens.push({ dir, contents });
}
return preopens;
}

/**
* Configure routes for the browser harness.
*
* @param {import('playwright').BrowserContext} context
* @param {string} harnessURL
*/
async function configureRoutes(context, harnessURL) {

// Serve the main test page.
context.route(`${harnessURL}/run-test.html`, async route => {
const dirname = new URL(".", import.meta.url).pathname;
const body = await fs.readFile(path.join(dirname, "run-test.html"), "utf8");
route.fulfill({
status: 200,
contentType: 'text/html',
// Browsers reduce the precision of performance.now() if the page is not
// isolated. To keep the precision for `clock_get_time` we need to set the
// following headers.
// See: https://developer.mozilla.org/en-US/docs/Web/API/Performance/now#security_requirements
headers: {
"Cross-Origin-Opener-Policy": "same-origin",
"Cross-Origin-Embedder-Policy": "require-corp",
},
body,
});
})

// Serve wasi-testsuite files.
// e.g. http://browser-wasi-shim.localhost/home/me/browser_wasi_shim/test/wasi-testsuite/xxx
let projectDir = path.join(new URL("../../wasi-testsuite", import.meta.url).pathname);
projectDir = path.resolve(projectDir);
context.route(`${harnessURL}${projectDir}/**/*`, async route => {
const pathname = new URL(route.request().url()).pathname;
const relativePath = pathname.slice(pathname.indexOf(projectDir) + projectDir.length);
const content = await fs.readFile(path.join(projectDir, relativePath));
route.fulfill({
status: 200,
contentType: 'application/javascript',
body: content,
});
});

// Serve transpiled browser_wasi_shim files under ./dist.
context.route(`${harnessURL}/dist/*.js`, async route => {
const pathname = new URL(route.request().url()).pathname;
const distRelativePath = pathname.slice(pathname.indexOf("/dist/"));
const distDir = new URL("../../..", import.meta.url);
const distPath = path.join(distDir.pathname, distRelativePath);
const content = await fs.readFile(distPath);
route.fulfill({
status: 200,
contentType: 'application/javascript',
body: content,
});
});
}

async function runWASIOnBrowser(options) {
const browser = await chromium.launch();
const context = await browser.newContext();
const harnessURL = 'http://browser-wasi-shim.localhost'

await configureRoutes(context, harnessURL);

const page = await context.newPage();
// Expose stdout/stderr bindings to allow test driver to capture output.
page.exposeBinding("bindingWriteIO", (_, buffer, destination) => {
buffer = Buffer.from(buffer);
switch (destination) {
case "stdout":
process.stdout.write(buffer);
break;
case "stderr":
process.stderr.write(buffer);
break;
default:
throw new Error(`Unknown destination ${destination}`);
}
});
// Expose a way to serialize preopened directories to the browser.
page.exposeBinding("bindingDerivePreopens", async (_, dirs) => {
return await derivePreopens(dirs);
});

page.on('console', msg => console.log(msg.text()));
page.on('pageerror', ({ message }) => {
console.log('PAGE ERROR:', message)
process.exit(1); // Unexpected error.
});

await page.goto(`${harnessURL}/run-test.html`, { waitUntil: "load" })
const status = await page.evaluate(async (o) => await window.runWASI(o), options)
await page.close();
process.exit(status);
}

async function main() {
const options = parseArgs();
if (options.version) {
const pkg = JSON.parse(await fs.readFile(new URL("../../../package.json", import.meta.url)));
console.log(`${pkg.name} v${pkg.version}`);
return;
}

await runWASIOnBrowser(options);
}

await main();
1 change: 1 addition & 0 deletions test/adapters/node/adapter.py
66 changes: 17 additions & 49 deletions test/run-wasi.mjs → test/adapters/node/run-wasi.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -2,37 +2,9 @@

import fs from 'fs/promises';
import path from 'path';
import { WASI, wasi, strace, OpenFile, File, Directory, PreopenDirectory, Fd } from "../dist/index.js"

function parseArgs() {
const args = process.argv.slice(2);
const options = {
"version": false,
"test-file": null,
"arg": [],
"env": [],
"dir": [],
};
while (args.length > 0) {
const arg = args.shift();
if (arg.startsWith("--")) {
let [name, value] = arg.split("=");
name = name.slice(2);
if (Object.prototype.hasOwnProperty.call(options, name)) {
if (value === undefined) {
value = args.shift() || true;
}
if (Array.isArray(options[name])) {
options[name].push(value);
} else {
options[name] = value;
}
}
}
}

return options;
}
import { WASI, wasi, strace, OpenFile, File, Directory, PreopenDirectory, Fd } from "../../../dist/index.js"
import { parseArgs } from "../shared/parseArgs.mjs"
import { walkFs } from "../shared/walkFs.mjs"

class NodeStdout extends Fd {
constructor(out) {
Expand Down Expand Up @@ -65,26 +37,22 @@ class NodeStdout extends Fd {
}
}

async function cloneToMemfs(dir) {
const destContents = {};
const srcContents = await fs.readdir(dir, { withFileTypes: true });
for (let entry of srcContents) {
const entryPath = path.join(dir, entry.name);
if (entry.isDirectory()) {
destContents[entry.name] = new Directory(await cloneToMemfs(entryPath));
} else {
const buffer = await fs.readFile(entryPath);
const file = new File(buffer);
destContents[entry.name] = file;
}
}
return destContents;
}

async function derivePreopens(dirs) {
const preopens = [];
for (let dir of dirs) {
const contents = await cloneToMemfs(dir);
const contents = await walkFs(dir, (name, entry, out) => {
switch (entry.kind) {
case "dir":
entry = new Directory(entry.contents);
break;
case "file":
entry = new File(entry.buffer);
break;
default:
throw new Error(`Unexpected entry kind: ${entry.kind}`);
}
return { ...out, [name]: entry}
}, {})
const preopen = new PreopenDirectory(dir, contents);
preopens.push(preopen);
}
Expand Down Expand Up @@ -123,7 +91,7 @@ async function runWASI(options) {
async function main() {
const options = parseArgs();
if (options.version) {
const pkg = JSON.parse(await fs.readFile(new URL("../package.json", import.meta.url)));
const pkg = JSON.parse(await fs.readFile(new URL("../../../package.json", import.meta.url)));
console.log(`${pkg.name} v${pkg.version}`);
return;
}
Expand Down
File renamed without changes.
31 changes: 31 additions & 0 deletions test/adapters/shared/parseArgs.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
/// Parse command line arguments given by `adapter.py` through
/// `wasi-testsuite`'s test runner.
export function parseArgs() {
const args = process.argv.slice(2);
const options = {
"version": false,
"test-file": null,
"arg": [],
"env": [],
"dir": [],
};
while (args.length > 0) {
const arg = args.shift();
if (arg.startsWith("--")) {
let [name, value] = arg.split("=");
name = name.slice(2);
if (Object.prototype.hasOwnProperty.call(options, name)) {
if (value === undefined) {
value = args.shift() || true;
}
if (Array.isArray(options[name])) {
options[name].push(value);
} else {
options[name] = value;
}
}
}
}

return options;
}
27 changes: 27 additions & 0 deletions test/adapters/shared/walkFs.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import fs from 'fs/promises';
import path from 'path';

/**
* Walks a directory recursively and returns the result of combining the found entries
* using the given reducer function.
*
* @typedef {{ kind: "dir", contents: any } | { kind: "file", buffer: Buffer }} Entry
* @param {string} dir
* @param {(name: string, entry: Entry, out: any) => any} nextPartialResult
* @param {any} initial
*/
export async function walkFs(dir, nextPartialResult, initial) {
let result = { ...initial }
const srcContents = await fs.readdir(dir, { withFileTypes: true });
for (let entry of srcContents) {
const entryPath = path.join(dir, entry.name);
if (entry.isDirectory()) {
const contents = await walkFs(entryPath, nextPartialResult, initial);
result = nextPartialResult(entry.name, { kind: "dir", contents }, result);
} else {
const buffer = await fs.readFile(entryPath);
result = nextPartialResult(entry.name, { kind: "file", buffer }, result);
}
}
return result;
}
Loading

0 comments on commit 50bc435

Please sign in to comment.