You can not select more than 25 topics Topics must start with a chinese character,a letter or number, can include dashes ('-') and can be up to 35 characters long.

junit.rb 9.2 kB

2 years ago
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265
  1. # frozen_string_literal: true
  2. require 'builder'
  3. require 'cucumber/formatter/backtrace_filter'
  4. require 'cucumber/formatter/io'
  5. require 'cucumber/formatter/interceptor'
  6. require 'fileutils'
  7. require 'cucumber/formatter/ast_lookup'
  8. module Cucumber
  9. module Formatter
  10. # The formatter used for <tt>--format junit</tt>
  11. class Junit
  12. include Io
  13. class UnNamedFeatureError < StandardError
  14. def initialize(feature_file)
  15. super("The feature in '#{feature_file}' does not have a name. The JUnit XML format requires a name for the testsuite element.")
  16. end
  17. end
  18. def initialize(config)
  19. @ast_lookup = AstLookup.new(config)
  20. config.on_event :test_case_started, &method(:on_test_case_started)
  21. config.on_event :test_case_finished, &method(:on_test_case_finished)
  22. config.on_event :test_step_finished, &method(:on_test_step_finished)
  23. config.on_event :test_run_finished, &method(:on_test_run_finished)
  24. @reportdir = ensure_dir(config.out_stream, 'junit')
  25. @config = config
  26. @features_data = Hash.new do |h, k|
  27. h[k] = {
  28. feature: nil,
  29. failures: 0,
  30. errors: 0,
  31. tests: 0,
  32. skipped: 0,
  33. time: 0,
  34. builder: Builder::XmlMarkup.new(indent: 2)
  35. }
  36. end
  37. end
  38. def on_test_case_started(event)
  39. test_case = event.test_case
  40. start_feature(test_case) unless same_feature_as_previous_test_case?(test_case)
  41. @failing_test_step = nil
  42. # In order to fill out <system-err/> and <system-out/>, we need to
  43. # intercept the $stderr and $stdout
  44. @interceptedout = Interceptor::Pipe.wrap(:stdout)
  45. @interceptederr = Interceptor::Pipe.wrap(:stderr)
  46. end
  47. def on_test_step_finished(event)
  48. test_step, result = *event.attributes
  49. return if @failing_test_step
  50. @failing_test_step = test_step unless result.ok?(@config.strict)
  51. end
  52. def on_test_case_finished(event)
  53. test_case, result = *event.attributes
  54. result = result.with_filtered_backtrace(Cucumber::Formatter::BacktraceFilter)
  55. test_case_name = NameBuilder.new(test_case, @ast_lookup)
  56. scenario = test_case_name.scenario_name
  57. scenario_designation = "#{scenario}#{test_case_name.name_suffix}"
  58. output = create_output_string(test_case, scenario, result, test_case_name.row_name)
  59. build_testcase(result, scenario_designation, output)
  60. Interceptor::Pipe.unwrap! :stdout
  61. Interceptor::Pipe.unwrap! :stderr
  62. end
  63. def on_test_run_finished(_event)
  64. @features_data.each { |_file, data| end_feature(data) }
  65. end
  66. private
  67. def same_feature_as_previous_test_case?(test_case)
  68. @current_feature_data && @current_feature_data[:uri] == test_case.location.file
  69. end
  70. def start_feature(test_case)
  71. uri = test_case.location.file
  72. feature = @ast_lookup.gherkin_document(uri).feature
  73. raise UnNamedFeatureError, uri if feature.name.empty?
  74. @current_feature_data = @features_data[uri]
  75. @current_feature_data[:uri] = uri unless @current_feature_data[:uri]
  76. @current_feature_data[:feature] = feature unless @current_feature_data[:feature]
  77. end
  78. def end_feature(feature_data)
  79. @testsuite = Builder::XmlMarkup.new(indent: 2)
  80. @testsuite.instruct!
  81. @testsuite.testsuite(
  82. failures: feature_data[:failures],
  83. errors: feature_data[:errors],
  84. skipped: feature_data[:skipped],
  85. tests: feature_data[:tests],
  86. time: format('%<time>.6f', time: feature_data[:time]),
  87. name: feature_data[:feature].name
  88. ) do
  89. @testsuite << feature_data[:builder].target!
  90. end
  91. write_file(feature_result_filename(feature_data[:uri]), @testsuite.target!)
  92. end
  93. def create_output_string(test_case, scenario, result, row_name)
  94. scenario_source = @ast_lookup.scenario_source(test_case)
  95. keyword = scenario_source.type == :Scenario ? scenario_source.scenario.keyword : scenario_source.scenario_outline.keyword
  96. output = "#{keyword}: #{scenario}\n\n"
  97. return output if result.ok?(@config.strict)
  98. if scenario_source.type == :Scenario
  99. if @failing_test_step
  100. if @failing_test_step.hook?
  101. output += "#{@failing_test_step.text} at #{@failing_test_step.location}\n"
  102. else
  103. step_source = @ast_lookup.step_source(@failing_test_step).step
  104. output += "#{step_source.keyword}#{@failing_test_step.text}\n"
  105. end
  106. else # An Around hook has failed
  107. output += "Around hook\n"
  108. end
  109. else
  110. output += "Example row: #{row_name}\n"
  111. end
  112. "#{output}\nMessage:\n"
  113. end
  114. def build_testcase(result, scenario_designation, output)
  115. duration = ResultBuilder.new(result).test_case_duration
  116. @current_feature_data[:time] += duration
  117. classname = @current_feature_data[:feature].name
  118. filename = @current_feature_data[:uri]
  119. name = scenario_designation
  120. testcase_attributes = get_testcase_attributes(classname, name, duration, filename)
  121. @current_feature_data[:builder].testcase(testcase_attributes) do
  122. if !result.passed? && result.ok?(@config.strict)
  123. @current_feature_data[:builder].skipped
  124. @current_feature_data[:skipped] += 1
  125. elsif !result.passed?
  126. status = result.to_sym
  127. exception = get_backtrace_object(result)
  128. @current_feature_data[:builder].failure(message: "#{status} #{name}", type: status) do
  129. @current_feature_data[:builder].cdata! output
  130. @current_feature_data[:builder].cdata!(format_exception(exception)) if exception
  131. end
  132. @current_feature_data[:failures] += 1
  133. end
  134. @current_feature_data[:builder].tag!('system-out') do
  135. @current_feature_data[:builder].cdata! strip_control_chars(@interceptedout.buffer_string)
  136. end
  137. @current_feature_data[:builder].tag!('system-err') do
  138. @current_feature_data[:builder].cdata! strip_control_chars(@interceptederr.buffer_string)
  139. end
  140. end
  141. @current_feature_data[:tests] += 1
  142. end
  143. def get_testcase_attributes(classname, name, duration, filename)
  144. { classname: classname, name: name, time: format('%<duration>.6f', duration: duration) }.tap do |attributes|
  145. attributes[:file] = filename if add_fileattribute?
  146. end
  147. end
  148. def add_fileattribute?
  149. return false if @config.formats.nil? || @config.formats.empty?
  150. !!@config.formats.find do |format|
  151. format.first == 'junit' && format.dig(1, 'fileattribute') == 'true'
  152. end
  153. end
  154. def get_backtrace_object(result)
  155. if result.failed?
  156. result.exception
  157. elsif result.backtrace
  158. result
  159. end
  160. end
  161. def format_exception(exception)
  162. (["#{exception.message} (#{exception.class})"] + exception.backtrace).join("\n")
  163. end
  164. def feature_result_filename(feature_file)
  165. File.join(@reportdir, "TEST-#{basename(feature_file)}.xml")
  166. end
  167. def basename(feature_file)
  168. File.basename(feature_file.gsub(/[\\\/]/, '-'), '.feature')
  169. end
  170. def write_file(feature_filename, data)
  171. File.open(feature_filename, 'w') { |file| file.write(data) }
  172. end
  173. # strip control chars from cdata, to make it safe for external parsers
  174. def strip_control_chars(cdata)
  175. cdata.scan(/[[:print:]\t\n\r]/).join
  176. end
  177. end
  178. class NameBuilder
  179. attr_reader :scenario_name, :name_suffix, :row_name
  180. def initialize(test_case, ast_lookup)
  181. @name_suffix = ''
  182. @row_name = ''
  183. scenario_source = ast_lookup.scenario_source(test_case)
  184. if scenario_source.type == :Scenario
  185. scenario(scenario_source.scenario)
  186. else
  187. scenario_outline(scenario_source.scenario_outline)
  188. examples_table_row(scenario_source.row)
  189. end
  190. end
  191. def scenario(scenario)
  192. @scenario_name = scenario.name.empty? ? 'Unnamed scenario' : scenario.name
  193. end
  194. def scenario_outline(outline)
  195. @scenario_name = outline.name.empty? ? 'Unnamed scenario outline' : outline.name
  196. end
  197. def examples_table_row(row)
  198. @row_name = "| #{row.cells.map(&:value).join(' | ')} |"
  199. @name_suffix = " (outline example : #{@row_name})"
  200. end
  201. end
  202. class ResultBuilder
  203. attr_reader :test_case_duration
  204. def initialize(result)
  205. @test_case_duration = 0
  206. result.describe_to(self)
  207. end
  208. def passed(*) end
  209. def failed(*) end
  210. def undefined(*) end
  211. def skipped(*) end
  212. def pending(*) end
  213. def exception(*) end
  214. def duration(duration, *)
  215. duration.tap { |dur| @test_case_duration = dur.nanoseconds / 10**9.0 }
  216. end
  217. def attach(*) end
  218. end
  219. end
  220. end

No Description

Contributors (1)