23 APR 2026

rahulmnavneeth

testing

homedocs

MOP has three layers of automated checks:

LayerScopeEntry
Unit testsPer-module public API behaviour, math invariants, struct layoutmake test
ConformanceLive-viewport render health — no Vulkan validation errors / sync hazards, no NaN pixels, CPU byte-level determinism, valid pick resultsmake conformance-run
Docs checksSlug / 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

MacroPurpose
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

  1. Create tests/test_mymodule.c following the pattern above.
  2. Run make test — the new file is picked up automatically.
  3. 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:

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 $@