MOP has three layers of automated checks:
| Layer | Scope | Entry |
|---|---|---|
| Unit tests | Per-module public API behaviour, math invariants, struct layout | make test |
| Conformance | Live-viewport render health — no Vulkan validation errors / sync hazards, no NaN pixels, CPU byte-level determinism, valid pick results | make conformance-run |
| Docs checks | Slug / frontmatter / link validity + compile every fenced c block with int main( | make docs-check |
All three run on every CI push (see CI reproduction below).
Running Tests
make test
This builds all test binaries in build/tests/ and runs them sequentially. The exit code is non-zero if any test fails. The suite currently covers ~50 modules — run ls tests/ for the full list.
Test Framework
MOP uses a custom minimal test harness with no external dependencies. The harness is defined in tests/test_harness.h.
Writing a Test
#include "test_harness.h"
#include <mop/mop.h>
static void test_example(void) {
TEST_BEGIN("example_name");
int result = 1 + 1;
TEST_ASSERT(result == 2);
TEST_ASSERT_FLOAT_EQ(3.14f, 3.14f);
TEST_ASSERT_VEC3_EQ(mop_vec3_add(
(MopVec3){1,0,0}, (MopVec3){0,1,0}),
1.0f, 1.0f, 0.0f);
TEST_END();
}
int main(void) {
TEST_SUITE_BEGIN("my_module");
TEST_RUN(test_example);
TEST_REPORT();
TEST_EXIT();
}
Available Assertions
| Macro | Description |
| --------------------------------- | ---------------------------------------- | ----- | ------- |
| TEST_ASSERT(expr) | Fails if expr is false |
| TEST_ASSERT_MSG(expr, msg) | Fails with a custom message |
| TEST_ASSERT_FLOAT_EQ(a, b) | Fails if | a - b | > 1e-4 |
| TEST_ASSERT_VEC3_EQ(v, x, y, z) | Fails if any component differs by > 1e-4 |
Test Structure
| Macro | Purpose |
|---|---|
TEST_SUITE_BEGIN(name) | Prints the suite name header |
TEST_BEGIN(name) | Starts a test case |
TEST_END() | Ends a test case, prints PASS/FAIL |
TEST_RUN(fn) | Calls a test function |
TEST_REPORT() | Prints the summary (total, passed, failed) |
TEST_EXIT() | Returns 0 on success, 1 on any failure |
Test Organization
One test file per module. The Makefile auto-discovers tests/test_*.c via $(wildcard); no explicit registration. Fixtures live in tests/fixtures/.
Tests that exercise Vulkan-only features are wrapped in #if defined(MOP_HAS_VULKAN) and fall through to a printf + return 0 stub on CPU-only builds, so every test builds regardless of the backend flag.
Adding a New Test
- Create
tests/test_mymodule.cfollowing the pattern above. - Run
make test— the new file is picked up automatically. - If it depends on Vulkan, wrap the body in
#if defined(MOP_HAS_VULKAN) ... #else int main(void) { printf("SKIP: Vulkan not enabled\n"); return 0; } #endif.
Conformance
make conformance-run builds and runs build/conformance_runner. It creates a live viewport on each available backend (CPU always, Vulkan if built in) and asserts:
- No Vulkan validation-layer errors (hook into
mop_vk_on_validation_error) - No Vulkan sync hazards
- No NaN in any rendered pixel across 60 frames
- CPU byte-level determinism (FNV-1a hash of two independent runs must match)
- Pick invariants (object_id never 0 when
hit == true)
Finishes in seconds. See conformance/runner.c for the full check list.
CI Reproduction
CI runs each job inside nixos/nix:2.25.2 on Linux. To reproduce byte-identically on any machine with docker:
make ci-linux # defaults to CI_JOB=test-gcc
make ci-linux CI_JOB=test-clang
make ci-linux CI_JOB=build-gcc
make ci-linux CI_JOB=build-clang
make ci-linux CI_JOB=docs
make ci-linux CI_JOB=conformance
The wrapper spawns the same docker image and runs ci/linux.sh <job> — the exact command every CI job uses. Same glibc, same libstdc++, same gcc/clang versions.
First run pulls the image (~100 MB) and populates a nix-store named volume (mop-ci-nix). Subsequent runs are fast.
Use this before pushing whenever you suspect Linux-specific behaviour (gcc -Wstringop-overflow, glibc malloc checks, libstdc++ cleanup) — cheaper than a round-trip through CI.
Build Integration
Tests link against build/lib/libmop.a with -lm -lpthread plus the platform C++ runtime (-lc++ on Darwin, -lstdc++ on Linux — wrapped in -Wl,--as-needed so the linker drops it for C-only tests that would otherwise trigger libstdc++ destructor cleanup on exit).
# From the root Makefile
$(TEST_BIN)/%: $(TEST_DIR)/%.c $(LIB_OUT) | $(TEST_BIN)
$(CC) $(CFLAGS) -I$(TEST_DIR) $< -L$(LIB_DIR) -lmop $(TEST_LDFLAGS) $(LDFLAGS) -o $@