generate_test_runner.rb 17.0 KB
Newer Older
M
Mark VanderVoord 已提交
1 2 3 4 5 6
# ==========================================
#   Unity Project - A Test Framework for C
#   Copyright (c) 2007 Mike Karlesky, Mark VanderVoord, Greg Williams
#   [Released under MIT License. Please refer to license.txt for details]
# ==========================================

7
File.expand_path(File.join(File.dirname(__FILE__), 'colour_prompt'))
M
Mark VanderVoord 已提交
8 9 10 11

class UnityTestRunnerGenerator
  def initialize(options = nil)
    @options = UnityTestRunnerGenerator.default_options
12
    case options
13 14 15
    when NilClass then @options
    when String   then @options.merge!(UnityTestRunnerGenerator.grab_config(options))
    when Hash     then @options.merge!(options)
16
    else raise 'If you specify arguments, it should be a filename or a hash of options'
M
Mark VanderVoord 已提交
17 18 19 20 21 22
    end
    require "#{File.expand_path(File.dirname(__FILE__))}/type_sanitizer"
  end

  def self.default_options
    {
23 24 25 26 27 28 29 30 31 32 33 34
      includes: [],
      defines: [],
      plugins: [],
      framework: :unity,
      test_prefix: 'test|spec|should',
      mock_prefix: 'Mock',
      setup_name: 'setUp',
      teardown_name: 'tearDown',
      main_name: 'main', # set to :auto to automatically generate each time
      main_export_decl: '',
      cmdline_args: false,
      use_param_tests: false
M
Mark VanderVoord 已提交
35 36 37 38
    }
  end

  def self.grab_config(config_file)
39 40
    options = default_options
    unless config_file.nil? || config_file.empty?
M
Mark VanderVoord 已提交
41 42 43 44 45
      require 'yaml'
      yaml_guts = YAML.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
46
    options
M
Mark VanderVoord 已提交
47 48
  end

49
  def run(input_file, output_file, options = nil)
M
Mark VanderVoord 已提交
50 51
    @options.merge!(options) unless options.nil?

52
    # pull required data from source file
M
Mark VanderVoord 已提交
53
    source = File.read(input_file)
54
    source = source.force_encoding('ISO-8859-1').encode('utf-8', replace: nil)
M
Mark VanderVoord 已提交
55 56 57 58 59
    tests               = find_tests(source)
    headers             = find_includes(source)
    testfile_includes   = (headers[:local] + headers[:system])
    used_mocks          = find_mocks(testfile_includes)
    testfile_includes   = (testfile_includes - used_mocks)
60
    testfile_includes.delete_if { |inc| inc =~ /(unity|cmock)/ }
M
Mark VanderVoord 已提交
61

62
    # build runner file
M
Mark VanderVoord 已提交
63 64
    generate(input_file, output_file, tests, used_mocks, testfile_includes)

65
    # determine which files were used to return them
M
Mark VanderVoord 已提交
66
    all_files_used = [input_file, output_file]
67
    all_files_used += testfile_includes.map { |filename| filename + '.c' } unless testfile_includes.empty?
M
Mark VanderVoord 已提交
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
M
Mark VanderVoord 已提交
71 72 73 74 75 76 77
  end

  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_externs(output, tests, used_mocks)
      create_mock_management(output, used_mocks)
78 79
      create_suite_setup(output)
      create_suite_teardown(output)
M
Mark VanderVoord 已提交
80 81 82 83
      create_reset(output, used_mocks)
      create_main(output, input_file, tests, used_mocks)
    end

84 85 86 87
    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, testfile_includes, used_mocks)
M
Mark VanderVoord 已提交
88 89 90 91 92 93
    end
  end

  def find_tests(source)
    tests_and_line_numbers = []

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
M
Mark VanderVoord 已提交
97 98 99 100
    source_scrubbed = source_scrubbed.gsub(/\/\*.*?\*\//m, '') # remove block comments
    lines = source_scrubbed.split(/(^\s*\#.*$)                 # Treat preprocessor directives as a logical line
                              | (;|\{|\}) /x)                  # Match ;, {, and } as end of lines

101 102 103 104 105 106 107 108 109 110 111
    lines.each_with_index do |line, _index|
      # find tests
      next unless line =~ /^((?:\s*TEST_CASE\s*\(.*?\)\s*)*)\s*void\s+((?:#{@options[:test_prefix]}).*)\s*\(\s*(.*)\s*\)/
      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 = []
        arguments.scan(/\s*TEST_CASE\s*\((.*)\)\s*$/) { |a| args << a[0] }
M
Mark VanderVoord 已提交
112
      end
113
      tests_and_line_numbers << { test: name, args: args, call: call, params: params, line_number: 0 }
M
Mark VanderVoord 已提交
114
    end
115
    tests_and_line_numbers.uniq! { |v| v[:test] }
M
Mark VanderVoord 已提交
116

117
    # determine line numbers and create tests to run
M
Mark VanderVoord 已提交
118
    source_lines = source.split("\n")
119
    source_index = 0
M
Mark VanderVoord 已提交
120 121
    tests_and_line_numbers.size.times do |i|
      source_lines[source_index..-1].each_with_index do |line, index|
122
        next unless line =~ /#{tests_and_line_numbers[i][:test]}/
123 124 125
        source_index += index
        tests_and_line_numbers[i][:line_number] = source_index + 1
        break
M
Mark VanderVoord 已提交
126 127 128
      end
    end

129
    tests_and_line_numbers
M
Mark VanderVoord 已提交
130 131 132
  end

  def find_includes(source)
133
    # remove comments (block and line, in three steps to ensure correct precedence)
M
Mark VanderVoord 已提交
134 135 136 137
    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)

138
    # parse out includes
M
Mark VanderVoord 已提交
139
    includes = {
140 141 142
      local: source.scan(/^\s*#include\s+\"\s*(.+)\.[hH]\s*\"/).flatten,
      system: source.scan(/^\s*#include\s+<\s*(.+)\s*>/).flatten.map { |inc| "<#{inc}>" },
      linkonly: source.scan(/^TEST_FILE\(\s*\"\s*(.+)\.[cC]\w*\s*\"/).flatten
M
Mark VanderVoord 已提交
143
    }
144
    includes
M
Mark VanderVoord 已提交
145 146 147 148
  end

  def find_mocks(includes)
    mock_headers = []
149 150
    includes.each do |include_path|
      include_file = File.basename(include_path)
151
      mock_headers << include_path if include_file =~ /^#{@options[:mock_prefix]}/i
M
Mark VanderVoord 已提交
152
    end
153
    mock_headers
M
Mark VanderVoord 已提交
154 155
  end

156
  def create_header(output, mocks, testfile_includes = [])
M
Mark VanderVoord 已提交
157 158
    output.puts('/* AUTOGENERATED FILE. DO NOT EDIT. */')
    create_runtest(output, mocks)
159
    output.puts("\n/*=======Automagically Detected Files To Include=====*/")
160 161
    output.puts("#include \"#{@options[:framework]}.h\"")
    output.puts('#include "cmock.h"') unless mocks.empty?
M
Mark VanderVoord 已提交
162 163
    output.puts('#include <setjmp.h>')
    output.puts('#include <stdio.h>')
164 165
    if @options[:defines] && !@options[:defines].empty?
      @options[:defines].each { |d| output.puts("#define #{d}") }
166
    end
167
    if @options[:header_file] && !@options[:header_file].empty?
M
Mark VanderVoord 已提交
168 169 170
      output.puts("#include \"#{File.basename(@options[:header_file])}\"")
    else
      @options[:includes].flatten.uniq.compact.each do |inc|
171
        output.puts("#include #{inc.include?('<') ? inc : "\"#{inc.gsub('.h', '')}.h\""}")
M
Mark VanderVoord 已提交
172 173
      end
      testfile_includes.each do |inc|
174
        output.puts("#include #{inc.include?('<') ? inc : "\"#{inc.gsub('.h', '')}.h\""}")
M
Mark VanderVoord 已提交
175 176 177
      end
    end
    mocks.each do |mock|
178
      output.puts("#include \"#{mock.gsub('.h', '')}.h\"")
M
Mark VanderVoord 已提交
179
    end
180
    output.puts('#include "CException.h"') if @options[:plugins].include?(:cexception)
181 182 183 184 185 186 187

    return unless @options[:enforce_strict_ordering]

    output.puts('')
    output.puts('int GlobalExpectCount;')
    output.puts('int GlobalVerifyOrder;')
    output.puts('char* GlobalOrderError;')
M
Mark VanderVoord 已提交
188 189
  end

190
  def create_externs(output, tests, _mocks)
191
    output.puts("\n/*=======External Functions This Runner Calls=====*/")
M
Mark VanderVoord 已提交
192 193 194 195 196 197 198 199
    output.puts("extern void #{@options[:setup_name]}(void);")
    output.puts("extern void #{@options[:teardown_name]}(void);")
    tests.each do |test|
      output.puts("extern void #{test[:test]}(#{test[:call] || 'void'});")
    end
    output.puts('')
  end

200
  def create_mock_management(output, mock_headers)
201
    return if mock_headers.empty?
M
Mark VanderVoord 已提交
202

203 204 205
    output.puts("\n/*=======Mock Management=====*/")
    output.puts('static void CMock_Init(void)')
    output.puts('{')
M
Mark VanderVoord 已提交
206

207 208 209 210
    if @options[:enforce_strict_ordering]
      output.puts('  GlobalExpectCount = 0;')
      output.puts('  GlobalVerifyOrder = 0;')
      output.puts('  GlobalOrderError = NULL;')
M
Mark VanderVoord 已提交
211 212
    end

213 214 215 216
    mocks = mock_headers.map { |mock| File.basename(mock) }
    mocks.each do |mock|
      mock_clean = TypeSanitizer.sanitize_c_identifier(mock)
      output.puts("  #{mock_clean}_Init();")
M
Mark VanderVoord 已提交
217
    end
218 219 220 221 222 223 224
    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();")
M
Mark VanderVoord 已提交
225
    end
226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254
    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_suite_setup(output)
    return if @options[:suite_setup].nil?

    output.puts("\n/*=======Suite Setup=====*/")
    output.puts('static void suite_setup(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('static int suite_teardown(int num_failures)')
    output.puts('{')
    output.puts(@options[:suite_teardown])
    output.puts('}')
M
Mark VanderVoord 已提交
255 256 257 258 259 260
  end

  def create_runtest(output, used_mocks)
    cexception = @options[:plugins].include? :cexception
    va_args1   = @options[:use_param_tests] ? ', ...' : ''
    va_args2   = @options[:use_param_tests] ? '__VA_ARGS__' : ''
261
    output.puts("\n/*=======Test Runner Used To Run Each Test Below=====*/")
262
    output.puts('#define RUN_TEST_NO_ARGS') if @options[:use_param_tests]
M
Mark VanderVoord 已提交
263
    output.puts("#define RUN_TEST(TestFunc, TestLineNum#{va_args1}) \\")
264
    output.puts('{ \\')
M
Mark VanderVoord 已提交
265
    output.puts("  Unity.CurrentTestName = #TestFunc#{va_args2.empty? ? '' : " \"(\" ##{va_args2} \")\""}; \\")
266 267 268 269 270 271 272 273 274
    output.puts('  Unity.CurrentTestLineNumber = TestLineNum; \\')
    output.puts('  if (UnityTestMatches()) { \\') if @options[:cmdline_args]
    output.puts('  Unity.NumberOfTests++; \\')
    output.puts('  CMock_Init(); \\') unless used_mocks.empty?
    output.puts('  UNITY_CLR_DETAILS(); \\') unless used_mocks.empty?
    output.puts('  if (TEST_PROTECT()) \\')
    output.puts('  { \\')
    output.puts('    CEXCEPTION_T e; \\') if cexception
    output.puts('    Try { \\') if cexception
M
Mark VanderVoord 已提交
275 276
    output.puts("      #{@options[:setup_name]}(); \\")
    output.puts("      TestFunc(#{va_args2}); \\")
277 278 279 280
    output.puts('    } Catch(e) { TEST_ASSERT_EQUAL_HEX32_MESSAGE(CEXCEPTION_NONE, e, "Unhandled Exception!"); } \\') if cexception
    output.puts('  } \\')
    output.puts('  if (TEST_PROTECT()) \\')
    output.puts('  { \\')
M
Mark VanderVoord 已提交
281
    output.puts("    #{@options[:teardown_name]}(); \\")
282 283 284 285 286
    output.puts('    CMock_Verify(); \\') unless used_mocks.empty?
    output.puts('  } \\')
    output.puts('  CMock_Destroy(); \\') unless used_mocks.empty?
    output.puts('  UnityConcludeTest(); \\')
    output.puts('  } \\') if @options[:cmdline_args]
M
Mark VanderVoord 已提交
287 288 289 290
    output.puts("}\n")
  end

  def create_reset(output, used_mocks)
291
    output.puts("\n/*=======Test Reset Option=====*/")
292 293 294 295 296
    output.puts('void resetTest(void);')
    output.puts('void resetTest(void)')
    output.puts('{')
    output.puts('  CMock_Verify();') unless used_mocks.empty?
    output.puts('  CMock_Destroy();') unless used_mocks.empty?
M
Mark VanderVoord 已提交
297
    output.puts("  #{@options[:teardown_name]}();")
298
    output.puts('  CMock_Init();') unless used_mocks.empty?
M
Mark VanderVoord 已提交
299
    output.puts("  #{@options[:setup_name]}();")
300
    output.puts('}')
M
Mark VanderVoord 已提交
301 302 303
  end

  def create_main(output, filename, tests, used_mocks)
304
    output.puts("\n\n/*=======MAIN=====*/")
305 306 307
    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'
308 309
        output.puts("#{@options[:main_export_decl]} int #{main_name}(int argc, char** argv);")
      end
310
      output.puts("#{@options[:main_export_decl]} int #{main_name}(int argc, char** argv)")
311 312 313 314 315 316 317 318 319
      output.puts('{')
      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('.c', '')}.\");")
      output.puts('      UNITY_PRINT_EOL();')
      if @options[:use_param_tests]
320
        tests.each do |test|
321
          if test[:args].nil? || test[:args].empty?
322
            output.puts("      UnityPrint(\"  #{test[:test]}(RUN_TEST_NO_ARGS)\");")
323
            output.puts('      UNITY_PRINT_EOL();')
324 325 326
          else
            test[:args].each do |args|
              output.puts("      UnityPrint(\"  #{test[:test]}(#{args})\");")
327
              output.puts('      UNITY_PRINT_EOL();')
328 329 330 331
            end
          end
        end
      else
332
        tests.each { |test| output.puts("      UnityPrint(\"  #{test[:test]}\");\n    UNITY_PRINT_EOL();") }
333
      end
334 335 336 337
      output.puts('    return 0;')
      output.puts('    }')
      output.puts('  return parse_status;')
      output.puts('  }')
338
    else
339
      if main_name != 'main'
340 341
        output.puts("#{@options[:main_export_decl]} int #{main_name}(void);")
      end
342
      output.puts("int #{main_name}(void)")
343
      output.puts('{')
344
    end
345 346 347
    output.puts('  suite_setup();') unless @options[:suite_setup].nil?
    output.puts("  UnityBegin(\"#{filename.gsub(/\\/, '\\\\\\')}\");")
    if @options[:use_param_tests]
M
Mark VanderVoord 已提交
348
      tests.each do |test|
349
        if test[:args].nil? || test[:args].empty?
M
Mark VanderVoord 已提交
350 351
          output.puts("  RUN_TEST(#{test[:test]}, #{test[:line_number]}, RUN_TEST_NO_ARGS);")
        else
352
          test[:args].each { |args| output.puts("  RUN_TEST(#{test[:test]}, #{test[:line_number]}, #{args});") }
M
Mark VanderVoord 已提交
353 354 355
        end
      end
    else
356
      tests.each { |test| output.puts("  RUN_TEST(#{test[:test]}, #{test[:line_number]});") }
M
Mark VanderVoord 已提交
357
    end
358 359 360 361
    output.puts
    output.puts('  CMock_Guts_MemFreeFinal();') unless used_mocks.empty?
    output.puts("  return #{@options[:suite_teardown].nil? ? '' : 'suite_teardown'}(UnityEnd());")
    output.puts('}')
M
Mark VanderVoord 已提交
362 363
  end

P
Peter Mendham 已提交
364
  def create_h_file(output, filename, tests, testfile_includes, used_mocks)
365 366
    filename = File.basename(filename).gsub(/[-\/\\\.\,\s]/, '_').upcase
    output.puts('/* AUTOGENERATED FILE. DO NOT EDIT. */')
M
Mark VanderVoord 已提交
367 368
    output.puts("#ifndef _#{filename}")
    output.puts("#define _#{filename}\n\n")
369 370
    output.puts("#include \"#{@options[:framework]}.h\"")
    output.puts('#include "cmock.h"') unless used_mocks.empty?
M
Mark VanderVoord 已提交
371
    @options[:includes].flatten.uniq.compact.each do |inc|
372
      output.puts("#include #{inc.include?('<') ? inc : "\"#{inc.gsub('.h', '')}.h\""}")
M
Mark VanderVoord 已提交
373 374
    end
    testfile_includes.each do |inc|
375
      output.puts("#include #{inc.include?('<') ? inc : "\"#{inc.gsub('.h', '')}.h\""}")
M
Mark VanderVoord 已提交
376 377
    end
    output.puts "\n"
378
    tests.each do |test|
379
      if test[:params].nil? || test[:params].empty?
P
Peter Mendham 已提交
380 381 382 383 384
        output.puts("void #{test[:test]}(void);")
      else
        output.puts("void #{test[:test]}(#{test[:params]});")
      end
    end
M
Mark VanderVoord 已提交
385 386 387 388
    output.puts("#endif\n\n")
  end
end

389
if $0 == __FILE__
390
  options = { includes: [] }
M
Mark VanderVoord 已提交
391

392
  # parse out all the options first (these will all be removed as we go)
M
Mark VanderVoord 已提交
393
  ARGV.reject! do |arg|
394
    case arg
395
    when '-cexception'
396 397
      options[:plugins] = [:cexception]
      true
398
    when /\.*\.ya?ml/
399 400
      options = UnityTestRunnerGenerator.grab_config(arg)
      true
401
    when /--(\w+)=\"?(.*)\"?/
402 403
      options[Regexp.last_match(1).to_sym] = Regexp.last_match(2)
      true
404
    when /\.*\.h/
405 406 407
      options[:includes] << arg
      true
    else false
M
Mark VanderVoord 已提交
408 409 410
    end
  end

411 412
  # make sure there is at least one parameter left (the input file)
  unless ARGV[0]
M
Mark VanderVoord 已提交
413
    puts ["\nusage: ruby #{__FILE__} (files) (options) input_test_file (output)",
414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429
          "\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',
          '    --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',
          '    --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)',
          '    --header_file=""      - path/name of test header file to generate too'].join("\n")
M
Mark VanderVoord 已提交
430 431 432
    exit 1
  end

433 434
  # create the default test runner name if not specified
  ARGV[1] = ARGV[0].gsub('.c', '_Runner.c') unless ARGV[1]
M
Mark VanderVoord 已提交
435

436
  UnityTestRunnerGenerator.new(options).run(ARGV[0], ARGV[1])
M
Mark VanderVoord 已提交
437
end