test(craftos): add suite runner
This commit is contained in:
parent
582bbf639f
commit
c7ba641e2d
@ -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.
|
||||
|
||||
@ -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.
|
||||
|
||||
108
Justfile
108
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
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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.
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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",
|
||||
|
||||
209
programs/runtest.lua
Normal file
209
programs/runtest.lua
Normal file
@ -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 <path>] [--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
|
||||
@ -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();
|
||||
|
||||
@ -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');
|
||||
|
||||
|
||||
@ -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();
|
||||
|
||||
Loading…
Reference in New Issue
Block a user