Skip to main content

Embedded C test runner with cross-compilation support

Project description

vyperling

Python 3.11+ License: MIT PyPI version Build Status

Embedded C unit test runner with cross-compilation support.
A pip-installable replacement for Ceedling โ€” no Ruby dependencies, modern Python-first design.

Overview

vyperling (vpl for short) is a complete embedded C testing framework that:

  • ๐Ÿ” Auto-discovers test_*.c files in your test directory
  • ๐ŸŽฏ Generates CMock-style mocks from C headers using pycparser + Jinja2
  • ๐Ÿ”จ Cross-compiles for native (GCC) and embedded targets (ARM, MIPS, RISC-V, AVR) with parallel jobs
  • ๐Ÿ–ฅ๏ธ Executes natively or under emulation (QEMU, simavr)
  • ๐Ÿ“Š Parses Unity test output with rich terminal reporting
  • ๐Ÿ“ˆ Generates coverage reports (gcov) for native targets
  • ๐Ÿ“‹ Exports JUnit XML for CI/CD integration

Perfect for firmware development, embedded systems testing, and hardware validation workflows.

Demo

vyperling demo

Quick Start

1. Install

pip install vyperling
# Alias 'vpl' is registered automatically
vpl --version

Development (editable install from source):

git clone https://github.com/ericsonjoseph/vyperling.git
cd vyperling
pip install -e .

2. Create a Project

vpl new myproject
cd myproject

This scaffolds:

  • forge.yml โ€” project configuration
  • src/ โ€” source files under test
  • test/ โ€” test files (test_*.c pattern)
  • mocks/ โ€” auto-generated mocks

3. Run Tests

# Test native target (default)
vpl test

# Cross-compile for MIPS32
vpl test --target mips32

# Run specific tests (pattern match)
vpl test -k uart

# Parallel compilation (4 jobs)
vpl test -j4

# Generate coverage report (native only)
vpl test --coverage

# Export JUnit XML for CI
vpl test --output junit

Commands Reference

All commands use vyperling or vpl interchangeably.

vpl test โ€” Discover, Compile, Run, Report

vpl test [OPTIONS]

OPTIONS:
  --target TEXT           Toolchain target (default: targets.default from forge.yml)
  -k, --filter PATTERN    Run only tests matching PATTERN
  -j, --jobs N            Parallel compile jobs (default: 1)
  --coverage              Enable gcov coverage (native target only)
  --output FORMAT         Export results: junit
  -v, --verbose           Print every compiler command
  --no-mock               Skip automatic mock generation

Example: Test UART module with coverage on 4 parallel jobs:

vpl test -k uart -j4 --coverage

vpl mock โ€” Generate Mocks from Headers

Generate CMock-style mocks from C header files:

vpl mock src/uart.h src/spi.h

# Or mock all headers in configured source dirs
vpl mock --all

# Use a specific toolchain's preprocessor
vpl mock --target arm-cortex-m4 src/uart.h

Output: mocks/mock_uart.{c,h} and mocks/mock_spi.{c,h}

vpl build โ€” Compile Only (No Execution)

vpl build [OPTIONS]

OPTIONS:
  --target TEXT           Toolchain target
  -j, --jobs N            Parallel compile jobs
  -v, --verbose           Print compiler commands
  --no-mock               Skip mock generation

Useful for checking compilation without running tests:

vpl build --target arm-cortex-m4 --verbose

vpl targets โ€” List Available Toolchains

vpl targets

Shows:

  • Built-in targets (native, mips32, mips32el, arm-cortex-m4, riscv32, avr)
  • Project-defined targets (from forge.yml)

vpl clean โ€” Remove Build Artifacts

vpl clean              # Remove all build directories
vpl clean --target mips32  # Remove only target's build dir

vpl --version / vpl --help

Show version or full command help. --version also lists the Unity/CMock/CException versions vyperling targets API-compat with:

$ vpl --version
vyperling, version 0.0.3
  Unity      2.6.1
  CMock      2.6.0 (API-compatible, reimplemented)
  CException 1.3.4

Cross-Compilation Targets

Target Triplet Emulator Install (Debian/Ubuntu)
native (native) direct exec (included with GCC)
mips32 mips-linux-gnu QEMU gcc-mips-linux-gnu qemu-user
mips32el mipsel-linux-gnu QEMU gcc-mipsel-linux-gnu qemu-user
arm-cortex-m4 arm-none-eabi QEMU gcc-arm-none-eabi qemu-user
riscv32 riscv64-unknown-elf QEMU gcc-riscv64-unknown-elf qemu-user
avr avr simavr gcc-avr avr-libc simavr

Installation on Ubuntu/Debian:

# Native (GCC)
sudo apt install gcc build-essential

# MIPS cross-compile
sudo apt install gcc-mips-linux-gnu qemu-user

# ARM Cortex-M4 (bare-metal)
sudo apt install gcc-arm-none-eabi qemu-user

# RISC-V
sudo apt install gcc-riscv64-unknown-elf qemu-user

# AVR (Arduino)
sudo apt install gcc-avr avr-libc simavr

Custom Toolchains

Define custom targets in forge.yml:

project:
  name: myproject

toolchains:
  custom-arm:
    description: "Custom ARM GCC 12.2"
    cc: "arm-linux-gcc-12.2"
    ar: "arm-linux-ar-12.2"
    cflags: ["-mcpu=cortex-a7", "-mfloat-abi=hard"]
    emulator: qemu-arm-static
    sysroot: /path/to/sysroot

targets:
  default: native

Configuration (forge.yml)

Minimal required:

project:
  name: MyProject

Full example with all options:

project:
  name: my-firmware
  src_dirs:
    - src
    - lib/hal
  test_dir: test
  include_dirs:
    - src
    - lib/hal/include
  build_dir: build
  mock_dir: mocks

targets:
  default: native

compiler:
  extra_cflags:
    - -Wall
    - -Wextra
    - -pedantic
  defines:
    - DEBUG=1
    - VERSION=1.0.0
  cexception: false   # set true to link vendored CException.c into every test binary

toolchains:
  custom-mcu:
    description: "STM32 Cross-Compile"
    cc: arm-none-eabi-gcc
    ar: arm-none-eabi-ar
    cflags:
      - -mcpu=cortex-m4
      - -mthumb
    emulator: qemu-arm-static

Project Layout

After vpl new myproject:

myproject/
โ”œโ”€โ”€ forge.yml              # Project configuration
โ”œโ”€โ”€ src/                   # Source files under test
โ”‚   โ”œโ”€โ”€ uart.h
โ”‚   โ”œโ”€โ”€ uart.c
โ”‚   โ””โ”€โ”€ spi.c
โ”œโ”€โ”€ test/                  # Unit tests
โ”‚   โ”œโ”€โ”€ test_uart.c
โ”‚   โ””โ”€โ”€ test_spi.c
โ””โ”€โ”€ mocks/                 # Auto-generated mocks (created by 'vpl mock')
    โ”œโ”€โ”€ mock_uart.h
    โ”œโ”€โ”€ mock_uart.c
    โ”œโ”€โ”€ mock_spi.h
    โ””โ”€โ”€ mock_spi.c

Test File Pattern

Tests use the test_*.c pattern. Each test file:

  • Includes unity.h (provided by vyperling)
  • Includes mocks via #include "mock_<dependency>.h"
  • Defines test cases with void test_<name>(void)

Example: test/test_uart.c

#include "unity.h"
#include "uart.h"
#include "mock_gpio.h"

void setUp(void) {
    // Called before each test
}

void tearDown(void) {
    // Called after each test
}

void test_uart_init_should_configure_pins(void) {
    gpio_init_Expect();
    uart_init();
    TEST_ASSERT_TRUE(1);
}

void test_uart_send_should_transmit_byte(void) {
    uart_send(0x42);
    TEST_ASSERT_EQUAL_INT(0x42, last_byte_sent);
}

Test Framework

vyperling uses:

  • Unity โ€” lightweight C assertion framework (ThrowTheSwitch)
  • CMock โ€” automated mocking for C functions (auto-generated via vpl mock)
  • CException โ€” exception-style error handling for C, vendored and opt-in via compiler.cexception: true (ThrowTheSwitch)
  • pycparser โ€” C header parser for mock generation

vpl --version prints the vendored/API-compat versions of all three frameworks alongside vyperling's own โ€” handy for bug reports and compat debugging.

Assertion Macros

Unity provides rich assertions:

// Basic checks
TEST_ASSERT_TRUE(condition)
TEST_ASSERT_FALSE(condition)
TEST_ASSERT_NULL(ptr)
TEST_ASSERT_NOT_NULL(ptr)

// Equality
TEST_ASSERT_EQUAL_INT(expected, actual)
TEST_ASSERT_EQUAL_UINT(expected, actual)
TEST_ASSERT_EQUAL_HEX(expected, actual)
TEST_ASSERT_EQUAL_STRING(expected, actual)

// Arrays
TEST_ASSERT_EQUAL_INT_ARRAY(expected, actual, len)
TEST_ASSERT_EQUAL_MEMORY(expected, actual, len)

// Floating point
TEST_ASSERT_EQUAL_FLOAT(expected, actual, delta)

See Unity documentation for complete reference.

Architecture

vyperling's pipeline flows through 8 independent modules, connected by dataclasses:

TestUnit โ”€โ”€โ†’ CompileResult โ”€โ”€โ†’ RunResult
Module Responsibility
config.py Load forge.yml, resolve paths, manage project settings
toolchains.py Maintain toolchain registry (builtin + user-defined)
discoverer.py Glob test_*.c, match source files, emit TestUnit list
mockgen.py Parse headers with pycparser, generate mocks via Jinja2
compiler.py Invoke GCC with ThreadPoolExecutor, cache via .forge_deps.json
runner.py Execute binaries natively or under QEMU/simavr, parse Unity output
reporter.py Rich terminal tables + JUnit XML export
coverage.py Generate gcov reports (native-only, best-effort)

Key invariant: Each module emits structured output (dataclass) that the next consumes โ€” no hidden state.

Development

Install for Development

git clone https://github.com/ericsonjoseph/vyperling.git
cd vyperling
pip install -e .

Run Tests

# Full test suite (coverage on by default)
pytest

# Single test file
pytest tests/test_mockgen.py

# Single test by name
pytest -k test_cross_compile

# Disable coverage
pytest --no-cov

Project Structure

  • vyperling/ โ€” main library
    • cli.py โ€” Click entry point (commands: test, build, mock, clean, targets, new)
    • config.py โ€” forge.yml loader and config validation
    • toolchains.py โ€” Toolchain dataclass + registry
    • discoverer.py โ€” test file discovery
    • mockgen.py โ€” C header parsing + mock generation
    • compiler.py โ€” GCC invocation with caching and parallel jobs
    • runner.py โ€” test execution and Unity output parsing
    • reporter.py โ€” rich terminal output + JUnit export
    • coverage.py โ€” gcov/gcovr pipeline
    • scaffold.py โ€” project template generation
    • errors.py โ€” exception hierarchy
    • unity.py โ€” Unity C framework accessor
    • c/ โ€” vendored C assets (Unity v2.6.1 + forge_mock)
    • templates/ โ€” Jinja2 templates for mocks and scaffolding
  • tests/ โ€” comprehensive Python test suite

Dependencies

Runtime:

  • click>=8.0 โ€” CLI framework
  • rich>=13.0 โ€” terminal formatting
  • pyyaml>=6.0 โ€” YAML config parsing
  • pycparser>=2.21 โ€” C header parsing
  • jinja2>=3.0 โ€” template engine
  • gcovr>=7.0 โ€” coverage reporting

Development:

  • pytest>=7.0 โ€” testing
  • pytest-cov>=4.0 โ€” coverage measurement

Mock Generation Capabilities

vpl mock (pycparser + Jinja2) emits the full CMock-style API per function: _Expect / _ExpectAndReturn, _ExpectAnyArgs, _Ignore / _IgnoreAndReturn, _IgnoreArg_<param>, _ReturnThruPtr_<param>, _AddCallback / _Stub, and mock_<module>_Init / _Verify / _Destroy.

Supported argument/function shapes:

Shape Example Handling
Scalars / typedefs uint16_t len Mapped to the matching UNITY_TEST_ASSERT_EQUAL_*
const char * const char *msg String compare
Other pointers uint8_t *buf Pointer-identity compare; writable ptrs get _ReturnThruPtr_*
Variadic int log_printf(const char *fmt, ...) Fixed params asserted; variadic tail ignored
Function-pointer params void register_cb(void (*cb)(int)) Stored as void *, asserted by identity (PTR)
Struct-by-value (complete) int classify(struct Point p) Byte-compared via UNITY_TEST_ASSERT_EQUAL_MEMORY

See examples/mock_features for a runnable project that exercises all three of the last group.

vpl mock is also fully #ifdef-aware: it forwards compiler.defines (and, for cross targets, arch-defining toolchain cflags like -march=/-mcpu=/ -mthumb) to the same cc -E preprocessor pass the real compile uses, so guarded declarations resolve identically in mocks and in the actual build โ€” they can never silently drift apart. See examples/guarded_api for a runnable project pairing this with CException-based error handling.

Known Limitations (v0.0.3)

  • Mock generation: Incomplete (opaque) struct-by-value params are skipped with a warning โ€” a forward-declared struct Foo has unknown sizeof, so it cannot be stored or compared. A pointer to the same struct mocks fine.
  • Coverage: Native target only; requires GCC with -fprofile-arcs -ftest-coverage support
  • Emulation: Timeout-based (default 30s per test binary)

Roadmap

v0.0.4

  • On-target execution via OpenOCD/pyOCD debug probe (--target on-device)
  • VS Code extension for inline pass/fail annotations

v0.0.N (Future)

  • --watch mode: rerun tests on file change
  • Argument capture in mocks (store last N calls, not just count)
  • vyperling report command to re-display results from previous run without recompiling

Contributing

Contributions welcome! Please:

  1. Fork the repository
  2. Create a feature branch (git checkout -b feature/my-feature)
  3. Write tests for your changes
  4. Run the full test suite (pytest)
  5. Commit with conventional messages
  6. Push and open a pull request

For major changes, please open an issue first to discuss.

License

MIT โ€” see LICENSE file for details.

Credits

Support

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

vyperling-0.0.3.tar.gz (72.2 kB view details)

Uploaded Source

Built Distribution

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

vyperling-0.0.3-py3-none-any.whl (78.9 kB view details)

Uploaded Python 3

File details

Details for the file vyperling-0.0.3.tar.gz.

File metadata

  • Download URL: vyperling-0.0.3.tar.gz
  • Upload date:
  • Size: 72.2 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: poetry/2.4.1 CPython/3.12.13 Linux/6.17.0-1015-azure

File hashes

Hashes for vyperling-0.0.3.tar.gz
Algorithm Hash digest
SHA256 ba6938807fd79bb79168b441eb7cfc7c09c68723e12d6b57e9b76894aa80725f
MD5 579bc89f3e847212ecc97f425cb00602
BLAKE2b-256 6a6f1bdbb817a4f6880cf81da548e56b53d41e732b3dacae304fde87fd97a470

See more details on using hashes here.

File details

Details for the file vyperling-0.0.3-py3-none-any.whl.

File metadata

  • Download URL: vyperling-0.0.3-py3-none-any.whl
  • Upload date:
  • Size: 78.9 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: poetry/2.4.1 CPython/3.12.13 Linux/6.17.0-1015-azure

File hashes

Hashes for vyperling-0.0.3-py3-none-any.whl
Algorithm Hash digest
SHA256 daaf3045c1384407665abb4584c487befeb9abaf90f2edca45783c07197a02c7
MD5 496efbde1c965c2bca6ea99bb62d5ad6
BLAKE2b-256 5cbd2401714dbc73c72340291c349adacd0e2719759797b60a395635ede648a4

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