diff --git a/CLAUDE.md b/CLAUDE.md index 97391f4..03e00d1 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -13,7 +13,7 @@ Use `docs/README.md` as the entrypoint for CC:Tweaked, CraftOS-PC, Advanced Peri - Do not add a standalone Lua test harness unless asked. Local execution happens through the CraftOS-PC harness (see `docs/install-craftos-pc.md`, `docs/craftos_pc_glossary.md`, and ADR-0005); code otherwise executes in-game. - Do not run `just repl` as an LLM agent; it is a human-only interactive CraftOS-PC wrapper. Use `just craftos --headless ...` for automated probes. - When changing behavior, add as many useful CraftOS-PC tests as practical. It is acceptable to skip tests that require human-only validation, such as complex turtle motion, in-game UX feel, or visual approval, but still add unit-style non-regression tests for deterministic parts when possible. -- Use `/apis/libtest.lua` for CraftOS-PC test scripts under `tests/`; scripts must print `__TRAPOS_TEST_OK__` only after all assertions pass. +- Use `/apis/libtest.lua` for test scripts under `tests/`; `/programs/runtest.lua` prints `__TRAPOS_TEST_OK__` only after the suite passes. - After editing Lua, run `just check` and fix all `luacheck` warnings. - Use 2-space indent, semicolons, and `local function`. - `require` paths are absolute ComputerCraft paths, for example `require('/apis/net')()`. @@ -22,7 +22,7 @@ Use `docs/README.md` as the entrypoint for CC:Tweaked, CraftOS-PC, Advanced Peri ## Architecture - `apis/eventloop.lua` is the single-threaded event loop around `os.pullEventRaw`; consider using it everywhere async behavior is needed. A handler that returns `api.STOP` auto-unregisters. -- `apis/libtest.lua` is the lightweight CraftOS-PC test helper used by scripts under `tests/`; it provides assertions, verbose case output, failure reporting, and the `__TRAPOS_TEST_OK__` success marker. +- `apis/libtest.lua` is the lightweight test helper used by scripts under `tests/`; `/programs/runtest.lua` discovers tests, renders suite output, and owns the `__TRAPOS_TEST_OK__` success marker. - `apis/net.lua` builds modem packet messaging, routing, and request/response RPC on the event loop. `sendRequest` returns `ok, result, packet` and defaults to a 0.5s timeout. - A router (`/programs/router.lua`) must be running somewhere on the network; without it, packets lack `routerId`, `isPacketOk` rejects them, and cross-machine messaging silently fails. - `servers/` listen for requests and start loops; `programs/` are clients that send requests and exit. diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md index b453caf..9643af0 100644 --- a/DEVELOPMENT.md +++ b/DEVELOPMENT.md @@ -16,6 +16,6 @@ This creates `.env` from `.env.sample` when needed and installs the local Git ho `just ci` is the local verification entry point. Today it verifies that `craftos --version` reports v2.8.3 or newer, runs `just check` for `luacheck`, and runs `just test` for CraftOS-PC headless tests. Use `just craftos` to launch CraftOS-PC with repo-local save data under `.craftos` and read-only mounts for `/trapos`, `/apis`, `/programs`, `/servers`, and `/startup`. `just repl` opens the same environment with `--cli` for human interactive use only; LLM agents must not run it. Use the CraftOS-PC glossary when adjusting `--headless`, `--exec`, `--script`, `--rom`, or `--mount-*` usage. -Tests live under `tests/` and run inside CraftOS-PC through `just test`. API-level tests should use `require('/apis/libtest')({ ... })`, register cases with `testlib.test(name, fn)`, and call `testlib.run()` at the end. `libtest` prints `__TRAPOS_TEST_OK__` only when every case passes, which is the contract consumed by the shell harness. Pass `--verbose` to `just test` to list each test script and each `libtest` case with colored `[OK]`/`[KO]` statuses. +Tests live under `tests/` and run inside CraftOS-PC through `just test`, which launches `/programs/runtest.lua`. API-level tests should use `require('/apis/libtest')({ ... })`, register cases with `testlib.test(name, fn)`, and call `testlib.run()` at the end. `libtest` tests remain normal ComputerCraft programs; the runner handles suite discovery, grouped output, and the `__TRAPOS_TEST_OK__` shell success contract. Pass `--pretty` to `just test` for grouped colored `[OK]`/`[KO]` statuses, or `--verbose` for additional runner/debug detail. Each CraftOS-PC test process is killed if it does not finish within `TRAP_CCLIBS_TEST_TIMEOUT_SECONDS`, defaulting to `3`. Override this in `.env` for slower local probes. diff --git a/Justfile b/Justfile index 72c516c..910543b 100644 --- a/Justfile +++ b/Justfile @@ -101,69 +101,53 @@ repl: ci *args: check-craftos check @just test {{args}} -# Run CraftOS-PC headless integration tests. Pass `--verbose` to list each case. +# Run CraftOS-PC headless integration tests. Pass `--pretty` for grouped output. test *args: - @if [ -f .env ]; then set -a; . ./.env; set +a; fi; \ - verbose=0; \ - timeout_seconds="${TRAP_CCLIBS_TEST_TIMEOUT_SECONDS:-3}"; \ - case "$timeout_seconds" in ''|*[!0-9]*) printf '%s\n' 'TRAP_CCLIBS_TEST_TIMEOUT_SECONDS must be a positive integer' >&2; exit 1 ;; esac; \ - if [ "$timeout_seconds" -lt 1 ]; then printf '%s\n' 'TRAP_CCLIBS_TEST_TIMEOUT_SECONDS must be >= 1' >&2; exit 1; fi; \ - for a in {{args}}; do case "$a" in --verbose|-v) verbose=1 ;; esac; done; \ - rom_arg=""; \ - if [ "$(uname -s)" = "Darwin" ]; then \ - rom_arg="--rom /Applications/CraftOS-PC.app/Contents/Resources"; \ - fi; \ - repo='{{justfile_directory()}}'; \ - mount_arg="--mount-ro /apis=$repo/apis --mount-ro /tests=$repo/tests"; \ - green=$(printf '\033[32m'); reset=$(printf '\033[0m'); red=$(printf '\033[31m'); \ - for script in tests/boot.lua tests/ready.lua tests/eventloop.lua; do \ - tmp="$(mktemp)"; \ - data_dir="$(mktemp -d)"; \ - exec_code="shell.run('/$script')"; \ - if [ "$verbose" -eq 1 ]; then exec_code="shell.run('/$script', '--verbose')"; fi; \ - craftos --directory "$data_dir" --headless $rom_arg $mount_arg --exec "$exec_code" >"$tmp" 2>&1 & \ - pid="$!"; \ - ( sleep "$timeout_seconds"; kill -TERM "$pid" >/dev/null 2>&1 ) & \ - watchdog="$!"; \ - wait "$pid" >/dev/null 2>&1; \ - status="$?"; \ - kill "$watchdog" >/dev/null 2>&1 || true; \ - wait "$watchdog" >/dev/null 2>&1 || true; \ - if grep -q __TRAPOS_TEST_OK__ "$tmp"; then \ - if [ "$verbose" -eq 1 ]; then \ - report="$data_dir/computer/0/trapos-test-report"; \ - if [ -f "$report" ]; then while IFS= read -r line; do \ - case "$line" in \ - 'OK '*) name="${line#OK }"; printf '%s\n' "${green}[OK]${reset} $name" ;; \ - 'KO '*) name="${line#KO }"; printf '%s\n' "${red}[KO]${reset} $name" ;; \ - esac; \ - done <"$report"; fi; \ - fi; \ - rm -f "$tmp"; \ - rm -rf "$data_dir"; \ - [ "$verbose" -eq 1 ] && printf '%s\n' "${green}PASS${reset} $script"; \ - else \ - if [ "$status" -eq 143 ]; then \ - printf '%s\n' "${red}FAIL${reset} $script timed out after ${timeout_seconds}s" >&2; \ - else \ - printf '%s\n' "${red}FAIL${reset} $script did not print __TRAPOS_TEST_OK__" >&2; \ - fi; \ - if [ "$verbose" -eq 1 ]; then \ - report="$data_dir/computer/0/trapos-test-report"; \ - if [ -f "$report" ]; then while IFS= read -r line; do \ - case "$line" in \ - 'OK '*) name="${line#OK }"; printf '%s\n' "${green}[OK]${reset} $name" >&2 ;; \ - 'KO '*) name="${line#KO }"; printf '%s\n' "${red}[KO]${reset} $name" >&2 ;; \ - esac; \ - done <"$report"; fi; \ - fi; \ - cat "$tmp" >&2; \ - rm -f "$tmp"; \ - rm -rf "$data_dir"; \ - exit 1; \ - fi; \ - done; \ - printf '%s\n' 'OK: CraftOS integration tests passed' + @if [ -f .env ]; then set -a; . ./.env; set +a; fi; \ + pretty=0; \ + verbose=0; \ + timeout_seconds="${TRAP_CCLIBS_TEST_TIMEOUT_SECONDS:-3}"; \ + case "$timeout_seconds" in ''|*[!0-9]*) printf '%s\n' 'TRAP_CCLIBS_TEST_TIMEOUT_SECONDS must be a positive integer' >&2; exit 1 ;; esac; \ + if [ "$timeout_seconds" -lt 1 ]; then printf '%s\n' 'TRAP_CCLIBS_TEST_TIMEOUT_SECONDS must be >= 1' >&2; exit 1; fi; \ + for a in {{args}}; do case "$a" in --pretty) pretty=1 ;; --verbose|-v) pretty=1; verbose=1 ;; esac; done; \ + rom_arg=""; \ + if [ "$(uname -s)" = "Darwin" ]; then \ + rom_arg="--rom /Applications/CraftOS-PC.app/Contents/Resources"; \ + fi; \ + repo='{{justfile_directory()}}'; \ + mount_arg="--mount-ro /apis=$repo/apis --mount-ro /programs=$repo/programs --mount-ro /tests=$repo/tests"; \ + tmp="$(mktemp)"; \ + data_dir="$(mktemp -d)"; \ + output_path="$data_dir/computer/0/trapos-test-output"; \ + exec_code="shell.run('/programs/runtest.lua', '--shutdown')"; \ + if [ "$pretty" -eq 1 ]; then exec_code="shell.run('/programs/runtest.lua', '--pretty', '--output', '/trapos-test-output', '--shutdown')"; fi; \ + if [ "$verbose" -eq 1 ]; then exec_code="shell.run('/programs/runtest.lua', '--verbose', '--output', '/trapos-test-output', '--shutdown')"; fi; \ + craftos --directory "$data_dir" --headless $rom_arg $mount_arg --exec "$exec_code" >"$tmp" 2>&1 & \ + pid="$!"; \ + ( sleep "$timeout_seconds"; kill -TERM "$pid" >/dev/null 2>&1 ) & \ + watchdog="$!"; \ + wait "$pid" >/dev/null 2>&1; \ + status="$?"; \ + kill "$watchdog" >/dev/null 2>&1 || true; \ + wait "$watchdog" >/dev/null 2>&1 || true; \ + if grep -q __TRAPOS_TEST_OK__ "$tmp"; then \ + if [ "$pretty" -eq 1 ] && [ -f "$output_path" ]; then cat "$output_path"; fi; \ + rm -f "$tmp"; \ + rm -rf "$data_dir"; \ + else \ + red=$(printf '\033[31m'); reset=$(printf '\033[0m'); \ + if [ "$status" -eq 143 ]; then \ + printf '%s\n' "${red}FAIL${reset} CraftOS integration tests timed out after ${timeout_seconds}s" >&2; \ + else \ + printf '%s\n' "${red}FAIL${reset} CraftOS integration tests did not print __TRAPOS_TEST_OK__" >&2; \ + fi; \ + if [ -f "$output_path" ]; then cat "$output_path" >&2; fi; \ + cat "$tmp" >&2; \ + rm -f "$tmp"; \ + rm -rf "$data_dir"; \ + exit 1; \ + fi; \ + printf '%s\n' 'OK: CraftOS integration tests passed' # Lint all Lua source with luacheck. check: check-luacheck diff --git a/apis/libtest.lua b/apis/libtest.lua index efd1caa..c0fd9ca 100644 --- a/apis/libtest.lua +++ b/apis/libtest.lua @@ -1,15 +1,28 @@ -local _VERSION = "1.3.0" +local _VERSION = "1.4.0" local function createLibTest(args) local api = {} local tests = {} + local pretty = false local verbose = false - local reportPath = "/trapos-test-report" + local reportPath = nil + local printMarker = true - for _, arg in ipairs(args or {}) do + local i = 1 + while i <= #(args or {}) do + local arg = args[i] if arg == "--verbose" then + pretty = true verbose = true + elseif arg == "--pretty" then + pretty = true + elseif arg == "--report" then + reportPath = args[i + 1] + i = i + 1 + elseif arg == "--no-marker" then + printMarker = false end + i = i + 1 end local function fail(message) @@ -17,7 +30,7 @@ local function createLibTest(args) end local function writeReport(line) - if not verbose then + if not reportPath then return end @@ -28,6 +41,12 @@ local function createLibTest(args) end end + function api.log(message) + if verbose then + writeReport("LOG " .. tostring(message)) + end + end + function api.test(name, fn) assert(type(name) == "string", "bad argument #1 (string expected)") assert(type(fn) == "function", "bad argument #2 (function expected)") @@ -67,19 +86,22 @@ local function createLibTest(args) function api.run() for _, t in ipairs(tests) do + api.log("RUN " .. t.name) local ok, err = pcall(t.fn) if not ok then writeReport("KO " .. t.name .. ": " .. tostring(err)) - if not verbose then + if not pretty then print("FAIL " .. t.name .. ": " .. tostring(err)) end - os.shutdown() + error(err, 0) end writeReport("OK " .. t.name) end - print("__TRAPOS_TEST_OK__") - os.shutdown() + if printMarker then + print("__TRAPOS_TEST_OK__") + end + return true end function api.version() diff --git a/docs/adrs/README.md b/docs/adrs/README.md index a5f8ed3..7235a15 100644 --- a/docs/adrs/README.md +++ b/docs/adrs/README.md @@ -15,3 +15,4 @@ Future ADRs can reuse the shape of the existing files when it is useful. - [`adr-0005-craftos-pc-harness.md`](adr-0005-craftos-pc-harness.md) - CraftOS-PC as the local harness. - [`adr-0006-simplify-periphemu-bootstrap.md`](adr-0006-simplify-periphemu-bootstrap.md) - Simplify periphemu bootstrap. - [`adr-0007-use-libtest-for-craftos-tests.md`](adr-0007-use-libtest-for-craftos-tests.md) - Use libtest for CraftOS tests. +- [`adr-0008-keep-tests-runnable-in-craftos-and-in-game.md`](adr-0008-keep-tests-runnable-in-craftos-and-in-game.md) - Keep tests runnable in CraftOS and in-game. diff --git a/docs/adrs/adr-0005-craftos-pc-harness.md b/docs/adrs/adr-0005-craftos-pc-harness.md index 3e74155..c866e36 100644 --- a/docs/adrs/adr-0005-craftos-pc-harness.md +++ b/docs/adrs/adr-0005-craftos-pc-harness.md @@ -36,7 +36,7 @@ The existing `CLAUDE.md` constraint ("Do not run Lua locally or add a test harne - Contributors must install CraftOS-PC before `just install` succeeds. The install guide makes this a 4-step copy/paste on macOS. - Contributors should use the local CraftOS-PC glossary first when researching emulator behavior, then follow the linked upstream pages for details. -- Headless tests live under `tests/` and are driven by `just test`. Basic boot tests still prove CraftOS-PC boots and the event queue works, and API behavior tests use `/apis/libtest.lua` for named cases and assertions. Tests are mounted into CraftOS-PC and invoked with `shell.run`, and stdout is grepped for `__TRAPOS_TEST_OK__`. Adding another test means dropping a Lua file in `tests/` and adding it to the loop in the `test:` recipe. +- Headless tests live under `tests/` and are driven by `just test`. Basic boot tests still prove CraftOS-PC boots and the event queue works, and API behavior tests use `/apis/libtest.lua` for named cases and assertions. The `Justfile` launches `/programs/runtest.lua`, which discovers tests, invokes them with `shell.run`, and prints `__TRAPOS_TEST_OK__` only after the suite passes. - Each test process is guarded by `TRAP_CCLIBS_TEST_TIMEOUT_SECONDS`, defaulting to `3`, so a blocked ComputerCraft event loop fails quickly and prints captured output. - The macOS install symlinks the binary into `/usr/local/bin`, which makes CraftOS-PC unable to auto-discover the ROM that ships inside the `.app` bundle (`Could not mount ROM`). The `test:` recipe works around this by passing `--rom /Applications/CraftOS-PC.app/Contents/Resources` on Darwin. Linux (AppImage) and Windows (installer) auto-discover correctly, so no flag is passed there. - `just craftos` uses repository-local save data under `.craftos/config/` and `.craftos/computer/`. This keeps emulator state out of `~/Library/Application Support/CraftOS-PC` during repository work and keeps repo files visible through read-only mounts instead of copying them into the VM save. diff --git a/docs/adrs/adr-0007-use-libtest-for-craftos-tests.md b/docs/adrs/adr-0007-use-libtest-for-craftos-tests.md index 45639e8..14e5b10 100644 --- a/docs/adrs/adr-0007-use-libtest-for-craftos-tests.md +++ b/docs/adrs/adr-0007-use-libtest-for-craftos-tests.md @@ -15,14 +15,14 @@ ADR 0005 made CraftOS-PC the local harness. The first real behavior test, `tests - Collect named cases. - Print per-case progress only in verbose mode. - Fail fast with a useful message. -- Print `__TRAPOS_TEST_OK__` only after every assertion passes. +- Report success only after every assertion passes, allowing the suite runner to print `__TRAPOS_TEST_OK__` once. - Shut the ComputerCraft process down cleanly. Those details are easy to copy incorrectly. A blocked eventloop test also showed that the shell harness needs a timeout and captured output so agentic debugging can proceed without manual interruption. ## Decision -Add `/apis/libtest.lua` as the repository's lightweight CraftOS-PC test helper. +Add `/apis/libtest.lua` as the repository's lightweight ComputerCraft test helper. Tests under `tests/` should require it with an absolute ComputerCraft path: @@ -37,20 +37,20 @@ end); testlib.run(); ``` -`libtest` intentionally stays small. It provides named cases, `assertEquals`, `assertTrue`, `assertErrors`, verbose case-status report output consumed by the shell harness, failure reporting, the `__TRAPOS_TEST_OK__` success marker, and `os.shutdown()` at process end. +`libtest` intentionally stays small. It provides named cases, `assertEquals`, `assertTrue`, `assertErrors`, optional case-status report output consumed by the suite runner, and failure reporting. -The shell harness keeps ownership of process-level concerns: CraftOS-PC launch flags, read-only mounts, stdout capture, and timeout enforcement through `TRAP_CCLIBS_TEST_TIMEOUT_SECONDS`. +`/programs/runtest.lua` owns suite-level concerns: test discovery, invoking test scripts, grouped pretty output, verbose runner diagnostics, the `__TRAPOS_TEST_OK__` success marker, and optional shutdown. The shell harness keeps only host-level concerns: CraftOS-PC launch flags, read-only mounts, stdout capture, and timeout enforcement through `TRAP_CCLIBS_TEST_TIMEOUT_SECONDS`. ## Consequences - New deterministic behavior should get as many useful CraftOS-PC tests as practical. - Tests that require human validation, such as complex turtle motion, in-game UX feel, or visual approval, may be skipped, but deterministic pieces should still get unit-style non-regression coverage. -- Test scripts remain normal ComputerCraft programs, not standalone Lua tests. They run through `just test`, not through a separate Lua test framework. +- Test scripts remain normal ComputerCraft programs, not standalone Lua tests. They can run through `just test`, `/programs/runtest.lua`, or direct in-game execution when copied with their dependencies. - `libtest` lives under `/apis` and is listed in `manifest.json`, so it can be required consistently in the mounted CraftOS-PC environment. -- The `__TRAPOS_TEST_OK__` marker remains the single shell-level success contract. +- The `__TRAPOS_TEST_OK__` marker remains the single shell-level success contract and is owned by `/programs/runtest.lua`. ## Future Work - Add more assertions only when tests need them; avoid growing a large framework. -- Consider making `just test` discover `tests/*.lua` automatically once the test set grows enough that the explicit list becomes noisy. +- Add more runner filters only when useful; keep the common no-argument path as automatic `tests/*.lua` discovery. - Explore GitHub Actions with the Linux CraftOS-PC AppImage after local coverage is broader. diff --git a/docs/adrs/adr-0008-keep-tests-runnable-in-craftos-and-in-game.md b/docs/adrs/adr-0008-keep-tests-runnable-in-craftos-and-in-game.md new file mode 100644 index 0000000..1b36c45 --- /dev/null +++ b/docs/adrs/adr-0008-keep-tests-runnable-in-craftos-and-in-game.md @@ -0,0 +1,37 @@ +# ADR 0008: Keep Tests Runnable In CraftOS And In-Game + +## Status + +Accepted + +## Date + +2026-06-08 + +## Context + +The initial CraftOS-PC harness proved that repository code can be tested locally, but the first shell recipe also owned too much suite behavior: explicit test lists, per-case rendering, and success formatting. That made the host shell script more complex than necessary and tied test orchestration to CraftOS-PC launch details. + +At the same time, tests in this repository are still ComputerCraft programs. They should be useful in CraftOS-PC and in-game, not only inside a host-side shell loop. + +## Decision + +Keep `/apis/libtest.lua` focused on test cases and assertions. It must stay usable from normal ComputerCraft programs in CraftOS-PC or in-game. + +Move suite orchestration into `/programs/runtest.lua`. The runner discovers tests under `/tests`, invokes each script with `shell.run`, renders grouped `--pretty` output, emits additional `--verbose` diagnostics, prints `__TRAPOS_TEST_OK__` only after the full suite passes, and can shut down when the host harness asks with `--shutdown`. + +Keep the `Justfile` minimal. It launches CraftOS-PC, mounts repository directories, enforces the process timeout, checks for the success marker, and prints runner output files. It should not know about individual test files or cases. + +## Consequences + +- Tests using `libtest` remain plain ComputerCraft programs. +- `/programs/runtest.lua` can be run inside CraftOS-PC or in-game when `/tests` and dependencies are present. +- Pretty colors and grouped suite output are runner concerns, not `libtest` concerns. +- Verbose mode is reserved for debugging and agent work loops, while `--pretty` is the normal human-readable mode. +- Host-specific concerns remain outside production Lua code. + +## Future Work + +- Add test selection filters when the suite grows. +- Add runner-level timing if slow tests become hard to diagnose. +- Add more `libtest` assertions only when real tests need them. diff --git a/docs/install-craftos-pc.md b/docs/install-craftos-pc.md index 2d46bd9..ba752f3 100644 --- a/docs/install-craftos-pc.md +++ b/docs/install-craftos-pc.md @@ -82,7 +82,7 @@ On macOS, use the `--rom` form shown above if the command fails with `Could not ## Running tests -`just test` runs the headless CraftOS integration tests in `tests/` through CraftOS-PC. On macOS the recipe passes `--rom /Applications/CraftOS-PC.app/Contents/Resources` because the `/usr/local/bin/craftos` symlink loses ROM auto-discovery; on Linux and Windows no flag is needed. `just ci` runs the same tests after `luacheck`. +`just test` runs `/programs/runtest.lua` headlessly through CraftOS-PC. The runner discovers tests under `/tests`, while the `Justfile` only owns host launch flags, timeout, and success-marker checks. On macOS the recipe passes `--rom /Applications/CraftOS-PC.app/Contents/Resources` because the `/usr/local/bin/craftos` symlink loses ROM auto-discovery; on Linux and Windows no flag is needed. `just ci` runs the same tests after `luacheck`. ## Repository-local launches diff --git a/manifest.json b/manifest.json index ac3a4c6..08ccb88 100644 --- a/manifest.json +++ b/manifest.json @@ -9,6 +9,7 @@ "programs/router.lua", "programs/events.lua", "programs/ping.lua", + "programs/runtest.lua", "programs/tuidemo.lua", "programs/upgrade.lua", "apis/net.lua", diff --git a/programs/runtest.lua b/programs/runtest.lua new file mode 100644 index 0000000..8e41aed --- /dev/null +++ b/programs/runtest.lua @@ -0,0 +1,209 @@ +local _VERSION = "1.0.0" + +local SUCCESS_MARKER = "__TRAPOS_TEST_OK__" +local DEFAULT_REPORT_PATH = "/trapos-test-report" + +local function printUsage() + print("runtest usage:") + print() + print("\t\truntest [--pretty] [--verbose] [--output ] [--shutdown] [test ...]") + print("\t\truntest --version") + print("\t\truntest --help") +end + +local function parseArgs(args) + local opts = { + pretty = false, + verbose = false, + shutdown = false, + outputPath = nil, + tests = {}, + } + + local i = 1 + while i <= #args do + local arg = args[i] + if arg == "--version" or arg == "-version" then + print("runtest v" .. _VERSION) + return nil + elseif arg == "--help" or arg == "-help" then + printUsage() + return nil + elseif arg == "--pretty" then + opts.pretty = true + elseif arg == "--verbose" or arg == "-v" then + opts.pretty = true + opts.verbose = true + elseif arg == "--output" then + opts.outputPath = args[i + 1] + i = i + 1 + elseif arg == "--shutdown" then + opts.shutdown = true + elseif string.sub(arg, 1, 1) == "-" then + print("Unknown option: " .. arg) + printUsage() + return nil + else + opts.tests[#opts.tests + 1] = arg + end + i = i + 1 + end + + return opts +end + +local function normalizeTestPath(path) + if string.sub(path, 1, 1) == "/" then + return path + end + return "/" .. path +end + +local function discoverTests() + local tests = {} + if not fs.exists("/tests") then + return tests + end + + for _, name in ipairs(fs.list("/tests")) do + local path = "/tests/" .. name + if not fs.isDir(path) and string.sub(name, -4) == ".lua" then + tests[#tests + 1] = path + end + end + table.sort(tests) + return tests +end + +local function readLines(path) + local lines = {} + local file = fs.open(path, "r") + if not file then + return lines + end + + while true do + local line = file.readLine() + if line == nil then + break + end + lines[#lines + 1] = line + end + file.close() + return lines +end + +local function createEmitter(outputPath) + local file = nil + if outputPath then + file = fs.open(outputPath, "w") + end + + local function emit(line) + if file then + file.writeLine(line) + else + print(line) + end + end + + local function close() + if file then + file.close() + end + end + + return emit, close +end + +local function renderReport(emit, script, reportLines, ok, verbose, color) + local green = color and string.char(27) .. "[32m" or "" + local red = color and string.char(27) .. "[31m" or "" + local dim = color and string.char(27) .. "[2m" or "" + local reset = color and string.char(27) .. "[0m" or "" + local displayScript = string.sub(script, 1, 1) == "/" and string.sub(script, 2) or script + + emit(displayScript) + if verbose then + emit(" " .. dim .. "report: " .. DEFAULT_REPORT_PATH .. reset) + end + + if #reportLines == 0 then + if ok then + emit(" " .. green .. "[OK]" .. reset .. " script completed") + else + emit(" " .. red .. "[KO]" .. reset .. " script failed before reporting a case") + end + return + end + + for _, line in ipairs(reportLines) do + if string.sub(line, 1, 3) == "OK " then + emit(" " .. green .. "[OK]" .. reset .. " " .. string.sub(line, 4)) + elseif string.sub(line, 1, 3) == "KO " then + emit(" " .. red .. "[KO]" .. reset .. " " .. string.sub(line, 4)) + elseif verbose and string.sub(line, 1, 4) == "LOG " then + emit(" " .. dim .. "[log] " .. string.sub(line, 5) .. reset) + end + end +end + +local opts = parseArgs({ ... }) +if not opts then + return +end + +local tests = opts.tests +if #tests == 0 then + tests = discoverTests() +else + for i, path in ipairs(tests) do + tests[i] = normalizeTestPath(path) + end +end + +if #tests == 0 then + print("FAIL: no tests found") + if opts.shutdown then + os.shutdown() + end + return +end + +local emit, closeOutput = createEmitter(opts.outputPath) +local suiteOk = true + +for _, script in ipairs(tests) do + fs.delete(DEFAULT_REPORT_PATH) + + local ok + if opts.verbose then + ok = shell.run(script, "--no-marker", "--report", DEFAULT_REPORT_PATH, "--verbose") + elseif opts.pretty then + ok = shell.run(script, "--no-marker", "--report", DEFAULT_REPORT_PATH, "--pretty") + else + ok = shell.run(script, "--no-marker", "--report", DEFAULT_REPORT_PATH) + end + local reportLines = readLines(DEFAULT_REPORT_PATH) + + if opts.pretty then + renderReport(emit, script, reportLines, ok, opts.verbose, opts.outputPath ~= nil) + end + + if not ok then + suiteOk = false + break + end +end + +closeOutput() + +if suiteOk then + print(SUCCESS_MARKER) +else + print("FAIL: CraftOS integration tests failed") +end + +if opts.shutdown then + os.shutdown() +end diff --git a/tests/boot.lua b/tests/boot.lua index 2dfad0c..fc4c1b4 100644 --- a/tests/boot.lua +++ b/tests/boot.lua @@ -1,4 +1,10 @@ --- Smoke test: prove CraftOS-PC boots and stdout flushes. --- Invoked via `craftos --headless --script tests/boot.lua` from `just test`. -print('__TRAPOS_TEST_OK__'); -os.shutdown(); +-- Basic integration test: prove CraftOS-PC boots and can run a test script. +local createLibTest = require('/apis/libtest'); + +local testlib = createLibTest({ ... }); + +testlib.test('CraftOS-PC boots and runs Lua', function() + testlib.assertTrue(true); +end); + +testlib.run(); diff --git a/tests/eventloop.lua b/tests/eventloop.lua index aaf9893..84715ad 100644 --- a/tests/eventloop.lua +++ b/tests/eventloop.lua @@ -1,5 +1,4 @@ -- Eventloop behavior tests for the CraftOS-PC harness. --- Invoked via `craftos --headless --script tests/eventloop.lua` from `just test`. local createEventLoop = require('/apis/eventloop'); local createLibTest = require('/apis/libtest'); diff --git a/tests/ready.lua b/tests/ready.lua index ac31cc0..72933b8 100644 --- a/tests/ready.lua +++ b/tests/ready.lua @@ -1,8 +1,15 @@ --- Smoke test: prove the CC event queue round-trips a custom event. --- Invoked via `craftos --headless --script tests/ready.lua` from `just test`. -os.queueEvent('craftos-ready'); -local ev = os.pullEventRaw(); -if ev == 'craftos-ready' then - print('__TRAPOS_TEST_OK__'); -end -os.shutdown(); +-- Basic integration test: prove the CC event queue round-trips a custom event. +local createLibTest = require('/apis/libtest'); + +local testlib = createLibTest({ ... }); + +testlib.test('event queue round-trips a custom event', function() + os.queueEvent('craftos-ready'); + local ev; + repeat + ev = os.pullEventRaw(); + until ev == 'craftos-ready'; + testlib.assertEquals(ev, 'craftos-ready'); +end); + +testlib.run();