You've already forked uutils.github.io
mirror of
https://github.com/uutils/uutils.github.io.git
synced 2026-06-10 16:12:28 -07:00
77efde6da3
grep ships as its own standalone WASM module (not part of the coreutils multicall binary) and depends on the Oniguruma C library, so the website workflow now checks out uutils/grep, installs the WASI SDK to provide a wasm sysroot for the onig_sys C sources, builds grep.wasm, and appends "grep" to the generated command list. The terminal loads grep.wasm as an optional second module and dispatches grep directly (argv[0] = "grep"). grep defaults to --color=never since the WASI shim reports stdout as a TTY, which would otherwise emit match- highlight escapes that corrupt piped output; explicit --color is honored. Adds a grep emoji example and grep WASM integration tests.
577 lines
18 KiB
HTML
577 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, executeCommandLine, resolvePath, lookupDir, getPersistentDir, readVirtualFile, writeVirtualFile, 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");
|
|
|
|
// Helper: extract just args arrays from parsed pipeline for simpler assertions
|
|
const argsOf = (p) => p.map(s => s.args);
|
|
|
|
assertDeep("simple command",
|
|
argsOf(parseCommandLine("echo hello")),
|
|
[["echo", "hello"]]);
|
|
|
|
assertDeep("multiple args",
|
|
argsOf(parseCommandLine("wc -l fruits.txt")),
|
|
[["wc", "-l", "fruits.txt"]]);
|
|
|
|
assertDeep("pipe",
|
|
argsOf(parseCommandLine("echo hello | wc -c")),
|
|
[["echo", "hello"], ["wc", "-c"]]);
|
|
|
|
assertDeep("double pipe",
|
|
argsOf(parseCommandLine("cat file | sort | uniq")),
|
|
[["cat", "file"], ["sort"], ["uniq"]]);
|
|
|
|
assertDeep("single quotes",
|
|
argsOf(parseCommandLine("echo 'hello world'")),
|
|
[["echo", "hello world"]]);
|
|
|
|
assertDeep("double quotes",
|
|
argsOf(parseCommandLine('echo "hello world"')),
|
|
[["echo", "hello world"]]);
|
|
|
|
assertDeep("escaped space",
|
|
argsOf(parseCommandLine("echo hello\\ world")),
|
|
[["echo", "hello world"]]);
|
|
|
|
assertDeep("mixed quotes",
|
|
argsOf(parseCommandLine("echo 'it\"s' \"a test\"")),
|
|
[["echo", 'it"s', "a test"]]);
|
|
|
|
assertDeep("empty input",
|
|
argsOf(parseCommandLine("")),
|
|
[[]]);
|
|
|
|
assertDeep("multiple spaces",
|
|
argsOf(parseCommandLine("echo hello world")),
|
|
[["echo", "hello", "world"]]);
|
|
|
|
assertDeep("pipe inside single quotes is literal",
|
|
argsOf(parseCommandLine("echo 'a|b'")),
|
|
[["echo", "a|b"]]);
|
|
|
|
assertDeep("pipe inside double quotes is literal",
|
|
argsOf(parseCommandLine('echo "a|b"')),
|
|
[["echo", "a|b"]]);
|
|
|
|
// Redirection parsing
|
|
let p = parseCommandLine("sort < input.txt");
|
|
assertDeep("stdin redirect args", p[0].args, ["sort"]);
|
|
assert("stdin redirect file", p[0].stdin, "input.txt");
|
|
|
|
p = parseCommandLine("echo hello > output.txt");
|
|
assertDeep("stdout redirect args", p[0].args, ["echo", "hello"]);
|
|
assert("stdout redirect file", p[0].stdout, "output.txt");
|
|
assert("stdout redirect not append", p[0].append, false);
|
|
|
|
p = parseCommandLine("echo hello >> output.txt");
|
|
assert("append redirect file", p[0].stdout, "output.txt");
|
|
assert("append redirect flag", p[0].append, true);
|
|
|
|
p = parseCommandLine("sort < in.txt | head -3 > out.txt");
|
|
assert("pipe + redirect: stdin", p[0].stdin, "in.txt");
|
|
assertDeep("pipe + redirect: first args", p[0].args, ["sort"]);
|
|
assert("pipe + redirect: stdout", p[1].stdout, "out.txt");
|
|
assertDeep("pipe + redirect: second args", p[1].args, ["head", "-3"]);
|
|
|
|
// ===== 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");
|
|
}
|
|
|
|
// ===== grep (standalone WASM module) =====
|
|
if (T.grepReady) {
|
|
section("grep WASM module");
|
|
|
|
assert("grep in AVAILABLE_COMMANDS",
|
|
AVAILABLE_COMMANDS.includes("grep"), true);
|
|
|
|
await assertAsync("grep from file",
|
|
executeCommandLine("grep Alice names.txt"),
|
|
"Alice\n");
|
|
|
|
await assertAsync("grep -i from stdin",
|
|
executeCommandLine("echo 'APPLE\npear\nApricot' | grep -i ap"),
|
|
"APPLE\nApricot\n");
|
|
|
|
await assertAsync("grep regex via pipe",
|
|
executeCommandLine("echo 'foo1\nbar\nfoo2' | grep 'foo[0-9]'"),
|
|
"foo1\nfoo2\n");
|
|
} else {
|
|
section("grep WASM module (SKIPPED - grep.wasm not loaded)");
|
|
}
|
|
|
|
} 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);
|
|
|
|
// ===== I/O redirection =====
|
|
section("I/O redirection");
|
|
|
|
// > creates a file
|
|
await executeCommandLine("echo 'hello redirect' > redir_test.txt");
|
|
assert("> creates file",
|
|
readVirtualFile("redir_test.txt"), "hello redirect\n");
|
|
|
|
// < reads from file
|
|
await assertAsync("< reads file as stdin",
|
|
executeCommandLine("wc -c < redir_test.txt"),
|
|
"15\n");
|
|
|
|
// >> appends
|
|
await executeCommandLine("echo 'line2' >> redir_test.txt");
|
|
assert(">> appends to file",
|
|
readVirtualFile("redir_test.txt"), "hello redirect\nline2\n");
|
|
|
|
// > overwrites
|
|
await executeCommandLine("echo 'overwritten' > redir_test.txt");
|
|
assert("> overwrites file",
|
|
readVirtualFile("redir_test.txt"), "overwritten\n");
|
|
|
|
// pipe + redirect
|
|
await executeCommandLine("echo '3\n1\n2' | sort > sorted.txt");
|
|
assert("pipe | sort > file",
|
|
readVirtualFile("sorted.txt"), "1\n2\n3\n");
|
|
|
|
// < with nonexistent file
|
|
await assertAsync("< nonexistent file",
|
|
executeCommandLine("sort < no_such_file.txt"),
|
|
"no_such_file.txt: No such file\n");
|
|
|
|
// ===== 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>
|