October 29, 2024 🚚 Ceedling 1.0.0 is a release candidate and will be shipping very soon. See the Release Notes for an overview of all that’s new since 0.31.1 plus links to the detailed Changelog and list of Breaking Changes.
Ceedling can build your release artifact but is especially adept at building unit test suites for your C projects — even in tricky embedded systems.
Ceedling and its complementary pieces and parts are and always will be freely available and open source. Ceedling Pro is a growing list of paid products and services to help you do even more with these tools.
⭐️ Eager to just get going? Jump to 📚 Documentation & Learning and 🚀 Getting Started.
Ceedling works the way developers want to work. It is flexible and entirely command-line driven. It drives code generation and command line tools for you. All generated and framework code is easy to see and understand.
Ceedling’s features support all types of C development from low-level embedded to enterprise systems. No tool is perfect, but Ceedling can do a whole lot to help you and your team produce quality software.
Ceedling is also a suite of tools. It is the glue for bringing together three other awesome open-source projects you can’t live without if you‘re creating awesomeness in the C language.
- Unity, an xUnit-style test framework.
- CMock†, a code generating, function mocking & stubbing kit for interaction-based testing.
- CException, a framework for adding simple exception handling to C projects in the style of higher-order programming languages.
† Through a plugin, Ceedling also supports FFF for fake functions as an alternative to CMock’s mocks and stubs.
For simple project structures, Ceedling can build and test an entire project from just a few lines in its project configuration file.
Because it handles all the nitty-gritty of rebuilds and becuase of Unity and CMock, Ceedling makes Test-Driven Development in C a breeze. It even provides handy backtrace debugging options for finding the source of crashing code exercised by your unit tests.
Ceedling is extensible with a simple plugin mechanism. It comes with a number of built-in plugins for code coverage, test suite report generation, Continuous Integration features, IDE integration, release library builds & dependency management, and more.
- Found a bug or want to suggest a feature? Submit an issue at this repo.
- Trying to understand features or solve a testing problem? Hit the discussion forums.
- Paid training, customizations, and support contracts are available through Ceedling Pro.
The ThrowTheSwitch community follows a code of conduct.
Please familiarize yourself with our guidelines for contributing to this project, be it code, reviews, documentation, or reports.
Yes, work has begun on certified versions of the Ceedling suite of tools to be available through Ceedling Pro. Reach out to ThingamaByte for more.
While Ceedling can build your release artifact, its claim to fame is building and running test suites.
There’s a good chance you’re looking at Ceedling because of its test suite abilities. And, you’d probably like to see what that looks like, huh? Well, let’s cook you up some realistic examples of tested code and running Ceedling with that code.
(A sample Ceedling project configuration file and links to documentation for it are a bit further down in 🚀 Getting Started.)
#include "Recipe.h"
#include "Kitchen.h"
#include <stdio.h>
#define MAX_SPICE_COUNT (4)
#define MAX_SPICE_AMOUNT_TSP (8.0f)
static float spice_amount = 0;
static uint8_t spice_count = 0;
void Recipe_Reset(char* recipe, size_t size) {
memset(recipe, 0, size);
spice_amount = 0;
spice_count = 0;
}
// Add ingredients to a spice list string with amounts (tsp.)
bool_t Recipe_BuildSpiceListTsp(char* list, size_t maxLen, SpiceId spice, float amount) {
if ((++spice_count > MAX_SPICE_COUNT) || ((spice_amount += amount) > MAX_SPICE_AMOUNT_TSP)) {
snprintf( list, maxLen, "Too spicy!" );
return FALSE;
}
// Kitchen_Ingredient() not shown
snprintf( list + strlen(list), maxLen, "%s\n", Kitchen_Ingredient( spice, amount, TEASPOON ) );
return TRUE;
}
#include "Oven.h"
#include "Time.h"
#include "Baking.h"
bool_t Baking_PreheatOven(float setTempF, duration_t timeout) {
float temperature = 0.0;
Timer* timer = Time_StartTimer( timeout );
Oven_SetTemperatureF( setTempF );
while (temperature < setTempF) {
Time_SleepMs( 250 );
if (Time_IsTimerExpired( timer )) break;
temperature = Oven_GetTemperatureReadingF();
}
return (temperature >= setTempF);
}
Some of what Ceedling does is by naming conventions. See Ceedling’s documentation for much more on this.
#include "unity.h" // Unity, unit test framework
#include "Recipe.h" // By convention, Recipe.c is part of TestRecipe executable build
#include "Kitchen.h" // By convention, Kitchen.c (not shown) is part of TestRecipe executable build
char recipe[100];
void setUp(void) {
// Execute reset before each test case
Recipe_Reset( recipe, sizeof(recipe) );
}
void test_Recipe_BuildSpiceListTsp_shouldBuildSpiceList(void) {
TEST_ASSERT_TRUE( Recipe_BuildSpiceListTsp( recipe, sizeof(recipe), OREGANO, 0.5 ) );
TEST_ASSERT_TRUE( Recipe_BuildSpiceListTsp( recipe, sizeof(recipe), ROSEMARY, 1.0 ) );
TEST_ASSERT_TRUE( Recipe_BuildSpiceListTsp( recipe, sizeof(recipe), THYME, 0.33 ) );
TEST_ASSERT_EQUAL_STRING( "1/2 tsp. Oregano\n1 tsp. Rosemary\n1/3 tsp. Thyme\n", recipe );
}
void test_Recipe_BuildSpiceListTsp_shouldFailIfTooMuchSpice(void) {
TEST_ASSERT_TRUE ( Recipe_BuildSpiceListTsp( recipe, sizeof(recipe), CORIANDER, 4.0 ) );
TEST_ASSERT_TRUE ( Recipe_BuildSpiceListTsp( recipe, sizeof(recipe), BLACK_PEPPER, 4.0 ) );
// Total spice = 8.0 + 0.1 tsp.
TEST_ASSERT_FALSE( Recipe_BuildSpiceListTsp( recipe, sizeof(recipe), BASIL, 0.1 ) );
TEST_ASSERT_EQUAL_STRING( "Too spicy!", recipe );
}
void test_Recipe_BuildSpiceListTsp_shouldFailIfTooManySpices(void) {
TEST_ASSERT_TRUE ( Recipe_BuildSpiceListTsp( recipe, sizeof(recipe), OREGANO, 1.0 ) );
TEST_ASSERT_TRUE ( Recipe_BuildSpiceListTsp( recipe, sizeof(recipe), CORIANDER, 1.0 ) );
TEST_ASSERT_TRUE ( Recipe_BuildSpiceListTsp( recipe, sizeof(recipe), BLACK_PEPPER, 1.0 ) );
TEST_ASSERT_TRUE ( Recipe_BuildSpiceListTsp( recipe, sizeof(recipe), THYME, 1.0 ) );
// Attempt to add 5th spice
TEST_ASSERT_FALSE( Recipe_BuildSpiceListTsp( recipe, sizeof(recipe), BASIL, 1.0 ) );
TEST_ASSERT_EQUAL_STRING( "Too spicy!", recipe );
}
Let’s flavor our test code with a dash of mocks as well…
#include "unity.h" // Unity, unit test framework
#include "Baking.h" // By convention, Baking.c is part of TestBaking executable build
#include "MockOven.h" // By convention, mock .h/.c code generated from Oven.h by CMock
#include "MockTime.h" // By convention, mock .h/.c code generated from Time.h by CMock
/*
* 🚫 This test will fail! Find the missing logic in `Baking_PreheatOven()`.
* (`Oven_SetTemperatureF()` returns success / failure.)
*/
void test_Baking_PreheatOven_shouldFailIfSettingOvenTemperatureFails(void) {
Timer timer; // Uninitialized struct
Time_StartTimer_ExpectAndReturn( TWENTY_MIN, &timer );
// Tell source code that setting the oven temperature did not work
Oven_SetTemperatureF_ExpectAndReturn( 350.0, FALSE );
TEST_ASSERT_FALSE( Baking_PreheatOven( 350.0, TWENTY_MIN ) );
}
void test_Baking_PreheatOven_shouldFailIfTimeoutExpires(void) {
Timer timer; // Uninitialized struct
Time_StartTimer_ExpectAndReturn( TEN_MIN, &timer );
Oven_SetTemperatureF_ExpectAndReturn( 200.0, TRUE );
// We only care that `sleep()` is called, not necessarily every call to it
Time_SleepMs_Ignore();
// Unrolled loop of timeout and temperature checks
Time_IsTimerExpired_ExpectAndReturn( &timer, FALSE );
Oven_GetTemperatureReadingF_ExpectAndReturn( 100.0 );
Time_IsTimerExpired_ExpectAndReturn( &timer, FALSE );
Oven_GetTemperatureReadingF_ExpectAndReturn( 105.0 );
Time_IsTimerExpired_ExpectAndReturn( &timer, FALSE );
Oven_GetTemperatureReadingF_ExpectAndReturn( 110.0 );
Time_IsTimerExpired_ExpectAndReturn( &timer, TRUE );
TEST_ASSERT_FALSE( Baking_PreheatOven( 200.0, TEN_MIN ) );
}
void test_Baking_PreheatOven_shouldSucceedAfterAWhile(void) {
Timer timer; // Uninitialized struct
Time_StartTimer_ExpectAndReturn( TEN_MIN, &timer );
Oven_SetTemperatureF_ExpectAndReturn( 400.0, TRUE );
// We only care that `sleep()` is called, not necessarily every call to it
Time_SleepMs_Ignore();
// Unrolled loop of timeout and temperature checks
Time_IsTimerExpired_ExpectAndReturn( &timer, FALSE );
Oven_GetTemperatureReadingF_ExpectAndReturn( 390.0 );
Time_IsTimerExpired_ExpectAndReturn( &timer, FALSE );
Oven_GetTemperatureReadingF_ExpectAndReturn( 395.0 );
Time_IsTimerExpired_ExpectAndReturn( &timer, FALSE );
Oven_GetTemperatureReadingF_ExpectAndReturn( 399.0 );
Time_IsTimerExpired_ExpectAndReturn( &timer, FALSE );
Oven_GetTemperatureReadingF_ExpectAndReturn( 401.0 );
TEST_ASSERT_TRUE( Baking_PreheatOven( 400.0, TEN_MIN ) );
}
See Ceedling’s documentation for examples and everything you need to know about Ceedling’s configuration file options (not shown here).
The super duper short version is that your project configuration file tells Ceedling where to find test and source files, what testing options you’re using, sets compilation symbols and build tool flags, enables your plugins, and configures your build tool command lines (Ceedling defaults to using the GNU compiler collection — which must be installed, if used).
> ceedling test:all
The test results below are one of the last bits of logging Ceedling produces for a test suite build. Not shown here are all the steps for extracting build details, C code generation, and compilation and linking.
-------------------
FAILED TEST SUMMARY
-------------------
[test/TestBaking.c]
Test: test_Baking_PreheatOven_shouldFailIfSettingOvenTemperatureFails
At line (7): "Function Time_SleepMs() called more times than expected."
-----------------------
❌ OVERALL TEST SUMMARY
-----------------------
TESTED: 6
PASSED: 5
FAILED: 1
IGNORED: 0
The Unity project supports parameterized test cases like this:
TEST_RANGE([5, 100, 5])
void test_should_handle_divisible_by_5_for_parameterized_test_range(int num) {
TEST_ASSERT_EQUAL(0, (num % 5));
}
Ceedling can do all the magic to build and run this test code simply by enabling parameterized test cases in its project configuration. Keep reading for more on how to configure a Ceedling build.
:unity:
:use_param_tests: TRUE
A variety of options for community-based support exist.
Training and support contracts are available through Ceedling Pro
- Ceedling Packet is Ceedling’s user manual. It also references and links to the documentation of the projects, Unity, CMock, and CException, that it weaves together into your test and release builds.
- Release Notes, Breaking Changes, and Changelog can be found in the docs/ directory along with a variety of guides and much more.
- The Plugins section within Ceedling Packet lists all of Ceedling’s built-in plugins providing overviews and links to their documentation.
- Provides a small but useful library of resources and guides on testing and using the Ceedling suite of tools.
- Discusses your options for running a test suite, particularly in the context of embedded systems.
- Links to paid courses, Dr. Surly’s School for Mad Scientists, that provide in-depth training on creating C unit tests and using Unity, CMock, and Ceedling to do so.
Matt Chernosky’s detailed tutorial demonstrates using Ceedling to build a C project with test suite. As the tutorial is a number of years old, the content is a bit out of date. That said, it provides an excellent overview of a real project. Matt is the author of FFF and the FFF plugin for Ceedling.
👀 See the Quick Start section in Ceedling’s user manual, Ceedling Packet.
- Install Ruby. (Only Ruby 3+ supported.)
- Install Ceedling. All supporting frameworks are included.
> gem install ceedling
- Begin crafting your project:
- Create an empty Ceedling project.
> ceedling new <name> [<destination path>]
- Or, add a Ceedling project file to the root of an existing code project.
- Create an empty Ceedling project.
- Run tasks like so:
> ceedling test:all release
As an alternative to local installation, fully packaged Docker images containing Ruby, Ceedling, the GCC toolchain, and more are also available. Docker is a virtualization technology that provides self-contained software bundles that are a portable, well-managed alternative to local installation of tools like Ceedling.
Four Docker image variants containing Ceedling and supporting tools exist. These four images are available for both Intel and ARM host platforms (Docker does the right thing based on your host environment). The latter includes ARM Linux and Apple’s M-series macOS devices.
- MadScienceLab. This image contains Ruby, Ceedling, CMock, Unity, CException, the GNU Compiler Collection (gcc), and a handful of essential C libraries and command line utilities.
- MadScienceLab Plugins. This image contains all of the above plus the command line tools that Ceedling’s built-in plugins rely on. Naturally, it is quite a bit larger than option (1) because of the additional tools and dependencies.
- MadScienceLab ARM. This image mirrors (1) with the compiler toolchain replaced with the GNU
arm-none-eabi
variant. - MadScienceLab ARM + Plugins. This image is (3) with the addition of all the complementary plugin tooling just like (2) provides.
See the Docker Hub pages linked above for more documentation on these images.
Just to be clear here, most users of the MadScienceLab Docker images will probably care about the ability to run unit tests on your own host. If you are one of those users, no matter what host platform you are on — Intel or ARM — you’ll want to go with (1) or (2) above. The tools within the image will automatically do the right thing within your environment. Options (3) and (4) are most useful for specialized cross-compilation scenarios.
To use a MadScienceLab image from your local terminal:
- Install Docker
- Determine:
- The local path of your Ceedling project
- The variant and revision of the Docker image you’ll be using
- Run the container with:
- The Docker
run
command and-it --rm
command line options - A Docker volume mapping from the root of your project to the default project path inside the container (/home/dev/project)
- The Docker
See the command line examples in the following two sections.
Note that all of these somewhat lengthy command lines lend themselves well to being wrapped up in simple helper scripts specific to your project and directory structure.
When the container launches as shown below, it will drop you into a Z-shell command line that has access to all the tools and utilities available within the container. In this usage, the Docker container becomes just another terminal, including ending its execution with exit
.
> docker run -it --rm -v /my/local/project/path:/home/dev/project throwtheswitch/madsciencelab-plugins:1.0.0
Once the MadScienceLab container’s command line is available, to run Ceedling, execute it just as you would after installing Ceedling locally:
~/project > ceedling help
~/project > ceedling new ...
~/project > ceedling test:all
Alternatively, you can run Ceedling through the MadScienceLab Docker container directly from the command line as a command line utility. The general pattern is immediately below.
> docker run -it --rm -v /my/local/project/path:/home/dev/project throwtheswitch/madsciencelab-plugins:1.0.0 <Ceedling command line>
As a specific example, to run all tests in a suite, the command line would be this:
> docker run -it --rm -v /my/local/project/path:/home/dev/project throwtheswitch/madsciencelab-plugins:1.0.0 ceedling test:all
In this usage, the container starts, executes Ceedling, and then ends.
:project:
:build_root: project/build/
:release_build: TRUE
:paths:
:test:
- tests/**
:source:
- source/**
:include:
- inc/**
See this commented project configuration file for a much more complete and sophisticated example of a project configuration.
Or, use Ceedling’s built-in examples
& example
commands to extract a sample project and reference its project file.
See the configuration section in Ceedling Packet for way more details on your project configuration options than we can provide here.
For an overview of all commands, it’s as easy as…
> ceedling help
For a detailed explanation of a single command…
> ceedling help <command>
Creating a project with Ceedling is easy. Simply tell Ceedling the name of the project, and it will create a directory with that name and fill it with a default subdirectory structure and configuration file. An optional destination path is also possible.
> ceedling new YourNewProjectName
You can add files to your src/
and test/
directories, and they will
instantly become part of your test and/or release build. Need a different
structure? You can modify the project.yml
file with your new path or
tooling setup.
Are you just getting started with Ceedling? Maybe you’d like your project to be installed with some of its handy documentation? No problem! You can do this when you create a new project…
> ceedling new --docs MyAwesomeProject
Ceedling can be installed as a globally available Ruby gem. Ceedling can also deploy all its guts into your project instead. This allows it to be used without worrying about external dependencies. More importantly, you don’t have to worry about Ceedling changing outside of your project just because you updated your gems. No need to worry about changes in Unity or CMock breaking your build in the future.
To use Ceedling this way, tell it you want a local copy when you create your project:
> ceedling new --local YourNewProjectName
This will install all of Unity, CMock, CException, and Ceedling itself
into a new folder vendor/
inside your project YourNewProjectName/
.
It will create the same simple empty directory structure for you with
src/
and test/
folders as the standard new
command.
You can view all the build and plugin tasks available to you thanks to your
Ceedling project file with ceedling help
. Ceedling’s command line help
provides a summary list from your project configuration if Ceedling is
able to find your project file (ceedling help help
for more on this).
Running Ceedling build tasks tends to look like this…
> ceedling test:all release
> ceedling gcov:all --verbosity=obnoxious --test-case=boot --mixin=code_cruncher_toolchain
You can upgrade to the latest version of Ceedling at any time, automatically gaining access to any accompanying updates to Unity, CMock, and CException.
To update a locally installed gem…
> gem update ceedling
Otherwise, if you are using the Docker image, you may upgrade by pulling a newer version of the image…
> docker pull throwtheswitch/madsciencelab:<tag>
If you want to force a vendored version of Ceedling inside your project to upgrade to match your latest gem, no problem. Just do the following…
> ceedling upgrade --local YourNewProjectName
Just like with the new
command, an upgrade
should be executed from
within the root directory of your project.
Are you using Git? You might want Ceedling to create a .gitignore
that ignores the build folder while retaining control of the artifacts
folder. This will also add a .gitkeep
file to your test/support
folder.
You can enable this by adding --gitsupport
to your new
call.
> ceedling new --gitsupport YourNewProjectName
After installing Ruby…
> git clone --recursive https://github.com/throwtheswitch/ceedling.git
> cd ceedling
> git submodule update --init --recursive
> bundle install
The Ceedling repository incorporates its supporting frameworks and some plugins via Git submodules. A simple clone may not pull in the latest and greatest.
The bundle
tool ensures you have all needed Ruby gems installed. If
Bundler isn’t installed on your system or you run into problems, you
might have to install it:
> sudo gem install bundler
If you run into trouble running bundler and get messages like can’t find gem bundler (>= 0.a) with executable bundle (Gem::GemNotFoundException), you may need to install a different version of Bundler. For this please reference the version in the Gemfile.lock.
> sudo gem install bundler -v <version in Gemfile.lock>
As an alternative to local installation of Ceedling, nearly all development tasks can be accomplished with the MadScienceLab Docker images.
When running an existing image as a development container, one merely needs to map a volume from your local Ceedling code repository to Ceedling’s installation location within the container. With that accomplished, experimenting with project builds and running self-tests is simple.
-
Start your target Docker container from your host system terminal:
> docker run -it --rm throwtheswitch/<image>:<tag>
-
Look up and note Ceedling’s installation path (listed in
version
output) from within the container command line:~/project > ceedling version
-
Exit the container.
-
Restart the container from your host system with the Ceedling installation volume mapping from (2) and any other command line options you need:
> docker run -it --rm -v /my/local/ceedling/repo:<container installation path> -v /my/local/experiment/path:/home/dev/project throwtheswitch/<image>:<tag>
For development tasks, from the container shell you can:
- Run experiment projects you map into the container (e.g. at /home/dev/project).
- Run the self-test suite. Navigate to the gem installation path discovered in (2) above. From this location, follow the instructions in the section that immediately follows.
Ceedling uses RSpec for its tests.
To execute tests you may run the following from the root of your local Ceedling repository. This test suite build option balances test coverage with suite execution time.
> rake spec
To run individual test files (Ceedling’s Ruby-based tests, that is) and perform other tasks, use the available Rake tasks. From the root of your local Ceedling repo, list those task like this:
> rake -T
Most of Ceedling’s functionality is contained in the application code residing
in lib/
. Ceedling’s command line handling, startup configuration, project
file loading, and mixin handling are contained in a “bootloader” in bin/
.
The code in bin/
is the source of the ceedling
command line tool and
launches the application from lib/
.
Depending on what you’re working on you may need to run Ceedling using a specialized approach.
If you are only working in lib/
, you can:
- Run Ceedling using the
ceedling
command line utility you already have installed. The code inbin/
will run from your locally installed gem or from within your Docker container and launch the Ceedling application for you. - Modify a project file by setting a path value for
:project
↳:which_ceedling
that points to the local copy of Ceedling you cloned from the Git repository. See CeedlingPacket for details.
If you are working in bin/
, running ceedling
at the command line will not
call your modified code. Instead, you must execute the path to the executable
ceedling
in the bin/
folder of the local Ceedling repository you are
working on.