parse_output.rb 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383
  1. #============================================================
  2. # Author: John Theofanopoulos
  3. # A simple parser. Takes the output files generated during the
  4. # build process and extracts information relating to the tests.
  5. #
  6. # Notes:
  7. # To capture an output file under VS builds use the following:
  8. # devenv [build instructions] > Output.txt & type Output.txt
  9. #
  10. # To capture an output file under Linux builds use the following:
  11. # make | tee Output.txt
  12. #
  13. # This script can handle the following output formats:
  14. # - normal output (raw unity)
  15. # - fixture output (unity_fixture.h/.c)
  16. # - fixture output with verbose flag set ("-v")
  17. # - time output flag set (UNITY_INCLUDE_EXEC_TIME define enabled with milliseconds output)
  18. #
  19. # To use this parser use the following command
  20. # ruby parseOutput.rb [options] [file]
  21. # options: -xml : produce a JUnit compatible XML file
  22. # -suiteRequiredSuiteName
  23. # : replace default test suite name to
  24. # "RequiredSuiteName" (can be any name)
  25. # file: file to scan for results
  26. #============================================================
  27. # Parser class for handling the input file
  28. class ParseOutput
  29. def initialize
  30. # internal data
  31. @class_name_idx = 0
  32. @result_usual_idx = 3
  33. @path_delim = nil
  34. # xml output related
  35. @xml_out = false
  36. @array_list = false
  37. # current suite name and statistics
  38. ## testsuite name
  39. @real_test_suite_name = 'Unity'
  40. ## classname for testcase
  41. @test_suite = nil
  42. @total_tests = 0
  43. @test_passed = 0
  44. @test_failed = 0
  45. @test_ignored = 0
  46. end
  47. # Set the flag to indicate if there will be an XML output file or not
  48. def set_xml_output
  49. @xml_out = true
  50. end
  51. # Set the flag to indicate if there will be an XML output file or not
  52. def test_suite_name=(cli_arg)
  53. @real_test_suite_name = cli_arg
  54. puts "Real test suite name will be '#{@real_test_suite_name}'"
  55. end
  56. def xml_encode_s(str)
  57. str.encode(:xml => :attr)
  58. end
  59. # If write our output to XML
  60. def write_xml_output
  61. output = File.open('report.xml', 'w')
  62. output << "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"
  63. @array_list.each do |item|
  64. output << item << "\n"
  65. end
  66. end
  67. # Pushes the suite info as xml to the array list, which will be written later
  68. def push_xml_output_suite_info
  69. # Insert opening tag at front
  70. heading = "<testsuite name=#{xml_encode_s(@real_test_suite_name)} tests=\"#{@total_tests}\" failures=\"#{@test_failed}\" skips=\"#{@test_ignored}\">"
  71. @array_list.insert(0, heading)
  72. # Push back the closing tag
  73. @array_list.push '</testsuite>'
  74. end
  75. # Pushes xml output data to the array list, which will be written later
  76. def push_xml_output_passed(test_name, execution_time = 0)
  77. @array_list.push " <testcase classname=#{xml_encode_s(@test_suite)} name=#{xml_encode_s(test_name)} time=#{xml_encode_s((execution_time / 1000.0).to_s)} />"
  78. end
  79. # Pushes xml output data to the array list, which will be written later
  80. def push_xml_output_failed(test_name, reason, execution_time = 0)
  81. @array_list.push " <testcase classname=#{xml_encode_s(@test_suite)} name=#{xml_encode_s(test_name)} time=#{xml_encode_s((execution_time / 1000.0).to_s)} >"
  82. @array_list.push " <failure type=\"ASSERT FAILED\">#{reason}</failure>"
  83. @array_list.push ' </testcase>'
  84. end
  85. # Pushes xml output data to the array list, which will be written later
  86. def push_xml_output_ignored(test_name, reason, execution_time = 0)
  87. @array_list.push " <testcase classname=#{xml_encode_s(@test_suite)} name=#{xml_encode_s(test_name)} time=#{xml_encode_s((execution_time / 1000.0).to_s)} >"
  88. @array_list.push " <skipped type=\"TEST IGNORED\">#{reason}</skipped>"
  89. @array_list.push ' </testcase>'
  90. end
  91. # This function will try and determine when the suite is changed. This is
  92. # is the name that gets added to the classname parameter.
  93. def test_suite_verify(test_suite_name)
  94. # Split the path name
  95. test_name = test_suite_name.split(@path_delim)
  96. # Remove the extension and extract the base_name
  97. base_name = test_name[test_name.size - 1].split('.')[0]
  98. # Return if the test suite hasn't changed
  99. return unless base_name.to_s != @test_suite.to_s
  100. @test_suite = base_name
  101. printf "New Test: %s\n", @test_suite
  102. end
  103. # Prepares the line for verbose fixture output ("-v")
  104. def prepare_fixture_line(line)
  105. line = line.sub('IGNORE_TEST(', '')
  106. line = line.sub('TEST(', '')
  107. line = line.sub(')', ',')
  108. line = line.chomp
  109. array = line.split(',')
  110. array.map { |x| x.to_s.lstrip.chomp }
  111. end
  112. # Test was flagged as having passed so format the output.
  113. # This is using the Unity fixture output and not the original Unity output.
  114. def test_passed_unity_fixture(array)
  115. class_name = array[0]
  116. test_name = array[1]
  117. test_suite_verify(class_name)
  118. printf "%-40s PASS\n", test_name
  119. push_xml_output_passed(test_name) if @xml_out
  120. end
  121. # Test was flagged as having failed so format the output.
  122. # This is using the Unity fixture output and not the original Unity output.
  123. def test_failed_unity_fixture(array)
  124. class_name = array[0]
  125. test_name = array[1]
  126. test_suite_verify(class_name)
  127. reason_array = array[2].split(':')
  128. reason = "#{reason_array[-1].lstrip.chomp} at line: #{reason_array[-4]}"
  129. printf "%-40s FAILED\n", test_name
  130. push_xml_output_failed(test_name, reason) if @xml_out
  131. end
  132. # Test was flagged as being ignored so format the output.
  133. # This is using the Unity fixture output and not the original Unity output.
  134. def test_ignored_unity_fixture(array)
  135. class_name = array[0]
  136. test_name = array[1]
  137. reason = 'No reason given'
  138. if array.size > 2
  139. reason_array = array[2].split(':')
  140. tmp_reason = reason_array[-1].lstrip.chomp
  141. reason = tmp_reason == 'IGNORE' ? 'No reason given' : tmp_reason
  142. end
  143. test_suite_verify(class_name)
  144. printf "%-40s IGNORED\n", test_name
  145. push_xml_output_ignored(test_name, reason) if @xml_out
  146. end
  147. # Test was flagged as having passed so format the output
  148. def test_passed(array)
  149. # ':' symbol will be valid in function args now
  150. real_method_name = array[@result_usual_idx - 1..-2].join(':')
  151. array = array[0..@result_usual_idx - 2] + [real_method_name] + [array[-1]]
  152. last_item = array.length - 1
  153. test_time = get_test_time(array[last_item])
  154. test_name = array[last_item - 1]
  155. test_suite_verify(array[@class_name_idx])
  156. printf "%-40s PASS %10d ms\n", test_name, test_time
  157. return unless @xml_out
  158. push_xml_output_passed(test_name, test_time) if @xml_out
  159. end
  160. # Test was flagged as having failed so format the line
  161. def test_failed(array)
  162. # ':' symbol will be valid in function args now
  163. real_method_name = array[@result_usual_idx - 1..-3].join(':')
  164. array = array[0..@result_usual_idx - 3] + [real_method_name] + array[-2..]
  165. last_item = array.length - 1
  166. test_time = get_test_time(array[last_item])
  167. test_name = array[last_item - 2]
  168. reason = "#{array[last_item].chomp.lstrip} at line: #{array[last_item - 3]}"
  169. class_name = array[@class_name_idx]
  170. if test_name.start_with? 'TEST('
  171. array2 = test_name.split(' ')
  172. test_suite = array2[0].sub('TEST(', '')
  173. test_suite = test_suite.sub(',', '')
  174. class_name = test_suite
  175. test_name = array2[1].sub(')', '')
  176. end
  177. test_suite_verify(class_name)
  178. printf "%-40s FAILED %10d ms\n", test_name, test_time
  179. push_xml_output_failed(test_name, reason, test_time) if @xml_out
  180. end
  181. # Test was flagged as being ignored so format the output
  182. def test_ignored(array)
  183. # ':' symbol will be valid in function args now
  184. real_method_name = array[@result_usual_idx - 1..-3].join(':')
  185. array = array[0..@result_usual_idx - 3] + [real_method_name] + array[-2..]
  186. last_item = array.length - 1
  187. test_time = get_test_time(array[last_item])
  188. test_name = array[last_item - 2]
  189. reason = array[last_item].chomp.lstrip
  190. class_name = array[@class_name_idx]
  191. if test_name.start_with? 'TEST('
  192. array2 = test_name.split(' ')
  193. test_suite = array2[0].sub('TEST(', '')
  194. test_suite = test_suite.sub(',', '')
  195. class_name = test_suite
  196. test_name = array2[1].sub(')', '')
  197. end
  198. test_suite_verify(class_name)
  199. printf "%-40s IGNORED %10d ms\n", test_name, test_time
  200. push_xml_output_ignored(test_name, reason, test_time) if @xml_out
  201. end
  202. # Test time will be in ms
  203. def get_test_time(value_with_time)
  204. test_time_array = value_with_time.scan(/\((-?\d+.?\d*) ms\)\s*$/).flatten.map do |arg_value_str|
  205. arg_value_str.include?('.') ? arg_value_str.to_f : arg_value_str.to_i
  206. end
  207. test_time_array.any? ? test_time_array[0] : 0
  208. end
  209. # Adjusts the os specific members according to the current path style
  210. # (Windows or Unix based)
  211. def detect_os_specifics(line)
  212. if line.include? '\\'
  213. # Windows X:\Y\Z
  214. @class_name_idx = 1
  215. @path_delim = '\\'
  216. else
  217. # Unix Based /X/Y/Z
  218. @class_name_idx = 0
  219. @path_delim = '/'
  220. end
  221. end
  222. # Main function used to parse the file that was captured.
  223. def process(file_name)
  224. @array_list = []
  225. puts "Parsing file: #{file_name}"
  226. @test_passed = 0
  227. @test_failed = 0
  228. @test_ignored = 0
  229. puts ''
  230. puts '=================== RESULTS ====================='
  231. puts ''
  232. # Apply binary encoding. Bad symbols will be unchanged
  233. File.open(file_name, 'rb').each do |line|
  234. # Typical test lines look like these:
  235. # ----------------------------------------------------
  236. # 1. normal output:
  237. # <path>/<test_file>.c:36:test_tc1000_opsys:FAIL: Expected 1 Was 0
  238. # <path>/<test_file>.c:112:test_tc5004_initCanChannel:IGNORE: Not Yet Implemented
  239. # <path>/<test_file>.c:115:test_tc5100_initCanVoidPtrs:PASS
  240. #
  241. # 2. fixture output
  242. # <path>/<test_file>.c:63:TEST(<test_group>, <test_function>):FAIL: Expected 0x00001234 Was 0x00005A5A
  243. # <path>/<test_file>.c:36:TEST(<test_group>, <test_function>):IGNORE
  244. # Note: "PASS" information won't be generated in this mode
  245. #
  246. # 3. fixture output with verbose information ("-v")
  247. # TEST(<test_group, <test_file>)<path>/<test_file>:168::FAIL: Expected 0x8D Was 0x8C
  248. # TEST(<test_group>, <test_file>)<path>/<test_file>:22::IGNORE: This Test Was Ignored On Purpose
  249. # IGNORE_TEST(<test_group, <test_file>)
  250. # TEST(<test_group, <test_file>) PASS
  251. #
  252. # Note: Where path is different on Unix vs Windows devices (Windows leads with a drive letter)!
  253. detect_os_specifics(line)
  254. line_array = line.split(':')
  255. # If we were able to split the line then we can look to see if any of our target words
  256. # were found. Case is important.
  257. next unless (line_array.size >= 4) || (line.start_with? 'TEST(') || (line.start_with? 'IGNORE_TEST(')
  258. # check if the output is fixture output (with verbose flag "-v")
  259. if (line.start_with? 'TEST(') || (line.start_with? 'IGNORE_TEST(')
  260. line_array = prepare_fixture_line(line)
  261. if line.include? ' PASS'
  262. test_passed_unity_fixture(line_array)
  263. @test_passed += 1
  264. elsif line.include? 'FAIL'
  265. test_failed_unity_fixture(line_array)
  266. @test_failed += 1
  267. elsif line.include? 'IGNORE'
  268. test_ignored_unity_fixture(line_array)
  269. @test_ignored += 1
  270. end
  271. # normal output / fixture output (without verbose "-v")
  272. elsif line.include? ':PASS'
  273. test_passed(line_array)
  274. @test_passed += 1
  275. elsif line.include? ':FAIL'
  276. test_failed(line_array)
  277. @test_failed += 1
  278. elsif line.include? ':IGNORE:'
  279. test_ignored(line_array)
  280. @test_ignored += 1
  281. elsif line.include? ':IGNORE'
  282. line_array.push('No reason given')
  283. test_ignored(line_array)
  284. @test_ignored += 1
  285. elsif line_array.size >= 4
  286. # We will check output from color compilation
  287. if line_array[@result_usual_idx..].any? { |l| l.include? 'PASS' }
  288. test_passed(line_array)
  289. @test_passed += 1
  290. elsif line_array[@result_usual_idx..].any? { |l| l.include? 'FAIL' }
  291. test_failed(line_array)
  292. @test_failed += 1
  293. elsif line_array[@result_usual_idx..-2].any? { |l| l.include? 'IGNORE' }
  294. test_ignored(line_array)
  295. @test_ignored += 1
  296. elsif line_array[@result_usual_idx..].any? { |l| l.include? 'IGNORE' }
  297. line_array.push("No reason given (#{get_test_time(line_array[@result_usual_idx..])} ms)")
  298. test_ignored(line_array)
  299. @test_ignored += 1
  300. end
  301. end
  302. @total_tests = @test_passed + @test_failed + @test_ignored
  303. end
  304. puts ''
  305. puts '=================== SUMMARY ====================='
  306. puts ''
  307. puts "Tests Passed : #{@test_passed}"
  308. puts "Tests Failed : #{@test_failed}"
  309. puts "Tests Ignored : #{@test_ignored}"
  310. return unless @xml_out
  311. # push information about the suite
  312. push_xml_output_suite_info
  313. # write xml output file
  314. write_xml_output
  315. end
  316. end
  317. # If the command line has no values in, used a default value of Output.txt
  318. parse_my_file = ParseOutput.new
  319. if ARGV.size >= 1
  320. ARGV.each do |arg|
  321. if arg == '-xml'
  322. parse_my_file.set_xml_output
  323. elsif arg.start_with?('-suite')
  324. parse_my_file.test_suite_name = arg.delete_prefix('-suite')
  325. else
  326. parse_my_file.process(arg)
  327. break
  328. end
  329. end
  330. end