• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1# ==========================================
2#   Unity Project - A Test Framework for C
3#   Copyright (c) 2007 Mike Karlesky, Mark VanderVoord, Greg Williams
4#   [Released under MIT License. Please refer to license.txt for details]
5# ==========================================
6
7File.expand_path(File.join(File.dirname(__FILE__), 'colour_prompt'))
8
9class UnityTestRunnerGenerator
10  def initialize(options = nil)
11    @options = UnityTestRunnerGenerator.default_options
12    case options
13    when NilClass then @options
14    when String   then @options.merge!(UnityTestRunnerGenerator.grab_config(options))
15    when Hash     then @options.merge!(options)
16    else raise 'If you specify arguments, it should be a filename or a hash of options'
17    end
18    require "#{File.expand_path(File.dirname(__FILE__))}/type_sanitizer"
19  end
20
21  def self.default_options
22    {
23      includes: [],
24      defines: [],
25      plugins: [],
26      framework: :unity,
27      test_prefix: 'test|spec|should',
28      mock_prefix: 'Mock',
29      setup_name: 'setUp',
30      teardown_name: 'tearDown',
31      main_name: 'main', # set to :auto to automatically generate each time
32      main_export_decl: '',
33      cmdline_args: false,
34      use_param_tests: false
35    }
36  end
37
38  def self.grab_config(config_file)
39    options = default_options
40    unless config_file.nil? || config_file.empty?
41      require 'yaml'
42      yaml_guts = YAML.load_file(config_file)
43      options.merge!(yaml_guts[:unity] || yaml_guts[:cmock])
44      raise "No :unity or :cmock section found in #{config_file}" unless options
45    end
46    options
47  end
48
49  def run(input_file, output_file, options = nil)
50    @options.merge!(options) unless options.nil?
51
52    # pull required data from source file
53    source = File.read(input_file)
54    source = source.force_encoding('ISO-8859-1').encode('utf-8', replace: nil)
55    tests               = find_tests(source)
56    headers             = find_includes(source)
57    testfile_includes   = (headers[:local] + headers[:system])
58    used_mocks          = find_mocks(testfile_includes)
59    testfile_includes   = (testfile_includes - used_mocks)
60    testfile_includes.delete_if { |inc| inc =~ /(unity|cmock)/ }
61
62    # build runner file
63    generate(input_file, output_file, tests, used_mocks, testfile_includes)
64
65    # determine which files were used to return them
66    all_files_used = [input_file, output_file]
67    all_files_used += testfile_includes.map { |filename| filename + '.c' } unless testfile_includes.empty?
68    all_files_used += @options[:includes] unless @options[:includes].empty?
69    all_files_used += headers[:linkonly] unless headers[:linkonly].empty?
70    all_files_used.uniq
71  end
72
73  def generate(input_file, output_file, tests, used_mocks, testfile_includes)
74    File.open(output_file, 'w') do |output|
75      create_header(output, used_mocks, testfile_includes)
76      create_externs(output, tests, used_mocks)
77      create_mock_management(output, used_mocks)
78      create_suite_setup(output)
79      create_suite_teardown(output)
80      create_reset(output, used_mocks)
81      create_main(output, input_file, tests, used_mocks)
82    end
83
84    return unless @options[:header_file] && !@options[:header_file].empty?
85
86    File.open(@options[:header_file], 'w') do |output|
87      create_h_file(output, @options[:header_file], tests, testfile_includes, used_mocks)
88    end
89  end
90
91  def find_tests(source)
92    tests_and_line_numbers = []
93
94    source_scrubbed = source.clone
95    source_scrubbed = source_scrubbed.gsub(/"[^"\n]*"/, '') # remove things in strings
96    source_scrubbed = source_scrubbed.gsub(/\/\/.*$/, '')      # remove line comments
97    source_scrubbed = source_scrubbed.gsub(/\/\*.*?\*\//m, '') # remove block comments
98    lines = source_scrubbed.split(/(^\s*\#.*$)                 # Treat preprocessor directives as a logical line
99                              | (;|\{|\}) /x)                  # Match ;, {, and } as end of lines
100
101    lines.each_with_index do |line, _index|
102      # find tests
103      next unless line =~ /^((?:\s*TEST_CASE\s*\(.*?\)\s*)*)\s*void\s+((?:#{@options[:test_prefix]}).*)\s*\(\s*(.*)\s*\)/
104      arguments = Regexp.last_match(1)
105      name = Regexp.last_match(2)
106      call = Regexp.last_match(3)
107      params = Regexp.last_match(4)
108      args = nil
109      if @options[:use_param_tests] && !arguments.empty?
110        args = []
111        arguments.scan(/\s*TEST_CASE\s*\((.*)\)\s*$/) { |a| args << a[0] }
112      end
113      tests_and_line_numbers << { test: name, args: args, call: call, params: params, line_number: 0 }
114    end
115    tests_and_line_numbers.uniq! { |v| v[:test] }
116
117    # determine line numbers and create tests to run
118    source_lines = source.split("\n")
119    source_index = 0
120    tests_and_line_numbers.size.times do |i|
121      source_lines[source_index..-1].each_with_index do |line, index|
122        next unless line =~ /\s+#{tests_and_line_numbers[i][:test]}(?:\s|\()/
123        source_index += index
124        tests_and_line_numbers[i][:line_number] = source_index + 1
125        break
126      end
127    end
128
129    tests_and_line_numbers
130  end
131
132  def find_includes(source)
133    # remove comments (block and line, in three steps to ensure correct precedence)
134    source.gsub!(/\/\/(?:.+\/\*|\*(?:$|[^\/])).*$/, '')  # remove line comments that comment out the start of blocks
135    source.gsub!(/\/\*.*?\*\//m, '')                     # remove block comments
136    source.gsub!(/\/\/.*$/, '')                          # remove line comments (all that remain)
137
138    # parse out includes
139    includes = {
140      local: source.scan(/^\s*#include\s+\"\s*(.+)\.[hH]\s*\"/).flatten,
141      system: source.scan(/^\s*#include\s+<\s*(.+)\s*>/).flatten.map { |inc| "<#{inc}>" },
142      linkonly: source.scan(/^TEST_FILE\(\s*\"\s*(.+)\.[cC]\w*\s*\"/).flatten
143    }
144    includes
145  end
146
147  def find_mocks(includes)
148    mock_headers = []
149    includes.each do |include_path|
150      include_file = File.basename(include_path)
151      mock_headers << include_path if include_file =~ /^#{@options[:mock_prefix]}/i
152    end
153    mock_headers
154  end
155
156  def create_header(output, mocks, testfile_includes = [])
157    output.puts('/* AUTOGENERATED FILE. DO NOT EDIT. */')
158    create_runtest(output, mocks)
159    output.puts("\n/*=======Automagically Detected Files To Include=====*/")
160    output.puts('#ifdef __WIN32__')
161    output.puts('#define UNITY_INCLUDE_SETUP_STUBS')
162    output.puts('#endif')
163    output.puts("#include \"#{@options[:framework]}.h\"")
164    output.puts('#include "cmock.h"') unless mocks.empty?
165    output.puts('#include <setjmp.h>')
166    output.puts('#include <stdio.h>')
167    if @options[:defines] && !@options[:defines].empty?
168      @options[:defines].each { |d| output.puts("#define #{d}") }
169    end
170    if @options[:header_file] && !@options[:header_file].empty?
171      output.puts("#include \"#{File.basename(@options[:header_file])}\"")
172    else
173      @options[:includes].flatten.uniq.compact.each do |inc|
174        output.puts("#include #{inc.include?('<') ? inc : "\"#{inc.gsub('.h', '')}.h\""}")
175      end
176      testfile_includes.each do |inc|
177        output.puts("#include #{inc.include?('<') ? inc : "\"#{inc.gsub('.h', '')}.h\""}")
178      end
179    end
180    mocks.each do |mock|
181      output.puts("#include \"#{mock.gsub('.h', '')}.h\"")
182    end
183    output.puts('#include "CException.h"') if @options[:plugins].include?(:cexception)
184
185    return unless @options[:enforce_strict_ordering]
186
187    output.puts('')
188    output.puts('int GlobalExpectCount;')
189    output.puts('int GlobalVerifyOrder;')
190    output.puts('char* GlobalOrderError;')
191  end
192
193  def create_externs(output, tests, _mocks)
194    output.puts("\n/*=======External Functions This Runner Calls=====*/")
195    output.puts("extern void #{@options[:setup_name]}(void);")
196    output.puts("extern void #{@options[:teardown_name]}(void);")
197    tests.each do |test|
198      output.puts("extern void #{test[:test]}(#{test[:call] || 'void'});")
199    end
200    output.puts('')
201  end
202
203  def create_mock_management(output, mock_headers)
204    return if mock_headers.empty?
205
206    output.puts("\n/*=======Mock Management=====*/")
207    output.puts('static void CMock_Init(void)')
208    output.puts('{')
209
210    if @options[:enforce_strict_ordering]
211      output.puts('  GlobalExpectCount = 0;')
212      output.puts('  GlobalVerifyOrder = 0;')
213      output.puts('  GlobalOrderError = NULL;')
214    end
215
216    mocks = mock_headers.map { |mock| File.basename(mock) }
217    mocks.each do |mock|
218      mock_clean = TypeSanitizer.sanitize_c_identifier(mock)
219      output.puts("  #{mock_clean}_Init();")
220    end
221    output.puts("}\n")
222
223    output.puts('static void CMock_Verify(void)')
224    output.puts('{')
225    mocks.each do |mock|
226      mock_clean = TypeSanitizer.sanitize_c_identifier(mock)
227      output.puts("  #{mock_clean}_Verify();")
228    end
229    output.puts("}\n")
230
231    output.puts('static void CMock_Destroy(void)')
232    output.puts('{')
233    mocks.each do |mock|
234      mock_clean = TypeSanitizer.sanitize_c_identifier(mock)
235      output.puts("  #{mock_clean}_Destroy();")
236    end
237    output.puts("}\n")
238  end
239
240  def create_suite_setup(output)
241    output.puts("\n/*=======Suite Setup=====*/")
242    output.puts('static void suite_setup(void)')
243    output.puts('{')
244    if @options[:suite_setup].nil?
245      # New style, call suiteSetUp() if we can use weak symbols
246      output.puts('#if defined(UNITY_WEAK_ATTRIBUTE) || defined(UNITY_WEAK_PRAGMA)')
247      output.puts('  suiteSetUp();')
248      output.puts('#endif')
249    else
250      # Old style, C code embedded in the :suite_setup option
251      output.puts(@options[:suite_setup])
252    end
253    output.puts('}')
254  end
255
256  def create_suite_teardown(output)
257    output.puts("\n/*=======Suite Teardown=====*/")
258    output.puts('static int suite_teardown(int num_failures)')
259    output.puts('{')
260    if @options[:suite_teardown].nil?
261      # New style, call suiteTearDown() if we can use weak symbols
262      output.puts('#if defined(UNITY_WEAK_ATTRIBUTE) || defined(UNITY_WEAK_PRAGMA)')
263      output.puts('  return suiteTearDown(num_failures);')
264      output.puts('#else')
265      output.puts('  return num_failures;')
266      output.puts('#endif')
267    else
268      # Old style, C code embedded in the :suite_teardown option
269      output.puts(@options[:suite_teardown])
270    end
271    output.puts('}')
272  end
273
274  def create_runtest(output, used_mocks)
275    cexception = @options[:plugins].include? :cexception
276    va_args1   = @options[:use_param_tests] ? ', ...' : ''
277    va_args2   = @options[:use_param_tests] ? '__VA_ARGS__' : ''
278    output.puts("\n/*=======Test Runner Used To Run Each Test Below=====*/")
279    output.puts('#define RUN_TEST_NO_ARGS') if @options[:use_param_tests]
280    output.puts("#define RUN_TEST(TestFunc, TestLineNum#{va_args1}) \\")
281    output.puts('{ \\')
282    output.puts("  Unity.CurrentTestName = #TestFunc#{va_args2.empty? ? '' : " \"(\" ##{va_args2} \")\""}; \\")
283    output.puts('  Unity.CurrentTestLineNumber = TestLineNum; \\')
284    output.puts('  if (UnityTestMatches()) { \\') if @options[:cmdline_args]
285    output.puts('  Unity.NumberOfTests++; \\')
286    output.puts('  CMock_Init(); \\') unless used_mocks.empty?
287    output.puts('  UNITY_CLR_DETAILS(); \\') unless used_mocks.empty?
288    output.puts('  if (TEST_PROTECT()) \\')
289    output.puts('  { \\')
290    output.puts('    CEXCEPTION_T e; \\') if cexception
291    output.puts('    Try { \\') if cexception
292    output.puts("      #{@options[:setup_name]}(); \\")
293    output.puts("      TestFunc(#{va_args2}); \\")
294    output.puts('    } Catch(e) { TEST_ASSERT_EQUAL_HEX32_MESSAGE(CEXCEPTION_NONE, e, "Unhandled Exception!"); } \\') if cexception
295    output.puts('  } \\')
296    output.puts('  if (TEST_PROTECT()) \\')
297    output.puts('  { \\')
298    output.puts("    #{@options[:teardown_name]}(); \\")
299    output.puts('    CMock_Verify(); \\') unless used_mocks.empty?
300    output.puts('  } \\')
301    output.puts('  CMock_Destroy(); \\') unless used_mocks.empty?
302    output.puts('  UnityConcludeTest(); \\')
303    output.puts('  } \\') if @options[:cmdline_args]
304    output.puts("}\n")
305  end
306
307  def create_reset(output, used_mocks)
308    output.puts("\n/*=======Test Reset Option=====*/")
309    output.puts('void resetTest(void);')
310    output.puts('void resetTest(void)')
311    output.puts('{')
312    output.puts('  CMock_Verify();') unless used_mocks.empty?
313    output.puts('  CMock_Destroy();') unless used_mocks.empty?
314    output.puts("  #{@options[:teardown_name]}();")
315    output.puts('  CMock_Init();') unless used_mocks.empty?
316    output.puts("  #{@options[:setup_name]}();")
317    output.puts('}')
318  end
319
320  def create_main(output, filename, tests, used_mocks)
321    output.puts("\n\n/*=======MAIN=====*/")
322    main_name = @options[:main_name].to_sym == :auto ? "main_#{filename.gsub('.c', '')}" : (@options[:main_name]).to_s
323    if @options[:cmdline_args]
324      if main_name != 'main'
325        output.puts("#{@options[:main_export_decl]} int #{main_name}(int argc, char** argv);")
326      end
327      output.puts("#{@options[:main_export_decl]} int #{main_name}(int argc, char** argv)")
328      output.puts('{')
329      output.puts('  int parse_status = UnityParseOptions(argc, argv);')
330      output.puts('  if (parse_status != 0)')
331      output.puts('  {')
332      output.puts('    if (parse_status < 0)')
333      output.puts('    {')
334      output.puts("      UnityPrint(\"#{filename.gsub('.c', '')}.\");")
335      output.puts('      UNITY_PRINT_EOL();')
336      if @options[:use_param_tests]
337        tests.each do |test|
338          if test[:args].nil? || test[:args].empty?
339            output.puts("      UnityPrint(\"  #{test[:test]}(RUN_TEST_NO_ARGS)\");")
340            output.puts('      UNITY_PRINT_EOL();')
341          else
342            test[:args].each do |args|
343              output.puts("      UnityPrint(\"  #{test[:test]}(#{args})\");")
344              output.puts('      UNITY_PRINT_EOL();')
345            end
346          end
347        end
348      else
349        tests.each { |test| output.puts("      UnityPrint(\"  #{test[:test]}\");\n    UNITY_PRINT_EOL();") }
350      end
351      output.puts('    return 0;')
352      output.puts('    }')
353      output.puts('  return parse_status;')
354      output.puts('  }')
355    else
356      if main_name != 'main'
357        output.puts("#{@options[:main_export_decl]} int #{main_name}(void);")
358      end
359      output.puts("int #{main_name}(void)")
360      output.puts('{')
361    end
362    output.puts('  suite_setup();')
363    output.puts("  UnityBegin(\"#{filename.gsub(/\\/, '\\\\\\')}\");")
364    if @options[:use_param_tests]
365      tests.each do |test|
366        if test[:args].nil? || test[:args].empty?
367          output.puts("  RUN_TEST(#{test[:test]}, #{test[:line_number]}, RUN_TEST_NO_ARGS);")
368        else
369          test[:args].each { |args| output.puts("  RUN_TEST(#{test[:test]}, #{test[:line_number]}, #{args});") }
370        end
371      end
372    else
373      tests.each { |test| output.puts("  RUN_TEST(#{test[:test]}, #{test[:line_number]});") }
374    end
375    output.puts
376    output.puts('  CMock_Guts_MemFreeFinal();') unless used_mocks.empty?
377    output.puts("  return suite_teardown(UnityEnd());")
378    output.puts('}')
379  end
380
381  def create_h_file(output, filename, tests, testfile_includes, used_mocks)
382    filename = File.basename(filename).gsub(/[-\/\\\.\,\s]/, '_').upcase
383    output.puts('/* AUTOGENERATED FILE. DO NOT EDIT. */')
384    output.puts("#ifndef _#{filename}")
385    output.puts("#define _#{filename}\n\n")
386    output.puts("#include \"#{@options[:framework]}.h\"")
387    output.puts('#include "cmock.h"') unless used_mocks.empty?
388    @options[:includes].flatten.uniq.compact.each do |inc|
389      output.puts("#include #{inc.include?('<') ? inc : "\"#{inc.gsub('.h', '')}.h\""}")
390    end
391    testfile_includes.each do |inc|
392      output.puts("#include #{inc.include?('<') ? inc : "\"#{inc.gsub('.h', '')}.h\""}")
393    end
394    output.puts "\n"
395    tests.each do |test|
396      if test[:params].nil? || test[:params].empty?
397        output.puts("void #{test[:test]}(void);")
398      else
399        output.puts("void #{test[:test]}(#{test[:params]});")
400      end
401    end
402    output.puts("#endif\n\n")
403  end
404end
405
406if $0 == __FILE__
407  options = { includes: [] }
408
409  # parse out all the options first (these will all be removed as we go)
410  ARGV.reject! do |arg|
411    case arg
412    when '-cexception'
413      options[:plugins] = [:cexception]
414      true
415    when /\.*\.ya?ml/
416      options = UnityTestRunnerGenerator.grab_config(arg)
417      true
418    when /--(\w+)=\"?(.*)\"?/
419      options[Regexp.last_match(1).to_sym] = Regexp.last_match(2)
420      true
421    when /\.*\.h/
422      options[:includes] << arg
423      true
424    else false
425    end
426  end
427
428  # make sure there is at least one parameter left (the input file)
429  unless ARGV[0]
430    puts ["\nusage: ruby #{__FILE__} (files) (options) input_test_file (output)",
431          "\n  input_test_file         - this is the C file you want to create a runner for",
432          '  output                  - this is the name of the runner file to generate',
433          '                            defaults to (input_test_file)_Runner',
434          '  files:',
435          '    *.yml / *.yaml        - loads configuration from here in :unity or :cmock',
436          '    *.h                   - header files are added as #includes in runner',
437          '  options:',
438          '    -cexception           - include cexception support',
439          '    --setup_name=""       - redefine setUp func name to something else',
440          '    --teardown_name=""    - redefine tearDown func name to something else',
441          '    --main_name=""        - redefine main func name to something else',
442          '    --test_prefix=""      - redefine test prefix from default test|spec|should',
443          '    --suite_setup=""      - code to execute for setup of entire suite',
444          '    --suite_teardown=""   - code to execute for teardown of entire suite',
445          '    --use_param_tests=1   - enable parameterized tests (disabled by default)',
446          '    --header_file=""      - path/name of test header file to generate too'].join("\n")
447    exit 1
448  end
449
450  # create the default test runner name if not specified
451  ARGV[1] = ARGV[0].gsub('.c', '_Runner.c') unless ARGV[1]
452
453  UnityTestRunnerGenerator.new(options).run(ARGV[0], ARGV[1])
454end
455