mirror of
https://github.com/ThrowTheSwitch/Unity.git
synced 2026-10-07 09:17:25 +00:00
The line-number search in UnityTestRunnerGenerator#find_tests requires
whitespace before the test name. A definition written as
void
test_something(void)
puts the name at column 0, so the regex never matches the definition
line. The search then scans to the end of the file for every test,
O(tests x lines), and reports line number 0 for all of them.
Accept start-of-line as well as whitespace before the name, and compile
the regex once per test instead of once per line.
687 lines
27 KiB
Ruby
Executable File
687 lines
27 KiB
Ruby
Executable File
#!/usr/bin/env ruby
|
|
# =========================================================================
|
|
# Unity - A Test Framework for C
|
|
# ThrowTheSwitch.org
|
|
# Copyright (c) 2007-26 Mike Karlesky, Mark VanderVoord, & Greg Williams
|
|
# SPDX-License-Identifier: MIT
|
|
# =========================================================================
|
|
|
|
class UnityTestRunnerGenerator
|
|
def initialize(options = nil)
|
|
@options = UnityTestRunnerGenerator.default_options
|
|
case options
|
|
when NilClass
|
|
nil # leave @options unchanged
|
|
when String
|
|
@options.merge!(UnityTestRunnerGenerator.grab_config(options))
|
|
when Hash
|
|
# Check if some of these have been specified
|
|
@options[:has_setup] = !options[:setup_name].nil?
|
|
@options[:has_teardown] = !options[:teardown_name].nil?
|
|
@options[:has_suite_setup] = !options[:suite_setup].nil?
|
|
@options[:has_suite_teardown] = !options[:suite_teardown].nil?
|
|
@options.merge!(options)
|
|
else
|
|
raise 'If you specify arguments, it should be a filename or a hash of options'
|
|
end
|
|
require_relative 'type_sanitizer'
|
|
end
|
|
|
|
def self.default_options
|
|
{
|
|
includes: [],
|
|
defines: [],
|
|
plugins: [],
|
|
framework: :unity,
|
|
test_prefix: 'test|spec|should',
|
|
mock_prefix: 'Mock',
|
|
mock_suffix: '',
|
|
setup_name: 'setUp',
|
|
teardown_name: 'tearDown',
|
|
test_reset_name: 'resetTest',
|
|
test_verify_name: 'verifyTest',
|
|
main_name: 'main', # set to :auto to automatically generate each time
|
|
main_export_decl: '',
|
|
cmdline_args: false,
|
|
omit_begin_end: false,
|
|
use_param_tests: false,
|
|
use_system_files: true,
|
|
include_extensions: '(?:hpp|hh|H|h)',
|
|
source_extensions: '(?:cpp|cc|ino|C|c)',
|
|
shuffle_tests: false,
|
|
rng_seed: 0
|
|
}
|
|
end
|
|
|
|
def self.grab_config(config_file)
|
|
options = default_options
|
|
unless config_file.nil? || config_file.empty?
|
|
require_relative 'yaml_helper'
|
|
yaml_guts = YamlHelper.load_file(config_file)
|
|
options.merge!(yaml_guts[:unity] || yaml_guts[:cmock])
|
|
raise "No :unity or :cmock section found in #{config_file}" unless options
|
|
end
|
|
options
|
|
end
|
|
|
|
def run(input_file, output_file, options = nil)
|
|
@options.merge!(options) unless options.nil?
|
|
|
|
# pull required data from source file
|
|
source = File.read(input_file)
|
|
source = source.force_encoding('ISO-8859-1').encode('utf-8', replace: nil)
|
|
tests = find_tests(source)
|
|
includes = find_includes(source)
|
|
headers = includes[:headers]
|
|
used_mocks = find_mocks(headers)
|
|
headers = (headers - used_mocks)
|
|
headers.delete_if { |inc| inc =~ /(unity|cmock)/ }
|
|
find_setup_and_teardown(source)
|
|
|
|
# build runner file
|
|
generate(input_file, output_file, tests, used_mocks, headers)
|
|
|
|
# determine which files were used to return them
|
|
all_files_used = [input_file, output_file]
|
|
all_files_used += headers.reject { |filename| filename =~ /^</ }.map { |filename| "#{filename}.c" } unless headers.empty?
|
|
all_files_used += @options[:includes] unless @options[:includes].empty?
|
|
all_files_used += includes[:linkonly] unless includes[:linkonly].empty?
|
|
all_files_used.uniq
|
|
end
|
|
|
|
# Generate a test file for the given features:
|
|
# input_file - the name of the test file this is based on
|
|
# output_file - the name of the test runner file to generate
|
|
# tests - an array of all the tests which can be run from this test runner
|
|
# used_mocks - an array of all mock files which are used by this test runner
|
|
# testfile_includes - an array of all included headers, in order, where system headers still have their <> attached
|
|
def generate(input_file, output_file, tests, used_mocks, testfile_includes)
|
|
File.open(output_file, 'w') do |output|
|
|
create_header(output, used_mocks, testfile_includes)
|
|
create_run_test_params_struct(output)
|
|
create_externs(output, tests, used_mocks)
|
|
create_mock_management(output, used_mocks)
|
|
create_setup(output)
|
|
create_teardown(output)
|
|
create_suite_setup(output)
|
|
create_suite_teardown(output)
|
|
create_reset(output)
|
|
create_run_test(output)
|
|
create_args_wrappers(output, tests)
|
|
create_shuffle_tests(output) if @options[:shuffle_tests]
|
|
tests = create_warning_test(output, input_file, tests)
|
|
create_main(output, input_file, tests, used_mocks)
|
|
end
|
|
|
|
return unless @options[:header_file] && !@options[:header_file].empty?
|
|
|
|
File.open(@options[:header_file], 'w') do |output|
|
|
create_h_file(output, @options[:header_file], tests, headers, used_mocks)
|
|
end
|
|
end
|
|
|
|
def find_tests(source)
|
|
tests_and_line_numbers = []
|
|
|
|
# contains characters which will be substituted from within strings, doing
|
|
# this prevents these characters from interfering with scrubbers
|
|
# @ is not a valid C character, so there should be no clashes with files genuinely containing these markers
|
|
substring_subs = { '{' => '@co@', '}' => '@cc@', ';' => '@ss@', '/' => '@fs@' }
|
|
substring_re = Regexp.union(substring_subs.keys)
|
|
substring_unsubs = substring_subs.invert # the inverse map will be used to fix the strings afterwords
|
|
substring_unsubs['@quote@'] = '\\"'
|
|
substring_unsubs['@apos@'] = '\\\''
|
|
substring_unre = Regexp.union(substring_unsubs.keys)
|
|
source_scrubbed = source.clone
|
|
source_scrubbed = source_scrubbed.gsub(/\\"/, '@quote@') # hide escaped quotes to allow capture of the full string/char
|
|
source_scrubbed = source_scrubbed.gsub(/\\'/, '@apos@') # hide escaped apostrophes to allow capture of the full string/char
|
|
source_scrubbed = source_scrubbed.gsub(/("[^"\n]*")|('[^'\n]*')/) { |s| s.gsub(substring_re, substring_subs) } # temporarily hide problematic characters within strings
|
|
source_scrubbed = source_scrubbed.gsub(/\/\/(?:.+\/\*|\*(?:$|[^\/])).*$/, '') # remove line comments that comment out the start of blocks
|
|
source_scrubbed = source_scrubbed.gsub(/\/\*.*?\*\//m, '') # remove block comments
|
|
source_scrubbed = source_scrubbed.gsub(/\/\/.*$/, '') # remove line comments (all that remain)
|
|
lines = source_scrubbed.split(/(^\s*\#.*$) | (;|\{|\}) /x) # Treat preprocessor directives as a logical line. Match ;, {, and } as end of lines
|
|
.map { |line| line.gsub(substring_unre, substring_unsubs) } # unhide the problematic characters previously removed
|
|
|
|
lines.each_with_index do |line, _index|
|
|
# find tests
|
|
next unless line =~ /^((?:\s*(?:TEST_(?:CASE|RANGE|MATRIX))\s*\(.*?\)\s*)*)\s*void\s+((?:#{@options[:test_prefix]}).*)\s*\(\s*(.*)\s*\)/m
|
|
next unless line =~ /^((?:\s*(?:TEST_(?:CASE|RANGE|MATRIX))\s*\(.*?\)\s*)*)\s*void\s+((?:#{@options[:test_prefix]})\w*)\s*\(\s*(.*)\s*\)/m
|
|
|
|
arguments = Regexp.last_match(1)
|
|
name = Regexp.last_match(2)
|
|
call = Regexp.last_match(3)
|
|
params = Regexp.last_match(4)
|
|
args = nil
|
|
|
|
if @options[:use_param_tests] && !arguments.empty?
|
|
args = []
|
|
type_and_args = arguments.split(/TEST_(CASE|RANGE|MATRIX)/)
|
|
(1...type_and_args.length).step(2).each do |i|
|
|
case type_and_args[i]
|
|
when 'CASE'
|
|
args << type_and_args[i + 1].sub(/^\s*\(\s*(.*?)\s*\)\s*$/m, '\1')
|
|
|
|
when 'RANGE'
|
|
args += type_and_args[i + 1].scan(/(\[|<)\s*(-?\d+.?\d*)\s*,\s*(-?\d+.?\d*)\s*,\s*(-?\d+.?\d*)\s*(\]|>)/m).map do |arg_values_str|
|
|
exclude_end = arg_values_str[0] == '<' && arg_values_str[-1] == '>'
|
|
arg_values_str[1...-1].map do |arg_value_str|
|
|
arg_value_str.include?('.') ? arg_value_str.to_f : arg_value_str.to_i
|
|
end.push(exclude_end)
|
|
end.map do |arg_values|
|
|
Range.new(arg_values[0], arg_values[1], arg_values[3]).step(arg_values[2]).to_a
|
|
end.reduce(nil) do |result, arg_range_expanded|
|
|
result.nil? ? arg_range_expanded.map { |a| [a] } : result.product(arg_range_expanded)
|
|
end.map do |arg_combinations|
|
|
arg_combinations.flatten.join(', ')
|
|
end
|
|
|
|
when 'MATRIX'
|
|
single_arg_regex_string = /(?:(?:"(?:\\"|[^\\])*?")+|(?:'\\?.')+|(?:[^\s\]\["',]|\[[\d\S_-]+\])+)/.source
|
|
args_regex = /\[((?:\s*#{single_arg_regex_string}\s*,?)*(?:\s*#{single_arg_regex_string})?\s*)\]/m
|
|
arg_elements_regex = /\s*(#{single_arg_regex_string})\s*,\s*/m
|
|
|
|
args += type_and_args[i + 1].scan(args_regex).flatten.map do |arg_values_str|
|
|
"#{arg_values_str},".scan(arg_elements_regex)
|
|
end.reduce do |result, arg_range_expanded|
|
|
result.product(arg_range_expanded)
|
|
end.map do |arg_combinations|
|
|
arg_combinations.flatten.join(', ')
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
tests_and_line_numbers << { test: name, args: args, call: call, params: params, line_number: 0 }
|
|
end
|
|
|
|
tests_and_line_numbers.uniq! { |v| v[:test] }
|
|
|
|
# determine line numbers and create tests to run
|
|
source_lines = source.split("\n")
|
|
source_index = 0
|
|
tests_and_line_numbers.size.times do |i|
|
|
# Compile once per test
|
|
name_regex = /(?:^|\s)#{tests_and_line_numbers[i][:test]}(?:\s|\()/
|
|
source_lines[source_index..].each_with_index do |line, index|
|
|
next unless line =~ name_regex
|
|
|
|
source_index += index
|
|
tests_and_line_numbers[i][:line_number] = source_index + 1
|
|
break
|
|
end
|
|
end
|
|
|
|
tests_and_line_numbers
|
|
end
|
|
|
|
def find_includes(source)
|
|
# remove comments (block and line, in three steps to ensure correct precedence)
|
|
source.gsub!(/\/\/(?:.+\/\*|\*(?:$|[^\/])).*$/, '') # remove line comments that comment out the start of blocks
|
|
source.gsub!(/\/\*.*?\*\//m, '') # remove block comments
|
|
source.gsub!(/\/\/.*$/, '') # remove line comments (all that remain)
|
|
|
|
# parse out includes (NOTICE: system includes KEEP the angle-brackets in the string, to preserve this information)
|
|
headers = if @options[:use_system_files]
|
|
source.scan(/^\s*#include\s+(?:(<)|")\s*(.+\.#{@options[:include_extensions]})\s*(?:(>)|")/).map(&:join).flatten
|
|
else
|
|
source.scan(/^\s*#include\s+"\s*(.+\.#{@options[:include_extensions]})\s*"/).flatten
|
|
end
|
|
{
|
|
headers: headers,
|
|
linkonly: source.scan(/^TEST_SOURCE_FILE\(\s*"\s*(.+\.#{@options[:source_extensions]})\s*"/).flatten
|
|
}
|
|
end
|
|
|
|
def find_mocks(includes)
|
|
mock_headers = []
|
|
includes.each do |include_path|
|
|
include_file = File.basename(include_path)
|
|
mock_headers << include_path if include_file =~ /^#{@options[:mock_prefix]}.*#{@options[:mock_suffix]}\.h$/i
|
|
end
|
|
mock_headers
|
|
end
|
|
|
|
def find_setup_and_teardown(source)
|
|
@options[:has_setup] = source =~ /void\s+#{@options[:setup_name]}\s*\(/
|
|
@options[:has_teardown] = source =~ /void\s+#{@options[:teardown_name]}\s*\(/
|
|
@options[:has_suite_setup] ||= (source =~ /void\s+suiteSetUp\s*\(/)
|
|
@options[:has_suite_teardown] ||= (source =~ /int\s+suiteTearDown\s*\(int\s+([a-zA-Z0-9_])+\s*\)/)
|
|
end
|
|
|
|
def count_tests(tests)
|
|
if @options[:use_param_tests]
|
|
idx = 0
|
|
tests.each do |test|
|
|
if test[:args].nil? || test[:args].empty?
|
|
idx += 1
|
|
else
|
|
test[:args].each { idx += 1 }
|
|
end
|
|
end
|
|
idx
|
|
else
|
|
tests.size
|
|
end
|
|
end
|
|
|
|
def create_header(output, mocks, headers = [])
|
|
output.puts('/* AUTOGENERATED FILE. DO NOT EDIT. */')
|
|
output.puts("\n/*=======Automagically Detected Files To Include=====*/")
|
|
output.puts('extern "C" {') if @options[:externcincludes]
|
|
if @options[:shuffle_tests]
|
|
output.puts('#include <stdlib.h>')
|
|
if @options[:rng_seed] == 0
|
|
output.puts('#include <time.h>')
|
|
end
|
|
end
|
|
output.puts("#include \"#{@options[:framework]}.h\"")
|
|
output.puts('#include "cmock.h"') unless mocks.empty?
|
|
output.puts('}') if @options[:externcincludes]
|
|
if @options[:defines] && !@options[:defines].empty?
|
|
output.puts('/* injected defines for unity settings, etc */')
|
|
@options[:defines].each do |d|
|
|
def_only = d.match(/(\w+).*/)[1]
|
|
output.puts("#ifndef #{def_only}\n#define #{d}\n#endif /* #{def_only} */")
|
|
end
|
|
end
|
|
if @options[:header_file] && !@options[:header_file].empty?
|
|
output.puts("#include \"#{File.basename(@options[:header_file])}\"")
|
|
else
|
|
[@options[:includes], headers].flatten.uniq.compact.each do |inc|
|
|
output.puts("#include #{inc.include?('<') ? inc : "\"#{inc}\""}")
|
|
end
|
|
end
|
|
output.puts('extern "C" {') if @options[:externcincludes]
|
|
mocks.each do |mock|
|
|
output.puts("#include \"#{mock}\"")
|
|
end
|
|
output.puts('}') if @options[:externcincludes]
|
|
output.puts('#include "CException.h"') if @options[:plugins].include?(:cexception)
|
|
|
|
return unless @options[:enforce_strict_ordering]
|
|
|
|
output.puts('')
|
|
output.puts('int GlobalExpectCount;')
|
|
output.puts('int GlobalVerifyOrder;')
|
|
output.puts('char* GlobalOrderError;')
|
|
end
|
|
|
|
def create_run_test_params_struct(output)
|
|
output.puts("\n/*=======Structure Used By Test Runner=====*/")
|
|
output.puts('struct UnityRunTestParameters')
|
|
output.puts('{')
|
|
output.puts(' UnityTestFunction func;')
|
|
output.puts(' const char* name;')
|
|
output.puts(' UNITY_LINE_TYPE line_num;')
|
|
output.puts('};')
|
|
end
|
|
|
|
def create_externs(output, tests, _mocks)
|
|
output.puts("\n/*=======External Functions This Runner Calls=====*/")
|
|
output.puts("extern void #{@options[:setup_name]}(void);")
|
|
output.puts("extern void #{@options[:teardown_name]}(void);")
|
|
output.puts("\n#ifdef __cplusplus\nextern \"C\"\n{\n#endif") if @options[:externc]
|
|
tests.each do |test|
|
|
output.puts("extern void #{test[:test]}(#{test[:call] || 'void'});")
|
|
end
|
|
output.puts("#ifdef __cplusplus\n}\n#endif") if @options[:externc]
|
|
output.puts('')
|
|
end
|
|
|
|
def create_mock_management(output, mock_headers)
|
|
output.puts("\n/*=======Mock Management=====*/")
|
|
output.puts('static void CMock_Init(void)')
|
|
output.puts('{')
|
|
|
|
if @options[:enforce_strict_ordering]
|
|
output.puts(' GlobalExpectCount = 0;')
|
|
output.puts(' GlobalVerifyOrder = 0;')
|
|
output.puts(' GlobalOrderError = NULL;')
|
|
end
|
|
|
|
mocks = mock_headers.map { |mock| File.basename(mock, '.*') }
|
|
mocks.each do |mock|
|
|
mock_clean = TypeSanitizer.sanitize_c_identifier(mock)
|
|
output.puts(" #{mock_clean}_Init();")
|
|
end
|
|
output.puts("}\n")
|
|
|
|
output.puts('static void CMock_Verify(void)')
|
|
output.puts('{')
|
|
mocks.each do |mock|
|
|
mock_clean = TypeSanitizer.sanitize_c_identifier(mock)
|
|
output.puts(" #{mock_clean}_Verify();")
|
|
end
|
|
output.puts("}\n")
|
|
|
|
output.puts('static void CMock_Destroy(void)')
|
|
output.puts('{')
|
|
mocks.each do |mock|
|
|
mock_clean = TypeSanitizer.sanitize_c_identifier(mock)
|
|
output.puts(" #{mock_clean}_Destroy();")
|
|
end
|
|
output.puts("}\n")
|
|
end
|
|
|
|
def create_setup(output)
|
|
return if @options[:has_setup]
|
|
|
|
output.puts("\n/*=======Setup (stub)=====*/")
|
|
output.puts("void #{@options[:setup_name]}(void) {}")
|
|
end
|
|
|
|
def create_teardown(output)
|
|
return if @options[:has_teardown]
|
|
|
|
output.puts("\n/*=======Teardown (stub)=====*/")
|
|
output.puts("void #{@options[:teardown_name]}(void) {}")
|
|
end
|
|
|
|
def create_suite_setup(output)
|
|
return if @options[:suite_setup].nil?
|
|
|
|
output.puts("\n/*=======Suite Setup=====*/")
|
|
output.puts('void suiteSetUp(void)')
|
|
output.puts('{')
|
|
output.puts(@options[:suite_setup])
|
|
output.puts('}')
|
|
end
|
|
|
|
def create_suite_teardown(output)
|
|
return if @options[:suite_teardown].nil?
|
|
|
|
output.puts("\n/*=======Suite Teardown=====*/")
|
|
output.puts('int suiteTearDown(int num_failures)')
|
|
output.puts('{')
|
|
output.puts(@options[:suite_teardown])
|
|
output.puts('}')
|
|
end
|
|
|
|
def create_reset(output)
|
|
output.puts("\n/*=======Test Reset Options=====*/")
|
|
output.puts("void #{@options[:test_reset_name]}(void);")
|
|
output.puts("void #{@options[:test_reset_name]}(void)")
|
|
output.puts('{')
|
|
output.puts(" #{@options[:teardown_name]}();")
|
|
output.puts(' CMock_Verify();')
|
|
output.puts(' CMock_Destroy();')
|
|
output.puts(' CMock_Init();')
|
|
output.puts(" #{@options[:setup_name]}();")
|
|
output.puts('}')
|
|
output.puts("void #{@options[:test_verify_name]}(void);")
|
|
output.puts("void #{@options[:test_verify_name]}(void)")
|
|
output.puts('{')
|
|
output.puts(' CMock_Verify();')
|
|
output.puts('}')
|
|
end
|
|
|
|
def create_run_test(output)
|
|
output.puts('/*=======Test Runner Used To Run Each Test=====*/')
|
|
output.puts('static void run_test(UnityTestFunction func, const char* name, UNITY_LINE_TYPE line_num)')
|
|
output.puts('{')
|
|
output.puts(' Unity.CurrentTestName = name;')
|
|
output.puts(' Unity.CurrentTestLineNumber = (UNITY_UINT) line_num;')
|
|
output.puts('#ifdef UNITY_USE_COMMAND_LINE_ARGS')
|
|
output.puts(' if (!UnityTestMatches())')
|
|
output.puts(' return;')
|
|
output.puts('#endif')
|
|
output.puts(' Unity.NumberOfTests++;')
|
|
output.puts(' UNITY_CLR_DETAILS();')
|
|
output.puts(' UNITY_EXEC_TIME_START();')
|
|
output.puts(' CMock_Init();')
|
|
output.puts(' if (TEST_PROTECT())')
|
|
output.puts(' {')
|
|
if @options[:plugins].include?(:cexception)
|
|
output.puts(' volatile CEXCEPTION_T e;')
|
|
output.puts(' Try {')
|
|
output.puts(" #{@options[:setup_name]}();")
|
|
output.puts(' func();')
|
|
output.puts(' } Catch(e) {')
|
|
output.puts(' TEST_ASSERT_EQUAL_HEX32_MESSAGE(CEXCEPTION_NONE, e, "Unhandled Exception!");')
|
|
output.puts(' }')
|
|
else
|
|
output.puts(" #{@options[:setup_name]}();")
|
|
output.puts(' func();')
|
|
end
|
|
output.puts(' }')
|
|
output.puts(' if (TEST_PROTECT())')
|
|
output.puts(' {')
|
|
output.puts(" #{@options[:teardown_name]}();")
|
|
output.puts(' CMock_Verify();')
|
|
output.puts(' }')
|
|
output.puts(' CMock_Destroy();')
|
|
output.puts(' UNITY_EXEC_TIME_STOP();')
|
|
output.puts(' UnityConcludeTest();')
|
|
output.puts('}')
|
|
end
|
|
|
|
def create_args_wrappers(output, tests)
|
|
return unless @options[:use_param_tests]
|
|
|
|
output.puts("\n/*=======Parameterized Test Wrappers=====*/")
|
|
tests.each do |test|
|
|
next if test[:args].nil? || test[:args].empty?
|
|
|
|
test[:args].each.with_index(1) do |args, idx|
|
|
output.puts("static void runner_args#{idx}_#{test[:test]}(void)")
|
|
output.puts('{')
|
|
output.puts(" #{test[:test]}(#{args});")
|
|
output.puts("}\n")
|
|
end
|
|
end
|
|
end
|
|
|
|
def create_warning_test(output, filename, tests)
|
|
if count_tests(tests) == 0
|
|
warning_test = {
|
|
:test => 'test_empty_warning',
|
|
:line_number => 0
|
|
}
|
|
output.puts("\n/*=======Warning Test=====*/")
|
|
output.puts("static void #{warning_test[:test]}(void)")
|
|
output.puts('{')
|
|
output.puts(" TEST_IGNORE_MESSAGE(\"Test file '#{filename.gsub(/\\/, '\\\\\\')}' contains no tests.\");")
|
|
output.puts("}\n")
|
|
tests = [warning_test]
|
|
end
|
|
tests
|
|
end
|
|
|
|
def create_shuffle_tests(output)
|
|
output.puts("\n/*=======Shuffle Test Order=====*/")
|
|
output.puts('static void shuffleTests(struct UnityRunTestParameters run_test_params_arr[], int num_of_tests)')
|
|
output.puts('{')
|
|
|
|
# Use Fisher-Yates shuffle algorithm
|
|
output.puts(' for (int i = num_of_tests - 1; i > 0; i--)')
|
|
output.puts(' {')
|
|
output.puts(' int j = rand() % (i + 1);')
|
|
output.puts(' struct UnityRunTestParameters temp = run_test_params_arr[i];')
|
|
output.puts(' run_test_params_arr[i] = run_test_params_arr[j];')
|
|
output.puts(' run_test_params_arr[j] = temp;')
|
|
output.puts(' }')
|
|
output.puts('}')
|
|
end
|
|
|
|
def create_main(output, filename, tests, used_mocks)
|
|
output.puts("\n/*=======MAIN=====*/")
|
|
main_name = @options[:main_name].to_sym == :auto ? "main_#{filename.gsub('.c', '')}" : @options[:main_name].to_s
|
|
if @options[:cmdline_args]
|
|
if main_name != 'main'
|
|
output.puts("#{@options[:main_export_decl]} int #{main_name}(int argc, char** argv);")
|
|
end
|
|
output.puts("#{@options[:main_export_decl]} int #{main_name}(int argc, char** argv)")
|
|
output.puts('{')
|
|
output.puts('(void)argc;')
|
|
output.puts('(void)argv;')
|
|
output.puts('#ifdef UNITY_USE_COMMAND_LINE_ARGS')
|
|
output.puts(' int parse_status = UnityParseOptions(argc, argv);')
|
|
output.puts(' if (parse_status != 0)')
|
|
output.puts(' {')
|
|
output.puts(' if (parse_status < 0)')
|
|
output.puts(' {')
|
|
output.puts(" UnityPrint(\"#{filename.gsub(/\\/, '\\\\\\')}\");")
|
|
output.puts(' UNITY_PRINT_EOL();')
|
|
tests.each do |test|
|
|
if !@options[:use_param_tests] || test[:args].nil? || test[:args].empty?
|
|
output.puts(" UnityPrint(\" #{test[:test]}\");")
|
|
output.puts(' UNITY_PRINT_EOL();')
|
|
else
|
|
test[:args].each do |args|
|
|
output.puts(" UnityPrint(\" #{test[:test]}(#{args.gsub('"', '').gsub("\n", '').gsub(/\\/, '\\\\\\')})\");")
|
|
output.puts(' UNITY_PRINT_EOL();')
|
|
end
|
|
end
|
|
end
|
|
output.puts(' return 0;')
|
|
output.puts(' }')
|
|
output.puts(' return parse_status;')
|
|
output.puts(' }')
|
|
output.puts('#endif')
|
|
else
|
|
main_return = @options[:omit_begin_end] ? 'void' : 'int'
|
|
if main_name != 'main'
|
|
output.puts("#{@options[:main_export_decl]} #{main_return} #{main_name}(void);")
|
|
end
|
|
output.puts("#{main_return} #{main_name}(void)")
|
|
output.puts('{')
|
|
end
|
|
output.puts(' suiteSetUp();') if @options[:has_suite_setup]
|
|
if @options[:omit_begin_end]
|
|
output.puts(" UnitySetTestFile(\"#{filename.gsub(/\\/, '\\\\\\')}\");")
|
|
else
|
|
output.puts(" UnityBegin(\"#{filename.gsub(/\\/, '\\\\\\')}\");")
|
|
end
|
|
if @options[:shuffle_tests]
|
|
output.puts
|
|
if @options[:rng_seed] == 0
|
|
output.puts(' srand(time(NULL));')
|
|
else
|
|
output.puts(" srand(#{@options[:rng_seed]});")
|
|
end
|
|
end
|
|
output.puts
|
|
output.puts(" struct UnityRunTestParameters run_test_params_arr[#{count_tests(tests)}];")
|
|
output.puts
|
|
idx = 0
|
|
tests.each do |test|
|
|
if !@options[:use_param_tests] || test[:args].nil? || test[:args].empty?
|
|
output.puts(" run_test_params_arr[#{idx}].func = #{test[:test]};")
|
|
output.puts(" run_test_params_arr[#{idx}].name = \"#{test[:test]}\";")
|
|
output.puts(" run_test_params_arr[#{idx}].line_num = #{test[:line_number]};")
|
|
idx += 1
|
|
else
|
|
test[:args].each.with_index(1) do |args, arg_idx|
|
|
wrapper = "runner_args#{arg_idx}_#{test[:test]}"
|
|
testname = "#{test[:test]}(#{args})".dump
|
|
output.puts(" run_test_params_arr[#{idx}].func = #{wrapper};")
|
|
output.puts(" run_test_params_arr[#{idx}].name = #{testname};")
|
|
output.puts(" run_test_params_arr[#{idx}].line_num = #{test[:line_number]};")
|
|
idx += 1
|
|
end
|
|
end
|
|
end
|
|
output.puts
|
|
if @options[:shuffle_tests]
|
|
output.puts(" shuffleTests(run_test_params_arr, #{count_tests(tests)});")
|
|
output.puts
|
|
end
|
|
output.puts(" for (int i = 0; i < #{count_tests(tests)}; i++)")
|
|
output.puts(' {')
|
|
output.puts(' run_test(run_test_params_arr[i].func, run_test_params_arr[i].name, run_test_params_arr[i].line_num);')
|
|
output.puts(' }')
|
|
output.puts
|
|
output.puts(' CMock_Guts_MemFreeFinal();') unless used_mocks.empty?
|
|
if @options[:has_suite_teardown]
|
|
if @options[:omit_begin_end]
|
|
output.puts(' (void) suiteTearDown(0);')
|
|
else
|
|
output.puts(' return suiteTearDown(UNITY_END());')
|
|
end
|
|
else
|
|
output.puts(' return UNITY_END();') unless @options[:omit_begin_end]
|
|
end
|
|
output.puts('}')
|
|
end
|
|
|
|
def create_h_file(output, filename, tests, headers, used_mocks)
|
|
filename = File.basename(filename).gsub(/[-\/\\.,\s]/, '_').upcase
|
|
output.puts('/* AUTOGENERATED FILE. DO NOT EDIT. */')
|
|
output.puts("#ifndef _#{filename}")
|
|
output.puts("#define _#{filename}\n\n")
|
|
output.puts("#include \"#{@options[:framework]}.h\"")
|
|
output.puts('#include "cmock.h"') unless used_mocks.empty?
|
|
[@options[:includes], headers].flatten.uniq.compact.each do |inc|
|
|
output.puts("#include #{inc.include?('<') ? inc : "\"#{inc}\""}")
|
|
end
|
|
output.puts "\n"
|
|
tests.each do |test|
|
|
if test[:params].nil? || test[:params].empty?
|
|
output.puts("void #{test[:test]}(void);")
|
|
else
|
|
output.puts("void #{test[:test]}(#{test[:params]});")
|
|
end
|
|
end
|
|
output.puts("#endif\n\n")
|
|
end
|
|
end
|
|
|
|
if $0 == __FILE__
|
|
options = { includes: [] }
|
|
|
|
# parse out all the options first (these will all be removed as we go)
|
|
ARGV.reject! do |arg|
|
|
case arg
|
|
when '-cexception'
|
|
options[:plugins] = [:cexception]
|
|
true
|
|
when '-externcincludes'
|
|
options[:externcincludes] = true
|
|
true
|
|
when /\.*\.ya?ml$/
|
|
options = UnityTestRunnerGenerator.grab_config(arg)
|
|
true
|
|
when /--(\w+)="?(.*)"?/
|
|
options[Regexp.last_match(1).to_sym] = Regexp.last_match(2)
|
|
true
|
|
when /\.*\.(?:hpp|hh|H|h)$/
|
|
options[:includes] << arg
|
|
true
|
|
else false
|
|
end
|
|
end
|
|
|
|
# make sure there is at least one parameter left (the input file)
|
|
unless ARGV[0]
|
|
puts ["\nusage: ruby #{__FILE__} (files) (options) input_test_file (output)",
|
|
"\n input_test_file - this is the C file you want to create a runner for",
|
|
' output - this is the name of the runner file to generate',
|
|
' defaults to (input_test_file)_Runner',
|
|
' files:',
|
|
' *.yml / *.yaml - loads configuration from here in :unity or :cmock',
|
|
' *.h - header files are added as #includes in runner',
|
|
' options:',
|
|
' -cexception - include cexception support',
|
|
' -externc - add extern "C" for cpp support',
|
|
' --setup_name="" - redefine setUp func name to something else',
|
|
' --teardown_name="" - redefine tearDown func name to something else',
|
|
' --main_name="" - redefine main func name to something else',
|
|
' --test_prefix="" - redefine test prefix from default test|spec|should',
|
|
' --test_reset_name="" - redefine resetTest func name to something else',
|
|
' --test_verify_name="" - redefine verifyTest func name to something else',
|
|
' --suite_setup="" - code to execute for setup of entire suite',
|
|
' --suite_teardown="" - code to execute for teardown of entire suite',
|
|
' --use_param_tests=1 - enable parameterized tests (disabled by default)',
|
|
' --omit_begin_end=1 - omit calls to UnityBegin and UNITY_END (disabled by default)',
|
|
' --header_file="" - path/name of test header file to generate too',
|
|
' --shuffle_tests=1 - enable shuffling of the test execution order (disabled by default)',
|
|
' --rng_seed=1 - seed value for randomization of test execution order'].join("\n")
|
|
exit 1
|
|
end
|
|
|
|
# create the default test runner name if not specified
|
|
ARGV[1] = ARGV[0].gsub('.c', '_Runner.c') unless ARGV[1]
|
|
|
|
UnityTestRunnerGenerator.new(options).run(ARGV[0], ARGV[1])
|
|
end
|