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: add browser test harness #563

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
5 changes: 5 additions & 0 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,11 @@ jobs:
TARGET_TRIPLE=wasm32-wasip2 make test
TARGET_TRIPLE=wasm32-wasi-threads make test
TARGET_TRIPLE=wasm32-wasip1-threads make test

npm -C scripts/browser-test install
npx -C scripts/browser-test playwright install chromium-headless-shell
ENGINE="$PWD/scripts/browser-test/harness.mjs" TARGET_TRIPLE=wasm32-wasip1 make test
ENGINE="$PWD/scripts/browser-test/harness.mjs" TARGET_TRIPLE=wasm32-wasip1-threads make test
# The older version of Clang does not provide the expected symbol for the
# test entrypoints: `undefined symbol: __main_argc_argv`.
# The older (<15.0.7) version of wasm-ld does not provide `__heap_end`,
Expand Down
1 change: 1 addition & 0 deletions test/.gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
build
run
node_modules
10 changes: 10 additions & 0 deletions test/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,16 @@ fs # a directory containing any test-created files
output.log # the captured printed output--only for errors
```

### Running tests in the browser

To run a test in the browser, use the `scripts/browser-test/harness.mjs` as `ENGINE`

```sh
$ npm -C scripts/browser-test install
$ npx -C scripts/browser-test playwright install chromium-headless-shell
$ make ENGINE="$PWD/scripts/browser-test/harness.mjs" TARGET_TRIPLE=...
```

### Adding tests

To add a test, create a new C file in [`src/misc`]:
Expand Down
172 changes: 172 additions & 0 deletions test/scripts/browser-test/harness.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
#!/usr/bin/env node

/*
* Run a WASI-compatible test program in the browser.
*
* This script behaves like `wasmtime` but runs given WASI-compatible test
* program in the browser.
*
* Example:
* $ ./harness.mjs check.wasm
*/

import { parseArgs } from 'node:util';
import { createServer } from 'node:http';
import { fileURLToPath } from 'node:url';
import { dirname, join } from 'node:path';
import { readFileSync } from 'node:fs';
import url from "node:url";
import { chromium } from 'playwright';

const SKIP_TESTS = [
// "poll_oneoff" can't be implemented in the browser
"libc-test/functional/pthread_cond",
// atomic.wait32 can't be executed on the main thread
"libc-test/functional/pthread_mutex",
"libc-test/functional/pthread_tsd",
// XFAIL: @bjorn3/browser_wasi_shim doesn't support symlinks for now
"misc/fts",
];

/**
* @param {{wasmPath: string, port: number}}
* @returns {Promise<{server: import('node:http').Server, port: number}>}
*/
async function startServer({ wasmPath, port }) {
const server = createServer((req, res) => {
// Set required headers for SharedArrayBuffer
res.setHeader('Cross-Origin-Opener-Policy', 'same-origin');
res.setHeader('Cross-Origin-Embedder-Policy', 'require-corp');

let filePath;
const parsed = url.parse(req.url, true);
const pathname = parsed.pathname;
if (pathname === "/target.wasm") {
// Serve the test target Wasm file
filePath = wasmPath;
res.setHeader('Content-Type', 'application/wasm');
} else {
// Serve other resources
const __dirname = dirname(fileURLToPath(import.meta.url));
filePath = join(__dirname, pathname);
const contentTypes = {
"mjs": "text/javascript",
"js": "text/javascript",
"html": "text/html",
}
res.setHeader('Content-Type', contentTypes[pathname.split('.').pop()] || 'text/plain');
}

try {
const content = readFileSync(filePath);
res.end(content);
} catch (error) {
res.statusCode = 404;
res.end('Not found');
}
});

return new Promise((resolve) => {
server.listen(port, () => {
const port = server.address().port;
resolve({ server, port });
});
});
}

/** @param {number} port */
function buildUrl(port) {
return `http://localhost:${port}/run-test.html`;
}

/** @param {import('playwright').Page} page */
/** @param {number} port */
/** @returns {Promise<{passed: boolean, error?: string}>} */
async function runTest(page, port) {
const url = buildUrl(port);
const onExit = new Promise((resolve) => {
page.exposeFunction("exitTest", resolve);
});
await page.goto(url);
return onExit;
}

async function main() {
// Parse and interpret a subset of the wasmtime CLI options used by the tests
const args = parseArgs({
args: process.argv.slice(2),
allowPositionals: true,
options: {
// MARK: wasmtime CLI options
wasi: {
type: "string",
multiple: true,
},
dir: {
type: "string",
multiple: true,
},
// MARK: For debugging purposes
headful: {
type: "boolean",
default: false,
},
port: {
type: "string",
default: "0",
}
}
});

const wasmPath = args.positionals[0];
if (!wasmPath) {
console.error('Test path not specified');
return 1;
}

if (SKIP_TESTS.some(test => wasmPath.includes(test + "."))) {
// Silently skip tests that are known to fail in the browser
return 0;
}

if (args.values.dir && args.values.dir.length > 0) {
// Silently skip tests that require preopened directories for now
// as it adds more complexity to the harness and file system emulation
// is not our primary testing target.
return 0;
}

// Start a HTTP server to serve the test files
const { server, port } = await startServer({ wasmPath, port: parseInt(args.values.port) });

const browser = await chromium.launch();
const page = await browser.newPage();

try {
if (args.values.headful) {
// Run in headful mode to allow manual testing
console.log(`Please visit ${buildUrl(port)}`);
console.log('Press Ctrl+C to stop');
await new Promise(resolve => process.on('SIGINT', resolve));
return 0;
}

// Run in headless mode
const result = await runTest(page, port);
if (!result.passed) {
console.error('Test failed:', result.error);
console.error(`Hint: You can debug the test by running it in headful mode by passing --headful
$ ${process.argv.join(' ')} --headful`);
return 1;
}
return 0;
} catch (error) {
console.error('Test failed:', error);
return 1;
} finally {
await browser.close();
server.close();
}
}

process.exit(await main());
75 changes: 75 additions & 0 deletions test/scripts/browser-test/package-lock.json

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

5 changes: 5 additions & 0 deletions test/scripts/browser-test/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"dependencies": {
"playwright": "^1.49.1"
}
}
38 changes: 38 additions & 0 deletions test/scripts/browser-test/run-test.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
<!DOCTYPE html>
<html>

<head>
<title>wasi-libc Browser Tests</title>
</head>

<body>
<h1>wasi-libc Browser Tests</h1>
<div id="results"></div>
<script type="module">
import { runWasmTest } from "./run-test.mjs";
function exitTest(result) {
if (typeof window.exitTest === 'function') {
window.exitTest(result);
}
}
async function runTests() {
const resultsDiv = document.getElementById('results');

try {
const passed = await runWasmTest("target.wasm");
resultsDiv.innerHTML = passed ?
'<p style="color: green">Test passed</p>' :
'<p style="color: red">Test failed</p>';
exitTest({ passed });
} catch (error) {
console.error(error);
resultsDiv.innerHTML = `<p style="color: red">Error: ${error.message}</p>`;
exitTest({ passed: false, error: error.message });
}
}

runTests();
</script>
</body>

</html>
Loading
Loading