Plugin Option 2: Plugin Subclass
Naming and location conventions:
<plugin_name>/lib/<plugin_name>.rb- The plugin’s class name must be the camelized version (a.k.a. "bumpy case")
of the plugin filename —
whiz_bang.rb➡️WhizBang.
This plugin option allows full programmatic ability connected to any of a number of predefined Ceedling build steps.
The contents of <plugin_name>.rb must implement a class that subclasses
Plugin, Ceedling’s plugin base class.
Example Plugin subclass
An incomplete Plugin subclass follows to illustrate the basics.
# whiz_bang/lib/whiz_bang.rb
require 'ceedling/plugins/plugin'
class WhizBang < Plugin
def setup
# ...
end
# Build step hook
def pre_test(test)
# ...
end
# Build step hook
def post_test(test)
# ...
end
end
Example programmatic plugin layout
Project configuration file:
Ceedling project directory structure:
It is possible and often convenient to add more .rb files to the containing
lib/ directory to allow good organization of plugin code. No Ceedling
conventions exist for these supplemental code files. Only standard Ruby
constraints exist for these filenames and content.
Plugin instance variables
Each Plugin subclass has access to the following instance variables:
@name@ceedling
@name is self explanatory. @ceedling is a hash containing every object
within the Ceedling application; its keys are the filenames of the objects
minus file extension.
Objects commonly used in plugins include:
@ceedling[:configurator]— Project configuration@ceedling[:streaminator]— Logging@ceedling[:reportinator]— String formatting for logging@ceedling[:file_wrapper]— File operations@ceedling[:plugin_reportinator]— Various needs including gathering test results
Plugin method setup()
If your plugin defines this method, it will be called during plugin creation at
Ceedling startup. It is effectively your constructor for your custom Plugin
subclass.
Plugin hook methods pre_ and post_ conventions & concerns
Multi-threaded protections
Because Ceedling can run build operations in multiple threads, build step hook
handlers must be thread safe. Practically speaking, this generally requires
a Mutex object synchronize()d around any code that writes to or reads from
a common data structure instantiated within a plugin.
A common example is collecting test results filepaths from the
post_test_fixture_execute() hook. A hash or array accumulating these
filepaths as text executables complete their runs must have appropriate
threading protections.
Command line tool shell results
Pre and post build step hooks are often called on either side of a command line
tool operation. If a command line tool is executed for a build step (e.g. test
compilation), the arg_hash will be the same for the pre and post hooks with
one difference.
In the post_ hook, the arg_hash parameter will contain a shell_result key
whose associated value is itself a hash with the following contents:
{
:output => "<Console output>", # String holding any $stdout / redirected $stderr output
:status => <Process::Status>, # Ruby object of type Process::Status
:exit_code => <int>, # Command line exit code (extracted from :status object)
:time => <float> # Seconds elapsed for shell operation
}
Preprocessing Hook Limitations
Test preprocessing steps are quite sophisticated and involve various combinations
of tool executions. The post_ preprocessing hooks do not include shell results.
Future updates to Ceedling’s plugin system will create a more robust means of
attaching custom behaviors to test preprocessing or connecting your own
preprocessing pipeline with toolchains other than GCC.
Plugin hook methods pre_mock_preprocess(arg_hash) and post_mock_preprocess(arg_hash)
These methods are called before and after execution of preprocessing for header files to be mocked (see Conventions & Behaviors for preprocessing details). If a project does not enable preprocessing or a build does not include tests, these are not called. This pair of methods is called a number of times equal to the number of mocks in a test build.
The argument arg_hash follows the structure below:
arg_hash = {
# Filepath of header file to be preprocessed on its way to being mocked
:header_file => "<filepath>",
# Filepath of processed header file
:preprocessed_header_file => "<filepath>",
# Filepath of tests C file the mock will be used by
:test => "<filepath>",
# List of flags to be provided to `cpp` GNU preprocessor tool
:flags => [<flags>],
# List of search paths to be provided to `cpp` GNU preprocessor tool
:include_paths => [<paths>],
# List of compilation symbols to be provided to `cpp` GNU preprocessor tool
:defines => [<defines>]
}
Plugin hook methods pre_test_preprocess(arg_hash) and post_test_preprocess(arg_hash)
These methods are called before and after execution of test file preprocessing (see Conventions & Behaviors for preprocessing details). If a project does not enable preprocessing or a build does not include tests, these are not called. This pair of methods is called a number of times equal to the number of test files in a test build.
The argument arg_hash follows the structure below:
arg_hash = {
# Filepath of C test file to be preprocessed on its way to being used to generate runner
:test_file => "<filepath>",
# Filepath of processed tests file
:preprocessed_test_file => "<filepath>",
# Filepath of tests C file the mock will be used by
:test => "<filepath>",
# List of flags to be provided to `cpp` GNU preprocessor tool
:flags => [<flags>],
# List of search paths to be provided to `cpp` GNU preprocessor tool
:include_paths => [<paths>],
# List of compilation symbols to be provided to `cpp` GNU preprocessor tool
:defines => [<defines>]
}
Plugin hook methods pre_mock_generate(arg_hash) and post_mock_generate(arg_hash)
These methods are called before and after mock generation. If a project does not enable mocks or a build does not include tests, these are not called. This pair of methods is called a number of times equal to the number of mocks in a test build.
The argument arg_hash follows the structure below:
arg_hash = {
# Filepath of the header file being mocked.
:header_file => "<filepath>",
# Additional context passed by the calling function.
# Ceedling passes the :test symbol by default while plugins may provide another
:context => :<context>,
# Filepath of the tests C file that references the requested mock
:test => "<filepath>",
# Filepath of the generated mock C code.
:output_path => "<filepath>"
}
Plugin hook methods pre_runner_generate(arg_hash) and post_runner_generate(arg_hash)
These methods are called before and after execution of test runner generation. A
test runner includes all the necessary C scaffolding (and main() entry point)
to call the test cases defined in a test file when a test executable runs. If a
build does not include tests, these are not called. This pair of methods is
called a number of times equal to the number of test files in a test build.
The argument arg_hash follows the structure below:
arg_hash = {
# Additional context passed by the calling function.
# Ceedling passes the :test symbol by default while plugins may provide another
:context => :<context>,
# Filepath of the tests C file.
:test_file => "<filepath>",
# Filepath of the test file to be processed (if preprocessing enabled, this is not the same as :test_file).
:input_file => "<filepath>",
# Filepath of the generated tests runner file.
:runner_file => "<filepath>"
}
Delta build hooks
Ceedling skips work when nothing has changed. This is called a delta build. See Delta Builds for the full picture.
Three hook methods run before Ceedling checks delta build state for a
target. Each one always runs, for every target, whether or not that target
turns out to need rebuilding. A plugin uses one of these hooks to swap in
its own tool for its own build context. Doing the swap here, not only in
the matching pre_*_execute hook below, makes sure Ceedling’s delta build
check sees the real tool. Otherwise a change to that tool’s own settings
will not force a rebuild.
Plugin hook method pre_test_compile_register(arg_hash)
Test builds only, not release builds. Runs once per C file in a test build. Runs before Ceedling registers the file as a delta build target.
The argument arg_hash follows the structure below:
arg_hash = {
:tool => {
# Hash holding compiler tool properties — see ':tools' in the Project Configuration Reference
},
# Symbol of the operation being considered, e.g. :compile or :assemble
:operation => :<operation>,
:context => :<context>,
:module_name => "<name>",
:source => "<filepath>",
:object => "<filepath>",
:flags => [<flags>],
:defines => [<defines>]
}
Plugin hook methods pre_compile_execute(arg_hash) and post_compile_execute(arg_hash)
These methods are called before and after source file compilation. These are called in both test and release builds. This pair of methods is called a number of times equal to the number of C files in a test or release build.
The argument arg_hash follows the structure below:
arg_hash = {
:tool => {
# Hash holding compiler tool properties — see ':tools' in the Project Configuration Reference
},
# Symbol of the operation being performed, e.g. :compile, :assemble or :link
:operation => :<operation>,
# Additional context passed by the calling function.
# Ceedling provides :test or :release by default while plugins may provide another.
:context => :<context>,
# Filepath of the input C file
:source => "<filepath>",
# Filepath of the output object file
:object => "<filepath>",
# List of flags to be provided to compiler tool
:flags => [<flags>],
# List of search paths to be provided to compiler tool
:search_paths => [<paths>],
# List of compilation symbols to be provided to compiler tool
:defines => [<defines>],
# Filepath of the listing file, e.g. .lst file
:list => "<filepath>",
# Filepath of the dependencies file, e.g. .d file
:dependencies => "<filepath>"
}
Plugin hook method pre_test_link_register(arg_hash)
Test builds only, not release builds. Runs once per test executable in a test build. Runs before Ceedling registers the executable as a delta build target. See Delta build hooks above for why this hook exists and when it runs.
A plugin swapping in its own linker tool for its own build context should
do so here, for the same reason described under pre_test_compile_register
above. Because this hook always runs, it also gives a plugin a reliable
place to note that its own build context ran at all, apart from whether
anything actually needed relinking.
The argument arg_hash follows the structure below:
arg_hash = {
:tool => {
# Hash holding linker tool properties — see ':tools' in the Project Configuration Reference
},
:context => :<context>,
:flags => [<flags>],
:executable => "<filepath>"
}
Plugin hook methods pre_link_execute(arg_hash) and post_link_execute(arg_hash)
These methods are called before and after linking an executable. These are called in both test and release builds. These are called for each test executable and each release artifact. This pair of methods is called a number of times equal to the number of test files in a test build or release artifacts in a release build.
The argument arg_hash follows the structure below:
arg_hash = {
# Hash holding linker tool properties.
:tool => {
# Hash holding compiler tool properties — see ':tools' in the Project Configuration Reference
},
# Additional context passed by the calling function.
# Ceedling provides :test or :release by default while plugins may provide another.
:context => :<context>,
# List of object files paths being linked, e.g. .o files
:objects => [],
# List of flags to be provided to linker tool
:flags => [<flags>],
# Filepath of the output file, e.g. .out file
:executable => "<filepath>",
# Filepath of the map file, e.g. .map file
:map => "<filepath>",
# List of libraries to link, e.g. those passed to the (GNU) linker with -l
:libraries => [<names>],
# List of libraries paths, e.g. the ones passed to the (GNU) linker with -L
:libpaths => [<paths>]
}
Plugin hook method pre_test_fixture_register(arg_hash)
Runs once per test executable. Runs before Ceedling registers the test run as a delta build target. See Delta build hooks above for why this hook exists and when it runs.
A plugin swapping in its own wrapper tool to run the test executable (as
Valgrind does) should do so here, for the same reason described under
pre_test_compile_register above. Otherwise a change to that wrapper tool’s own
settings will not force a rerun.
A wrapper tool runs the test executable as one of its own arguments. Ceedling
places Unity’s test case filters (from --test-case, --exclude_test_case, and
backtrace reruns) after the tool’s arguments, where the executable is.
A crashed test executable reruns one test case at a time when
:use_backtrace is :simple. By default the reruns go through Ceedling’s own
backtrace tool, not a swapped wrapper. A wrapper the reruns should run through
too (as the Sanitize plugin’s does, to keep its runtime options) sets
:backtrace_rerun => true in its tool hash. Such a tool can't run under gdb, so
:gdb backtraces fall back to :simple for it, with a notice. Leave the key out
for a wrapper that writes one report per test, as Valgrind’s does, since each
rerun would overwrite it.
The argument arg_hash follows the structure below:
arg_hash = {
:tool => {
# Hash holding execution tool properties — see ':tools' in the Project Configuration Reference
},
:context => :<context>,
:test_name => "<name>",
# The delta build target for this test run. A plugin can register its own
# additional data against this same target — see Delta Builds.
:target => "<filepath>"
}
Plugin hook methods pre_test_fixture_execute(arg_hash) and post_test_fixture_execute(arg_hash)
These methods are called before and after running a test executable. If a build does not include tests, these are not called. This pair of methods is called for each test executable in a build (each test file is ultimately built into a test executable).
The argument arg_hash follows the structure below:
arg_hash = {
# Hash holding execution tool properties.
:tool => {
# Hash holding compiler tool properties — see ':tools' in the Project Configuration Reference
},
# Additional context passed by the calling function.
# Ceedling provides :test or :release by default while plugins may provide another.
:context => :<context>,
# Name of the test file minus path and extension (`test/TestIness.c` -> 'TestIness')
:test_name => "<name>",
# Filepath of original tests C file that became the test executable
:test_filepath => "<filepath>",
# Path to the tests executable file, e.g. .out file
:executable => "<filepath>",
# Path to the tests result file, e.g. .pass/.fail file
:result_file => "<filepath>"
}
Plugin hook method explain_test_crash(arg_hash)
Called when a test executable crashed, before Ceedling writes the crash as test
failures. A plugin that recognizes the cause, such as a sanitizer’s report in the
run’s output, sets arg_hash[:explanation] to a short message. That message
replaces the generic crash message in the test’s failure, except with :gdb
backtraces, whose failures carry gdb’s own trace. It also replaces Ceedling’s
generic crash notice and its backtrace advice.
Two optional keys shape the notice:
:explanation_headingcompletes its first line, “Test executablex…”. Without it, the line ends “crashed.”:explanation_detailslists lines that follow the cause, such as where a log is. The failure message adds them in parentheses.
With :use_backtrace set to :simple, it is called again for each rerun that
crashes, with that rerun’s own shell result, so each crashed test case can be
explained on its own.
The argument arg_hash holds the same keys as pre_test_fixture_execute above,
plus:
arg_hash = {
# ...the keys above...
# The crashed run’s shell result -- see Command line tool shell results
:shell_result => { ... },
# Set by a plugin that recognizes the cause; nil otherwise
:explanation => nil,
# True for the crash's main run, whose explanation reaches the crash notice;
# false for a backtrace rerun
:crash_notice => true,
# Optional, set alongside :explanation
:explanation_details => nil,
:explanation_heading => nil
}
Plugin hook methods pre_test(test) and post_test(test)
These methods are called before and after performing all steps needed to run a test file — i.e. configure, preprocess, compile, link, run, get results, etc. This pair of methods is called for each test file in a test build.
The argument test corresponds to the path of the test C file being processed.
Plugin hook methods pre_test_build(context, timestamp_s) and post_test_build(context, timestamp_s)
These methods are called before and after the full test build pipeline — i.e.
all steps needed to configure, preprocess, compile, link, execute, and collect
results for every test file in the build. post_test_build is called at all
termination points, including early exit due to a build error.
The argument context is the build context symbol (e.g. :test, :gcov) that
was passed to TestInvoker#setup_and_invoke. This lets a plugin respond
differently depending on which kind of build triggered the pipeline.
Each context also gets its own separate delta build cache. A plugin that runs its own build under a distinct context symbol keeps that build’s tracked state apart from ordinary test builds and from any other plugin’s builds. See Delta Builds for more.
The argument timestamp_s is a floating-point stopwatch value in seconds
(from SystemWrapper.time_stopwatch_s). Compare the pre_test_build and
post_test_build timestamps to compute the total test build duration using
Reportinator.generate_duration_string.
Plugin hook methods pre_release_build(timestamp_s) and post_release_build(timestamp_s)
These methods are called before and after performing all steps needed to run the
release task — i.e. configure, preprocess, compile, link, etc. post_release_build
is called at all termination points, including early exit due to a build error.
The argument timestamp_s is a floating-point stopwatch value in seconds
(from SystemWrapper.time_stopwatch_s). Compare the pre_release_build and
post_release_build timestamps to compute the total release build duration using
Reportinator.generate_duration_string.
Plugin hook methods pre_build(timestamp_s) and post_build(timestamp_s)
These methods are called before and after executing any Ceedling task — e.g. test, release, coverage, etc.
The argument timestamp_s is a floating-point stopwatch value in seconds
(from SystemWrapper.time_stopwatch_s). Compare the pre_build and
post_build timestamps to compute the total build duration using
Reportinator.generate_duration_string.
Plugin hook method post_error(timestamp_s)
This method is called at the conclusion of a Ceedling build that encounters any error that halts the build process. To be clear, a test build with failing test cases is not a build error.
The argument timestamp_s is a floating-point stopwatch value in seconds
(from SystemWrapper.time_stopwatch_s) at the moment the error is handled.
Plugin hook methods summary()
This method is called when invoking the summary task, ceedling summary. This
method facilitates logging the results of the last build without running the
previous build again.
Validating a plugin’s tools
By default, Ceedling validates configured tools at startup according to a
simple setting within the tool definition. This works just fine for default
core tools and options. However, in the case of plugins, tools may not be even
relevant to a plugin’s operation depending on its configurable options. It’s
a bit silly for a tool not needed by your project to fail validation if
Ceedling can't find it in your $PATH. Similarly, it’s irresponsible to skip
validating a tool just because it may not be needed.
Ceedling provides optional, programmatic tool validation for these cases.
@ceedling[:tool_validator].validate() can be forced to ignore a tool’s
required: setting to validate it. In such a scenario, a plugin should
configure its own tools as :optional => true but forcibly validate them at
plugin startup if the plugin’s configuration options require said tool.
An example from the gcov plugin illustrates this.
# Validate gcov summary tool if coverage summaries are enabled (summaries rely on the `gcov` tool)
if summaries_enabled?( @project_config )
@ceedling[:tool_validator].validate(
tool: TOOLS_GCOV_SUMMARY, # Tool defintion as Ruby hash
boom: true # Ignore optional status (raise exception if invalid)
)
end
The tool TOOLS_GCOV_SUMMARY is defined with a Ruby hash in the plugin code.
It is configured with :optional => true. At plugin startup, configuration
options determine if the tool is needed. It is forcibly validated if the plugin
configuration requires it.
Collecting test results from within Plugin subclass
Some testing-specific plugins need access to test results to do their work. A utility method is available for this purpose.
@ceedling[:plugin_reportinator].assemble_test_results()
This method takes as an argument a list of results filepaths. These typically
correspond directly to the collection of test files Ceedling processed in a
given test build. It’s common for this list of filepaths to be assembled from
the post_test_fixture_execute build step execution hook.
The data that assemble_test_results() returns has a structure as follows. In
this example, actual results from a single, real test file are presented as
hash/array Ruby code with comments and with some edits to reduce line length.
{
# Associates each test executable (i.e. test file) with an execution run time
:times => {
"test/TestUsartModel.c" => 0.21196400001645088
},
# List of succeeding test cases, grouped by test file.
:successes => [
{
:source => {:file => "test/TestUsartModel.c", :dirname => "test", :basename => "TestUsartModel.c"},
:collection => [
# If Unity is configured to do so, it will output execution run time for each test case.
# Ceedling creates a zero entry if the Unity option is not enabled.
{:test => "testCase1", :line => 17, :message => "", :unity_test_time => 0.0},
{:test => "testCase2", :line => 31, :message => "", :unity_test_time => 0.0}
]
}
],
# List of failing test cases, grouped by test file.
:failures => [
{
:source => {:file => "test/TestUsartModel.c", :dirname => "test", :basename => "TestUsartModel.c"},
:collection => [
{:test => "testCase3", :line => 25, :message => "<failure message>", :unity_test_time => 0.0}
]
}
],
# List of ignored test cases, grouped by test file.
:ignores => [
{
:source => {:file => "test/TestUsartModel.c", :dirname => "test", :basename => "TestUsartModel.c"},
:collection => [
{:test => "testCase4", :line => 39, :message => "", :unity_test_time => 0.0}
]
}
],
# List of strings printed to $stdout, grouped by test file.
:stdout => [
{
:source => {:file => "test/TestUsartModel.c", :dirname => "test", :basename => "TestUsartModel.c"},
# Calls to print to $stdout are outside Unity’s scope, preventing attaching test file line numbers
:collection => [
"<$stdout string (e.g. printf() call)>"
]
}
],
# Test suite run stats
:counts => {
:total => 4,
:passed => 2,
:failed => 1,
:ignored => 1,
:stdout => 1},
# The sum of all test file execution run times
:total_time => 0.21196400001645088
}
Test results report templates
Ceedling renders test results reports from templates. A plugin that prints test results supplies a template, and Ceedling compiles these templates with its own compiler rather than with ERB.
ERB was removed as a Ceedling dependency in 1.2.0. The template language Ceedling accepts is now a deliberate subset of what ERB accepted. Every template Ceedling ships stays within that subset. A template written for an earlier Ceedling works unchanged if it does the same.
Supplying a template
A ReportTestsStdoutPlugin subclass supplies a template by overriding the private
load_template method. That method returns the template as a String. The base
class registers the template during setup() and owns every build step hook, so a
subclass needs nothing else.
The default load_template reads assets/test_results.template from the plugin’s
own root path. It falls back to the historical assets/template.erb name when only
that older file exists. A plugin shipping the old filename therefore keeps working.
Every stock stdout report plugin uses the asset file default.
report_tests_pretty_stdout has an empty subclass body.
A template can also call its own plugin object. Ceedling’s system objects include
each loaded plugin, named for it, so a template reaches its plugin as
@ceedling[:<plugin name>]. report_tests_gtestlike_stdout and
report_tests_ide_stdout work this way. Their templates hand results to plugin
methods that build lines in Ruby. That keeps formatting logic testable and
configurable. Gcov, Bullseye, and Valgrind render test results through whichever
template is registered, so they get the same lines.
A plugin can also render a template directly. @ceedling[:plugin_reportinator]
exposes run_report(template, hash) for this. The Bullseye plugin uses it to
render its coverage summary from assets/coverage.template.
Supported syntax
Five constructs are available, and two of them carry nearly every template. A
percent sign in column zero introduces a line of Ruby. A <%= expression %> tag
inserts that expression’s value into the output.
| Construct | Effect |
|---|---|
% ruby_code |
Executes the line as Ruby. Nothing is emitted. |
%% |
Emits one literal percent sign. Column zero only. |
<%= expression %> |
Emits the expression, converted with to_s. |
<% ruby_code %> |
Executes the code. Nothing is emitted. |
<%# comment %> |
Emits nothing. |
A percent sign is only special in column zero. Whitespace may follow it freely, so a template can indent Ruby lines to show nesting. A percent sign anywhere else on a line is literal text.
All Ruby in a template shares one scope. A value assigned on a percent line is visible to every later line and tag, including across block boundaries.
Newline trimming
A line whose first tag opens it and whose last tag closes it contributes no newline of its own. Text between those two tags does not change that. Every other line keeps its newline. Ceedling’s own templates depend on this behavior to control blank lines in report output.
What a template can reference
Templates render against PluginReportinator’s own binding. The report data
arrives as a local named hash. Global constants such as TEST_SYM resolve
normally.
@ceedling[:plugin_reportinator] exposes several helpers intended for templates.
| Helper | Purpose |
|---|---|
test_report_preamble(hash) |
Returns the counts and banner prefix every stdout report needs. |
generate_banner(text) |
Formats a section divider. |
generate_heading(text) |
Formats a heading. |
reflow_message(message, continuation_prefix) |
Indents the second and later lines of a multi-line message. |
decorate_summary_banner(text, failed_count) |
Colors a summary banner by outcome. |
Unsupported syntax
Four ERB constructs are unavailable. A template relying on any of them needs revision.
| Construct | Behavior |
|---|---|
-%> |
Raises SyntaxError. ERB offered this only under a trim mode Ceedling never used. |
<%% |
Not an escape. The characters are not translated to a literal <%. |
| A tag spanning lines | Raises CeedlingException naming the template line. |
| A tag never closed on its line | Raises CeedlingException naming the template line. |
The last two are deliberate. ERB accepted a multi-line tag. ERB also responded to an unclosed tag by treating the rest of the template as Ruby, which produced garbled output rather than an error. Naming the offending line is more useful to a template author than either behavior.
Porting a template written for ERB
Ceedling’s own templates never used the unsupported constructs above, and
neither did any template this project has seen. Check a template against the
table before assuming it needs no changes. A Ruby error inside a template
reports its location as (ceedling template):<line>, where the line number is
the template’s own.