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.

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.

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

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)
  • pycparser โ€” C header parser for mock generation

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
  • DEVELOPMENT_PLAN.md โ€” implementation checklist (all 17 steps โœ…)

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.

Known Limitations (v0.0.1)

  • 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.2

  • Add examples in repository root (FreeRTOS + STM32, etc.) with Ceedling/vyperling compatibility validation
  • Refine coverage report: summary percentage, threshold enforcement, terminal details
  • โœ… Lifted mockgen limitations: variadic functions, function-pointer params, and complete struct-by-value params are now mocked (see examples/mock_features); only opaque struct-by-value remains skipped

v0.0.3

  • Full C preprocessor awareness in mockgen (#ifdef-guarded declarations)
  • CException support
  • Plugin API for custom reporters and emulator adapters

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.2.tar.gz (68.8 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.2-py3-none-any.whl (74.7 kB view details)

Uploaded Python 3

File details

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

File metadata

  • Download URL: vyperling-0.0.2.tar.gz
  • Upload date:
  • Size: 68.8 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.2.tar.gz
Algorithm Hash digest
SHA256 e67fd7d073b5ccd0ca15c64237c45e8154968a178a45f834e5389c07dadcb5be
MD5 786310b4ccb676a7969a10372efeb816
BLAKE2b-256 f50da502cf8ed628af03bb4d4ac1318212bc7a4293406c3ac0ec01b478e2bce4

See more details on using hashes here.

File details

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

File metadata

  • Download URL: vyperling-0.0.2-py3-none-any.whl
  • Upload date:
  • Size: 74.7 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.2-py3-none-any.whl
Algorithm Hash digest
SHA256 a6ee86596a0da4b0f491ee9d6c13572c4ed3da648f742e2e2deba6f53d7be710
MD5 48eed307ec329e583ffeb4941958fe98
BLAKE2b-256 a93b51283ffa8a09c2e4e333f3711b127b596586efb5527e6f1e4c627615225e

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