Skip to content
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
19 changes: 19 additions & 0 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -42,3 +42,22 @@ jobs:

- name: run instruction tests
run: docker run --rm -w /root/elfconv/build elfconv-image "ninja test_dependencies && ctest --output-on-failure"

browser-test-aarch64:
runs-on: ubuntu-22.04-arm
name: Browser Integration Test
steps:
- name: Checkout
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4

- name: build container image
run: docker build . --build-arg ECV_AARCH64=1 -t elfconv-image

- name: run browser test
run: |
docker run --rm -w /root/elfconv elfconv-image \
"cd tests/browser && \
npm install && \
npx playwright install --with-deps chromium && \
bash build.sh && \
npx playwright test"
8 changes: 7 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -131,8 +131,14 @@ elfconv-v*

!browser/*
!release/outdir/index.html
!tests/browser/*.js
!tests/browser/*.html
!tests/browser/package.json
tests/browser/wasm-out/
tests/browser/test-results/

# AI
CLAUDE.md
SKILLS.md
skills
skills
.claude
2 changes: 1 addition & 1 deletion scripts/elfconv.sh
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ setting() {
ELFPATH=$( realpath "$1" )
ELFNAME=$( basename "$ELFPATH" )
# dir
CUR_DIR="${PWD}"
CUR_DIR="${ECV_OUT_DIR:-${PWD}}"
RUNTIME_DIR=${ROOT_DIR}/runtime
UTILS_DIR=${ROOT_DIR}/utils
BROWSER_DIR=${ROOT_DIR}/browser
Expand Down
14 changes: 14 additions & 0 deletions tests/browser/build.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
#!/usr/bin/env bash
set -e

SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
ROOT_DIR="$(cd "${SCRIPT_DIR}/../.." && pwd)"
OUT_DIR="${SCRIPT_DIR}/wasm-out"

mkdir -p "${OUT_DIR}"

cd "${ROOT_DIR}/build"
TARGET=aarch64-wasm INITWASM=1 ECV_OUT_DIR="${OUT_DIR}" \
"${ROOT_DIR}/scripts/dev.sh" "${ROOT_DIR}/examples/hello/c/hello_stripped"

echo "Browser Wasm artifacts built in ${OUT_DIR}"
28 changes: 28 additions & 0 deletions tests/browser/hello.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
const { test, expect } = require('@playwright/test');

test('hello world ELF runs correctly in browser', async ({ page }) => {
const consoleLogs = [];
page.on('console', msg => consoleLogs.push(`[${msg.type()}] ${msg.text()}`));
page.on('pageerror', err => consoleLogs.push(`PAGE ERROR: ${err.message}`));

await page.goto('/');

const output = await page.waitForFunction(() => {
const xterm = window.__test_xterm;
if (!xterm) return false;
const buf = xterm.buffer.active;
let text = '';
for (let i = 0; i <= buf.cursorY + buf.baseY; i++) {
const line = buf.getLine(i);
if (line) {
text += line.translateToString(true) + '\n';
}
}
if (text.includes('Hello, World!')) return text;
return false;
}, null, { timeout: 30000 });

const text = await output.jsonValue();
console.log(`Terminal output: "${text.trim()}"`);
expect(text).toContain('Hello, World!');
});
15 changes: 15 additions & 0 deletions tests/browser/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"name": "browser",
"version": "1.0.0",
"main": "index.js",
"scripts": {
"test": "npx playwright test"
},
"keywords": [],
"author": "",
"license": "ISC",
"description": "",
"devDependencies": {
"@playwright/test": "^1.58.2"
}
}
21 changes: 21 additions & 0 deletions tests/browser/playwright.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
const { defineConfig } = require('@playwright/test');

module.exports = defineConfig({
testDir: '.',
testMatch: '*.spec.js',
timeout: 60000,
use: {
baseURL: 'http://localhost:3000',
},
webServer: {
command: 'node test-server.js',
port: 3000,
reuseExistingServer: false,
},
projects: [
{
name: 'chromium',
use: { browserName: 'chromium' },
},
],
});
52 changes: 52 additions & 0 deletions tests/browser/test-main.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
<!DOCTYPE html>
<html>

<head>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/xterm@4.17.0/css/xterm.css" />
</head>

<body>
<div id="terminal"></div>
<script src="coi-serviceworker.js"></script>
<script type="module">
import 'https://cdn.jsdelivr.net/npm/xterm@4.17.0/lib/xterm.min.js';
import 'https://cdn.jsdelivr.net/npm/xterm-pty@0.10.1/index.js';
import initEmscripten from './js-kernel.js';

var xterm = new Terminal();
xterm.open(document.getElementById('terminal'));

window.__test_xterm = xterm;
window.__test_done = false;
window.__test_output = '';

const { master, slave } = openpty();

var binList = ["hello_stripped"];

xterm.loadAddon(master);

xterm.onData(() => { });
xterm.onLineFeed(() => {
const buf = xterm.buffer.active;
let text = '';
for (let i = 0; i <= buf.cursorY + buf.baseY; i++) {
const line = buf.getLine(i);
if (line) {
text += line.translateToString(true) + '\n';
}
}
window.__test_output = text;
});

await initEmscripten({
pty: slave,
initProgram: 'hello_stripped.wasm',
executables: binList,
});

window.__test_done = true;
</script>
</body>

</html>
40 changes: 40 additions & 0 deletions tests/browser/test-server.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
const http = require('http');
const fs = require('fs');
const path = require('path');

const PORT = process.env.TEST_PORT || 3000;
const SERVE_DIR = process.env.SERVE_DIR || path.resolve(__dirname, 'wasm-out');

const MIME_TYPES = {
'.html': 'text/html',
'.js': 'application/javascript',
'.mjs': 'application/javascript',
'.wasm': 'application/wasm',
'.css': 'text/css',
'.json': 'application/json',
};

const TEST_HTML = path.resolve(__dirname, 'test-main.html');

const server = http.createServer((req, res) => {
const filePath = req.url === '/' ? TEST_HTML : path.join(SERVE_DIR, req.url);
const ext = path.extname(filePath);
const contentType = MIME_TYPES[ext] || 'application/octet-stream';

res.setHeader('Cross-Origin-Opener-Policy', 'same-origin');
res.setHeader('Cross-Origin-Embedder-Policy', 'credentialless');

fs.readFile(filePath, (err, data) => {
if (err) {
res.writeHead(404);
res.end('Not Found');
return;
}
res.writeHead(200, { 'Content-Type': contentType });
res.end(data);
});
});

server.listen(PORT, () => {
console.log(`Test server running at http://localhost:${PORT} serving ${SERVE_DIR}`);
});