Skip to main content

Python Framework to help run Unity tests on C code

Project description

tddl

TDD launcher for Unity-based C tests.

tddl is a single command that takes your C test file and does everything around it: builds it with CMake, runs it under Unity, optionally checks for memory errors with valgrind, measures code coverage with gcovr, runs static complexity analysis with lizard, compiles with the memory-safe Fil-C compiler, and bundles all results into a polished PDF report.

You write tests; tddl handles the rest.


Table of contents


What problem does this solve?

Setting up a proper test environment for a C project is tedious. You need:

  • A test framework (Unity)
  • A build system (CMake) generating per-test executables
  • Memory checkers (valgrind, or Fil-C if you prefer compile-time safety)
  • Coverage tools (gcovr) with HTML reports
  • Complexity metrics (lizard) with sensible thresholds
  • Glue code to wire all of that together
  • A way to share results — usually a PDF for instructors, reviewers, or yourself later

Each of these is its own learning curve. tddl collapses them into a single workflow: install with tddl --build, then run tddl test_foo.c --src foo.c --valgrind --lizard --coverage --pdf and get a complete report.


Quick start

# 1. From your project root (or an empty directory you want to bootstrap)
tddl --build

# 2. Verify everything is ready (optional but recommended)
tddl --doctor

# 3. Write a test in tests/test_foo/test_foo.c

# 4. Run it
tddl test_foo.c

If you want the full treatment — coverage, memory check, complexity, PDF — chain the flags:

tddl test_foo.c --src foo.c --valgrind --coverage --lizard --pdf

Project layout

tddl expects this structure (and creates it for you with --build):

project/
├── include/      Public headers (.h files) shared across tests
├── src/          Your implementation files (.c)
├── tests/
│   └── test_foo/
│       └── test_foo.c   The test file
├── vendor/       Third-party dependencies (Unity, optionally Fil-C)
├── reports/      Generated PDF reports (one folder per test)
├── coverage/     Generated HTML coverage reports
└── .gitignore    tddl adds vendor/, reports/, coverage/ here automatically

Key convention: each test file lives in its own subdirectory under tests/, where the subdirectory name matches the file's stem (the name without .c). So tests/test_foo/test_foo.c, not tests/test_foo.c directly. This is what lets tddl give each test isolated build artifacts and reports.


The five commands

tddl has five top-level modes. The first three are setup/diagnostics; the last two are how you actually use it.

tddl --build

The bootstrap command. Run it once per project (or once per new machine — it's per-project, not global). It does everything you need to go from "fresh git clone" to "ready to test":

  1. Detects your package manager (apt, dnf, pacman, or brew).
  2. Installs system tools that need root: cmake, valgrind, git, a C compiler. Asks for sudo when needed.
  3. Installs Python tools via pipx (isolated venvs): lizard, gcovr. Plus reportlab via pip install --user for PDF generation.
  4. Clones Unity into ./vendor/unity/ (pinned to a stable release tag).
  5. Creates the directory structure (include/, src/, tests/) if missing.
  6. **Creates a small working example: include/example.h, src/example.c, tests/example_test/example_test.c
  7. Updates .gitignore with a managed block that ignores vendor/, reports/, and coverage/. Your existing rules are preserved — the block is appended with marker comments so re-runs don't duplicate it.

It's idempotent: re-running it is safe. Tools already installed are detected via which and skipped. Unity already vendored is left alone. Your existing .gitignore rules are never touched.

What it does not install: Fil-C. That's a separate command because it's a 30–60 minute compile.

tddl --build-filc

Builds Fil-C into ./vendor/filc/. This is split from --build because the Fil-C build is heavy: 30–60 minutes of compile time and 10–20 GB of disk space. Only run this if you actually plan to use the --filc flag.

Prerequisites are checked first (you need git, cmake, and gcc to be present — running tddl --build first guarantees this), so you get a clean error message up front rather than 5 minutes into a clone.

tddl --doctor

A read-only health check. Inspired by flutter doctor and brew doctor. Tells you exactly what's installed, what's missing, and where dependencies resolve to. Installs nothing.

The output looks like:

tddl --doctor — checking environment
============================================================

System:
  platform: Linux 6.18.5 (x86_64)
  ✓  package manager      apt (/usr/bin/apt-get)

Build toolchain:
  ✓  cmake                /usr/bin/cmake
  ✓  git                  /usr/bin/git
  ✓  gcc                  /usr/bin/gcc

Testing & analysis tools:
  ✓  valgrind             /usr/bin/valgrind
  ✗  lizard               not found in PATH
  ✓  gcovr                /home/user/.local/bin/gcovr
  ✓  pipx                 /usr/bin/pipx

Python libraries:
  ✓  reportlab            v4.4.10

Project dependencies (resolved against current directory):
  project root: /home/user/myproj
  ✓  Unity                /home/user/myproj/vendor/unity (vendored)
  ⚠  Fil-C                not built (optional — run `tddl --build-filc` if you need --filc)

Project structure:
  ✓  include/             /home/user/myproj/include
  ✓  src/                 /home/user/myproj/src
  ✓  tests/               /home/user/myproj/tests

A few details worth knowing:

  • Each OK line shows the exact path where the tool was found. Useful for catching "wrong version on the PATH" issues.
  • For Unity and Fil-C, it shows the resolution source: (vendored) if it came from ./vendor/, or (via $UNITY_PATH) if you have the env var set as an override.
  • It uses ANSI colors in a terminal ( green, red, yellow) and degrades to plain OK/MISSING/WARN text when redirected to a file, so tddl --doctor > log.txt doesn't pollute your log with escape codes.
  • Exit code 0 if everything needed for the basic flow is there; exit code 1 otherwise. This makes it usable in CI: tddl --doctor && tddl test_foo.c.

Run --doctor after --build to confirm everything went well, or whenever you suspect something's wrong with your environment.

tddl <test_file.c>

The main command — runs a test. Combine it with any of the flags below.

The test file is looked up at ./tests/<stem>/<test_file.c>, where <stem> is the filename without the .c extension. So tddl test_list.c looks for ./tests/test_list/test_list.c.

Without any flags, it just builds and runs your Unity tests, printing each PASS/FAIL/IGNORE result to the terminal.

tddl --help

Prints a structured usage guide. Same content as this README in compressed form. Try it with tddl -h or tddl --help from anywhere.


Flags reference

All flags below apply to tddl <test_file.c>. They can (usually) be combined.

--src <file.c>

Links an extra source file from src/ into the test build. You almost always need this — your test file calls functions, and those functions are defined in src/foo.c, so the test build needs to link them in.

tddl test_list.c --src list.c

Pass only the basename — --src list.c, not --src src/list.c. The path is always relative to ./src/.

When combined with --coverage, this also tells tddl to filter coverage to this file only, so test code itself doesn't count toward coverage numbers (which is what you want — 100% coverage of the test file is meaningless).

--include <files>

Comma-separated list of additional files from include/ to link into the build. Useful for shared helpers, mocks, or test fixtures.

tddl test_list.c --src list.c --include mocks.c,helpers.c

The list is comma-separated with no spaces around the commas. Each file is looked up under ./include/.

tddl also automatically scans the test file, --src file, and --include files for __wrap_<funcname> definitions. Any found are passed to the linker as -Wl,--wrap=<funcname>, so function mocking with linker wrapping works out of the box. Define __wrap_malloc somewhere and your test will call your wrapper instead of libc's malloc.

--coverage

Runs gcovr after the tests pass. Reports line coverage to the terminal and writes:

  • A line-by-line HTML report to coverage/<stem>/index.html — open it in a browser to see which lines were and weren't hit, color-coded.
  • A summary.json with machine-readable numbers next to it.

Coverage below 100% causes tddl to exit non-zero. This is intentional — coverage is a gate, not a metric. If you don't want strict 100%, don't use --coverage; use gcovr directly instead.

When combined with --src, coverage is filtered to that source file only. Without --src, coverage covers everything in the build directory (including the test file, which you typically don't want).

Cannot be combined with --filc.

--valgrind

Runs the test binary under valgrind with strict memory checks:

--leak-check=full
--show-leak-kinds=all
--track-origins=yes
--error-exitcode=1

--error-exitcode=1 is the important one: it makes valgrind itself exit non-zero on any memory error, even if your Unity tests technically passed. So a test that leaks 8 bytes but doesn't TEST_ASSERT_FAIL will still fail the build.

Cannot be combined with --filc. Fil-C does memory safety at compile time; running its instrumented binary under valgrind produces false positives on Fil-C's runtime metadata.

When combined with --pdf, the full valgrind output is parsed and rendered into the PDF report (see The PDF report for what that looks like).

--filc

Compiles the test with Fil-C, a memory-safe C compiler. Where valgrind catches memory bugs at runtime by instrumenting allocations, Fil-C catches them at compile time and during execution through a much stronger model (bounded pointers, no use-after-free, no buffer overflows). It's slower but catches bugs valgrind can't.

Requires Fil-C to be built first: tddl --build-filc.

Resolved from $FIL_C_PATH if set, otherwise from ./vendor/filc/build/bin/clang.

Cannot be combined with --valgrind or --coverage. Fil-C's instrumentation interferes with both.

--lizard [--ccn=N] [--length=N] [--args=N]

Runs lizard static complexity analysis on the test file (and on the --src file, if passed). Any function exceeding the thresholds causes tddl to exit non-zero.

Default thresholds:

Metric Default Meaning
--ccn=N 10 Maximum cyclomatic complexity per function (number of distinct paths)
--length=N 50 Maximum lines of code per function
--args=N 5 Maximum parameter count per function

Override individually:

tddl test_foo.c --src foo.c --lizard --ccn=15 --length=80

Threshold flags only work alongside --lizard — using --ccn=20 without --lizard is an error (because it would silently have no effect, which is worse than failing).

Can be combined with any other flag.

--pdf

Generates a combined PDF report at reports/<stem>/report.pdf. The report includes whichever sections actually ran: Unity test results, valgrind diagnostics, lizard complexity findings. Sections for tools you didn't enable are simply omitted (no empty pages).

When --pdf is set, the terminal output is condensed to a short summary plus the path to the PDF. The full output is captured into the report. This keeps your terminal clean during long runs and gives you a permanent artifact afterward.

See The PDF report below for what each section contains.


Writing tests

tddl uses Unity, a small testing framework for C. A minimal test file looks like this:

#ifdef TEST

#include "unity.h"
#include "list.h"   // header for src/list.c

void setUp(void)    {}   // called before each test
void tearDown(void) {}   // called after each test

void test_list_new_returns_empty(void) {
    list_t *l = list_new();
    TEST_ASSERT_NOT_NULL(l);
    TEST_ASSERT_EQUAL_INT(0, list_length(l));
    list_free(l);
}

void test_list_append_increments_length(void) {
    list_t *l = list_new();
    list_append(l, 42);
    TEST_ASSERT_EQUAL_INT(1, list_length(l));
    list_free(l);
}

int main(void) {
    UNITY_BEGIN();
    RUN_TEST(test_list_new_returns_empty);
    RUN_TEST(test_list_append_increments_length);
    return UNITY_END();
}

#endif

A few conventions worth noting:

  • Wrap everything in #ifdef TEST. tddl defines TEST when building, so this code only compiles in test builds. Your production builds (if you have any) ignore it entirely.
  • setUp and tearDown are called by Unity before and after each RUN_TEST — use them for fixtures.
  • Use the TEST_ASSERT_* macros from Unity. There are dozens — see Unity's documentation for the full list.
  • Each RUN_TEST call adds a test to the suite. You're explicit about which tests run; nothing is auto-discovered.

The PDF report

When --pdf is enabled, you get a single-document report with these sections (in order):

Cover page — Status banner (green OK / red FAILED), test file path, src file path, the mode string (gcc + valgrind + lizard + pdf), and timestamp.

Unity tests — Three summary cards (passed / failed / ignored), then a row-per-test table with status, name, line number, and message. Failures and ignores are highlighted in red and yellow respectively.

Valgrind memory check (only if --valgrind was used) — Three summary cards (total errors / definitely lost bytes / indirect+possible bytes), a heap usage row showing allocs / frees / bytes allocated / in use at exit (the last cell goes green if everything was freed, red otherwise), the leak summary table with all 5 categories (definitely / indirectly / possibly lost / still reachable / suppressed), and finally a list of every error/leak with its full stack trace, each in its own card with a colored left border indicating severity.

Lizard complexity (only if --lizard was used) — Three summary cards (functions analyzed / violations / files), the thresholds used, and a per-file table listing every function with its NLOC, CCN, length, arg count, token count, and source location. Functions that violate any threshold are highlighted in red with the offending column bolded.

The report uses a consistent visual language across all sections: same color palette (green for pass, red for fail, yellow for warning, blue for informational), same card layout, same table style. Each section can stand alone but they share design.


Exit codes

tddl returns:

  • 0 if everything succeeded: all tests passed, coverage was 100% (if --coverage was used), no memory errors (if --valgrind was used), no complexity violations (if --lizard was used).
  • 1 if any of the above failed.

This makes tddl directly usable in CI pipelines, pre-commit hooks, or shell && chains. There's no "passed with warnings" middle ground — anything you opted into has to pass.

For setup/diagnostic commands:

  • tddl --build exits 0 on success, 1 if installation failed.
  • tddl --build-filc same.
  • tddl --doctor exits 0 if the environment is fully ready, 1 if anything important is missing. Fil-C missing is not a failure (it's optional).

Where things go

Path What's there Tracked in git?
include/ Your public headers Yes
src/ Your implementation .c files Yes
tests/<stem>/<stem>.c Your test files Yes
vendor/unity/ Vendored Unity (cloned by --build) No (gitignored)
vendor/filc/ Vendored Fil-C (built by --build-filc) No (gitignored)
reports/<stem>/report.pdf PDF reports (one per test) No (gitignored)
coverage/<stem>/index.html HTML coverage reports No (gitignored)
coverage/<stem>/summary.json Machine-readable coverage summary No (gitignored)

CMake build directories are temporary (created under /tmp/tddl_build_*) and removed automatically after each run. You don't need to worry about them or clean anything up.


Environment variables

tddl works without any environment variables after tddl --build — that's the point. The two variables below exist as escape hatches for when you want to override the defaults:

Variable Default Purpose
UNITY_PATH ./vendor/unity/ Path to a Unity checkout if you have one installed elsewhere
FIL_C_PATH ./vendor/filc/build/bin/clang Path to the Fil-C clang binary

When set, these take precedence over the vendored versions. Useful if you have one global Unity install shared across many projects and don't want a copy per repo. To go back to the vendored version, just unset UNITY_PATH.

If a variable is set but points to an invalid path, tddl errors immediately — it never silently falls back to the vendored version when an explicit override is set wrong.


Examples

First-time project setup:

mkdir my-c-project && cd my-c-project
tddl --build
tddl --doctor    # verify everything is green

Basic test run:

tddl test_list.c

Test against an implementation in src/:

tddl test_list.c --src list.c

Test with helpers from include/:

tddl test_list.c --src list.c --include mocks.c,fixtures.c

Strict CI-style run: coverage + memory check + complexity, fail on anything:

tddl test_list.c --src list.c --coverage --valgrind --lizard

Full deliverable: same as above, but with a PDF report to share:

tddl test_list.c --src list.c --coverage --valgrind --lizard --pdf
# → reports/test_list/report.pdf

Loose complexity thresholds for legacy code:

tddl test_legacy.c --src legacy.c --lizard --ccn=20 --length=100 --args=8

Memory-safe compilation instead of valgrind:

tddl --build-filc    # one-time, takes 30-60 min
tddl test_list.c --src list.c --filc

Troubleshooting

"Required tool not found in PATH: cmake (always required to build tests)" Run tddl --build. If it ran but cmake is still missing, run tddl --doctor to see what failed.

"Unity not found" Run tddl --build. If you have Unity installed somewhere else, export UNITY_PATH=/path/to/Unity instead.

"Test subdirectory not found: tests/test_foo" Each test file lives in its own subdirectory. Run mkdir -p tests/test_foo && touch tests/test_foo/test_foo.c and write your test inside #ifdef TEST.

"--src file not found" Make sure you pass only the basename: --src list.c, not --src src/list.c. The path is always relative to ./src/.

Coverage shows 0% even though tests are passing You probably forgot --src. Without it, tddl doesn't know which file to measure coverage against.

"--filc and --valgrind cannot be used together" This is by design. Fil-C does memory safety at compile time; running its binaries under valgrind produces false positives. Pick one approach per run.

Tests pass but tddl exits non-zero with --coverage Coverage is gated at 100%. If you don't want that, drop the flag.

tddl --build-filc says it'll take 30-60 minutes It will. Fil-C is a full LLVM build. Run it in the background and go for a walk.

Environment was working yesterday, now something's missing Run tddl --doctor first. It'll tell you exactly what's broken in under a second.


License

BSD 2-Clause License. See source headers for the full text.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

tddl-1.0.1.tar.gz (56.5 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

tddl-1.0.1-py3-none-any.whl (56.4 kB view details)

Uploaded Python 3

File details

Details for the file tddl-1.0.1.tar.gz.

File metadata

  • Download URL: tddl-1.0.1.tar.gz
  • Upload date:
  • Size: 56.5 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.12.3

File hashes

Hashes for tddl-1.0.1.tar.gz
Algorithm Hash digest
SHA256 b64e428f5e505b0e88fac6518de75e4e69d432d1090dc135c51f366debae3516
MD5 a2570a3f1606196eb3f215477d49cbc6
BLAKE2b-256 9789c9f2b1e0f490b4768a5c8bbe260bc439e4db4531486b7543559da645a0a4

See more details on using hashes here.

File details

Details for the file tddl-1.0.1-py3-none-any.whl.

File metadata

  • Download URL: tddl-1.0.1-py3-none-any.whl
  • Upload date:
  • Size: 56.4 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.12.3

File hashes

Hashes for tddl-1.0.1-py3-none-any.whl
Algorithm Hash digest
SHA256 075905731693bfe946cca1ed0c2e3353bcf2b239fcc7d33d3940733a040c47fa
MD5 19d74601f91c79d2f5318b568cea74ad
BLAKE2b-256 8d33be1a40b6c80e4c713546ce099883c4a6e1aedd248fc224d29b7b30a7a0ae

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page