Skip to content

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/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:

:plugins:
  :load_paths:
    - support/plugins
  :enabled:
    - whiz_bang

Ceedling project directory structure:

project/
├── project.yml
└── support/
    └── plugins/
        └── whiz_bang/
            └── lib/
                └── whiz_bang.rb

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>"
}

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 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 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 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_release() and post_release()

These methods are called before and after performing all steps needed to run the release task — i.e. configure, preprocess, compile, link, etc.

Plugin hook methods pre_build and post_build

These methods are called before and after executing any ceedling task — e.g: test, release, coverage, etc.

Plugin hook methods post_error()

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.

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
}