test(craftos): add eventloop harness coverage
This commit is contained in:
parent
81bd524e9f
commit
7f7209c795
1
.env.sample
Normal file
1
.env.sample
Normal file
@ -0,0 +1 @@
|
||||
TRAP_CCLIBS_TEST_TIMEOUT_SECONDS=3
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@ -1 +1,2 @@
|
||||
.craftos
|
||||
.env
|
||||
|
||||
41
Justfile
41
Justfile
@ -6,7 +6,14 @@ default:
|
||||
@just --list
|
||||
|
||||
# Install local development tooling.
|
||||
install: install-git-hooks check-install
|
||||
install: init-env install-git-hooks check-install
|
||||
|
||||
# Create a local environment file when one does not exist.
|
||||
init-env:
|
||||
@if [ ! -f .env ]; then \
|
||||
cp .env.sample .env; \
|
||||
printf '%s\n' 'Created .env from .env.sample'; \
|
||||
fi
|
||||
|
||||
# Install Git hooks for this repository.
|
||||
install-git-hooks:
|
||||
@ -96,18 +103,42 @@ ci *args: check-craftos check
|
||||
|
||||
# Run CraftOS-PC headless smoke tests. Pass `--verbose` to list each test.
|
||||
test *args:
|
||||
@verbose=0; \
|
||||
@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 [ "$a" = "--verbose" ] && verbose=1; done; \
|
||||
script_args=""; \
|
||||
if [ "$verbose" -eq 1 ]; then script_args="--verbose"; fi; \
|
||||
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"; \
|
||||
green=$(printf '\033[32m'); reset=$(printf '\033[0m'); red=$(printf '\033[31m'); \
|
||||
for script in tests/boot.lua tests/ready.lua; do \
|
||||
if craftos --headless $rom_arg --script "$script" | grep -q __READY__; then \
|
||||
for script in tests/boot.lua tests/ready.lua tests/eventloop.lua; do \
|
||||
tmp="$(mktemp)"; \
|
||||
craftos --headless $rom_arg $mount_arg --script "$script" $script_args >"$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 __READY__ "$tmp"; then \
|
||||
rm -f "$tmp"; \
|
||||
[ "$verbose" -eq 1 ] && printf '%s\n' "${green}PASS${reset} $script"; \
|
||||
else \
|
||||
printf '%s\n' "${red}FAIL${reset} $script did not print __READY__" >&2; \
|
||||
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 __READY__" >&2; \
|
||||
fi; \
|
||||
cat "$tmp" >&2; \
|
||||
rm -f "$tmp"; \
|
||||
exit 1; \
|
||||
fi; \
|
||||
done; \
|
||||
|
||||
229
tests/eventloop.lua
Normal file
229
tests/eventloop.lua
Normal file
@ -0,0 +1,229 @@
|
||||
-- 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 args = { ... };
|
||||
local verbose = false;
|
||||
local tests = {};
|
||||
|
||||
for _, arg in ipairs(args) do
|
||||
if arg == '--verbose' then
|
||||
verbose = true;
|
||||
end
|
||||
end
|
||||
|
||||
local function test(name, fn)
|
||||
tests[#tests + 1] = { name = name, fn = fn };
|
||||
end
|
||||
|
||||
local function fail(message)
|
||||
error(message, 2);
|
||||
end
|
||||
|
||||
local function assertEquals(actual, expected, message)
|
||||
if actual ~= expected then
|
||||
fail((message or 'assertEquals failed') .. ': expected ' .. tostring(expected) .. ', got ' .. tostring(actual));
|
||||
end
|
||||
end
|
||||
|
||||
local function assertTrue(value, message)
|
||||
if not value then
|
||||
fail(message or 'assertTrue failed');
|
||||
end
|
||||
end
|
||||
|
||||
local function assertErrors(fn, expected)
|
||||
local ok, err = pcall(fn);
|
||||
if ok then
|
||||
fail('expected error');
|
||||
end
|
||||
if expected and not string.find(tostring(err), expected, 1, true) then
|
||||
fail('expected error containing ' .. expected .. ', got ' .. tostring(err));
|
||||
end
|
||||
end
|
||||
|
||||
test('register dispatches queued event args', function()
|
||||
local events = createEventLoop();
|
||||
local called = 0;
|
||||
|
||||
events.register('eventloop_test_basic', function(a, b)
|
||||
called = called + 1;
|
||||
assertEquals(a, 'first');
|
||||
assertEquals(b, 42);
|
||||
events.stopLoop();
|
||||
end);
|
||||
|
||||
os.queueEvent('eventloop_test_basic', 'first', 42);
|
||||
events.runLoop();
|
||||
|
||||
assertEquals(called, 1);
|
||||
assertTrue(not events.isRunningLoop());
|
||||
end);
|
||||
|
||||
test('STOP unregisters handler after first call', function()
|
||||
local events = createEventLoop();
|
||||
local stoppedHandlerCalls = 0;
|
||||
local observerCalls = 0;
|
||||
|
||||
events.register('eventloop_test_stop', function()
|
||||
stoppedHandlerCalls = stoppedHandlerCalls + 1;
|
||||
return events.STOP;
|
||||
end);
|
||||
|
||||
events.register('eventloop_test_stop', function()
|
||||
observerCalls = observerCalls + 1;
|
||||
if observerCalls == 1 then
|
||||
os.queueEvent('eventloop_test_stop');
|
||||
else
|
||||
events.stopLoop();
|
||||
end
|
||||
end);
|
||||
|
||||
os.queueEvent('eventloop_test_stop');
|
||||
events.runLoop();
|
||||
|
||||
assertEquals(stoppedHandlerCalls, 1);
|
||||
assertEquals(observerCalls, 2);
|
||||
end);
|
||||
|
||||
test('manual unregister prevents dispatch', function()
|
||||
local events = createEventLoop();
|
||||
local called = 0;
|
||||
local dispose = events.register('eventloop_test_unregister', function()
|
||||
called = called + 1;
|
||||
end);
|
||||
|
||||
events.setTimeout(function()
|
||||
events.stopLoop();
|
||||
end, 0);
|
||||
dispose();
|
||||
|
||||
os.queueEvent('eventloop_test_unregister');
|
||||
events.runLoop();
|
||||
|
||||
assertEquals(called, 0);
|
||||
end);
|
||||
|
||||
test('setTimeout before runLoop fires once', function()
|
||||
local events = createEventLoop();
|
||||
local called = 0;
|
||||
|
||||
events.setTimeout(function()
|
||||
called = called + 1;
|
||||
events.stopLoop();
|
||||
end, 0);
|
||||
|
||||
events.runLoop();
|
||||
|
||||
assertEquals(called, 1);
|
||||
end);
|
||||
|
||||
test('setTimeout during runLoop fires after event handler', function()
|
||||
local events = createEventLoop();
|
||||
local order = '';
|
||||
|
||||
events.register('eventloop_test_runtime_timeout', function()
|
||||
order = order .. 'event>';
|
||||
events.setTimeout(function()
|
||||
order = order .. 'timeout';
|
||||
events.stopLoop();
|
||||
end, 0);
|
||||
return events.STOP;
|
||||
end);
|
||||
|
||||
os.queueEvent('eventloop_test_runtime_timeout');
|
||||
events.runLoop();
|
||||
|
||||
assertEquals(order, 'event>timeout');
|
||||
end);
|
||||
|
||||
test('cleared timeout before runLoop does not fire', function()
|
||||
local events = createEventLoop();
|
||||
local called = 0;
|
||||
|
||||
local clear = events.setTimeout(function()
|
||||
called = called + 1;
|
||||
end, 0);
|
||||
clear();
|
||||
|
||||
events.setTimeout(function()
|
||||
events.stopLoop();
|
||||
end, 0);
|
||||
events.runLoop();
|
||||
|
||||
assertEquals(called, 0);
|
||||
end);
|
||||
|
||||
test('onStart and onStop run around loop', function()
|
||||
local events = createEventLoop();
|
||||
local order = '';
|
||||
|
||||
events.onStart(function()
|
||||
order = order .. 'start>';
|
||||
end);
|
||||
|
||||
events.onStop(function()
|
||||
order = order .. 'stop';
|
||||
end);
|
||||
|
||||
events.setTimeout(function()
|
||||
order = order .. 'timeout>';
|
||||
events.stopLoop();
|
||||
end, 0);
|
||||
|
||||
events.runLoop();
|
||||
|
||||
assertEquals(order, 'start>timeout>stop');
|
||||
end);
|
||||
|
||||
test('empty loop returns and runs onStop', function()
|
||||
local events = createEventLoop();
|
||||
local stopped = false;
|
||||
|
||||
events.onStop(function()
|
||||
stopped = true;
|
||||
end);
|
||||
|
||||
events.runLoop();
|
||||
|
||||
assertTrue(stopped);
|
||||
assertTrue(not events.isRunningLoop());
|
||||
end);
|
||||
|
||||
test('error contracts are enforced', function()
|
||||
local events = createEventLoop();
|
||||
local function handler()
|
||||
end
|
||||
|
||||
events.register('eventloop_test_errors', handler);
|
||||
|
||||
assertErrors(function()
|
||||
events.register('eventloop_test_errors', handler);
|
||||
end, 'handler already registered');
|
||||
|
||||
assertErrors(function()
|
||||
events.stopLoop();
|
||||
end, 'loop is already stopped');
|
||||
|
||||
assertErrors(function()
|
||||
events.register(1, handler);
|
||||
end, 'string expected');
|
||||
|
||||
assertErrors(function()
|
||||
events.register('eventloop_test_errors_2', 'not a function');
|
||||
end, 'function expected');
|
||||
end);
|
||||
|
||||
for _, t in ipairs(tests) do
|
||||
if verbose then
|
||||
print('RUN ' .. t.name);
|
||||
end
|
||||
local ok, err = pcall(t.fn);
|
||||
if not ok then
|
||||
print('FAIL ' .. t.name .. ': ' .. tostring(err));
|
||||
os.shutdown();
|
||||
end
|
||||
end
|
||||
|
||||
print('__READY__');
|
||||
os.shutdown();
|
||||
Loading…
Reference in New Issue
Block a user