Files
uutils.github.io/static/js/wasm-terminal.test.html
T
Sylvestre Ledru 17e38beade Playground: fix corrupted file sizes in ls output
browser_wasi_shim's Filestat.write_bytes() uses setUint8 for the
1-byte filetype field at offset 16, leaving bytes 17-23 unwritten.
Since Rust allocates the stat buffer with MaybeUninit, those padding
bytes contain stack garbage that corrupts the struct.

Fix by replacing write_bytes with a version that writes filetype as
a full 8-byte BigUint64 (zero-extended), clearing the padding.
Also add a test asserting file sizes are reasonable.
2026-04-04 11:55:54 +02:00

602 lines
18 KiB
HTML

<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>wasm-terminal unit tests</title>
<style>
body { font-family: monospace; margin: 2em; background: #1e1e2e; color: #cdd6f4; }
.pass { color: #a6e3a1; }
.fail { color: #f38ba8; font-weight: bold; }
.section { color: #f9e2af; margin-top: 1.5em; font-size: 1.1em; }
#summary { margin-top: 2em; font-size: 1.2em; padding: 0.5em; border-top: 1px solid #45475a; }
</style>
</head>
<body>
<h1>wasm-terminal unit tests</h1>
<div id="results"></div>
<div id="summary"></div>
<script src="wasm-terminal.js"></script>
<script>
(async function() {
const T = window._uutilsTestInternals;
const { parseCommandLine, jsFallback, executeCommandLine, resolvePath, lookupDir, getPersistentDir, SAMPLE_FILES, AVAILABLE_COMMANDS } = T;
let passed = 0;
let failed = 0;
const results = document.getElementById("results");
function section(name) {
const el = document.createElement("div");
el.className = "section";
el.textContent = "--- " + name + " ---";
results.appendChild(el);
}
function assert(name, actual, expected) {
const el = document.createElement("div");
if (actual === expected) {
el.className = "pass";
el.textContent = " PASS: " + name;
passed++;
} else {
el.className = "fail";
el.textContent = " FAIL: " + name + "\n expected: " + JSON.stringify(expected) + "\n actual: " + JSON.stringify(actual);
console.log("FAIL: " + name + " | expected: " + JSON.stringify(expected) + " | actual: " + JSON.stringify(actual)); // log for headless runners
failed++;
}
results.appendChild(el);
}
function assertDeep(name, actual, expected) {
const a = JSON.stringify(actual);
const e = JSON.stringify(expected);
assert(name, a, e);
}
async function assertAsync(name, promise, expected) {
const actual = await promise;
assert(name, actual, expected);
}
async function runTests() {
// Try to load WASM binary for integration tests
try {
await T.initWasm();
} catch (e) {
console.log("WASM load failed (integration tests will be skipped):", e.message);
}
// ===== parseCommandLine =====
section("parseCommandLine");
assertDeep("simple command",
parseCommandLine("echo hello"),
[["echo", "hello"]]);
assertDeep("multiple args",
parseCommandLine("wc -l fruits.txt"),
[["wc", "-l", "fruits.txt"]]);
assertDeep("pipe",
parseCommandLine("echo hello | wc -c"),
[["echo", "hello"], ["wc", "-c"]]);
assertDeep("double pipe",
parseCommandLine("cat file | sort | uniq"),
[["cat", "file"], ["sort"], ["uniq"]]);
assertDeep("single quotes",
parseCommandLine("echo 'hello world'"),
[["echo", "hello world"]]);
assertDeep("double quotes",
parseCommandLine('echo "hello world"'),
[["echo", "hello world"]]);
assertDeep("escaped space",
parseCommandLine("echo hello\\ world"),
[["echo", "hello world"]]);
assertDeep("mixed quotes",
parseCommandLine("echo 'it\"s' \"a test\""),
[["echo", 'it"s', "a test"]]);
assertDeep("empty input",
parseCommandLine(""),
[[]]);
assertDeep("multiple spaces",
parseCommandLine("echo hello world"),
[["echo", "hello", "world"]]);
assertDeep("pipe inside single quotes is literal",
parseCommandLine("echo 'a|b'"),
[["echo", "a|b"]]);
assertDeep("pipe inside double quotes is literal",
parseCommandLine('echo "a|b"'),
[["echo", "a|b"]]);
// ===== jsFallback: echo =====
section("jsFallback: echo");
assert("echo simple",
jsFallback(["echo", "hello"], ""),
"hello\n");
assert("echo multiple words",
jsFallback(["echo", "hello", "world"], ""),
"hello world\n");
assert("echo empty",
jsFallback(["echo"], ""),
"\n");
// ===== jsFallback: cat =====
section("jsFallback: cat");
assert("cat existing file",
jsFallback(["cat", "names.txt"], ""),
SAMPLE_FILES["names.txt"]);
assert("cat missing file",
jsFallback(["cat", "nope.txt"], ""),
"cat: nope.txt: No such file or directory\n");
assert("cat with stdin passthrough",
jsFallback(["cat"], "piped input"),
"piped input");
// ===== jsFallback: ls =====
section("jsFallback: ls");
assert("ls in AVAILABLE_COMMANDS (WASM)",
AVAILABLE_COMMANDS.includes("ls"), true);
assert("ls fallback still works",
jsFallback(["ls"], ""),
Object.keys(SAMPLE_FILES).join(" ") + "\n");
// ===== jsFallback: seq =====
section("jsFallback: seq");
assert("seq 1 to 5",
jsFallback(["seq", "5"], ""),
"1\n2\n3\n4\n5\n");
assert("seq 3 to 6",
jsFallback(["seq", "3", "6"], ""),
"3\n4\n5\n6\n");
assert("seq with step",
jsFallback(["seq", "1", "2", "7"], ""),
"1\n3\n5\n7\n");
// ===== jsFallback: factor =====
section("jsFallback: factor");
assert("factor 12",
jsFallback(["factor"], "12"),
"12: 2 2 3\n");
assert("factor prime",
jsFallback(["factor"], "17"),
"17: 17\n");
assert("factor multiple lines",
jsFallback(["factor"], "6\n10"),
"6: 2 3\n10: 2 5\n");
// ===== jsFallback: basename =====
section("jsFallback: basename");
assert("basename path",
jsFallback(["basename", "/usr/local/bin/rustc"], ""),
"rustc\n");
assert("basename simple",
jsFallback(["basename", "file.txt"], ""),
"file.txt\n");
// ===== jsFallback: wc =====
section("jsFallback: wc");
assert("wc -l",
jsFallback(["wc", "-l", "names.txt"], ""),
"8\n");
assert("wc -w",
jsFallback(["wc", "-w", "names.txt"], ""),
"8\n");
// ===== jsFallback: unknown command =====
section("jsFallback: unknown");
assert("unknown command returns null",
jsFallback(["nonexistent"], ""),
null);
assert("WASM command without WASM returns loading message",
jsFallback(["date"], "").includes("WASM binary loading"),
true);
// ===== executeCommandLine: builtins =====
section("executeCommandLine: builtins");
await assertAsync("empty line",
executeCommandLine(""), "");
await assertAsync("whitespace only",
executeCommandLine(" "), "");
const helpOutput = await executeCommandLine("help");
assert("help contains Available",
helpOutput.includes("Available commands"), true);
assert("help contains examples",
helpOutput.includes("Examples"), true);
await assertAsync("clear returns empty",
executeCommandLine("clear"), "");
// ls output format depends on whether WASM is loaded (WASM ls uses tabs
// and sorts alphabetically; JS fallback joins with double spaces in
// insertion order). Just verify all sample files appear.
const lsOutput = await executeCommandLine("ls");
for (const f of Object.keys(SAMPLE_FILES)) {
assert("ls contains " + f, lsOutput.includes(f), true);
}
const lsDotOutput = await executeCommandLine("ls .");
for (const f of Object.keys(SAMPLE_FILES)) {
assert("ls . contains " + f, lsDotOutput.includes(f), true);
}
// ===== executeCommandLine: JS fallback commands =====
section("executeCommandLine: JS fallback commands");
await assertAsync("echo via execute",
executeCommandLine("echo test"),
"test\n");
await assertAsync("seq via execute",
executeCommandLine("seq 3"),
"1\n2\n3\n");
await assertAsync("cat via execute",
executeCommandLine("cat names.txt"),
SAMPLE_FILES["names.txt"]);
// ===== executeCommandLine: pipes =====
section("executeCommandLine: pipes");
await assertAsync("echo | wc -w",
executeCommandLine("echo one two three | wc -w"),
"3\n");
await assertAsync("seq | factor",
executeCommandLine("seq 4 | factor"),
"1:\n2: 2\n3: 3\n4: 2 2\n");
// ===== AVAILABLE_COMMANDS: sort =====
section("sort availability");
assert("sort in AVAILABLE_COMMANDS",
AVAILABLE_COMMANDS.includes("sort"), true);
// ===== executeCommandLine: unknown command =====
section("executeCommandLine: error handling");
const notFound = await executeCommandLine("foobar");
assert("unknown command error",
notFound.includes("command not found"), true);
// ===== AVAILABLE_COMMANDS sanity =====
section("AVAILABLE_COMMANDS");
assert("echo in AVAILABLE_COMMANDS",
AVAILABLE_COMMANDS.includes("echo"), true);
assert("wc in AVAILABLE_COMMANDS",
AVAILABLE_COMMANDS.includes("wc"), true);
assert("seq in AVAILABLE_COMMANDS",
AVAILABLE_COMMANDS.includes("seq"), true);
assert("ls in AVAILABLE_COMMANDS",
AVAILABLE_COMMANDS.includes("ls"), true);
// ===== SAMPLE_FILES sanity =====
section("SAMPLE_FILES");
assert("names.txt exists",
SAMPLE_FILES["names.txt"] !== undefined, true);
assert("names.txt has content",
SAMPLE_FILES["names.txt"].includes("Alice"), true);
assert("numbers.txt has numbers",
SAMPLE_FILES["numbers.txt"].includes("42"), true);
assert("fruits.txt has fruits",
SAMPLE_FILES["fruits.txt"].includes("banana"), true);
// ===== resolvePath =====
section("resolvePath");
T.cwd = "";
assert("resolve simple file at root",
resolvePath("foo.txt"), "foo.txt");
assert("resolve . at root",
resolvePath("."), ".");
assert("resolve subdir/file at root",
resolvePath("sub/file.txt"), "sub/file.txt");
assert("resolve flags unchanged",
resolvePath("-n"), "-n");
assert("resolve empty unchanged",
resolvePath(""), "");
T.cwd = "mydir";
assert("resolve file in cwd",
resolvePath("foo.txt"), "mydir/foo.txt");
assert("resolve .. from cwd",
resolvePath(".."), ".");
assert("resolve ../other from cwd",
resolvePath("../other"), "other");
assert("resolve sub/file from cwd",
resolvePath("sub/file"), "mydir/sub/file");
assert("resolve . from cwd",
resolvePath("."), "mydir");
T.cwd = "a/b/c";
assert("resolve ../../x from deep cwd",
resolvePath("../../x"), "a/x");
assert("resolve ../../../ from deep cwd",
resolvePath("../../.."), ".");
T.cwd = "";
// ===== cd builtin =====
section("cd builtin");
T.cwd = "";
// cd to nonexistent dir uses lookupDir which needs wasiShim.
// When WASM isn't loaded, cd falls back to checking the JS SAMPLE_FILES
// which don't have directories, so all cd <dir> should fail.
const cdResult = await executeCommandLine("cd nonexistent");
assert("cd to nonexistent dir",
cdResult.includes("No such directory"), true);
assert("cwd unchanged after failed cd",
T.cwd, "");
await assertAsync("cd with no args stays at root",
executeCommandLine("cd"), "");
assert("cwd is root after cd",
T.cwd, "");
await assertAsync("cd / goes to root",
executeCommandLine("cd /"), "");
assert("cwd is root after cd /",
T.cwd, "");
await assertAsync("cd ~ goes to root",
executeCommandLine("cd ~"), "");
assert("cwd is root after cd ~",
T.cwd, "");
// ===== pwd builtin =====
section("pwd builtin");
T.cwd = "";
await assertAsync("pwd at root",
executeCommandLine("pwd"), "/\n");
T.cwd = "mydir";
await assertAsync("pwd in subdir",
executeCommandLine("pwd"), "/mydir\n");
T.cwd = "a/b";
await assertAsync("pwd in deep subdir",
executeCommandLine("pwd"), "/a/b\n");
T.cwd = "";
// ===== locale builtin =====
section("locale builtin");
T.locale = "en-US";
await assertAsync("locale shows current",
executeCommandLine("locale"), "LANG=en-US.UTF-8\n");
await assertAsync("locale set full form",
executeCommandLine("locale fr-FR"), "Locale set to fr-FR\n");
assert("locale changed to fr-FR",
T.locale, "fr-FR");
await assertAsync("locale set shortcut",
executeCommandLine("locale de"), "Locale set to de-DE\n");
assert("locale changed to de-DE",
T.locale, "de-DE");
await assertAsync("locale set shortcut ja",
executeCommandLine("locale ja"), "Locale set to ja-JP\n");
assert("locale changed to ja-JP",
T.locale, "ja-JP");
// Unknown shortcut passes through as-is
await assertAsync("locale unknown passes through",
executeCommandLine("locale xx-YY"), "Locale set to xx-YY\n");
assert("locale set to xx-YY",
T.locale, "xx-YY");
T.locale = "en-US";
// ===== l10n WASM integration (only when WASM binary is loaded) =====
if (T.wasmReady) {
section("l10n WASM integration");
T.locale = "en-US";
const enHelp = await executeCommandLine("arch --help");
assert("arch --help in English contains English text",
enHelp.includes("Display machine architecture"), true);
T.locale = "fr-FR";
const frHelp = await executeCommandLine("arch --help");
assert("arch --help in French contains French text",
frHelp.includes("Afficher l'architecture de la machine"), true);
T.locale = "en-US";
// ===== ls fixes =====
section("ls WASI fixes");
const lsAl = await executeCommandLine("ls -al");
assert("ls -al does not contain 'Capabilities insufficient'",
lsAl.includes("Capabilities insufficient"), false);
assert("ls -al shows '..' entry",
lsAl.includes(".."), true);
assert("ls -al does not show 1970 date",
lsAl.includes("1970"), false);
// File sizes should be reasonable (< 1000 bytes for sample files)
const sizeMatch = lsAl.match(/(\d+)\s+\w+\s+\d+.*csv\.txt/);
assert("csv.txt size is reasonable",
sizeMatch && parseInt(sizeMatch[1]) < 1000, true);
// ===== UTF-8 / multibyte WASM tests =====
section("UTF-8 multibyte WASM");
const echoJp = await executeCommandLine("echo 'こんにちは世界'");
assert("echo Japanese text",
echoJp.trimEnd(), "こんにちは世界");
const b64Round = await executeCommandLine("echo 'こんにちは世界' | base64 | base64 -d");
assert("base64 round-trip Japanese",
b64Round.trimEnd(), "こんにちは世界");
const echoEmoji = await executeCommandLine("echo '🍎🍌🍒'");
assert("echo emoji",
echoEmoji.trimEnd(), "🍎🍌🍒");
const b64Emoji = await executeCommandLine("echo '🍎🍌🍒' | base64 | base64 -d");
assert("base64 round-trip emoji",
b64Emoji.trimEnd(), "🍎🍌🍒");
const echoArabic = await executeCommandLine("echo 'مرحبا'");
assert("echo Arabic text",
echoArabic.trimEnd(), "مرحبا");
// ===== WASM pipe tests =====
section("WASM pipe commands");
// Test sort reading from stdin — sort may hit "unreachable" in some
// WASM builds, so guard these tests and skip if sort traps.
const sortProbe = await executeCommandLine("echo 'b\na' | sort");
const sortWorks = sortProbe === "a\nb\n";
if (sortWorks) {
await assertAsync("echo | sort",
executeCommandLine("echo 'c\nb\na' | sort"),
"a\nb\nc\n");
await assertAsync("echo | sort -r",
executeCommandLine("echo 'a\nb\nc' | sort -r"),
"c\nb\na\n");
} else {
section("WASM sort tests (SKIPPED - sort traps in this build)");
}
// Test uniq with piped stdin
await assertAsync("echo | uniq",
executeCommandLine("echo 'a\na\nb\nb\nc' | uniq"),
"a\nb\nc\n");
// Test tail with piped stdin
await assertAsync("seq | tail -2",
executeCommandLine("seq 5 | tail -2"),
"4\n5\n");
// Test cut with piped stdin
await assertAsync("echo | cut -d: -f1",
executeCommandLine("echo 'a:b:c' | cut -d: -f1"),
"a\n");
// Test multi-command pipes (depend on sort)
if (sortWorks) {
await assertAsync("echo | sort | uniq -c",
executeCommandLine("echo 'b\na\nb\na\na' | sort | uniq -c"),
" 3 a\n 2 b\n");
await assertAsync("sort | uniq -c | sort -rn",
executeCommandLine("echo 'b\na\nb\na\na' | sort | uniq -c | sort -rn"),
" 3 a\n 2 b\n");
await assertAsync("echo csv | cut | tail | sort",
executeCommandLine("echo '1,z\n2,y\n3,x\n4,w' | cut -d, -f2 | tail -2 | sort"),
"w\nx\n");
}
} else {
section("l10n WASM integration (SKIPPED - WASM not loaded)");
}
// ===== LOCALE_SHORTCUTS =====
section("LOCALE_SHORTCUTS");
assert("fr shortcut", T.LOCALE_SHORTCUTS["fr"], "fr-FR");
assert("en shortcut", T.LOCALE_SHORTCUTS["en"], "en-US");
assert("de shortcut", T.LOCALE_SHORTCUTS["de"], "de-DE");
assert("ja shortcut", T.LOCALE_SHORTCUTS["ja"], "ja-JP");
assert("pt shortcut", T.LOCALE_SHORTCUTS["pt"], "pt-BR");
// ===== Tab completion =====
section("Tab completion");
const tc = T.tabComplete;
// Single command match
let r = tc("fac", 3);
assert("complete 'fac' -> 'factor '", r.buffer, "factor ");
assert("complete 'fac' completed", r.completed, true);
// Single filename match
r = tc("cat na", 6);
assert("complete 'cat na' -> 'cat names.txt '", r.buffer, "cat names.txt ");
assert("complete filename completed", r.completed, true);
// Multiple matches with common prefix
r = tc("sha2", 4);
assert("complete 'sha2' partial -> 'sha2'", r.buffer.startsWith("sha2"), true);
assert("complete 'sha2' has candidates", r.candidates.length > 1, true);
// No match
r = tc("zzzzz", 5);
assert("complete 'zzzzz' no match", r.candidates.length, 0);
assert("complete 'zzzzz' not completed", r.completed, false);
// Builtin completion
r = tc("hel", 3);
assert("complete 'hel' -> 'help '", r.buffer, "help ");
// Multiple command matches shown
r = tc("sh", 2);
assert("complete 'sh' multiple candidates", r.candidates.length > 1, true);
assert("complete 'sh' includes shuf", r.candidates.includes("shuf"), true);
assert("complete 'sh' includes shred", r.candidates.includes("shred"), true);
// ===== Summary =====
const summary = document.getElementById("summary");
const total = passed + failed;
if (failed === 0) {
summary.className = "pass";
summary.textContent = `All ${total} tests passed.`;
} else {
summary.className = "fail";
summary.textContent = `${failed} of ${total} tests FAILED.`;
}
// Set exit code for headless runners
window.__testsPassed = passed;
window.__testsFailed = failed;
}
await runTests();
})();
</script>
</body>
</html>