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?
- Quick start
- Project layout
- The five commands
- Flags reference
- Writing tests
- The PDF report
- Exit codes
- Where things go
- Environment variables
- Examples
- Troubleshooting
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":
- Detects your package manager (apt, dnf, pacman, or brew).
- Installs system tools that need root:
cmake,valgrind,git, a C compiler. Asks for sudo when needed. - Installs Python tools via
pipx(isolated venvs):lizard,gcovr. Plusreportlabviapip install --userfor PDF generation. - Clones Unity into
./vendor/unity/(pinned to a stable release tag). - Creates the directory structure (
include/,src/,tests/) if missing. - **Creates a small working example: include/example.h, src/example.c, tests/example_test/example_test.c
- Updates
.gitignorewith a managed block that ignoresvendor/,reports/, andcoverage/. 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
OKline 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 plainOK/MISSING/WARNtext when redirected to a file, sotddl --doctor > log.txtdoesn'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.jsonwith 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.tddldefinesTESTwhen building, so this code only compiles in test builds. Your production builds (if you have any) ignore it entirely. setUpandtearDownare called by Unity before and after eachRUN_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_TESTcall 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:
0if everything succeeded: all tests passed, coverage was 100% (if--coveragewas used), no memory errors (if--valgrindwas used), no complexity violations (if--lizardwas used).1if 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 --buildexits 0 on success, 1 if installation failed.tddl --build-filcsame.tddl --doctorexits 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
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
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
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
b64e428f5e505b0e88fac6518de75e4e69d432d1090dc135c51f366debae3516
|
|
| MD5 |
a2570a3f1606196eb3f215477d49cbc6
|
|
| BLAKE2b-256 |
9789c9f2b1e0f490b4768a5c8bbe260bc439e4db4531486b7543559da645a0a4
|
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
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
075905731693bfe946cca1ed0c2e3353bcf2b239fcc7d33d3940733a040c47fa
|
|
| MD5 |
19d74601f91c79d2f5318b568cea74ad
|
|
| BLAKE2b-256 |
8d33be1a40b6c80e4c713546ce099883c4a6e1aedd248fc224d29b7b30a7a0ae
|