Testing Guide¶
This page documents the ORCA test harness, how to write and run tests, and common gotchas that contributors encounter.
Running Tests¶
Headless layout tests (no display needed)¶
make test-properties # C unit tests for the property VM
make test-headless # Lua layout tests (orca -test=tests/test_layout.lua)
These run without a display or OpenGL context.
Display-required tests¶
These require either a real display or a virtual framebuffer (xvfb-run on Linux, Xvfb on macOS CI). The engine creates a real window, initializes the renderer, and runs the test Lua/XML file.
Running a single XML test¶
The -test=<file> flag wraps the file in a short bootstrap script that requires all modules and then calls doxmlfile(file) (for .xml) or luaL_dofile (for .lua).
Writing Layout Tests¶
Lua test files¶
Layout tests are plain Lua scripts that use the orca.UIKit module. The engine bootstraps orca, orca.UIKit, and all plugins before running the file.
local orca = require "orca"
orca.init()
local ui = require "orca.UIKit"
-- IMPORTANT: Always set ResizeMode = "NoResize" for test screens
local screen = ui.Screen { Width = 1000, Height = 1000, ResizeMode = "NoResize" }
screen:UpdateLayout(screen.Width, screen.Height)
assert(screen.Width == 1000, "Width should be 1000")
XML test files¶
XML test files are loaded by doxmlfile(). Any <script> element inside the root element is executed after the node tree is constructed.
<?xml version="1.0" encoding="UTF-8"?>
<Screen Name="test" Width="1000" Height="1000" ResizeMode="NoResize">
<Node2D Name="box" Width="100" Height="50" />
<script>
updateLayout(_ENV, Width, Height)
local box = _ENV:findChild("box", true)
assert(box.ActualWidth == 100)
assert(box.ActualHeight == 50)
</script>
</Screen>
Common Gotchas¶
Screen dimensions are overridden by the window size¶
Screen.MeasureOverride calls axGetSize() and overrides the screen's Width/Height with the actual window dimensions when ResizeMode == kResizeModeCanResize, which is the default.
On CI virtual displays (xvfb-run), the window is typically 640×480 — so a screen created without ResizeMode = "NoResize" silently becomes 640×480 instead of your intended size, causing layout assertions like assert(screen.Width == 1000) to fail.
Always set ResizeMode = "NoResize" for test screens:
test-properties vs display tests¶
make test-properties compiles and runs tests/test_properties.c as a standalone C binary with TEST_MEMORY=1. This binary intercepts malloc/free to track leaks — it does not link against the main orca library and has no Lua or renderer dependencies. It runs entirely headless.
make test runs test-properties first, then test-headless, then the display-required tests (test1.lua, test.xml). If test-properties fails, the display tests are never reached.
The TEST_MEMORY allocator¶
The tests/test_properties.c test binary defines custom malloc/calloc/realloc/free wrappers that track allocation counts:
- On first call,
init_alloc_funcs()resolves the real libc symbols viadlsym(RTLD_NEXT, ...). A 1 KB bootstrap buffer handles the onecallocthatdlsymmakes internally. - A recursion guard (
s_init_in_progress) prevents re-entrancy duringdlsym. - Tracking is only active after
mem_init()is called; allocations made during module initialization (GnuTLS, libEGL) are not counted.
macOS note: The
__libc_malloc/__libc_freesymbols are glibc-internal and do not exist on Apple platforms. Always usedlsym(RTLD_NEXT, "malloc")for portable interception.
Property VM Bindings¶
Binding expressions¶
Property bindings in XML use a formula language evaluated by the property VM (source/core/property/property_runtime.c):
<!-- Simple property reference -->
<property name="Visible">
<Binding>{./IsActive}</Binding>
</property>
<!-- Arithmetic -->
<property name="Width">
<Binding>MUL({./Scale}, 100)</Binding>
</property>
<!-- Conditional (IF operator) -->
<property name="Text">
<Binding>IF(EQUAL({./IsActive}, 1), "Active", "Inactive")</Binding>
</property>
Supported operators¶
| Operator | Description |
|---|---|
{./PropertyPath} |
Property reference — reads from the binding target |
ADD(a, b) |
Addition |
SUB(a, b) |
Subtraction |
MUL(a, b) |
Multiplication |
DIV(a, b) |
Division |
EQUAL(a, b) |
Returns 1 if equal, 0 otherwise |
IF(cond, true-val, false-val) |
Conditional — returns true-val if cond != 0 (or non-empty string), else false-val |
"string literal" |
String constant |
123, 1.5 |
Numeric constants |
IF operator semantics¶
- Condition is truthy when: numeric
cond != 0, or stringcondis non-empty. - The result type follows the selected branch (
true-valorfalse-val). - Both branches can be any expression, including nested operators.
Binding modes¶
The Mode attribute on a <Binding> element controls when the binding is re-evaluated:
| Mode | Description |
|---|---|
Expression (default) |
Re-evaluated whenever the source property changes |
OneTime |
Evaluated once when the node is created |
OneWay |
Same as Expression |
<property name="Opacity">
<Binding Mode="Expression" Attribute="WholeProperty">{./Alpha}</Binding>
</property>
XML Attribute Parsing¶
Namespace-aware attribute iteration¶
When iterating XML attributes in C (source/parsers/p_xml.c), use xmlGetProp/xmlGetNsProp rather than xmlNodeListGetString:
/* Correct — works with XML_PARSE_COMPACT */
xmlChar* value = attr->ns
? xmlGetNsProp(attr->parent, attr->name, attr->ns->href)
: xmlGetProp(attr->parent, attr->name);
lua_pushstring(L, (const char*)value);
xmlFree(value);
xmlNodeListGetString(doc, attr->children, 1) can return NULL when XML_PARSE_COMPACT alters how small text nodes are stored internally. The canonical xmlGetProp API is immune to this.