DEMIENGINEDOCUMENTATION Everything is free
Ship

In this guide

E2E scripts, deterministic replay, profiler reports, and debug overlays.

Testing & debugging

Project E2E tests run through the real runtime. Deterministic replay, profiler reports, and debug overlays make regressions visible before they ship.

E2E tests

Tests live at scripts/tests/e2e.lua and return a list of named tests. Each test is a function that runs like a coroutine, so you can yield on waits and scene loads:

local Test = require("demi.test")

local tests = {}

tests[#tests + 1] = {
    name = "menu_starts_game",
    func = function()
        Test.expect_scene("scene://game/menu", 5)   -- wait up to 5s
        Test.touch("start")                          -- tap the HUD node
        Test.expect_scene("scene://game/main", 5)
        Test.expect(Test.node_center("hud/health") ~= nil, "health widget exists")
    end,
}

return { tests = tests }

The file must return { tests = tests }. The harness runs each entry in order and prints a summary.

Test API

CallBehaviorReturns
Test.touch(node_id)Taps an interactive HUD node by stable ID — survives resolution, DPI, and safe-area changes—
Test.tap(x, y)Taps a canvas point—
Test.swipe(from_node_id, to_node_id, duration?)Swipe between two HUD node centers (default 0.4 s)—
Test.swipe_xy(x1, y1, x2, y2, duration?)Swipe between two canvas-space positions—
Test.wait(seconds)Waits (yields) for the given time—
Test.expect_scene(scene_id, timeout?)Waits until the scene is active, up to the timeout—
Test.expect(condition, message)Asserts a condition; fails the test with message when false—
Test.node_center(node_id)Center point of a HUD nodeVec2 numeric array ([1], [2]) or nil

The harness prints one line per test and a final summary:

[test] PASS menu_starts_game.
[test] FAIL menu_starts_game: scene never activated.
[test] SUMMARY passed=3 failed=1.

Prefer stable HUD node IDs and scene IDs over screen coordinates so tests survive resolution and orientation changes. Keep tests deterministic and bounded by explicit timeouts.

Run tests

# Desktop E2E through the real runtime
demi test linux

# Device E2E (attached physical Android device via adb)
demi test android --serial <device>

# E2E during a normal launch
demi run --e2e-tests

# Headless one-frame smoke
DEMI_HEADLESS=1 demi test

Android qualification records lua_tests in build/android/qualification/qualification.json.

Deterministic input replay

demi run \
  --input-replay fixtures/smoke.replay.json

Recorded action frames re-apply through the authored context filter. The project fixed timestep must match the fixture. Use replays as CI smoke tests and for bug reproduction.

Profiler

demi run linux --profiler

demi run linux \
  --profile-report report.csv --profile-frames frames.csv

--profile-report writes aggregate scope totals; --profile-frames writes per-frame rows. Profile a repeatable scene and keep the CSV with the change that motivated it.

local Profile = require("demi.profile")

Profile.scope("ai_update", function()
    updateAI()
end)
-- The scope appears in profiler reports as "ai_update".

Debug overlays

demi run \
  --debug-overlays colliders,contacts,grid,entity_ids,draw_order,ui_bounds,profiler

Or set the matching project debug flags. Overlay geometry matches the authored simulation shapes.

Saves

local Save = require("demi.save")

Save.write("slot1", { level = 3, gold = 120 })
local data = Save.read("slot1")     -- table, or nil on failure
if data then print(data.level, data.gold) end

Save.set_number("slot1", "gold", 150)
print(Save.get_number("slot1", "gold", 0))   -- value or the default

Save.register_migration(2, 3, function(state, from_version, to_version)
    state.level = state.level or 1
    return state
end)

print(Save.metadata("slot1"))      -- metadata table (autosave, sequence, reason)
print(Save.last_error())           -- string or nil
print(Save.exists("slot1"))        -- boolean
Save.delete("slot1")
  • Slots survive install, upgrade, and rollback. Migrations run when the stored version is older than the current one.
  • write_state / read_state handle structured state blobs alongside slot values.
  • Inspect a save on disk: demi save inspect saves/slot1.save.json.

Validation in CI

demi validate examples/minimal_3d
demi validate examples/minimal_2d_android --platform android
demi script check games/my_game/scripts/player.lua

Exit code 1 means validation or test failure — fail the pipeline on it.