adding new ceedling test project

This commit is contained in:
hathach
2019-06-10 16:18:27 +07:00
parent 579f468d38
commit 97c5c7a937
199 changed files with 23201 additions and 0 deletions

View File

@@ -0,0 +1,22 @@
ceedling-beep
=============
This is a simple plugin that just beeps at the end of a build and/or test sequence. Are you getting too distracted surfing
the internet, chatting with coworkers, or swordfighting while it's building or testing? The friendly beep will let you know
it's time to pay attention again.
This plugin has very few configuration options. At this time it can beep on completion of a task and/or on an error condition.
For each of these, you can configure the method that it should beep.
```
:tools:
:beep_on_done: :bell
:beep_on_error: :bell
```
Each of these have the following options:
- :bell - this option uses the ASCII bell character out stdout
- :speaker_test - this uses the linux speaker-test command if installed
Very likely, we'll be adding to this list if people find this to be useful.

View File

@@ -0,0 +1,40 @@
require 'ceedling/plugin'
require 'ceedling/constants'
class Beep < Plugin
attr_reader :config
def setup
@config = {
:on_done => ((defined? TOOLS_BEEP_ON_DONE) ? TOOLS_BEEP_ON_DONE : :bell ),
:on_error => ((defined? TOOLS_BEEP_ON_ERROR) ? TOOLS_BEEP_ON_ERROR : :bell ),
}
end
def post_build
beep @config[:on_done]
end
def post_error
beep @config[:on_error]
end
private
def beep(method = :none)
case method
when :bell
if (SystemWrapper.windows?)
puts "echo '\007'"
else
puts "echo -ne '\007'"
end
when :speaker_test
`speaker-test -t sine -f 1000 -l 1`
else
#do nothing with illegal or :none
end
end
end

View File

@@ -0,0 +1,15 @@
% function_string = hash[:coverage][:functions].to_s
% branch_string = hash[:coverage][:branches].to_s
% format_string = "%#{[function_string.length, branch_string.length].max}i"
<%=@ceedling[:plugin_reportinator].generate_banner("#{hash[:header]}: CODE COVERAGE SUMMARY")%>
% if (!hash[:coverage][:functions].nil?)
FUNCTIONS: <%=sprintf(format_string, hash[:coverage][:functions])%>%
% else
FUNCTIONS: none
% end
% if (!hash[:coverage][:branches].nil?)
BRANCHES: <%=sprintf(format_string, hash[:coverage][:branches])%>%
% else
BRANCHES: none
% end

View File

@@ -0,0 +1,169 @@
directory(BULLSEYE_BUILD_OUTPUT_PATH)
directory(BULLSEYE_RESULTS_PATH)
directory(BULLSEYE_ARTIFACTS_PATH)
directory(BULLSEYE_DEPENDENCIES_PATH)
CLEAN.include(File.join(BULLSEYE_BUILD_OUTPUT_PATH, '*'))
CLEAN.include(File.join(BULLSEYE_RESULTS_PATH, '*'))
CLEAN.include(File.join(BULLSEYE_DEPENDENCIES_PATH, '*'))
CLOBBER.include(File.join(BULLSEYE_BUILD_PATH, '**/*'))
PLUGINS_BULLSEYE_LIB_PATH = 'C:\\tools\\BullseyeCoverage\\lib' if not defined?(PLUGINS_BULLSEYE_LIB_PATH)
rule(/#{BULLSEYE_BUILD_OUTPUT_PATH}\/#{'.+\\'+EXTENSION_OBJECT}$/ => [
proc do |task_name|
@ceedling[:file_finder].find_compilation_input_file(task_name)
end
]) do |object|
if File.basename(object.source) =~ /^(#{PROJECT_TEST_FILE_PREFIX}|#{CMOCK_MOCK_PREFIX}|#{BULLSEYE_IGNORE_SOURCES.join('|')})/i
@ceedling[:generator].generate_object_file(
TOOLS_BULLSEYE_COMPILER,
OPERATION_COMPILE_SYM,
BULLSEYE_SYM,
object.source,
object.name,
@ceedling[:file_path_utils].form_test_build_list_filepath(object.name)
)
else
@ceedling[BULLSEYE_SYM].generate_coverage_object_file(object.source, object.name)
end
end
rule(/#{BULLSEYE_BUILD_OUTPUT_PATH}\/#{'.+\\'+EXTENSION_EXECUTABLE}$/) do |bin_file|
@ceedling[:generator].generate_executable_file(
TOOLS_BULLSEYE_LINKER,
BULLSEYE_SYM,
bin_file.prerequisites,
bin_file.name,
@ceedling[:file_path_utils].form_test_build_map_filepath(bin_file.name)
)
end
rule(/#{BULLSEYE_RESULTS_PATH}\/#{'.+\\'+EXTENSION_TESTPASS}$/ => [
proc do |task_name|
@ceedling[:file_path_utils].form_test_executable_filepath(task_name)
end
]) do |test_result|
@ceedling[:generator].generate_test_results(TOOLS_BULLSEYE_FIXTURE, BULLSEYE_SYM, test_result.source, test_result.name)
end
rule(/#{BULLSEYE_DEPENDENCIES_PATH}\/#{'.+\\'+EXTENSION_DEPENDENCIES}$/ => [
proc do |task_name|
@ceedling[:file_finder].find_compilation_input_file(task_name)
end
]) do |dep|
@ceedling[:generator].generate_dependencies_file(
TOOLS_TEST_DEPENDENCIES_GENERATOR,
BULLSEYE_SYM,
dep.source,
File.join(BULLSEYE_BUILD_OUTPUT_PATH, File.basename(dep.source).ext(EXTENSION_OBJECT) ),
dep.name
)
end
task :directories => [BULLSEYE_BUILD_OUTPUT_PATH, BULLSEYE_RESULTS_PATH, BULLSEYE_DEPENDENCIES_PATH, BULLSEYE_ARTIFACTS_PATH]
namespace BULLSEYE_SYM do
task source_coverage: COLLECTION_ALL_SOURCE.pathmap("#{BULLSEYE_BUILD_OUTPUT_PATH}/%n#{@ceedling[:configurator].extension_object}")
desc 'Run code coverage for all tests'
task all: [:directories] do
@ceedling[:configurator].replace_flattened_config(@ceedling[BULLSEYE_SYM].config)
@ceedling[BULLSEYE_SYM].enableBullseye(true)
@ceedling[:test_invoker].setup_and_invoke(COLLECTION_ALL_TESTS, BULLSEYE_SYM)
@ceedling[:configurator].restore_config
end
desc "Run single test w/ coverage ([*] real test or source file name, no path)."
task :* do
message = "\nOops! '#{BULLSEYE_ROOT_NAME}:*' isn't a real task. " +
"Use a real test or source file name (no path) in place of the wildcard.\n" +
"Example: rake #{BULLSEYE_ROOT_NAME}:foo.c\n\n"
@ceedling[:streaminator].stdout_puts( message )
end
desc 'Run tests by matching regular expression pattern.'
task :pattern, [:regex] => [:directories] do |_t, args|
matches = []
COLLECTION_ALL_TESTS.each do |test|
matches << test if test =~ /#{args.regex}/
end
if !matches.empty?
@ceedling[:configurator].replace_flattened_config(@ceedling[BULLSEYE_SYM].config)
@ceedling[BULLSEYE_SYM].enableBullseye(true)
@ceedling[:test_invoker].setup_and_invoke(matches, BULLSEYE_SYM, force_run: false)
@ceedling[:configurator].restore_config
else
@ceedling[:streaminator].stdout_puts("\nFound no tests matching pattern /#{args.regex}/.")
end
end
desc 'Run tests whose test path contains [dir] or [dir] substring.'
task :path, [:dir] => [:directories] do |_t, args|
matches = []
COLLECTION_ALL_TESTS.each do |test|
matches << test if File.dirname(test).include?(args.dir.tr('\\', '/'))
end
if !matches.empty?
@ceedling[:configurator].replace_flattened_config(@ceedling[BULLSEYE_SYM].config)
@ceedling[BULLSEYE_SYM].enableBullseye(true)
@ceedling[:test_invoker].setup_and_invoke(matches, BULLSEYE_SYM, force_run: false)
@ceedling[:configurator].restore_config
else
@ceedling[:streaminator].stdout_puts("\nFound no tests including the given path or path component.")
end
end
desc 'Run code coverage for changed files'
task delta: [:directories] do
@ceedling[:configurator].replace_flattened_config(@ceedling[BULLSEYE_SYM].config)
@ceedling[BULLSEYE_SYM].enableBullseye(true)
@ceedling[:test_invoker].setup_and_invoke(COLLECTION_ALL_TESTS, BULLSEYE_SYM, {:force_run => false})
@ceedling[:configurator].restore_config
end
# use a rule to increase efficiency for large projects
# bullseye test tasks by regex
rule(/^#{BULLSEYE_TASK_ROOT}\S+$/ => [
proc do |task_name|
test = task_name.sub(/#{BULLSEYE_TASK_ROOT}/, '')
test = "#{PROJECT_TEST_FILE_PREFIX}#{test}" unless test.start_with?(PROJECT_TEST_FILE_PREFIX)
@ceedling[:file_finder].find_test_from_file_path(test)
end
]) do |test|
@ceedling[:rake_wrapper][:directories].invoke
@ceedling[:configurator].replace_flattened_config(@ceedling[BULLSEYE_SYM].config)
@ceedling[BULLSEYE_SYM].enableBullseye(true)
@ceedling[:test_invoker].setup_and_invoke([test.source], BULLSEYE_SYM)
@ceedling[:configurator].restore_config
end
end
if PROJECT_USE_DEEP_DEPENDENCIES
namespace REFRESH_SYM do
task BULLSEYE_SYM do
@ceedling[:configurator].replace_flattened_config(@ceedling[BULLSEYE_SYM].config)
@ceedling[BULLSEYE_SYM].enableBullseye(true)
@ceedling[:test_invoker].refresh_deep_dependencies
@ceedling[:configurator].restore_config
end
end
end
namespace UTILS_SYM do
desc "Open Bullseye code coverage browser"
task BULLSEYE_SYM do
command = @ceedling[:tool_executor].build_command_line(TOOLS_BULLSEYE_BROWSER, [])
@ceedling[:tool_executor].exec(command[:line], command[:options])
end
end

View File

@@ -0,0 +1,57 @@
---
:bullseye:
:auto_license: TRUE
:plugins:
:bullseye_lib_path: []
:paths:
:bullseye_toolchain_include: []
:tools:
:bullseye_instrumentation:
:executable: covc
:arguments:
- '--file $': ENVIRONMENT_COVFILE
- -q
- ${1}
:bullseye_compiler:
:executable: gcc
:arguments:
- -g
- -I"$": COLLECTION_PATHS_TEST_SUPPORT_SOURCE_INCLUDE_VENDOR
- -I"$": COLLECTION_PATHS_BULLSEYE_TOOLCHAIN_INCLUDE
- -D$: COLLECTION_DEFINES_TEST_AND_VENDOR
- -DBULLSEYE_COMPILER
- -c "${1}"
- -o "${2}"
:bullseye_linker:
:executable: gcc
:arguments:
- ${1}
- -o ${2}
- -L$: PLUGINS_BULLSEYE_LIB_PATH
- -lcov
:bullseye_fixture:
:executable: ${1}
:bullseye_report_covsrc:
:executable: covsrc
:arguments:
- '--file $': ENVIRONMENT_COVFILE
- -q
- -w140
:bullseye_report_covfn:
:executable: covfn
:stderr_redirect: :auto
:arguments:
- '--file $': ENVIRONMENT_COVFILE
- --width 120
- --no-source
- '"${1}"'
:bullseye_browser:
:executable: CoverageBrowser
:background_exec: :auto
:optional: TRUE
:arguments:
- '"$"': ENVIRONMENT_COVFILE
...

View File

@@ -0,0 +1,194 @@
require 'ceedling/plugin'
require 'ceedling/constants'
BULLSEYE_ROOT_NAME = 'bullseye'
BULLSEYE_TASK_ROOT = BULLSEYE_ROOT_NAME + ':'
BULLSEYE_SYM = BULLSEYE_ROOT_NAME.to_sym
BULLSEYE_BUILD_PATH = "#{PROJECT_BUILD_ROOT}/#{BULLSEYE_ROOT_NAME}"
BULLSEYE_BUILD_OUTPUT_PATH = "#{BULLSEYE_BUILD_PATH}/out"
BULLSEYE_RESULTS_PATH = "#{BULLSEYE_BUILD_PATH}/results"
BULLSEYE_DEPENDENCIES_PATH = "#{BULLSEYE_BUILD_PATH}/dependencies"
BULLSEYE_ARTIFACTS_PATH = "#{PROJECT_BUILD_ARTIFACTS_ROOT}/#{BULLSEYE_ROOT_NAME}"
BULLSEYE_IGNORE_SOURCES = ['unity', 'cmock', 'cexception']
class Bullseye < Plugin
def setup
@result_list = []
@environment = [ {:covfile => File.join( BULLSEYE_ARTIFACTS_PATH, 'test.cov' )} ]
@plugin_root = File.expand_path(File.join(File.dirname(__FILE__), '..'))
@coverage_template_all = @ceedling[:file_wrapper].read(File.join(@plugin_root, 'assets/template.erb'))
end
def config
{
:project_test_build_output_path => BULLSEYE_BUILD_OUTPUT_PATH,
:project_test_results_path => BULLSEYE_RESULTS_PATH,
:project_test_dependencies_path => BULLSEYE_DEPENDENCIES_PATH,
:defines_test => DEFINES_TEST + ['CODE_COVERAGE'],
:collection_defines_test_and_vendor => COLLECTION_DEFINES_TEST_AND_VENDOR + ['CODE_COVERAGE']
}
end
def generate_coverage_object_file(source, object)
arg_hash = {:tool => TOOLS_BULLSEYE_INSTRUMENTATION, :context => BULLSEYE_SYM, :source => source, :object => object}
@ceedling[:plugin_manager].pre_compile_execute(arg_hash)
@ceedling[:streaminator].stdout_puts("Compiling #{File.basename(source)} with coverage...")
compile_command =
@ceedling[:tool_executor].build_command_line(
TOOLS_BULLSEYE_COMPILER,
@ceedling[:flaginator].flag_down( OPERATION_COMPILE_SYM, BULLSEYE_SYM, source ),
source,
object,
@ceedling[:file_path_utils].form_test_build_list_filepath( object ) )
coverage_command = @ceedling[:tool_executor].build_command_line(TOOLS_BULLSEYE_INSTRUMENTATION, [], compile_command[:line] )
shell_result = @ceedling[:tool_executor].exec( coverage_command[:line], coverage_command[:options] )
arg_hash[:shell_result] = shell_result
@ceedling[:plugin_manager].post_compile_execute(arg_hash)
end
def post_test_fixture_execute(arg_hash)
result_file = arg_hash[:result_file]
if ((result_file =~ /#{BULLSEYE_RESULTS_PATH}/) and (not @result_list.include?(result_file)))
@result_list << arg_hash[:result_file]
end
end
def post_build
return if (not @ceedling[:task_invoker].invoked?(/^#{BULLSEYE_TASK_ROOT}/))
# test results
results = @ceedling[:plugin_reportinator].assemble_test_results(@result_list)
hash = {
:header => BULLSEYE_ROOT_NAME.upcase,
:results => results
}
@ceedling[:plugin_reportinator].run_test_results_report(hash) do
message = ''
message = 'Unit test failures.' if (results[:counts][:failed] > 0)
message
end
# coverage results
return if (verify_coverage_file() == false)
if (@ceedling[:task_invoker].invoked?(/^#{BULLSEYE_TASK_ROOT}(all|delta)/))
command = @ceedling[:tool_executor].build_command_line(TOOLS_BULLSEYE_REPORT_COVSRC, [])
shell_result = @ceedling[:tool_executor].exec(command[:line], command[:options])
report_coverage_results_all(shell_result[:output])
else
report_per_function_coverage_results(@ceedling[:test_invoker].sources)
end
end
def summary
return if (verify_coverage_file() == false)
result_list = @ceedling[:file_path_utils].form_pass_results_filelist( BULLSEYE_RESULTS_PATH, COLLECTION_ALL_TESTS )
# test results
# get test results for only those tests in our configuration and of those only tests with results on disk
hash = {
:header => BULLSEYE_ROOT_NAME.upcase,
:results => @ceedling[:plugin_reportinator].assemble_test_results(result_list, {:boom => false})
}
@ceedling[:plugin_reportinator].run_test_results_report(hash)
# coverage results
command = @ceedling[:tool_executor].build_command_line(TOOLS_BULLSEYE_REPORT_COVSRC)
shell_result = @ceedling[:tool_executor].exec(command[:line], command[:options])
report_coverage_results_all(shell_result[:output])
end
def enableBullseye(enable)
if BULLSEYE_AUTO_LICENSE
if (enable)
args = ['push', 'on']
@ceedling[:streaminator].stdout_puts("Enabling Bullseye")
else
args = ['pop']
@ceedling[:streaminator].stdout_puts("Reverting Bullseye to previous state")
end
args.each do |arg|
command = @ceedling[:tool_executor].build_command_line(TOOLS_BULLSEYE_BUILD_ENABLE_DISABLE, [], arg)
shell_result = @ceedling[:tool_executor].exec(command[:line], command[:options])
end
end
end
private ###################################
def report_coverage_results_all(coverage)
results = {
:header => BULLSEYE_ROOT_NAME.upcase,
:coverage => {
:functions => nil,
:branches => nil
}
}
if (coverage =~ /^Total.*?=\s+([0-9]+)\%/)
results[:coverage][:functions] = $1.to_i
end
if (coverage =~ /^Total.*=\s+([0-9]+)\%\s*$/)
results[:coverage][:branches] = $1.to_i
end
@ceedling[:plugin_reportinator].run_report($stdout, @coverage_template_all, results)
end
def report_per_function_coverage_results(sources)
banner = @ceedling[:plugin_reportinator].generate_banner( "#{BULLSEYE_ROOT_NAME.upcase}: CODE COVERAGE SUMMARY" )
@ceedling[:streaminator].stdout_puts "\n" + banner
coverage_sources = sources.clone
coverage_sources.delete_if {|item| item =~ /#{CMOCK_MOCK_PREFIX}.+#{EXTENSION_SOURCE}$/}
coverage_sources.delete_if {|item| item =~ /#{BULLSEYE_IGNORE_SOURCES.join('|')}#{EXTENSION_SOURCE}$/}
coverage_sources.each do |source|
command = @ceedling[:tool_executor].build_command_line(TOOLS_BULLSEYE_REPORT_COVFN, [], source)
shell_results = @ceedling[:tool_executor].exec(command[:line], command[:options])
coverage_results = shell_results[:output].deep_clone
coverage_results.sub!(/.*\n.*\n/,'') # Remove the Bullseye tool banner
if (coverage_results =~ /warning cov814: report is empty/)
coverage_results = "WARNING: #{source} contains no coverage data!\n\n"
@ceedling[:streaminator].stdout_puts(coverage_results, Verbosity::COMPLAIN)
else
coverage_results += "\n"
@ceedling[:streaminator].stdout_puts(coverage_results)
end
end
end
def verify_coverage_file
exist = @ceedling[:file_wrapper].exist?( ENVIRONMENT_COVFILE )
if (!exist)
banner = @ceedling[:plugin_reportinator].generate_banner( "#{BULLSEYE_ROOT_NAME.upcase}: CODE COVERAGE SUMMARY" )
@ceedling[:streaminator].stdout_puts "\n" + banner + "\nNo coverage file.\n\n"
end
return exist
end
end
# end blocks always executed following rake run
END {
# cache our input configurations to use in comparison upon next execution
if (@ceedling[:task_invoker].invoked?(/^#{BULLSEYE_TASK_ROOT}/))
@ceedling[:cacheinator].cache_test_config( @ceedling[:setupinator].config_hash )
@ceedling[BULLSEYE_SYM].enableBullseye(false)
end
}

View File

View File

@@ -0,0 +1,52 @@
ceedling-command-hooks
======================
Plugin for easily calling command line tools at various points in the build process
Define any of these sections in :tools: to provide additional hooks to be called on demand:
```
:pre_mock_generate
:post_mock_generate
:pre_runner_generate
:post_runner_generate
:pre_compile_execute
:post_compile_execute
:pre_link_execute
:post_link_execute
:pre_test_fixture_execute
:pre_test_fixture_execute
:pre_test
:post_test
:pre_release
:post_release
:pre_build
:post_build
```
Each of these tools can support an :executable string and an :args list, like so:
```
:tools:
:post_link_execute:
:executable: objcopy.exe
:args:
- ${1} #This is replaced with the executable name
- output.srec
- --strip-all
```
You may also specify an array of executables to be called in a particular place, like so:
```
:tools:
:post_test:
- :executable: echo
:args: "${1} was glorious!"
- :executable: echo
:args:
- it kinda made me cry a little.
- you?
```
Happy Tweaking!

View File

@@ -0,0 +1,75 @@
require 'ceedling/plugin'
require 'ceedling/constants'
class CommandHooks < Plugin
attr_reader :config
def setup
@config = {
:pre_mock_generate => ((defined? TOOLS_PRE_MOCK_GENERATE) ? TOOLS_PRE_MOCK_GENERATE : nil ),
:post_mock_generate => ((defined? TOOLS_POST_MOCK_GENERATE) ? TOOLS_POST_MOCK_GENERATE : nil ),
:pre_runner_generate => ((defined? TOOLS_PRE_RUNNER_GENERATE) ? TOOLS_PRE_RUNNER_GENERATE : nil ),
:post_runner_generate => ((defined? TOOLS_POST_RUNNER_GENERATE) ? TOOLS_POST_RUNNER_GENERATE : nil ),
:pre_compile_execute => ((defined? TOOLS_PRE_COMPILE_EXECUTE) ? TOOLS_PRE_COMPILE_EXECUTE : nil ),
:post_compile_execute => ((defined? TOOLS_POST_COMPILE_EXECUTE) ? TOOLS_POST_COMPILE_EXECUTE : nil ),
:pre_link_execute => ((defined? TOOLS_PRE_LINK_EXECUTE) ? TOOLS_PRE_LINK_EXECUTE : nil ),
:post_link_execute => ((defined? TOOLS_POST_LINK_EXECUTE) ? TOOLS_POST_LINK_EXECUTE : nil ),
:pre_test_fixture_execute => ((defined? TOOLS_PRE_TEST_FIXTURE_EXECUTE) ? TOOLS_PRE_TEST_FIXTURE_EXECUTE : nil ),
:post_test_fixture_execute => ((defined? TOOLS_POST_TEST_FIXTURE_EXECUTE) ? TOOLS_POST_TEST_FIXTURE_EXECUTE : nil ),
:pre_test => ((defined? TOOLS_PRE_TEST) ? TOOLS_PRE_TEST : nil ),
:post_test => ((defined? TOOLS_POST_TEST) ? TOOLS_POST_TEST : nil ),
:pre_release => ((defined? TOOLS_PRE_RELEASE) ? TOOLS_PRE_RELEASE : nil ),
:post_release => ((defined? TOOLS_POST_RELEASE) ? TOOLS_POST_RELEASE : nil ),
:pre_build => ((defined? TOOLS_PRE_BUILD) ? TOOLS_PRE_BUILD : nil ),
:post_build => ((defined? TOOLS_POST_BUILD) ? TOOLS_POST_BUILD : nil ),
:post_build => ((defined? TOOLS_POST_BUILD) ? TOOLS_POST_BUILD : nil ),
:post_error => ((defined? TOOLS_POST_ERROR) ? TOOLS_POST_ERROR : nil ),
}
@plugin_root = File.expand_path(File.join(File.dirname(__FILE__), '..'))
end
def pre_mock_generate(arg_hash); run_hook(:pre_mock_generate, arg_hash[:header_file] ); end
def post_mock_generate(arg_hash); run_hook(:post_mock_generate, arg_hash[:header_file] ); end
def pre_runner_generate(arg_hash); run_hook(:pre_runner_generate, arg_hash[:source ] ); end
def post_runner_generate(arg_hash); run_hook(:post_runner_generate, arg_hash[:runner_file] ); end
def pre_compile_execute(arg_hash); run_hook(:pre_compile_execute, arg_hash[:source_file] ); end
def post_compile_execute(arg_hash); run_hook(:post_compile_execute, arg_hash[:object_file] ); end
def pre_link_execute(arg_hash); run_hook(:pre_link_execute, arg_hash[:executable] ); end
def post_link_execute(arg_hash); run_hook(:post_link_execute, arg_hash[:executable] ); end
def pre_test_fixture_execute(arg_hash); run_hook(:pre_test_fixture_execute, arg_hash[:executable] ); end
def post_test_fixture_execute(arg_hash); run_hook(:post_test_fixture_execute, arg_hash[:executable] ); end
def pre_test(test); run_hook(:pre_test, test ); end
def post_test(test); run_hook(:post_test, test ); end
def pre_release; run_hook(:pre_release ); end
def post_release; run_hook(:post_release ); end
def pre_build; run_hook(:pre_build ); end
def post_build; run_hook(:post_build ); end
def post_error; run_hook(:post_error ); end
private
def run_hook_step(hook, name="")
if (hook[:executable])
args = ( (hook[:args].is_a? Array) ? hook[:args] : [] )
cmd = @ceedling[:tool_executor].build_command_line( hook, args, name )
shell_result = @ceedling[:tool_executor].exec( cmd[:line], cmd[:options] )
end
end
def run_hook(which_hook, name="")
if (@config[which_hook])
@ceedling[:streaminator].stdout_puts("Running Hook #{which_hook}...", Verbosity::NORMAL)
if (@config[which_hook].is_a? Array)
@config[which_hook].each do |hook|
run_hook_step(hook, name)
end
elsif (@config[which_hook].is_a? Hash)
run_hook_step( @config[which_hook], name )
else
@ceedling[:streaminator].stdout_puts("Hook #{which_hook} was poorly formed", Verbosity::COMPLAINT)
end
end
end
end

View File

@@ -0,0 +1,250 @@
# A Fake Function Framework Plug-in for Ceedling
This is a plug-in for [Ceedling](https://github.com/ThrowTheSwitch/Ceedling) to use the [Fake Function Framework](https://github.com/meekrosoft/fff) for mocking instead of CMock.
Using fff provides less strict mocking than CMock, and allows for more loosely-coupled tests.
And, when tests fail -- since you get the actual line number of the failure -- it's a lot easier to figure out what went wrong.
## Installing the plug-in
To use the plugin you need to 1) get the contents of this repo and 2) configure your project to use it.
### Get the source
The easiest way to get the source is to just clone this repo into the Ceedling plugin folder for your existing Ceedling project.
(Don't have a Ceedling project already? [Here are instructions to create one.](http://www.electronvector.com/blog/try-embedded-test-driven-development-right-now-with-ceedling))
From within `<your-project>/vendor/ceedling/plugins`, run:
`git clone https://github.com/ElectronVector/fake_function_framework.git`
This will create a new folder named `fake_function_framework` in the plugins folder.
### Enable the plug-in.
The plug-in is enabled from within your project.yml file.
In the `:plugins` configuration, add `fake_function_framework` to the list of enabled plugins:
```yaml
:plugins:
:load_paths:
- vendor/ceedling/plugins
:enabled:
- stdout_pretty_tests_report
- module_generator
- fake_function_framework
```
*Note that you could put the plugin source in some other loaction.
In that case you'd need to add a new path the `:load_paths`.*
## How to use it
You use fff with Ceedling the same way you used to use CMock.
Modules can still be generated with the default module generator: `rake module:create[my_module]`.
If you want to "mock" `some_module.h` in your tests, just `#include "mock_some_module.h"`.
This creates a fake function for each of the functions defined in `some_module.h`.
The name of each fake is the original function name with an appended `_fake`.
For example, if we're generating fakes for a stack module with `push` and `pop` functions, we would have the fakes `push_fake` and `pop_fake`.
These fakes are linked into our test executable so that any time our unit under test calls `push` or `pop` our fakes are called instead.
Each of these fakes is actually a structure containing information about how the function was called, and what it might return.
We can use Unity to inspect these fakes in our tests, and verify the interactions of our units.
There is also a global structure named `fff` which we can use to check the sequence of calls.
The fakes can also be configured to return particular values, so you can exercise the unit under test however you want.
The examples below explain how to use fff to test a variety of module interactions.
Each example uses fakes for a "display" module, created from a display.h file with `#include "mock_display.h"`. The `display.h` file must exist and must contain the prototypes for the functions to be faked.
### Test that a function was called once
```c
void
test_whenTheDeviceIsReset_thenTheStatusLedIsTurnedOff()
{
// When
event_deviceReset();
// Then
TEST_ASSERT_EQUAL(1, display_turnOffStatusLed_fake.call_count);
}
```
### Test that a function was NOT called
```c
void
test_whenThePowerReadingIsLessThan5_thenTheStatusLedIsNotTurnedOn(void)
{
// When
event_powerReadingUpdate(4);
// Then
TEST_ASSERT_EQUAL(0, display_turnOnStatusLed_fake.call_count);
}
```
## Test that a single function was called with the correct argument
```c
void
test_whenTheVolumeKnobIsMaxed_thenVolumeDisplayIsSetTo11(void)
{
// When
event_volumeKnobMaxed();
// Then
TEST_ASSERT_EQUAL(1, display_setVolume_fake.call_count);
TEST_ASSERT_EQUAL(11, display_setVolume_fake.arg0_val);
}
```
## Test that calls are made in a particular sequence
```c
void
test_whenTheModeSelectButtonIsPressed_thenTheDisplayModeIsCycled(void)
{
// When
event_modeSelectButtonPressed();
event_modeSelectButtonPressed();
event_modeSelectButtonPressed();
// Then
TEST_ASSERT_EQUAL_PTR((void*)display_setModeToMinimum, fff.call_history[0]);
TEST_ASSERT_EQUAL_PTR((void*)display_setModeToMaximum, fff.call_history[1]);
TEST_ASSERT_EQUAL_PTR((void*)display_setModeToAverage, fff.call_history[2]);
}
```
## Fake a return value from a function
```c
void
test_givenTheDisplayHasAnError_whenTheDeviceIsPoweredOn_thenTheDisplayIsPoweredDown(void)
{
// Given
display_isError_fake.return_val = true;
// When
event_devicePoweredOn();
// Then
TEST_ASSERT_EQUAL(1, display_powerDown_fake.call_count);
}
```
## Fake a function with a value returned by reference
```c
void
test_givenTheUserHasTypedSleep_whenItIsTimeToCheckTheKeyboard_theDisplayIsPoweredDown(void)
{
// Given
char mockedEntry[] = "sleep";
void return_mock_value(char * entry, int length)
{
if (length > strlen(mockedEntry))
{
strncpy(entry, mockedEntry, length);
}
}
display_getKeyboardEntry_fake.custom_fake = return_mock_value;
// When
event_keyboardCheckTimerExpired();
// Then
TEST_ASSERT_EQUAL(1, display_powerDown_fake.call_count);
}
```
## Fake a function with a function pointer parameter
```
void
test_givenNewDataIsAvailable_whenTheDisplayHasUpdated_thenTheEventIsComplete(void)
{
// A mock function for capturing the callback handler function pointer.
void(*registeredCallback)(void) = 0;
void mock_display_updateData(int data, void(*callback)(void))
{
//Save the callback function.
registeredCallback = callback;
}
display_updateData_fake.custom_fake = mock_display_updateData;
// Given
event_newDataAvailable(10);
// When
if (registeredCallback != 0)
{
registeredCallback();
}
// Then
TEST_ASSERT_EQUAL(true, eventProcessor_isLastEventComplete());
}
```
## Helper macros
For convenience, there are also some helper macros that create new Unity-style asserts:
- `TEST_ASSERT_CALLED(function)`: Asserts that a function was called once.
- `TEST_ASSERT_NOT_CALLED(function)`: Asserts that a function was never called.
- `TEST_ASSERT_CALLED_TIMES(times, function)`: Asserts that a function was called a particular number of times.
- `TEST_ASSERT_CALLED_IN_ORDER(order, function)`: Asserts that a function was called in a particular order.
Here's how you might use one of these instead of simply checking the call_count value:
```c
void
test_whenTheDeviceIsReset_thenTheStatusLedIsTurnedOff()
{
// When
event_deviceReset();
// Then
// This how to directly use fff...
TEST_ASSERT_EQUAL(1, display_turnOffStatusLed_fake.call_count);
// ...and this is how to use the helper macro.
TEST_ASSERT_CALLED(display_turnOffStatusLed);
}
```
## Test setup
All of the fake functions, and any fff global state are all reset automatically between each test.
## CMock configuration
Use still use some of the CMock configuration options for setting things like the mock prefix, and for including additional header files in the mock files.
```yaml
:cmock:
:mock_prefix: mock_
:includes:
-
:includes_h_pre_orig_header:
-
:includes_h_post_orig_header:
-
:includes_c_pre_header:
-
:includes_c_post_header:
```
## Running the tests
There are unit and integration tests for the plug-in itself.
These are run with the default `rake` task.
The integration test runs the tests for the example project in examples/fff_example.
For the integration tests to succeed, this repository must be placed in a Ceedling tree in the plugins folder.
## More examples
There is an example project in examples/fff_example.
It shows how to use the plug-in with some full-size examples.

View File

@@ -0,0 +1,19 @@
require 'rake'
require 'rspec/core/rake_task'
desc "Run all rspecs"
RSpec::Core::RakeTask.new(:spec) do |t|
t.pattern = Dir.glob('spec/**/*_spec.rb')
t.rspec_opts = '--format documentation'
# t.rspec_opts << ' more options'
end
desc "Run integration test on example"
task :integration_test do
chdir("./examples/fff_example") do
sh "rake clobber"
sh "rake test:all"
end
end
task :default => [:spec, :integration_test]

View File

@@ -0,0 +1,71 @@
---
# Notes:
# Sample project C code is not presently written to produce a release artifact.
# As such, release build options are disabled.
# This sample, therefore, only demonstrates running a collection of unit tests.
:project:
:use_exceptions: FALSE
:use_test_preprocessor: TRUE
:use_auxiliary_dependencies: TRUE
:build_root: build
# :release_build: TRUE
:test_file_prefix: test_
#:release_build:
# :output: MyApp.out
# :use_assembly: FALSE
:environment:
:extension:
:executable: .out
:paths:
:test:
- +:test/**
:source:
- src/**
:support:
:defines:
# in order to add common defines:
# 1) remove the trailing [] from the :common: section
# 2) add entries to the :common: section (e.g. :test: has TEST defined)
:commmon: &common_defines []
:test:
- *common_defines
- TEST
:test_preprocess:
- *common_defines
- TEST
:cmock:
:mock_prefix: mock_
:when_no_prototypes: :warn
:enforce_strict_ordering: TRUE
:plugins:
- :ignore
- :callback
:treat_as:
uint8: HEX8
uint16: HEX16
uint32: UINT32
int8: INT8
bool: UINT8
#:tools:
# Ceedling defaults to using gcc for compiling, linking, etc.
# As [:tools] is blank, gcc will be used (so long as it's in your system path)
# See documentation to configure a given toolchain for use
:plugins:
:load_paths:
# This change from the default is for running Ceedling out of another folder.
- ../../../../plugins
:enabled:
- stdout_pretty_tests_report
- module_generator
- fake_function_framework
...

View File

@@ -0,0 +1,7 @@
# This change from the default is for running Ceedling out of another folder.
PROJECT_CEEDLING_ROOT = "../../../.."
load "#{PROJECT_CEEDLING_ROOT}/lib/ceedling.rb"
Ceedling.load_project
task :default => %w[ test:all release ]

View File

@@ -0,0 +1 @@
#include "bar.h"

View File

@@ -0,0 +1,14 @@
#ifndef bar_H
#define bar_H
#include "custom_types.h"
void bar_turn_on(void);
void bar_print_message(const char * message);
void bar_print_message_formatted(const char * format, ...);
void bar_numbers(int one, int two, char three);
void bar_const_test(const char * a, char * const b, const int c);
custom_t bar_needs_custom_type(void);
const char * bar_return_const_ptr(int one);
#endif // bar_H

View File

@@ -0,0 +1,6 @@
#ifndef custom_types_H
#define custom_types_H
typedef int custom_t;
#endif

View File

@@ -0,0 +1,7 @@
#include <stdio.h>
#include "display.h"
void display_turnOffStatusLed(void)
{
printf("Display: Status LED off");
}

View File

@@ -0,0 +1,16 @@
#include <stdbool.h>
void display_turnOffStatusLed(void);
void display_turnOnStatusLed(void);
void display_setVolume(int level);
void display_setModeToMinimum(void);
void display_setModeToMaximum(void);
void display_setModeToAverage(void);
bool display_isError(void);
void display_powerDown(void);
void display_updateData(int data, void(*updateCompleteCallback)(void));
/*
The entry is returned (up to `length` bytes) in the provided `entry` buffer.
*/
void display_getKeyboardEntry(char * entry, int length);

View File

@@ -0,0 +1,93 @@
/*
This module implements some business logic to test.
Signal events by calling the functions on the module.
*/
#include <stdio.h>
#include <string.h>
#include "event_processor.h"
#include "display.h"
void event_deviceReset(void)
{
//printf ("Device reset\n");
display_turnOffStatusLed();
}
void event_volumeKnobMaxed(void)
{
display_setVolume(11);
}
void event_powerReadingUpdate(int powerReading)
{
if (powerReading >= 5)
{
display_turnOnStatusLed();
}
}
void event_modeSelectButtonPressed(void)
{
static int mode = 0;
if (mode == 0)
{
display_setModeToMinimum();
mode++;
}
else if (mode == 1)
{
display_setModeToMaximum();
mode++;
}
else if (mode == 2)
{
display_setModeToAverage();
mode++;
}
else
{
mode = 0;
}
}
void event_devicePoweredOn(void)
{
if (display_isError())
{
display_powerDown();
}
}
void event_keyboardCheckTimerExpired(void)
{
char userEntry[100];
display_getKeyboardEntry(userEntry, 100);
if (strcmp(userEntry, "sleep") == 0)
{
display_powerDown();
}
}
static bool event_lastComplete = false;
/* Function called when the display update is complete. */
static void displayUpdateComplete(void)
{
event_lastComplete = true;
}
void event_newDataAvailable(int data)
{
event_lastComplete = false;
display_updateData(data, displayUpdateComplete);
}
bool eventProcessor_isLastEventComplete(void)
{
return event_lastComplete;
}

View File

@@ -0,0 +1,11 @@
#include <stdbool.h>
void event_deviceReset(void);
void event_volumeKnobMaxed(void);
void event_powerReadingUpdate(int powerReading);
void event_modeSelectButtonPressed(void);
void event_devicePoweredOn(void);
void event_keyboardCheckTimerExpired(void);
void event_newDataAvailable(int data);
bool eventProcessor_isLastEventComplete(void);

View File

@@ -0,0 +1,16 @@
#include "foo.h"
#include "bar.h"
#include "subfolder/zzz.h"
void foo_turn_on(void) {
bar_turn_on();
zzz_sleep(1, "sleepy");
}
void foo_print_message(const char * message) {
bar_print_message(message);
}
void foo_print_special_message(void) {
bar_print_message_formatted("The numbers are %d, %d and %d", 1, 2, 3);
}

View File

@@ -0,0 +1,8 @@
#ifndef foo_H
#define foo_H
void foo_turn_on(void);
void foo_print_message(const char * message);
void foo_print_special_message(void);
#endif // foo_H

View File

@@ -0,0 +1 @@
#include "zzz.h"

View File

@@ -0,0 +1,6 @@
#ifndef zzz_H
#define zzz_H
int zzz_sleep(int time, char * name);
#endif // zzz_H

View File

@@ -0,0 +1,155 @@
#include "unity.h"
#include "event_processor.h"
#include "mock_display.h"
#include <string.h>
void setUp (void)
{
}
void tearDown (void)
{
}
/*
Test that a single function was called.
*/
void
test_whenTheDeviceIsReset_thenTheStatusLedIsTurnedOff()
{
// When
event_deviceReset();
// Then
TEST_ASSERT_EQUAL(1, display_turnOffStatusLed_fake.call_count);
// or use the helper macro...
TEST_ASSERT_CALLED(display_turnOffStatusLed);
}
/*
Test that a single function is NOT called.
*/
void
test_whenThePowerReadingIsLessThan5_thenTheStatusLedIsNotTurnedOn(void)
{
// When
event_powerReadingUpdate(4);
// Then
TEST_ASSERT_EQUAL(0, display_turnOnStatusLed_fake.call_count);
// or use the helper macro...
TEST_ASSERT_NOT_CALLED(display_turnOffStatusLed);
}
/*
Test that a single function was called with the correct arugment.
*/
void
test_whenTheVolumeKnobIsMaxed_thenVolumeDisplayIsSetTo11(void)
{
// When
event_volumeKnobMaxed();
// Then
TEST_ASSERT_EQUAL(1, display_setVolume_fake.call_count);
// or use the helper macro...
TEST_ASSERT_CALLED(display_setVolume);
TEST_ASSERT_EQUAL(11, display_setVolume_fake.arg0_val);
}
/*
Test a sequence of calls.
*/
void
test_whenTheModeSelectButtonIsPressed_thenTheDisplayModeIsCycled(void)
{
// When
event_modeSelectButtonPressed();
event_modeSelectButtonPressed();
event_modeSelectButtonPressed();
// Then
TEST_ASSERT_EQUAL_PTR((void *)display_setModeToMinimum, fff.call_history[0]);
TEST_ASSERT_EQUAL_PTR((void *)display_setModeToMaximum, fff.call_history[1]);
TEST_ASSERT_EQUAL_PTR((void *)display_setModeToAverage, fff.call_history[2]);
// or use the helper macros...
TEST_ASSERT_CALLED_IN_ORDER(0, display_setModeToMinimum);
TEST_ASSERT_CALLED_IN_ORDER(1, display_setModeToMaximum);
TEST_ASSERT_CALLED_IN_ORDER(2, display_setModeToAverage);
}
/*
Mock a return value from a function.
*/
void
test_givenTheDisplayHasAnError_whenTheDeviceIsPoweredOn_thenTheDisplayIsPoweredDown(void)
{
// Given
display_isError_fake.return_val = true;
// When
event_devicePoweredOn();
// Then
TEST_ASSERT_EQUAL(1, display_powerDown_fake.call_count);
// or use the helper macro...
TEST_ASSERT_CALLED(display_powerDown);
}
/*
Mock a sequence of calls with return values.
*/
/*
Mocking a function with a value returned by reference.
*/
void
test_givenTheUserHasTypedSleep_whenItIsTimeToCheckTheKeyboard_theDisplayIsPoweredDown(void)
{
// Given
char mockedEntry[] = "sleep";
void return_mock_value(char * entry, int length)
{
if (length > strlen(mockedEntry))
{
strncpy(entry, mockedEntry, length);
}
}
display_getKeyboardEntry_fake.custom_fake = return_mock_value;
// When
event_keyboardCheckTimerExpired();
// Then
TEST_ASSERT_EQUAL(1, display_powerDown_fake.call_count);
// or use the helper macro...
TEST_ASSERT_CALLED(display_powerDown);
}
/*
Mock a function with a function pointer parameter.
*/
void
test_givenNewDataIsAvailable_whenTheDisplayHasUpdated_thenTheEventIsComplete(void)
{
// A mock function for capturing the callback handler function pointer.
void(*registeredCallback)(void) = 0;
void mock_display_updateData(int data, void(*callback)(void))
{
//Save the callback function.
registeredCallback = callback;
}
display_updateData_fake.custom_fake = mock_display_updateData;
// Given
event_newDataAvailable(10);
// When
if (registeredCallback != 0)
{
registeredCallback();
}
// Then
TEST_ASSERT_EQUAL(true, eventProcessor_isLastEventComplete());
}

View File

@@ -0,0 +1,47 @@
#include "unity.h"
#include "foo.h"
#include "mock_bar.h"
#include "mock_zzz.h"
void setUp(void)
{
}
void tearDown(void)
{
}
void test_foo(void)
{
//When
foo_turn_on();
//Then
TEST_ASSERT_EQUAL(1, bar_turn_on_fake.call_count);
TEST_ASSERT_EQUAL(1, zzz_sleep_fake.call_count);
TEST_ASSERT_EQUAL_STRING("sleepy", zzz_sleep_fake.arg1_val);
}
void test_foo_again(void)
{
//When
foo_turn_on();
//Then
TEST_ASSERT_EQUAL(1, bar_turn_on_fake.call_count);
}
void test_foo_mock_with_const(void)
{
foo_print_message("123");
TEST_ASSERT_EQUAL(1, bar_print_message_fake.call_count);
TEST_ASSERT_EQUAL_STRING("123", bar_print_message_fake.arg0_val);
}
void test_foo_mock_with_variable_args(void)
{
foo_print_special_message();
TEST_ASSERT_EQUAL(1, bar_print_message_formatted_fake.call_count);
TEST_ASSERT_EQUAL_STRING("The numbers are %d, %d and %d", bar_print_message_formatted_fake.arg0_val);
}

View File

@@ -0,0 +1,87 @@
require 'ceedling/plugin'
require 'fff_mock_generator'
class FakeFunctionFramework < Plugin
# Set up Ceedling to use this plugin.
def setup
# Get the location of this plugin.
@plugin_root = File.expand_path(File.join(File.dirname(__FILE__), '..'))
puts "Using fake function framework (fff)..."
# Switch out the cmock_builder with our own.
@ceedling[:cmock_builder].cmock = FffMockGeneratorForCMock.new(@ceedling[:setupinator].config_hash[:cmock])
# Add the path to fff.h to the include paths.
COLLECTION_PATHS_TEST_SUPPORT_SOURCE_INCLUDE_VENDOR << "#{@plugin_root}/vendor/fff"
COLLECTION_PATHS_TEST_SUPPORT_SOURCE_INCLUDE_VENDOR << "#{@plugin_root}/src"
end
def post_runner_generate(arg_hash)
# After the test runner file has been created, append the FFF globals
# definition to the end of the test runner. These globals will be shared by
# all mocks linked into the test.
File.open(arg_hash[:runner_file], 'a') do |f|
f.puts
f.puts "//=======Defintions of FFF variables====="
f.puts %{#include "fff.h"}
f.puts "DEFINE_FFF_GLOBALS;"
end
end
end # class FakeFunctionFramework
class FffMockGeneratorForCMock
def initialize(options=nil)
@cm_config = CMockConfig.new(options)
@cm_parser = CMockHeaderParser.new(@cm_config)
@silent = (@cm_config.verbosity < 2)
# These are the additional files to include in the mock files.
@includes_h_pre_orig_header = (@cm_config.includes || @cm_config.includes_h_pre_orig_header || []).map{|h| h =~ /</ ? h : "\"#{h}\""}
@includes_h_post_orig_header = (@cm_config.includes_h_post_orig_header || []).map{|h| h =~ /</ ? h : "\"#{h}\""}
@includes_c_pre_header = (@cm_config.includes_c_pre_header || []).map{|h| h =~ /</ ? h : "\"#{h}\""}
@includes_c_post_header = (@cm_config.includes_c_post_header || []).map{|h| h =~ /</ ? h : "\"#{h}\""}
end
def setup_mocks(files)
[files].flatten.each do |src|
generate_mock (src)
end
end
def generate_mock (header_file_to_mock)
module_name = File.basename(header_file_to_mock, '.h')
puts "Creating mock for #{module_name}..." unless @silent
mock_name = @cm_config.mock_prefix + module_name + @cm_config.mock_suffix
mock_path = @cm_config.mock_path
if @cm_config.subdir
# If a subdirectory has been configured, append it to the mock path.
mock_path = "#{mock_path}/#{@cm_config.subdir}"
end
full_path_for_mock = "#{mock_path}/#{mock_name}"
# Parse the header file so we know what to mock.
parsed_header = @cm_parser.parse(module_name, File.read(header_file_to_mock))
# Create the directory if it doesn't exist.
mkdir_p full_path_for_mock.pathmap("%d")
# Generate the mock header file.
puts "Creating mock: #{full_path_for_mock}.h"
# Create the mock header.
File.open("#{full_path_for_mock}.h", 'w') do |f|
f.write(FffMockGenerator.create_mock_header(module_name, mock_name, parsed_header,
@includes_h_pre_orig_header, @includes_h_post_orig_header))
end
# Create the mock source file.
File.open("#{full_path_for_mock}.c", 'w') do |f|
f.write(FffMockGenerator.create_mock_source(mock_name, parsed_header,
@includes_c_pre_orig_header, @includes_c_post_orig_header))
end
end
end

View File

@@ -0,0 +1,163 @@
# Creates mock files from parsed header files that can be linked into applications.
# The mocks created are compatible with CMock for use with Ceedling.
class FffMockGenerator
def self.create_mock_header(module_name, mock_name, parsed_header, pre_includes=nil,
post_includes=nil)
output = StringIO.new
write_opening_include_guard(mock_name, output)
output.puts
write_extra_includes(pre_includes, output)
write_header_includes(module_name, output)
write_extra_includes(post_includes, output)
output.puts
write_typedefs(parsed_header, output)
output.puts
write_function_declarations(parsed_header, output)
output.puts
write_control_function_prototypes(mock_name, output)
output.puts
write_closing_include_guard(mock_name, output)
output.string
end
def self.create_mock_source (mock_name, parsed_header, pre_includes=nil,
post_includes=nil)
output = StringIO.new
write_extra_includes(pre_includes, output)
write_source_includes(mock_name, output)
write_extra_includes(post_includes, output)
output.puts
write_function_definitions(parsed_header, output)
output.puts
write_control_function_definitions(mock_name, parsed_header, output)
output.string
end
private
# Header file generation functions.
def self.write_opening_include_guard(mock_name, output)
output.puts "#ifndef #{mock_name}_H"
output.puts "#define #{mock_name}_H"
end
def self.write_header_includes(module_name, output)
output.puts %{#include "fff.h"}
output.puts %{#include "fff_unity_helper.h"}
output.puts %{#include "#{module_name}.h"}
end
def self.write_typedefs(parsed_header, output)
return unless parsed_header.key?(:typedefs)
parsed_header[:typedefs].each do |typedef|
output.puts typedef
end
end
def self.write_function_declarations(parsed_header, output)
write_function_macros("DECLARE", parsed_header, output)
end
def self.write_control_function_prototypes(mock_name, output)
output.puts "void #{mock_name}_Init(void);"
output.puts "void #{mock_name}_Verify(void);"
output.puts "void #{mock_name}_Destroy(void);"
end
def self.write_closing_include_guard(mock_name, output)
output.puts "#endif // #{mock_name}_H"
end
# Source file generation functions.
def self.write_source_includes (mock_name, output)
output.puts "#include <string.h>"
output.puts %{#include "fff.h"}
output.puts %{#include "#{mock_name}.h"}
end
def self.write_function_definitions(parsed_header, output)
write_function_macros("DEFINE", parsed_header, output)
end
def self.write_control_function_definitions(mock_name, parsed_header, output)
output.puts "void #{mock_name}_Init(void)"
output.puts "{"
# In the init function, reset the FFF globals. These are used for things
# like the call history.
output.puts " FFF_RESET_HISTORY();"
# Also, reset all of the fakes.
if parsed_header[:functions]
parsed_header[:functions].each do |function|
output.puts " RESET_FAKE(#{function[:name]})"
end
end
output.puts "}"
output.puts "void #{mock_name}_Verify(void)"
output.puts "{"
output.puts "}"
output.puts "void #{mock_name}_Destroy(void)"
output.puts "{"
output.puts "}"
end
# Shared functions.
def self.write_extra_includes(includes, output)
if includes
includes.each {|inc| output.puts "#include #{inc}\n"}
end
end
def self.write_function_macros(macro_type, parsed_header, output)
return unless parsed_header.key?(:functions)
parsed_header[:functions].each do |function|
name = function[:name]
return_type = function[:return][:type]
if function.has_key? :modifier
# Prepend any modifier. If there isn't one, trim any leading whitespace.
return_type = "#{function[:modifier]} #{return_type}".lstrip
end
arg_count = function[:args].size
# Check for variable arguments.
var_arg_suffix = ""
if function[:var_arg]
# If there are are variable arguments, then we need to add this argument
# to the count, update the suffix that will get added to the macro.
arg_count += 1
var_arg_suffix = "_VARARG"
end
# Generate the correct macro.
if return_type == 'void'
output.print "#{macro_type}_FAKE_VOID_FUNC#{arg_count}#{var_arg_suffix}(#{name}"
else
output.print "#{macro_type}_FAKE_VALUE_FUNC#{arg_count}#{var_arg_suffix}(#{return_type}, #{name}"
end
# Append each argument type.
function[:args].each do |arg|
output.print ", "
if arg[:const?]
output.print "const "
end
output.print "#{arg[:type]}"
end
# If this argument list ends with a variable argument, add it here at the end.
if function[:var_arg]
output.print ", ..."
end
# Close the declaration.
output.puts ");"
end
end
end

View File

@@ -0,0 +1,304 @@
require 'stringio'
require 'fff_mock_generator.rb'
require 'header_generator.rb'
# Test the contents of the .h file created for the mock.
describe "FffMockGenerator.create_mock_header" do
context "when there is nothing to mock," do
let(:mock_header) {
parsed_header = {}
FffMockGenerator.create_mock_header("display", "mock_display", parsed_header)
}
it "then the generated header file starts with an opening include guard" do
expect(mock_header).to start_with(
"#ifndef mock_display_H\n" +
"#define mock_display_H")
end
it "then the generated file ends with a closing include guard" do
expect(mock_header).to end_with(
"#endif // mock_display_H\n")
end
it "then the generated file includes the fff header" do
expect(mock_header).to include(
%{#include "fff.h"\n})
end
it "then the generated file has a prototype for the init function" do
expect(mock_header).to include(
"void mock_display_Init(void);")
end
it "then the generated file has a prototype for the verify function" do
expect(mock_header).to include(
"void mock_display_Verify(void);")
end
it "then the generated file has a prototype for the destroy function" do
expect(mock_header).to include(
"void mock_display_Destroy(void);")
end
end
context "when there is a function with no args and a void return," do
let(:mock_header) {
parsed_header = create_cmock_style_parsed_header(
[{:name => 'display_turnOffStatusLed', :return_type => 'void'}])
FffMockGenerator.create_mock_header("display", "mock_display", parsed_header)
}
it "then the generated header file starts with an opening include guard" do
expect(mock_header).to start_with(
"#ifndef mock_display_H\n" +
"#define mock_display_H")
end
it "then the generated header file contains a fake function declaration" do
expect(mock_header).to include(
"DECLARE_FAKE_VOID_FUNC0(display_turnOffStatusLed);"
)
end
it "then the generated file ends with a closing include guard" do
expect(mock_header).to end_with(
"#endif // mock_display_H\n")
end
end
context "when there is a function with no args and a bool return," do
let(:mock_header) {
parsed_header = create_cmock_style_parsed_header(
[{:name => 'display_isError', :return_type => 'bool'}])
FffMockGenerator.create_mock_header("display", "mock_display", parsed_header)
}
it "then the generated file contains the fake function declaration" do
expect(mock_header).to include(
"DECLARE_FAKE_VALUE_FUNC0(bool, display_isError);"
)
end
end
context "when there is a function with no args and an int return," do
let(:mock_header) {
parsed_header = create_cmock_style_parsed_header(
[{:name => 'display_isError', :return_type => 'int'}])
FffMockGenerator.create_mock_header("display", "mock_display", parsed_header)
}
it "then the generated file contains the fake function declaration" do
expect(mock_header).to include(
"DECLARE_FAKE_VALUE_FUNC0(int, display_isError);"
)
end
end
context "when there is a function with args and a void return," do
let(:mock_header) {
parsed_header = create_cmock_style_parsed_header(
[{:name => 'display_setVolume', :return_type => 'void', :args => ['int']}])
FffMockGenerator.create_mock_header("display", "mock_display", parsed_header)
}
it "then the generated file contains the fake function declaration" do
expect(mock_header).to include(
"DECLARE_FAKE_VOID_FUNC1(display_setVolume, int);"
)
end
end
context "when there is a function with args and a value return," do
let(:mock_header) {
parsed_header = create_cmock_style_parsed_header(
[{:name => 'a_function', :return_type => 'int', :args => ['char *']}])
FffMockGenerator.create_mock_header("display", "mock_display", parsed_header)
}
it "then the generated file contains the fake function declaration" do
expect(mock_header).to include(
"FAKE_VALUE_FUNC1(int, a_function, char *);"
)
end
end
context "when there is a function with many args and a void return," do
let(:mock_header) {
parsed_header = create_cmock_style_parsed_header(
[{:name => 'a_function', :return_type => 'void',
:args => ['int', 'char *', 'int', 'int', 'bool', 'applesauce']}])
FffMockGenerator.create_mock_header("display", "mock_display", parsed_header)
}
it "then the generated file contains the fake function declaration" do
expect(mock_header).to include(
"DECLARE_FAKE_VOID_FUNC6(a_function, int, char *, int, int, bool, applesauce);"
)
end
end
context "when there are multiple functions," do
let(:mock_header) {
parsed_header = create_cmock_style_parsed_header(
[ {:name => 'a_function', :return_type => 'int', :args => ['char *']},
{:name => 'another_function', :return_type => 'void'},
{:name => 'three', :return_type => 'bool', :args => ['float', 'int']}
])
FffMockGenerator.create_mock_header("display", "mock_display", parsed_header)
}
it "then the generated file contains the first fake function declaration" do
expect(mock_header).to include(
"DECLARE_FAKE_VALUE_FUNC1(int, a_function, char *);"
)
end
it "then the generated file contains the second fake function declaration" do
expect(mock_header).to include(
"DECLARE_FAKE_VOID_FUNC0(another_function);"
)
end
it "then the generated file contains the third fake function declaration" do
expect(mock_header).to include(
"DECLARE_FAKE_VALUE_FUNC2(bool, three, float, int);"
)
end
end
context "when there is a typedef," do
let(:mock_header) {
parsed_header = create_cmock_style_parsed_header(
nil, ["typedef void (*displayCompleteCallback) (void);"])
FffMockGenerator.create_mock_header("display", "mock_display", parsed_header)
}
it "then the generated file contains the typedef" do
expect(mock_header).to include(
"typedef void (*displayCompleteCallback) (void);"
)
end
end
context "when there is a void function with variable arguments" do
let(:mock_header){
parsed_header = {}
parsed_header[:functions] = [{
:name => "function_with_var_args",
:return => {:type => "void"},
:var_arg => "...",
:args => [{:type => 'char *'}]
}]
FffMockGenerator.create_mock_header("display", "mock_display", parsed_header)
}
it "then the generated file contains the vararg declaration" do
expect(mock_header).to include(
"DECLARE_FAKE_VOID_FUNC2_VARARG(function_with_var_args, char *, ...)"
)
end
end
context "when there is a function with a return value and variable arguments" do
let(:mock_header){
parsed_header = {}
parsed_header[:functions] = [{
:name => "function_with_var_args",
:return => {:type => "int"},
:var_arg => "...",
:args => [{:type => 'char *'}]
}]
FffMockGenerator.create_mock_header("display", "mock_display", parsed_header)
}
it "then the generated file contains the vararg declaration" do
expect(mock_header).to include(
"DECLARE_FAKE_VALUE_FUNC2_VARARG(int, function_with_var_args, char *, ...)"
)
end
end
context "when there is a void function with variable arguments and " +
"additional arguments" do
let(:mock_header){
parsed_header = {}
parsed_header[:functions] = [{
:name => "function_with_var_args",
:return => {:type => "void"},
:var_arg => "...",
:args => [{:type => 'char *'}, {:type => 'int'}]
}]
FffMockGenerator.create_mock_header("display", "mock_display", parsed_header)
}
it "then the generated file contains the vararg declaration" do
expect(mock_header).to include(
"DECLARE_FAKE_VOID_FUNC3_VARARG(function_with_var_args, char *, int, ...)"
)
end
end
context "when there is a function with a pointer to a const value" do
let(:mock_header){
parsed_header = {}
parsed_header[:functions] = [{
:name => "const_test_function",
:return => {:type => "void"},
:args => [{:type => "char *", :name => "a", :ptr? => false, :const? => true},
{:type => "char *", :name => "b", :ptr? => false, :const? => false}]
}]
FffMockGenerator.create_mock_header("display", "mock_display", parsed_header)
}
it "then the generated file contains the correct const argument in the declaration" do
expect(mock_header).to include(
"DECLARE_FAKE_VOID_FUNC2(const_test_function, const char *, char *)"
)
end
end
context "when there is a function that returns a const pointer" do
let(:mock_header){
parsed_header = {}
parsed_header[:functions] = [{
:name => "return_const_pointer_test_function",
:modifier => "const",
:return => {:type => "char *" },
:args => [{:type => "int", :name => "a"}]
}]
FffMockGenerator.create_mock_header("display", "mock_display", parsed_header)
}
it "then the generated file contains the correct const return value in the declaration" do
expect(mock_header).to include(
"DECLARE_FAKE_VALUE_FUNC1(const char *, return_const_pointer_test_function, int)"
)
end
end
context "when there is a function that returns a const int" do
let(:mock_header){
parsed_header = {}
parsed_header[:functions] = [{
:name => "return_const_int_test_function",
:modifier => "const",
:return => {:type => "int" },
:args => []
}]
FffMockGenerator.create_mock_header("display", "mock_display", parsed_header)
}
it "then the generated file contains the correct const return value in the declaration" do
expect(mock_header).to include(
"DECLARE_FAKE_VALUE_FUNC0(const int, return_const_int_test_function)"
)
end
end
context "when there are pre-includes" do
let(:mock_header) {
parsed_header = {}
FffMockGenerator.create_mock_header("display", "mock_display", parsed_header,
[%{"another_header.h"}])
}
it "then they are included before the other files" do
expect(mock_header).to include(
%{#include "another_header.h"\n} +
%{#include "fff.h"}
)
end
end
context "when there are post-includes" do
let(:mock_header) {
parsed_header = {}
FffMockGenerator.create_mock_header("display", "mock_display", parsed_header,
nil, [%{"another_header.h"}])
}
it "then they are included after the other files" do
expect(mock_header).to include(
%{#include "display.h"\n} +
%{#include "another_header.h"\n}
)
end
end
end

View File

@@ -0,0 +1,149 @@
require 'stringio'
require 'fff_mock_generator.rb'
# Test the contents of the .c file created for the mock.
describe "FffMockGenerator.create_mock_source" do
context "when there is nothing to mock," do
let(:mock_source) {
parsed_header = {}
FffMockGenerator.create_mock_source("mock_my_module", parsed_header)
}
it "then the generated file includes the fff header" do
expect(mock_source).to include(
# fff.h also requires including string.h
%{#include <string.h>\n} +
%{#include "fff.h"}
)
end
it "then the generated file includes the mock header" do
expect(mock_source).to include(
%{#include "mock_my_module.h"\n}
)
end
it "then the generated file defines the init function" do
expect(mock_source).to include(
"void mock_my_module_Init(void)\n" +
"{\n" +
" FFF_RESET_HISTORY();\n" +
"}"
)
end
it "then the generated file defines the verify function" do
expect(mock_source).to include(
"void mock_my_module_Verify(void)\n" +
"{\n" +
"}"
)
end
it "then the generated file defines the destroy function" do
expect(mock_source).to include(
"void mock_my_module_Destroy(void)\n" +
"{\n" +
"}"
)
end
end
context "when there are multiple functions," do
let(:mock_source) {
parsed_header = create_cmock_style_parsed_header(
[ {:name => 'a_function', :return_type => 'int', :args => ['char *']},
{:name => 'another_function', :return_type => 'void'},
{:name => 'three', :return_type => 'bool', :args => ['float', 'int']}
])
FffMockGenerator.create_mock_source("mock_display", parsed_header)
}
it "then the generated file contains the first fake function definition" do
expect(mock_source).to include(
"DEFINE_FAKE_VALUE_FUNC1(int, a_function, char *);"
)
end
it "then the generated file contains the second fake function definition" do
expect(mock_source).to include(
"DEFINE_FAKE_VOID_FUNC0(another_function);"
)
end
it "then the generated file contains the third fake function definition" do
expect(mock_source).to include(
"DEFINE_FAKE_VALUE_FUNC2(bool, three, float, int);"
)
end
it "then the init function resets all of the fakes" do
expect(mock_source).to include(
"void mock_display_Init(void)\n" +
"{\n" +
" FFF_RESET_HISTORY();\n" +
" RESET_FAKE(a_function)\n" +
" RESET_FAKE(another_function)\n" +
" RESET_FAKE(three)\n" +
"}"
)
end
end
context "when there is a void function with variable arguments and " +
"additional arguments" do
let(:mock_source){
parsed_header = {}
parsed_header[:functions] = [{
:name => "function_with_var_args",
:return => {:type => "void"},
:var_arg => "...",
:args => [{:type => 'char *'}, {:type => 'int'}]
}]
FffMockGenerator.create_mock_source("mock_display", parsed_header)
}
it "then the generated file contains the vararg definition" do
expect(mock_source).to include(
"DEFINE_FAKE_VOID_FUNC3_VARARG(function_with_var_args, char *, int, ...)"
)
end
end
context "when there is a function with a pointer to a const value" do
let(:mock_source){
parsed_header = {}
parsed_header[:functions] = [{
:name => "const_test_function",
:return => {:type => "void"},
:args => [{:type => "char *", :name => "a", :ptr? => false, :const? => true},
{:type => "char *", :name => "b", :ptr? => false, :const? => false}]
}]
FffMockGenerator.create_mock_source("mock_display", parsed_header)
}
it "then the generated file contains the correct const argument in the declaration" do
expect(mock_source).to include(
"DEFINE_FAKE_VOID_FUNC2(const_test_function, const char *, char *)"
)
end
end
context "when there are pre-includes" do
let(:mock_source) {
parsed_source = {}
FffMockGenerator.create_mock_source("mock_display", parsed_source,
[%{"another_header.h"}])
}
it "then they are included before the other files" do
expect(mock_source).to include(
%{#include "another_header.h"\n} +
%{#include <string.h>}
)
end
end
context "when there are post-includes" do
let(:mock_source) {
parsed_source = {}
FffMockGenerator.create_mock_source("mock_display", parsed_source,
nil, [%{"another_header.h"}])
}
it "then they are included before the other files" do
expect(mock_source).to include(
%{#include "mock_display.h"\n} +
%{#include "another_header.h"\n}
)
end
end
end

View File

@@ -0,0 +1,51 @@
# Create a CMock-style parsed header hash. This the type of hash created by
# CMock when parsing header files for automock generation. It contains all of
# includes, typedefs and functions (with return types and arguments) parsed from
# the header file.
def create_cmock_style_parsed_header(functions, typedefs = nil)
parsed_header = {
:includes => nil,
:functions => [],
:typedefs => []
}
# Add the typedefs.
if typedefs
typedefs.each do |typedef|
parsed_header[:typedefs] << typedef
end
end
# Add the functions.
if functions
functions.each do |function|
# Build the array of arguments.
args = []
if function.key?(:args)
function[:args].each do |arg|
args << {
:type => arg
}
end
end
parsed_header[:functions] << {
:name => function[:name],
:modifier => "",
:return => {
:type => function[:return_type],
:name => "cmock_to_return",
:ptr? => false,
:const? => false,
:str => "void cmock_to_return",
:void? => true
},
:var_arg => nil,
:args_string => "void",
:args => args,
:args_call => "",
:contains_ptr? => false
}
end
end
parsed_header
end

View File

@@ -0,0 +1,96 @@
# This file was generated by the `rspec --init` command. Conventionally, all
# specs live under a `spec` directory, which RSpec adds to the `$LOAD_PATH`.
# The generated `.rspec` file contains `--require spec_helper` which will cause
# this file to always be loaded, without a need to explicitly require it in any
# files.
#
# Given that it is always loaded, you are encouraged to keep this file as
# light-weight as possible. Requiring heavyweight dependencies from this file
# will add to the boot time of your test suite on EVERY test run, even for an
# individual file that may not need all of that loaded. Instead, consider making
# a separate helper file that requires the additional dependencies and performs
# the additional setup, and require it from the spec files that actually need
# it.
#
# The `.rspec` file also contains a few flags that are not defaults but that
# users commonly want.
#
# See http://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration
RSpec.configure do |config|
# rspec-expectations config goes here. You can use an alternate
# assertion/expectation library such as wrong or the stdlib/minitest
# assertions if you prefer.
config.expect_with :rspec do |expectations|
# This option will default to `true` in RSpec 4. It makes the `description`
# and `failure_message` of custom matchers include text for helper methods
# defined using `chain`, e.g.:
# be_bigger_than(2).and_smaller_than(4).description
# # => "be bigger than 2 and smaller than 4"
# ...rather than:
# # => "be bigger than 2"
expectations.include_chain_clauses_in_custom_matcher_descriptions = true
end
# rspec-mocks config goes here. You can use an alternate test double
# library (such as bogus or mocha) by changing the `mock_with` option here.
config.mock_with :rspec do |mocks|
# Prevents you from mocking or stubbing a method that does not exist on
# a real object. This is generally recommended, and will default to
# `true` in RSpec 4.
mocks.verify_partial_doubles = true
end
# The settings below are suggested to provide a good initial experience
# with RSpec, but feel free to customize to your heart's content.
=begin
# These two settings work together to allow you to limit a spec run
# to individual examples or groups you care about by tagging them with
# `:focus` metadata. When nothing is tagged with `:focus`, all examples
# get run.
config.filter_run :focus
config.run_all_when_everything_filtered = true
# Allows RSpec to persist some state between runs in order to support
# the `--only-failures` and `--next-failure` CLI options. We recommend
# you configure your source control system to ignore this file.
config.example_status_persistence_file_path = "spec/examples.txt"
# Limits the available syntax to the non-monkey patched syntax that is
# recommended. For more details, see:
# - http://rspec.info/blog/2012/06/rspecs-new-expectation-syntax/
# - http://www.teaisaweso.me/blog/2013/05/27/rspecs-new-message-expectation-syntax/
# - http://rspec.info/blog/2014/05/notable-changes-in-rspec-3/#zero-monkey-patching-mode
config.disable_monkey_patching!
# This setting enables warnings. It's recommended, but in some cases may
# be too noisy due to issues in dependencies.
config.warnings = true
# Many RSpec users commonly either run the entire suite or an individual
# file, and it's useful to allow more verbose output when running an
# individual spec file.
if config.files_to_run.one?
# Use the documentation formatter for detailed output,
# unless a formatter has already been configured
# (e.g. via a command-line flag).
config.default_formatter = 'doc'
end
# Print the 10 slowest examples and example groups at the
# end of the spec run, to help surface which specs are running
# particularly slow.
config.profile_examples = 10
# Run specs in random order to surface order dependencies. If you find an
# order dependency and want to debug it, you can fix the order by providing
# the seed, which is printed after each run.
# --seed 1234
config.order = :random
# Seed global randomization in this process using the `--seed` CLI option.
# Setting this allows you to use `--seed` to deterministically reproduce
# test failures related to randomization by passing the same `--seed` value
# as the one that triggered the failure.
Kernel.srand config.seed
=end
end

View File

@@ -0,0 +1,33 @@
#ifndef fff_unity_helper_H
#define fff_unity_helper_H
/*
FFF helper macros for Unity.
*/
/*
Fail if the function was not called the expected number of times.
*/
#define TEST_ASSERT_CALLED_TIMES(times_, function_) \
TEST_ASSERT_EQUAL_MESSAGE(times_, \
function_ ## _fake.call_count, \
"Function " #function_ " called the incorrect number of times.")
/*
Fail if the function was not called exactly once.
*/
#define TEST_ASSERT_CALLED(function_) TEST_ASSERT_CALLED_TIMES(1, function_)
/*
Fail if the function was called 1 or more times.
*/
#define TEST_ASSERT_NOT_CALLED(function_) TEST_ASSERT_CALLED_TIMES(0, function_)
/*
Fail if the function was not called in this particular order.
*/
#define TEST_ASSERT_CALLED_IN_ORDER(order_, function_) \
TEST_ASSERT_EQUAL_PTR_MESSAGE((void *) function_, \
fff.call_history[order_], \
"Function " #function_ " not called in order " #order_ )
#endif

View File

@@ -0,0 +1,42 @@
ceedling-gcov
=============
Plugin for integrating GNU GCov code coverage tool into Ceedling projects.
Currently only designed for the gcov command (like LCOV for example). In the
future we could configure this to work with other code coverage tools.
This plugin currently uses `gcovr` to generate HTML reports as a utility. The
normal gcov plugin _must_ be run first for this report to generate.
Gcovr can be installed via pip like so:
```
pip install gcovr
```
There are two types of gcovr HTML reports that can be configured in your
`project.yml`. To create a basic HTML report with only the overall file
information use the following config.
```
:gcov:
:html_report_type: basic
```
To create a detailed HTML report with line by line breakdown of the coverage use
the following config.
```
:gcov:
:html_report_type: detailed
```
These reports will be found in `build/artifacts/gcov`.
# To-Do list
- Generate overall report (combined statistics from all files with coverage)
- Generate coverage output files
- Easier option override for better customisation

View File

@@ -0,0 +1,15 @@
% function_string = hash[:coverage][:functions].to_s
% branch_string = hash[:coverage][:branches].to_s
% format_string = "%#{[function_string.length, branch_string.length].max}i"
<%=@ceedling[:plugin_reportinator].generate_banner("#{GCOV_ROOT_NAME.upcase}: CODE COVERAGE SUMMARY")%>
% if (!hash[:coverage][:functions].nil?)
FUNCTIONS: <%=sprintf(format_string, hash[:coverage][:functions])%>%
% else
FUNCTIONS: none
% end
% if (!hash[:coverage][:branches].nil?)
BRANCHES: <%=sprintf(format_string, hash[:coverage][:branches])%>%
% else
BRANCHES: none
% end

View File

@@ -0,0 +1,66 @@
---
:tools:
:gcov_compiler:
:executable: gcc
:arguments:
- -g
- -fprofile-arcs
- -ftest-coverage
- -I"$": COLLECTION_PATHS_TEST_SUPPORT_SOURCE_INCLUDE_VENDOR
- -I"$": COLLECTION_PATHS_TEST_TOOLCHAIN_INCLUDE
- -D$: COLLECTION_DEFINES_TEST_AND_VENDOR
- -DGCOV_COMPILER
- -c "${1}"
- -o "${2}"
:gcov_linker:
:executable: gcc
:arguments:
- -fprofile-arcs
- -ftest-coverage
- ${1}
- -o ${2}
:gcov_fixture:
:executable: ${1}
:gcov_report:
:executable: gcov
:arguments:
- -n
- -p
- -b
- -o "$": GCOV_BUILD_OUTPUT_PATH
- "\"${1}\""
:gcov_post_report:
:executable: gcovr
:optional: TRUE
:arguments:
- -p
- -b
- -e "${1}"
- --html
- -r .
- -o GcovCoverageResults.html
:gcov_post_report_basic:
:executable: gcovr
:optional: TRUE
:arguments:
- -p
- -b
- -e "${1}"
- --html
- -r .
- -o "$": GCOV_ARTIFACTS_FILE
:gcov_post_report_advanced:
:executable: gcovr
:optional: TRUE
:arguments:
- -p
- -b
- -e "${1}"
- --html
- --html-details
- -r .
- -o "$": GCOV_ARTIFACTS_FILE
...

View File

@@ -0,0 +1,180 @@
directory(GCOV_BUILD_OUTPUT_PATH)
directory(GCOV_RESULTS_PATH)
directory(GCOV_ARTIFACTS_PATH)
directory(GCOV_DEPENDENCIES_PATH)
CLEAN.include(File.join(GCOV_BUILD_OUTPUT_PATH, '*'))
CLEAN.include(File.join(GCOV_RESULTS_PATH, '*'))
CLEAN.include(File.join(GCOV_ARTIFACTS_PATH, '*'))
CLEAN.include(File.join(GCOV_DEPENDENCIES_PATH, '*'))
CLOBBER.include(File.join(GCOV_BUILD_PATH, '**/*'))
rule(/#{GCOV_BUILD_OUTPUT_PATH}\/#{'.+\\' + EXTENSION_OBJECT}$/ => [
proc do |task_name|
@ceedling[:file_finder].find_compilation_input_file(task_name)
end
]) do |object|
if File.basename(object.source) =~ /^(#{PROJECT_TEST_FILE_PREFIX}|#{CMOCK_MOCK_PREFIX}|#{GCOV_IGNORE_SOURCES.join('|')})/i
@ceedling[:generator].generate_object_file(
TOOLS_GCOV_COMPILER,
OPERATION_COMPILE_SYM,
GCOV_SYM,
object.source,
object.name,
@ceedling[:file_path_utils].form_test_build_list_filepath(object.name)
)
else
@ceedling[GCOV_SYM].generate_coverage_object_file(object.source, object.name)
end
end
rule(/#{GCOV_BUILD_OUTPUT_PATH}\/#{'.+\\' + EXTENSION_EXECUTABLE}$/) do |bin_file|
@ceedling[:generator].generate_executable_file(
TOOLS_GCOV_LINKER,
GCOV_SYM,
bin_file.prerequisites,
bin_file.name,
@ceedling[:file_path_utils].form_test_build_map_filepath(bin_file.name)
)
end
rule(/#{GCOV_RESULTS_PATH}\/#{'.+\\' + EXTENSION_TESTPASS}$/ => [
proc do |task_name|
@ceedling[:file_path_utils].form_test_executable_filepath(task_name)
end
]) do |test_result|
@ceedling[:generator].generate_test_results(TOOLS_GCOV_FIXTURE, GCOV_SYM, test_result.source, test_result.name)
end
rule(/#{GCOV_DEPENDENCIES_PATH}\/#{'.+\\' + EXTENSION_DEPENDENCIES}$/ => [
proc do |task_name|
@ceedling[:file_finder].find_compilation_input_file(task_name)
end
]) do |dep|
@ceedling[:generator].generate_dependencies_file(
TOOLS_TEST_DEPENDENCIES_GENERATOR,
GCOV_SYM,
dep.source,
File.join(GCOV_BUILD_OUTPUT_PATH, File.basename(dep.source).ext(EXTENSION_OBJECT)),
dep.name
)
end
task directories: [GCOV_BUILD_OUTPUT_PATH, GCOV_RESULTS_PATH, GCOV_DEPENDENCIES_PATH, GCOV_ARTIFACTS_PATH]
namespace GCOV_SYM do
task source_coverage: COLLECTION_ALL_SOURCE.pathmap("#{GCOV_BUILD_OUTPUT_PATH}/%n#{@ceedling[:configurator].extension_object}")
desc 'Run code coverage for all tests'
task all: [:directories] do
@ceedling[:configurator].replace_flattened_config(@ceedling[GCOV_SYM].config)
@ceedling[:test_invoker].setup_and_invoke(COLLECTION_ALL_TESTS, GCOV_SYM)
@ceedling[:configurator].restore_config
end
desc 'Run single test w/ coverage ([*] real test or source file name, no path).'
task :* do
message = "\nOops! '#{GCOV_ROOT_NAME}:*' isn't a real task. " \
"Use a real test or source file name (no path) in place of the wildcard.\n" \
"Example: rake #{GCOV_ROOT_NAME}:foo.c\n\n"
@ceedling[:streaminator].stdout_puts(message)
end
desc 'Run tests by matching regular expression pattern.'
task :pattern, [:regex] => [:directories] do |_t, args|
matches = []
COLLECTION_ALL_TESTS.each do |test|
matches << test if test =~ /#{args.regex}/
end
if !matches.empty?
@ceedling[:configurator].replace_flattened_config(@ceedling[GCOV_SYM].config)
@ceedling[:test_invoker].setup_and_invoke(matches, GCOV_SYM, force_run: false)
@ceedling[:configurator].restore_config
else
@ceedling[:streaminator].stdout_puts("\nFound no tests matching pattern /#{args.regex}/.")
end
end
desc 'Run tests whose test path contains [dir] or [dir] substring.'
task :path, [:dir] => [:directories] do |_t, args|
matches = []
COLLECTION_ALL_TESTS.each do |test|
matches << test if File.dirname(test).include?(args.dir.tr('\\', '/'))
end
if !matches.empty?
@ceedling[:configurator].replace_flattened_config(@ceedling[GCOV_SYM].config)
@ceedling[:test_invoker].setup_and_invoke(matches, GCOV_SYM, force_run: false)
@ceedling[:configurator].restore_config
else
@ceedling[:streaminator].stdout_puts("\nFound no tests including the given path or path component.")
end
end
desc 'Run code coverage for changed files'
task delta: [:directories] do
@ceedling[:configurator].replace_flattened_config(@ceedling[GCOV_SYM].config)
@ceedling[:test_invoker].setup_and_invoke(COLLECTION_ALL_TESTS, GCOV_SYM, force_run: false)
@ceedling[:configurator].restore_config
end
# use a rule to increase efficiency for large projects
# gcov test tasks by regex
rule(/^#{GCOV_TASK_ROOT}\S+$/ => [
proc do |task_name|
test = task_name.sub(/#{GCOV_TASK_ROOT}/, '')
test = "#{PROJECT_TEST_FILE_PREFIX}#{test}" unless test.start_with?(PROJECT_TEST_FILE_PREFIX)
@ceedling[:file_finder].find_test_from_file_path(test)
end
]) do |test|
@ceedling[:rake_wrapper][:directories].invoke
@ceedling[:configurator].replace_flattened_config(@ceedling[GCOV_SYM].config)
@ceedling[:test_invoker].setup_and_invoke([test.source], GCOV_SYM)
@ceedling[:configurator].restore_config
end
end
if PROJECT_USE_DEEP_DEPENDENCIES
namespace REFRESH_SYM do
task GCOV_SYM do
@ceedling[:configurator].replace_flattened_config(@ceedling[GCOV_SYM].config)
@ceedling[:test_invoker].refresh_deep_dependencies
@ceedling[:configurator].restore_config
end
end
end
namespace UTILS_SYM do
desc 'Create gcov code coverage html report (must run ceedling gcov first)'
task GCOV_SYM do
if !File.directory? GCOV_ARTIFACTS_PATH
FileUtils.mkdir_p GCOV_ARTIFACTS_PATH
end
filter = @ceedling[:configurator].project_config_hash[:gcov_html_report_filter] || GCOV_FILTER_EXPR
if @ceedling[:configurator].project_config_hash[:gcov_html_report_type] == 'basic'
puts "Creating a basic html report of gcov results in #{GCOV_ARTIFACTS_FILE}..."
command = @ceedling[:tool_executor].build_command_line(TOOLS_GCOV_POST_REPORT_BASIC, [], filter)
@ceedling[:tool_executor].exec(command[:line], command[:options])
elsif @ceedling[:configurator].project_config_hash[:gcov_html_report_type] == 'detailed'
puts "Creating a detailed html report of gcov results in #{GCOV_ARTIFACTS_FILE}..."
command = @ceedling[:tool_executor].build_command_line(TOOLS_GCOV_POST_REPORT_ADVANCED, [], filter)
@ceedling[:tool_executor].exec(command[:line], command[:options])
else
puts "In your project.yml, define: \n\n:gcov:\n :html_report_type:\n\n to basic or detailed to refine this feature."
puts "For now, just creating basic."
puts "Creating a basic html report of gcov results in #{GCOV_ARTIFACTS_FILE}..."
command = @ceedling[:tool_executor].build_command_line(TOOLS_GCOV_POST_REPORT_BASIC, [], filter)
@ceedling[:tool_executor].exec(command[:line], command[:options])
end
puts "Done."
end
end

View File

@@ -0,0 +1,106 @@
require 'ceedling/plugin'
require 'ceedling/constants'
require 'gcov_constants'
class Gcov < Plugin
attr_reader :config
def setup
@result_list = []
@config = {
project_test_build_output_path: GCOV_BUILD_OUTPUT_PATH,
project_test_build_output_c_path: GCOV_BUILD_OUTPUT_PATH,
project_test_results_path: GCOV_RESULTS_PATH,
project_test_dependencies_path: GCOV_DEPENDENCIES_PATH,
defines_test: DEFINES_TEST + ['CODE_COVERAGE'],
collection_defines_test_and_vendor: COLLECTION_DEFINES_TEST_AND_VENDOR + ['CODE_COVERAGE'],
gcov_html_report_filter: GCOV_FILTER_EXPR
}
@plugin_root = File.expand_path(File.join(File.dirname(__FILE__), '..'))
@coverage_template_all = @ceedling[:file_wrapper].read(File.join(@plugin_root, 'assets/template.erb'))
end
def generate_coverage_object_file(source, object)
compile_command =
@ceedling[:tool_executor].build_command_line(
TOOLS_GCOV_COMPILER,
@ceedling[:flaginator].flag_down(OPERATION_COMPILE_SYM, GCOV_SYM, source),
source,
object,
@ceedling[:file_path_utils].form_test_build_list_filepath(object)
)
@ceedling[:streaminator].stdout_puts("Compiling #{File.basename(source)} with coverage...")
@ceedling[:tool_executor].exec(compile_command[:line], compile_command[:options])
end
def post_test_fixture_execute(arg_hash)
result_file = arg_hash[:result_file]
if (result_file =~ /#{GCOV_RESULTS_PATH}/) && !@result_list.include?(result_file)
@result_list << arg_hash[:result_file]
end
end
def post_build
return unless @ceedling[:task_invoker].invoked?(/^#{GCOV_TASK_ROOT}/)
# test results
results = @ceedling[:plugin_reportinator].assemble_test_results(@result_list)
hash = {
header: GCOV_ROOT_NAME.upcase,
results: results
}
@ceedling[:plugin_reportinator].run_test_results_report(hash) do
message = ''
message = 'Unit test failures.' if results[:counts][:failed] > 0
message
end
report_per_file_coverage_results(@ceedling[:test_invoker].sources)
end
def summary
result_list = @ceedling[:file_path_utils].form_pass_results_filelist(GCOV_RESULTS_PATH, COLLECTION_ALL_TESTS)
# test results
# get test results for only those tests in our configuration and of those only tests with results on disk
hash = {
header: GCOV_ROOT_NAME.upcase,
results: @ceedling[:plugin_reportinator].assemble_test_results(result_list, boom: false)
}
@ceedling[:plugin_reportinator].run_test_results_report(hash)
end
private ###################################
def report_per_file_coverage_results(sources)
banner = @ceedling[:plugin_reportinator].generate_banner "#{GCOV_ROOT_NAME.upcase}: CODE COVERAGE SUMMARY"
@ceedling[:streaminator].stdout_puts "\n" + banner
coverage_sources = sources.clone
coverage_sources.delete_if { |item| item =~ /#{CMOCK_MOCK_PREFIX}.+#{EXTENSION_SOURCE}$/ }
coverage_sources.delete_if { |item| item =~ /#{GCOV_IGNORE_SOURCES.join('|')}#{EXTENSION_SOURCE}$/ }
coverage_sources.each do |source|
basename = File.basename(source)
command = @ceedling[:tool_executor].build_command_line(TOOLS_GCOV_REPORT, [], [basename])
shell_results = @ceedling[:tool_executor].exec(command[:line], command[:options])
coverage_results = shell_results[:output]
if coverage_results.strip =~ /(File\s+'#{Regexp.escape(source)}'.+$)/m
report = Regexp.last_match(1).lines.to_a[1..-1].map { |line| basename + ' ' + line }.join('')
@ceedling[:streaminator].stdout_puts(report + "\n\n")
end
end
end
end
# end blocks always executed following rake run
END {
# cache our input configurations to use in comparison upon next execution
@ceedling[:cacheinator].cache_test_config(@ceedling[:setupinator].config_hash) if @ceedling[:task_invoker].invoked?(/^#{GCOV_TASK_ROOT}/)
}

View File

@@ -0,0 +1,18 @@
GCOV_ROOT_NAME = 'gcov'.freeze
GCOV_TASK_ROOT = GCOV_ROOT_NAME + ':'
GCOV_SYM = GCOV_ROOT_NAME.to_sym
GCOV_BUILD_PATH = File.join(PROJECT_BUILD_ROOT, GCOV_ROOT_NAME)
GCOV_BUILD_OUTPUT_PATH = File.join(GCOV_BUILD_PATH, "out")
GCOV_RESULTS_PATH = File.join(GCOV_BUILD_PATH, "results")
GCOV_DEPENDENCIES_PATH = File.join(GCOV_BUILD_PATH, "dependencies")
GCOV_ARTIFACTS_PATH = File.join(PROJECT_BUILD_ARTIFACTS_ROOT, GCOV_ROOT_NAME)
GCOV_ARTIFACTS_FILE = File.join(GCOV_ARTIFACTS_PATH, "GcovCoverageResults.html")
GCOV_IGNORE_SOURCES = %w(unity cmock cexception).freeze
GCOV_FILTER_EXPR = '^vendor.*|^build.*|^test.*|^lib.*'

View File

@@ -0,0 +1,118 @@
require 'ceedling/plugin'
require 'ceedling/constants'
class JunitTestsReport < Plugin
def setup
@results_list = {}
@test_counter = 0
@time_result = []
end
def post_test_fixture_execute(arg_hash)
context = arg_hash[:context]
@results_list[context] = [] if (@results_list[context].nil?)
@results_list[context] << arg_hash[:result_file]
@time_result << arg_hash[:shell_result][:time]
end
def post_build
@results_list.each_key do |context|
results = @ceedling[:plugin_reportinator].assemble_test_results(@results_list[context])
file_path = File.join( PROJECT_BUILD_ARTIFACTS_ROOT, context.to_s, 'report.xml' )
@ceedling[:file_wrapper].open( file_path, 'w' ) do |f|
@testsuite_counter = 0
@testcase_counter = 0
suites = reorganise_results( results )
write_header( results, f )
suites.each{|suite| write_suite( suite, f ) }
write_footer( f )
end
end
end
private
def write_header( results, stream )
results[:counts][:time] = @time_result.reduce(0, :+)
stream.puts '<?xml version="1.0" encoding="utf-8" ?>'
stream.puts('<testsuites tests="%<total>d" failures="%<failed>d" skipped="%<ignored>d" time="%<time>f">' % results[:counts])
end
def write_footer( stream )
stream.puts '</testsuites>'
end
def reorganise_results( results )
# Reorganise the output by test suite instead of by result
suites = Hash.new{ |h,k| h[k] = {collection: [], total: 0, success: 0, failed: 0, ignored: 0, stdout: []} }
results[:successes].each do |result|
source = result[:source]
name = source[:file].sub(/\..{1,4}$/, "")
suites[name][:collection] += result[:collection].map{|test| test.merge(result: :success)}
suites[name][:total] += result[:collection].length
suites[name][:success] += result[:collection].length
end
results[:failures].each do |result|
source = result[:source]
name = source[:file].sub(/\..{1,4}$/, "")
suites[name][:collection] += result[:collection].map{|test| test.merge(result: :failed)}
suites[name][:total] += result[:collection].length
suites[name][:failed] += result[:collection].length
end
results[:ignores].each do |result|
source = result[:source]
name = source[:file].sub(/\..{1,4}$/, "")
suites[name][:collection] += result[:collection].map{|test| test.merge(result: :ignored)}
suites[name][:total] += result[:collection].length
suites[name][:ignored] += result[:collection].length
end
results[:stdout].each do |result|
source = result[:source]
name = source[:file].sub(/\..{1,4}$/, "")
suites[name][:stdout] += result[:collection]
end
suites.map{|name, data| data.merge(name: name) }
end
def write_suite( suite, stream )
suite[:time] = @time_result.shift
stream.puts(' <testsuite name="%<name>s" tests="%<total>d" failures="%<failed>d" skipped="%<ignored>d" time="%<time>f">' % suite)
suite[:collection].each do |test|
write_test( test, stream )
end
unless suite[:stdout].empty?
stream.puts(' <system-out>')
suite[:stdout].each{|line| stream.puts line }
stream.puts(' </system-out>')
end
stream.puts(' </testsuite>')
end
def write_test( test, stream )
case test[:result]
when :success
stream.puts(' <testcase name="%<test>s" />' % test)
when :failed
stream.puts(' <testcase name="%<test>s">' % test)
if test[:message].empty?
stream.puts(' <failure />')
else
stream.puts(' <failure message="%s" />' % test[:message])
end
stream.puts(' </testcase>')
when :ignored
stream.puts(' <testcase name="%<test>s">' % test)
stream.puts(' <skipped />')
stream.puts(' </testcase>')
end
end
end

View File

@@ -0,0 +1,4 @@
:module_generator:
:project_root: ./
:source_root: src/
:test_root: test/

View File

@@ -0,0 +1,51 @@
require 'ceedling/plugin'
require 'ceedling/constants'
require 'erb'
require 'fileutils'
class ModuleGenerator < Plugin
attr_reader :config
def create(module_name, optz={})
require "generate_module.rb" #From Unity Scripts
if ((!optz.nil?) && (optz[:destroy]))
UnityModuleGenerator.new( divine_options(optz) ).destroy(module_name)
else
UnityModuleGenerator.new( divine_options(optz) ).generate(module_name)
end
end
private
def divine_options(optz={})
unity_generator_options =
{
:path_src => ((defined? MODULE_GENERATOR_SOURCE_ROOT ) ? MODULE_GENERATOR_SOURCE_ROOT.gsub('\\', '/').sub(/^\//, '').sub(/\/$/, '') : "src" ),
:path_inc => ((defined? MODULE_GENERATOR_INC_ROOT ) ?
MODULE_GENERATOR_INC_ROOT.gsub('\\', '/').sub(/^\//, '').sub(/\/$/, '')
: (defined? MODULE_GENERATOR_SOURCE_ROOT ) ?
MODULE_GENERATOR_SOURCE_ROOT.gsub('\\', '/').sub(/^\//, '').sub(/\/$/, '')
: "src" ),
:path_tst => ((defined? MODULE_GENERATOR_TEST_ROOT ) ? MODULE_GENERATOR_TEST_ROOT.gsub( '\\', '/').sub(/^\//, '').sub(/\/$/, '') : "test" ),
:pattern => optz[:pattern],
:test_prefix => ((defined? PROJECT_TEST_FILE_PREFIX ) ? PROJECT_TEST_FILE_PREFIX : "Test" ),
:mock_prefix => ((defined? CMOCK_MOCK_PREFIX ) ? CMOCK_MOCK_PREFIX : "Mock" ),
:includes => ((defined? MODULE_GENERATOR_INCLUDES ) ? MODULE_GENERATOR_INCLUDES : {} ),
:boilerplates => ((defined? MODULE_GENERATOR_BOILERPLATES) ? MODULE_GENERATOR_BOILERPLATES : {} ),
:naming => ((defined? MODULE_GENERATOR_NAMING ) ? MODULE_GENERATOR_NAMING : nil ),
:update_svn => ((defined? MODULE_GENERATOR_UPDATE_SVN ) ? MODULE_GENERATOR_UPDATE_SVN : false ),
}
unless optz[:module_root_path].to_s.empty?
unity_generator_options[:path_src] = File.join(optz[:module_root_path], unity_generator_options[:path_src])
unity_generator_options[:path_inc] = File.join(optz[:module_root_path], unity_generator_options[:path_inc])
unity_generator_options[:path_tst] = File.join(optz[:module_root_path], unity_generator_options[:path_tst])
end
return unity_generator_options
end
end

View File

@@ -0,0 +1,43 @@
namespace :module do
module_root_separator = ":"
desc "Generate module (source, header and test files)"
task :create, :module_path do |t, args|
files = [args[:module_path]] + (args.extras || [])
optz = { :module_root_path => "" }
["dh", "dih", "mch", "mvp", "src", "test"].each do |pat|
p = files.delete(pat)
optz[:pattern] = p unless p.nil?
end
files.each {
|v|
module_root_path, module_name = v.split(module_root_separator, 2)
if module_name
optz[:module_root_path] = module_root_path
v = module_name
end
@ceedling[:module_generator].create(v, optz)
}
end
desc "Destroy module (source, header and test files)"
task :destroy, :module_path do |t, args|
files = [args[:module_path]] + (args.extras || [])
optz = { :destroy => true, :module_root_path => "" }
["dh", "dih", "mch", "mvp", "src", "test"].each do |pat|
p = files.delete(pat)
optz[:pattern] = p unless p.nil?
end
files.each {
|v|
module_root_path, module_name = v.split(module_root_separator, 2)
if module_name
optz[:module_root_path] = module_root_path
v = module_name
end
@ceedling[:module_generator].create(v, optz)
}
end
end

View File

@@ -0,0 +1,41 @@
require 'ceedling/plugin'
require 'ceedling/constants'
class RawOutputReport < Plugin
def setup
@log_paths = {}
end
def post_test_fixture_execute(arg_hash)
output = strip_output(arg_hash[:shell_result][:output])
write_raw_output_log(arg_hash, output)
end
private
def strip_output(raw_output)
output = ""
raw_output.each_line do |line|
next if line =~ /^\n$/
next if line =~ /^.*:\d+:.*:(IGNORE|PASS|FAIL)/
return output if line =~/^-----------------------\n$/
output << line
end
end
def write_raw_output_log(arg_hash, output)
logging = generate_log_path(arg_hash)
@ceedling[:file_wrapper].write(logging[:path], output , logging[:flags]) unless logging.nil?
end
def generate_log_path(arg_hash)
f_name = File.basename(arg_hash[:result_file], '.pass')
base_path = File.join(PROJECT_BUILD_ARTIFACTS_ROOT, arg_hash[:context].to_s)
file_path = File.join(base_path, f_name + '.log')
if @ceedling[:file_wrapper].exist?(base_path)
return { path: file_path, flags: 'w' }
end
nil
end
end

View File

@@ -0,0 +1,84 @@
% ignored = hash[:results][:counts][:ignored]
% failed = hash[:results][:counts][:failed]
% stdout_count = hash[:results][:counts][:stdout]
% header_prepend = ((hash[:header].length > 0) ? "#{hash[:header]}: " : '')
% banner_width = 25 + header_prepend.length # widest message
% results = {}
% hash[:results][:successes].each do |testresult|
% results[ testresult[:source][:file] ] = testresult[:collection]
% results[ testresult[:source][:file] ].length.times do |i|
% results[ testresult[:source][:file] ][i][:pass] = true
% end
% end
% hash[:results][:ignores].each do |testresult|
% if (results[ testresult[:source][:file] ].nil?)
% results[ testresult[:source][:file] ] = testresult[:collection]
% else
% results[ testresult[:source][:file] ] += testresult[:collection]
% end
% results[ testresult[:source][:file] ].length.times do |i|
% results[ testresult[:source][:file] ][i][:pass] = true
% end
% end
% hash[:results][:failures].each do |testresult|
% if (results[ testresult[:source][:file] ].nil?)
% results[ testresult[:source][:file] ] = testresult[:collection]
% else
% results[ testresult[:source][:file] ] += testresult[:collection]
% end
% end
[==========] Running <%=hash[:results][:counts][:total].to_s%> tests from <%=results.length.to_s%> test cases.
[----------] Global test environment set-up.
% results.each_pair do |modulename, moduledetails|
[----------] <%=moduledetails.length.to_s%> tests from <%=modulename%>
% moduledetails.each do |item|
[ RUN ] <%=modulename%>.<%=item[:test]%>
% if (not item[:pass])
% if (not item[:message].empty?)
<%=modulename%>(<%=item[:line]%>): error: <%=item[:message]%>
% m = item[:message].match(/Expected\s+(.*)\s+Was\s+([^\.]*)\./)
% if m.nil?
Actual: FALSE
Expected: TRUE
% else
Actual: <%=m[2]%>
Expected: <%=m[1]%>
% end
% else
<%=modulename%>(<%=item[:line]%>): fail: <%=item[:message]%>
Actual: FALSE
Expected: TRUE
% end
[ FAILED ] <%=modulename%>.<%=item[:test]%> (0 ms)
% else
[ OK ] <%=modulename%>.<%=item[:test]%> (0 ms)
% end
% end
[----------] <%=moduledetails.length.to_s%> tests from <%=modulename%> (0 ms total)
% end
% if (hash[:results][:counts][:total] > 0)
[----------] Global test environment tear-down.
[==========] <%=hash[:results][:counts][:total].to_s%> tests from <%=hash[:results][:stdout].length.to_s%> test cases ran.
[ PASSED ] <%=hash[:results][:counts][:passed].to_s%> tests.
% if (failed == 0)
[ FAILED ] 0 tests.
0 FAILED TESTS
% else
[ FAILED ] <%=failed.to_s%> tests, listed below:
% hash[:results][:failures].each do |failure|
% failure[:collection].each do |item|
[ FAILED ] <%=failure[:source][:file]%>.<%=item[:test]%>
% end
% end
% end
<%=failed.to_s%> FAILED TESTS
% else
No tests executed.
% end

View File

@@ -0,0 +1,59 @@
% ignored = hash[:results][:counts][:ignored]
% failed = hash[:results][:counts][:failed]
% stdout_count = hash[:results][:counts][:stdout]
% header_prepend = ((hash[:header].length > 0) ? "#{hash[:header]}: " : '')
% banner_width = 25 + header_prepend.length # widest message
% if (stdout_count > 0)
[==========] Running <%=hash[:results][:counts][:total].to_s%> tests from <%=hash[:results][:stdout].length.to_s%> test cases.
[----------] Global test environment set-up.
% end
% if (failed > 0)
% hash[:results][:failures].each do |failure|
[----------] <%=failure[:collection].length.to_s%> tests from <%=failure[:source][:file]%>
% failure[:collection].each do |item|
[ RUN ] <%=failure[:source][:file]%>.<%=item[:test]%>
% if (not item[:message].empty?)
<%=failure[:source][:file]%>(<%=item[:line]%>): error: <%=item[:message]%>
% m = item[:message].match(/Expected\s+(.*)\s+Was\s+([^\.]*)\./)
% if m.nil?
Actual: FALSE
Expected: TRUE
% else
Actual: <%=m[2]%>
Expected: <%=m[1]%>
% end
% else
<%=failure[:source][:file]%>(<%=item[:line]%>): fail: <%=item[:message]%>
Actual: FALSE
Expected: TRUE
% end
[ FAILED ] <%=failure[:source][:file]%>.<%=item[:test]%> (0 ms)
% end
[----------] <%=failure[:collection].length.to_s%> tests from <%=failure[:source][:file]%> (0 ms total)
% end
% end
% if (hash[:results][:counts][:total] > 0)
[----------] Global test environment tear-down.
[==========] <%=hash[:results][:counts][:total].to_s%> tests from <%=hash[:results][:stdout].length.to_s%> test cases ran.
[ PASSED ] <%=hash[:results][:counts][:passed].to_s%> tests.
% if (failed == 0)
[ FAILED ] 0 tests.
0 FAILED TESTS
% else
[ FAILED ] <%=failed.to_s%> tests, listed below:
% hash[:results][:failures].each do |failure|
% failure[:collection].each do |item|
[ FAILED ] <%=failure[:source][:file]%>.<%=item[:test]%>
% end
% end
<%=failed.to_s%> FAILED TESTS
% end
% else
No tests executed.
% end

View File

@@ -0,0 +1,4 @@
---
:plugins:
# tell Ceedling we got results display taken care of
:display_raw_test_results: FALSE

View File

@@ -0,0 +1,43 @@
require 'ceedling/plugin'
require 'ceedling/defaults'
class StdoutGtestlikeTestsReport < Plugin
def setup
@result_list = []
@plugin_root = File.expand_path(File.join(File.dirname(__FILE__), '..'))
template = @ceedling[:file_wrapper].read(File.join(@plugin_root, 'assets/template.erb'))
@ceedling[:plugin_reportinator].register_test_results_template( template )
end
def post_test_fixture_execute(arg_hash)
return if not (arg_hash[:context] == TEST_SYM)
@result_list << arg_hash[:result_file]
end
def post_build
return if not (@ceedling[:task_invoker].test_invoked?)
results = @ceedling[:plugin_reportinator].assemble_test_results(@result_list)
hash = {
:header => '',
:results => results
}
@ceedling[:plugin_reportinator].run_test_results_report(hash)
end
def summary
result_list = @ceedling[:file_path_utils].form_pass_results_filelist( PROJECT_TEST_RESULTS_PATH, COLLECTION_ALL_TESTS )
# get test results for only those tests in our configuration and of those only tests with results on disk
hash = {
:header => '',
:results => @ceedling[:plugin_reportinator].assemble_test_results(result_list, {:boom => false})
}
@ceedling[:plugin_reportinator].run_test_results_report(hash)
end
end

View File

@@ -0,0 +1,4 @@
---
:plugins:
# tell Ceedling we got results display taken care of
:display_raw_test_results: FALSE

View File

@@ -0,0 +1,44 @@
require 'ceedling/plugin'
require 'ceedling/defaults'
class StdoutIdeTestsReport < Plugin
def setup
@result_list = []
end
def post_test_fixture_execute(arg_hash)
return if not (arg_hash[:context] == TEST_SYM)
@result_list << arg_hash[:result_file]
end
def post_build
return if (not @ceedling[:task_invoker].test_invoked?)
results = @ceedling[:plugin_reportinator].assemble_test_results(@result_list)
hash = {
:header => '',
:results => results
}
@ceedling[:plugin_reportinator].run_test_results_report(hash) do
message = ''
message = 'Unit test failures.' if (hash[:results][:counts][:failed] > 0)
message
end
end
def summary
result_list = @ceedling[:file_path_utils].form_pass_results_filelist( PROJECT_TEST_RESULTS_PATH, COLLECTION_ALL_TESTS )
# get test results for only those tests in our configuration and of those only tests with results on disk
hash = {
:header => '',
:results => @ceedling[:plugin_reportinator].assemble_test_results(result_list, {:boom => false})
}
@ceedling[:plugin_reportinator].run_test_results_report(hash)
end
end

View File

@@ -0,0 +1,59 @@
% ignored = hash[:results][:counts][:ignored]
% failed = hash[:results][:counts][:failed]
% stdout_count = hash[:results][:counts][:stdout]
% header_prepend = ((hash[:header].length > 0) ? "#{hash[:header]}: " : '')
% banner_width = 25 + header_prepend.length # widest message
% if (stdout_count > 0)
<%=@ceedling[:plugin_reportinator].generate_banner(header_prepend + 'TEST OUTPUT')%>
% hash[:results][:stdout].each do |string|
[<%=string[:source][:file]%>]
% string[:collection].each do |item|
- "<%=item%>"
% end
% end
% end
% if (ignored > 0)
<%=@ceedling[:plugin_reportinator].generate_banner(header_prepend + 'IGNORED TEST SUMMARY')%>
% hash[:results][:ignores].each do |ignore|
[<%=ignore[:source][:file]%>]
% ignore[:collection].each do |item|
Test: <%=item[:test]%>
% if (not item[:message].empty?)
At line (<%=item[:line]%>): "<%=item[:message]%>"
% else
At line (<%=item[:line]%>)
% end
% end
% end
% end
% if (failed > 0)
<%=@ceedling[:plugin_reportinator].generate_banner(header_prepend + 'FAILED TEST SUMMARY')%>
% hash[:results][:failures].each do |failure|
[<%=failure[:source][:file]%>]
% failure[:collection].each do |item|
Test: <%=item[:test]%>
% if (not item[:message].empty?)
At line (<%=item[:line]%>): "<%=item[:message]%>"
% else
At line (<%=item[:line]%>)
% end
% end
% end
% end
% total_string = hash[:results][:counts][:total].to_s
% format_string = "%#{total_string.length}i"
<%=@ceedling[:plugin_reportinator].generate_banner(header_prepend + 'OVERALL TEST SUMMARY')%>
% if (hash[:results][:counts][:total] > 0)
TESTED: <%=hash[:results][:counts][:total].to_s%>
PASSED: <%=sprintf(format_string, hash[:results][:counts][:passed])%>
FAILED: <%=sprintf(format_string, failed)%>
IGNORED: <%=sprintf(format_string, ignored)%>
% else
No tests executed.
% end

View File

@@ -0,0 +1,4 @@
---
:plugins:
# tell Ceedling we got results display taken care of
:display_raw_test_results: FALSE

View File

@@ -0,0 +1,47 @@
require 'ceedling/plugin'
require 'ceedling/defaults'
class StdoutPrettyTestsReport < Plugin
def setup
@result_list = []
@plugin_root = File.expand_path(File.join(File.dirname(__FILE__), '..'))
template = @ceedling[:file_wrapper].read(File.join(@plugin_root, 'assets/template.erb'))
@ceedling[:plugin_reportinator].register_test_results_template( template )
end
def post_test_fixture_execute(arg_hash)
return if not (arg_hash[:context] == TEST_SYM)
@result_list << arg_hash[:result_file]
end
def post_build
return if not (@ceedling[:task_invoker].test_invoked?)
results = @ceedling[:plugin_reportinator].assemble_test_results(@result_list)
hash = {
:header => '',
:results => results
}
@ceedling[:plugin_reportinator].run_test_results_report(hash) do
message = ''
message = 'Unit test failures.' if (results[:counts][:failed] > 0)
message
end
end
def summary
result_list = @ceedling[:file_path_utils].form_pass_results_filelist( PROJECT_TEST_RESULTS_PATH, COLLECTION_ALL_TESTS )
# get test results for only those tests in our configuration and of those only tests with results on disk
hash = {
:header => '',
:results => @ceedling[:plugin_reportinator].assemble_test_results(result_list, {:boom => false})
}
@ceedling[:plugin_reportinator].run_test_results_report(hash)
end
end

View File

@@ -0,0 +1,63 @@
ceedling-subprojects
====================
Plugin for supporting subprojects that are built as static libraries. It continues to support
dependency tracking, without getting confused between your main project files and your
subproject files. It accepts different compiler flags and linker flags, allowing you to
optimize for your situation.
First, you're going to want to add the extension to your list of known extensions:
```
:extension:
:subprojects: '.a'
```
Define a new section called :subprojects. There, you can list as many subprojects
as you may need under the :paths key. For each, you specify a unique place to build
and a unique name.
```
:subprojects:
:paths:
- :name: libprojectA
:source:
- ./subprojectA/first/dir
- ./subprojectA/second/dir
:include:
- ./subprojectA/include/dir
:build_root: ./subprojectA/build/dir
:defines:
- DEFINE_JUST_FOR_THIS_FILE
- AND_ANOTHER
- :name: libprojectB
:source:
- ./subprojectB/only/dir
:include:
- ./subprojectB/first/include/dir
- ./subprojectB/second/include/dir
:build_root: ./subprojectB/build/dir
:defines: [] #none for this one
```
You can specify the compiler and linker, just as you would a release build:
```
:tools:
:subprojects_compiler:
:executable: gcc
:arguments:
- -g
- -I"$": COLLECTION_PATHS_SUBPROJECTS
- -D$: COLLECTION_DEFINES_SUBPROJECTS
- -c "${1}"
- -o "${2}"
:subprojects_linker:
:executable: ar
:arguments:
- rcs
- ${2}
- ${1}
```
That's all there is to it! Happy Hacking!

View File

@@ -0,0 +1,33 @@
---
#:extension:
# :subprojects: '.a'
:subprojects:
:paths: []
# - :name: subprojectA
# :source:
# - ./first/subproject/dir
# - ./second/subproject/dir
# :include:
# - ./first/include/dir
# :build_root: ./subproject/build/dir
# :defines:
# - FIRST_DEFINE
:tools:
:subprojects_compiler:
:executable: gcc
:arguments:
- -g
- -I"$": COLLECTION_PATHS_SUBPROJECTS
- -D$: COLLECTION_DEFINES_SUBPROJECTS
- -c "${1}"
- -o "${2}"
:subprojects_linker:
:executable: ar
:arguments:
- rcs
- ${2}
- ${1}
...

View File

@@ -0,0 +1,92 @@
require 'ceedling/plugin'
require 'ceedling/constants'
SUBPROJECTS_ROOT_NAME = 'subprojects'
SUBPROJECTS_TASK_ROOT = SUBPROJECTS_ROOT_NAME + ':'
SUBPROJECTS_SYM = SUBPROJECTS_ROOT_NAME.to_sym
class Subprojects < Plugin
def setup
@plugin_root = File.expand_path(File.join(File.dirname(__FILE__), '..'))
# Add to the test paths
SUBPROJECTS_PATHS.each do |subproj|
subproj[:source].each do |path|
COLLECTION_PATHS_TEST_SUPPORT_SOURCE_INCLUDE_VENDOR << path
end
subproj[:include].each do |path|
COLLECTION_PATHS_TEST_SUPPORT_SOURCE_INCLUDE_VENDOR << path
end
end
#gather information about the subprojects
@subprojects = {}
@subproject_lookup_by_path = {}
SUBPROJECTS_PATHS.each do |subproj|
@subprojects[ subproj[:name] ] = subproj.clone
@subprojects[ subproj[:name] ][:c] = []
@subprojects[ subproj[:name] ][:asm] = []
subproj[:source].each do |path|
search_path = "#{path[-1].match(/\\|\//) ? path : "#{path}/"}*#{EXTENSION_SOURCE}"
@subprojects[ subproj[:name] ][:c] += Dir[search_path]
if (EXTENSION_ASSEMBLY && !EXTENSION_ASSEMBLY.empty?)
search_path = "#{path[-1].match(/\\|\//) ? path : "#{path}/"}*#{EXTENSION_ASSEMBLY}"
@subprojects[ subproj[:name] ][:asm] += Dir[search_path]
end
end
@subproject_lookup_by_path[ subproj[:build_root] ] = subproj[:name]
end
end
def find_my_project( c_file, file_type = :c )
@subprojects.each_pair do |subprojname, subproj|
return subprojname if (subproj[file_type].include?(c_file))
end
end
def find_my_paths( c_file, file_type = :c )
@subprojects.each_pair do |subprojname, subproj|
return (subproj[:source] + (subproj[:include] || [])) if (subproj[file_type].include?(c_file))
end
return []
end
def find_my_defines( c_file, file_type = :c )
@subprojects.each_pair do |subprojname, subproj|
return (subproj[:defines] || []) if (subproj[file_type].include?(c_file))
end
return []
end
def list_all_object_files_for_subproject( lib_name )
subproj = File.basename(lib_name, EXTENSION_SUBPROJECTS)
objpath = "#{@subprojects[subproj][:build_root]}/out/c"
bbb = @subprojects[subproj][:c].map{|f| "#{objpath}/#{File.basename(f,EXTENSION_SOURCE)}#{EXTENSION_OBJECT}" }
bbb
end
def find_library_source_file_for_object( obj_name )
cname = "#{File.basename(obj_name, EXTENSION_OBJECT)}#{EXTENSION_SOURCE}"
dname = File.dirname(obj_name)[0..-7]
pname = @subproject_lookup_by_path[dname]
return @ceedling[:file_finder].find_file_from_list(cname, @subprojects[pname][:c], :error)
end
def find_library_assembly_file_for_object( obj_name )
cname = "#{File.basename(obj_name, EXTENSION_OBJECT)}#{EXTENSION_ASEMBLY}"
dname = File.dirname(obj_name)[0..-7]
pname = @subproject_lookup_by_path[dname]
return @ceedling[:file_finder].find_file_from_list(cname, @subprojects[pname][:asm], :error)
end
def replace_constant(constant, new_value)
Object.send(:remove_const, constant.to_sym) if (Object.const_defined? constant)
Object.const_set(constant, new_value)
end
end
# end blocks always executed following rake run
END {
}

View File

@@ -0,0 +1,78 @@
SUBPROJECTS_PATHS.each do |subproj|
subproj_source = subproj[:source]
subproj_include = subproj[:include]
subproj_name = subproj[:name]
subproj_build_root = subproj[:build_root]
subproj_build_out = "#{subproj[:build_root]}/out"
subproj_build_c = "#{subproj[:build_root]}/out/c"
subproj_build_asm = "#{subproj[:build_root]}/out/asm"
subproj_directories = [ subproj_build_root, subproj_build_out, subproj_build_c, subproj_build_asm ]
subproj_directories.each do |subdir|
directory(subdir)
end
CLEAN.include(File.join(subproj_build_root, '*'))
CLEAN.include(File.join(subproj_build_out, '*'))
CLOBBER.include(File.join(subproj_build_root, '**/*'))
# Add a rule for building the actual static library from our object files
rule(/#{subproj_build_root}#{'.+\\'+EXTENSION_SUBPROJECTS}$/ => [
proc do |task_name|
@ceedling[SUBPROJECTS_SYM].list_all_object_files_for_subproject(task_name)
end
]) do |bin_file|
@ceedling[:generator].generate_executable_file(
TOOLS_SUBPROJECTS_LINKER,
SUBPROJECTS_SYM,
bin_file.prerequisites,
bin_file.name,
@ceedling[:file_path_utils].form_test_build_map_filepath(bin_file.name))
end
# Add a rule for building object files from assembly files to link into a library
if (RELEASE_BUILD_USE_ASSEMBLY)
rule(/#{subproj_build_asm}#{'.+\\'+EXTENSION_OBJECT}$/ => [
proc do |task_name|
@ceedling[SUBPROJECTS_SYM].find_library_assembly_file_for_object(task_name)
end
]) do |object|
@ceedling[SUBPROJECTS_SYM].replace_constant(:COLLECTION_PATHS_SUBPROJECTS, @ceedling[SUBPROJECTS_SYM].find_my_paths(object.source, :asm))
@ceedling[SUBPROJECTS_SYM].replace_constant(:COLLECTION_DEFINES_SUBPROJECTS, @ceedling[SUBPROJECTS_SYM].find_my_defines(object.source, :asm))
@ceedling[:generator].generate_object_file(
TOOLS_SUBPROJECTS_ASSEMBLER,
OPERATION_ASSEMBLE_SYM,
SUBPROJECTS_SYM,
object.source,
object.name )
end
end
# Add a rule for building object files from C files to link into a library
rule(/#{subproj_build_c}#{'.+\\'+EXTENSION_OBJECT}$/ => [
proc do |task_name|
@ceedling[SUBPROJECTS_SYM].find_library_source_file_for_object(task_name)
end
]) do |object|
@ceedling[SUBPROJECTS_SYM].replace_constant(:COLLECTION_PATHS_SUBPROJECTS, @ceedling[SUBPROJECTS_SYM].find_my_paths(object.source, :c))
@ceedling[SUBPROJECTS_SYM].replace_constant(:COLLECTION_DEFINES_SUBPROJECTS, @ceedling[SUBPROJECTS_SYM].find_my_defines(object.source, :c))
@ceedling[:generator].generate_object_file(
TOOLS_SUBPROJECTS_COMPILER,
OPERATION_COMPILE_SYM,
SUBPROJECTS_SYM,
object.source,
object.name,
@ceedling[:file_path_utils].form_release_build_c_list_filepath( object.name ) )
end
# Add the subdirectories involved to our list of those that should be autogenerated
task :directories => subproj_directories.clone
# Finally, add the static library to our RELEASE build dependency list
task RELEASE_SYM => ["#{subproj_build_root}/#{subproj_name}#{EXTENSION_SUBPROJECTS}"]
end

View File

@@ -0,0 +1,4 @@
---
:plugins:
# tell Ceedling we got results display taken care of
:display_raw_test_results: FALSE

View File

@@ -0,0 +1,57 @@
require 'ceedling/plugin'
require 'ceedling/defaults'
class TeamcityTestsReport < Plugin
def setup
@suite_started = nil
@output_enabled = !defined?(TEAMCITY_BUILD) || TEAMCITY_BUILD
end
def escape(string)
string.gsub(/['|\[\]]/, '|\0').gsub('\r', '|r').gsub('\n', '|n')
end
def pre_test(test)
teamcity_message "testSuiteStarted name='#{File.basename(test, '.c')}'"
@suite_started = Time.now
end
def post_test(test)
teamcity_message "testSuiteFinished name='#{File.basename(test, '.c')}'"
end
def post_test_fixture_execute(arg_hash)
duration = (Time.now - @suite_started) * 1000
results = @ceedling[:plugin_reportinator].assemble_test_results([arg_hash[:result_file]])
avg_duration = (duration / [1, results[:counts][:passed] + results[:counts][:failed]].max).round
results[:successes].each do |success|
success[:collection].each do |test|
teamcity_message "testStarted name='#{test[:test]}'"
teamcity_message "testFinished name='#{test[:test]}' duration='#{avg_duration}'"
end
end
results[:failures].each do |failure|
failure[:collection].each do |test|
teamcity_message "testStarted name='#{test[:test]}'"
teamcity_message "testFailed name='#{test[:test]}' message='#{escape(test[:message])}' details='File: #{failure[:source][:path]}/#{failure[:source][:file]} Line: #{test[:line]}'"
teamcity_message "testFinished name='#{test[:test]}' duration='#{avg_duration}'"
end
end
results[:ignores].each do |failure|
failure[:collection].each do |test|
teamcity_message "testIgnored name='#{test[:test]}' message='#{escape(test[:message])}'"
end
end
# We ignore stdout
end
def teamcity_message(content)
puts "##teamcity[#{content}]" unless !@output_enabled
end
end

View File

@@ -0,0 +1,69 @@
require 'ceedling/plugin'
require 'ceedling/constants'
class WarningsReport < Plugin
def setup
@stderr_redirect = nil
@log_paths = {}
end
def pre_compile_execute(arg_hash)
# at beginning of compile, override tool's stderr_redirect so we can parse $stderr + $stdout
set_stderr_redirect(arg_hash)
end
def post_compile_execute(arg_hash)
# after compilation, grab output for parsing/logging, restore stderr_redirect, log warning if it exists
output = arg_hash[:shell_result][:output]
restore_stderr_redirect(arg_hash)
write_warning_log(arg_hash[:context], output)
end
def pre_link_execute(arg_hash)
# at beginning of link, override tool's stderr_redirect so we can parse $stderr + $stdout
set_stderr_redirect(arg_hash)
end
def post_link_execute(arg_hash)
# after linking, grab output for parsing/logging, restore stderr_redirect, log warning if it exists
output = arg_hash[:shell_result][:output]
restore_stderr_redirect(arg_hash)
write_warning_log(arg_hash[:context], output)
end
private
def set_stderr_redirect(hash)
@stderr_redirect = hash[:tool][:stderr_redirect]
hash[:tool][:stderr_redirect] = StdErrRedirect::AUTO
end
def restore_stderr_redirect(hash)
hash[:tool][:stderr_redirect] = @stderr_redirect
end
def write_warning_log(context, output)
# if $stderr/$stdout contain "warning", log it
if output =~ /warning/i
# generate a log path & file io write flags
logging = generate_log_path(context)
@ceedling[:file_wrapper].write(logging[:path], output + "\n", logging[:flags]) unless logging.nil?
end
end
def generate_log_path(context)
# if path has already been generated, return it & 'append' file io flags (append to log)
return { path: @log_paths[context], flags: 'a' } unless @log_paths[context].nil?
# first time through, generate path & 'write' file io flags (create new log)
base_path = File.join(PROJECT_BUILD_ARTIFACTS_ROOT, context.to_s)
file_path = File.join(base_path, 'warnings.log')
if @ceedling[:file_wrapper].exist?(base_path)
@log_paths[context] = file_path
return { path: file_path, flags: 'w' }
end
nil
end
end

View File

@@ -0,0 +1,108 @@
require 'ceedling/plugin'
require 'ceedling/constants'
class XmlTestsReport < Plugin
def setup
@results_list = {}
@test_counter = 0
end
def post_test_fixture_execute(arg_hash)
context = arg_hash[:context]
@results_list[context] = [] if @results_list[context].nil?
@results_list[context] << arg_hash[:result_file]
end
def post_build
@results_list.each_key do |context|
results = @ceedling[:plugin_reportinator].assemble_test_results(@results_list[context])
file_path = File.join(PROJECT_BUILD_ARTIFACTS_ROOT, context.to_s, 'report.xml')
@ceedling[:file_wrapper].open(file_path, 'w') do |f|
@test_counter = 1
write_results(results, f)
end
end
end
private
def write_results(results, stream)
write_header(stream)
write_failures(results[:failures], stream)
write_tests(results[:successes], stream, 'SuccessfulTests')
write_tests(results[:ignores], stream, 'IgnoredTests')
write_statistics(results[:counts], stream)
write_footer(stream)
end
def write_header(stream)
stream.puts "<?xml version='1.0' encoding='utf-8' ?>"
stream.puts '<TestRun>'
end
def write_failures(results, stream)
if results.size.zero?
stream.puts "\t<FailedTests/>"
return
end
stream.puts "\t<FailedTests>"
results.each do |result|
result[:collection].each do |item|
filename = File.join(result[:source][:path], result[:source][:file])
stream.puts "\t\t<Test id=\"#{@test_counter}\">"
stream.puts "\t\t\t<Name>#{filename}::#{item[:test]}</Name>"
stream.puts "\t\t\t<FailureType>Assertion</FailureType>"
stream.puts "\t\t\t<Location>"
stream.puts "\t\t\t\t<File>#{filename}</File>"
stream.puts "\t\t\t\t<Line>#{item[:line]}</Line>"
stream.puts "\t\t\t</Location>"
stream.puts "\t\t\t<Message>#{item[:message]}</Message>"
stream.puts "\t\t</Test>"
@test_counter += 1
end
end
stream.puts "\t</FailedTests>"
end
def write_tests(results, stream, tag)
if results.size.zero?
stream.puts "\t<#{tag}/>"
return
end
stream.puts "\t<#{tag}>"
results.each do |result|
result[:collection].each do |item|
stream.puts "\t\t<Test id=\"#{@test_counter}\">"
stream.puts "\t\t\t<Name>#{File.join(result[:source][:path], result[:source][:file])}::#{item[:test]}</Name>"
stream.puts "\t\t</Test>"
@test_counter += 1
end
end
stream.puts "\t</#{tag}>"
end
def write_statistics(counts, stream)
stream.puts "\t<Statistics>"
stream.puts "\t\t<Tests>#{counts[:total]}</Tests>"
stream.puts "\t\t<Ignores>#{counts[:ignored]}</Ignores>"
stream.puts "\t\t<FailuresTotal>#{counts[:failed]}</FailuresTotal>"
stream.puts "\t\t<Errors>0</Errors>"
stream.puts "\t\t<Failures>#{counts[:failed]}</Failures>"
stream.puts "\t</Statistics>"
end
def write_footer(stream)
stream.puts '</TestRun>'
end
end