Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

test: Setup browser test adapter for wasi-testsuite #62

Merged
merged 2 commits into from
Dec 19, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 45 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 4 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 All @@ -32,6 +34,7 @@
"@typescript-eslint/eslint-plugin": "^6.7.4",
"@typescript-eslint/parser": "^6.7.4",
"eslint": "^8.50.0",
"playwright": "^1.40.1",
"prettier": "^3.0.3",
"typescript": "^4.9.5"
}
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 => {
bjorn3 marked this conversation as resolved.
Show resolved Hide resolved
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
Loading