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.

2 years ago
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333
  1. # frozen_string_literal: true
  2. require 'json'
  3. require 'base64'
  4. require 'cucumber/formatter/backtrace_filter'
  5. require 'cucumber/formatter/io'
  6. require 'cucumber/formatter/ast_lookup'
  7. module Cucumber
  8. module Formatter
  9. # The formatter used for <tt>--format json</tt>
  10. class Json
  11. include Io
  12. def initialize(config)
  13. @io = ensure_io(config.out_stream, config.error_stream)
  14. @ast_lookup = AstLookup.new(config)
  15. @feature_hashes = []
  16. @step_or_hook_hash = {}
  17. config.on_event :test_case_started, &method(:on_test_case_started)
  18. config.on_event :test_case_finished, &method(:on_test_case_finished)
  19. config.on_event :test_step_started, &method(:on_test_step_started)
  20. config.on_event :test_step_finished, &method(:on_test_step_finished)
  21. config.on_event :test_run_finished, &method(:on_test_run_finished)
  22. end
  23. def on_test_case_started(event)
  24. test_case = event.test_case
  25. builder = Builder.new(test_case, @ast_lookup)
  26. unless same_feature_as_previous_test_case?(test_case)
  27. @feature_hash = builder.feature_hash
  28. @feature_hashes << @feature_hash
  29. end
  30. @test_case_hash = builder.test_case_hash
  31. @element_hash = nil
  32. @element_background_hash = builder.background_hash
  33. @in_background = builder.background?
  34. @any_step_failed = false
  35. end
  36. def on_test_step_started(event)
  37. test_step = event.test_step
  38. return if internal_hook?(test_step)
  39. if @element_hash.nil?
  40. @element_hash = create_element_hash(test_step)
  41. feature_elements << @element_hash
  42. end
  43. if test_step.hook?
  44. @step_or_hook_hash = {}
  45. hooks_of_type(test_step) << @step_or_hook_hash
  46. return
  47. end
  48. if first_step_after_background?(test_step)
  49. @in_background = false
  50. feature_elements << @test_case_hash
  51. @element_hash = @test_case_hash
  52. end
  53. @step_or_hook_hash = create_step_hash(test_step)
  54. steps << @step_or_hook_hash
  55. @step_hash = @step_or_hook_hash
  56. end
  57. def on_test_step_finished(event)
  58. test_step, result = *event.attributes
  59. result = result.with_filtered_backtrace(Cucumber::Formatter::BacktraceFilter)
  60. return if internal_hook?(test_step)
  61. add_match_and_result(test_step, result)
  62. @any_step_failed = true if result.failed?
  63. end
  64. def on_test_case_finished(event)
  65. feature_elements << @test_case_hash if @in_background
  66. _test_case, result = *event.attributes
  67. result = result.with_filtered_backtrace(Cucumber::Formatter::BacktraceFilter)
  68. add_failed_around_hook(result) if result.failed? && !@any_step_failed
  69. end
  70. def on_test_run_finished(_event)
  71. @io.write(JSON.pretty_generate(@feature_hashes))
  72. end
  73. def attach(src, mime_type)
  74. if mime_type == 'text/x.cucumber.log+plain'
  75. test_step_output << src
  76. return
  77. end
  78. if mime_type =~ /;base64$/
  79. mime_type = mime_type[0..-8]
  80. data = src
  81. else
  82. data = encode64(src)
  83. end
  84. test_step_embeddings << { mime_type: mime_type, data: data }
  85. end
  86. private
  87. def same_feature_as_previous_test_case?(test_case)
  88. current_feature[:uri] == test_case.location.file
  89. end
  90. def first_step_after_background?(test_step)
  91. @in_background && test_step.location.file == @feature_hash[:uri] && test_step.location.lines.max >= @test_case_hash[:line]
  92. end
  93. def internal_hook?(test_step)
  94. test_step.location.file.include?('lib/cucumber/')
  95. end
  96. def current_feature
  97. @feature_hash ||= {} # rubocop:disable Naming/MemoizedInstanceVariableName
  98. end
  99. def feature_elements
  100. @feature_hash[:elements] ||= []
  101. end
  102. def steps
  103. @element_hash[:steps] ||= []
  104. end
  105. def hooks_of_type(hook_step)
  106. case hook_step.text
  107. when 'Before hook'
  108. before_hooks
  109. when 'After hook'
  110. after_hooks
  111. when 'AfterStep hook'
  112. after_step_hooks
  113. else
  114. raise "Unknown hook type #{hook_step}"
  115. end
  116. end
  117. def before_hooks
  118. @element_hash[:before] ||= []
  119. end
  120. def after_hooks
  121. @element_hash[:after] ||= []
  122. end
  123. def around_hooks
  124. @element_hash[:around] ||= []
  125. end
  126. def after_step_hooks
  127. @step_hash[:after] ||= []
  128. end
  129. def test_step_output
  130. @step_or_hook_hash[:output] ||= []
  131. end
  132. def test_step_embeddings
  133. @step_or_hook_hash[:embeddings] ||= []
  134. end
  135. def create_element_hash(test_step)
  136. return @element_background_hash if @in_background && !first_step_after_background?(test_step)
  137. @in_background = false
  138. @test_case_hash
  139. end
  140. def create_step_hash(test_step)
  141. step_source = @ast_lookup.step_source(test_step).step
  142. step_hash = {
  143. keyword: step_source.keyword,
  144. name: test_step.text,
  145. line: test_step.location.lines.min
  146. }
  147. step_hash[:doc_string] = create_doc_string_hash(step_source.doc_string, test_step.multiline_arg.content) unless step_source.doc_string.nil?
  148. step_hash[:rows] = create_data_table_value(step_source.data_table) unless step_source.data_table.nil?
  149. step_hash
  150. end
  151. def create_doc_string_hash(doc_string, doc_string_content)
  152. content_type = doc_string.media_type || ''
  153. {
  154. value: doc_string_content,
  155. content_type: content_type,
  156. line: doc_string.location.line
  157. }
  158. end
  159. def create_data_table_value(data_table)
  160. data_table.rows.map do |row|
  161. { cells: row.cells.map(&:value) }
  162. end
  163. end
  164. def add_match_and_result(test_step, result)
  165. @step_or_hook_hash[:match] = create_match_hash(test_step, result)
  166. @step_or_hook_hash[:result] = create_result_hash(result)
  167. result.embeddings.each { |e| embed(e['src'], e['mime_type'], e['label']) } if result.respond_to?(:embeddings)
  168. end
  169. def add_failed_around_hook(result)
  170. @step_or_hook_hash = {}
  171. around_hooks << @step_or_hook_hash
  172. @step_or_hook_hash[:match] = { location: 'unknown_hook_location:1' }
  173. @step_or_hook_hash[:result] = create_result_hash(result)
  174. end
  175. def create_match_hash(test_step, _result)
  176. { location: test_step.action_location.to_s }
  177. end
  178. def create_result_hash(result)
  179. result_hash = {
  180. status: result.to_sym
  181. }
  182. result_hash[:error_message] = create_error_message(result) if result.failed? || result.pending?
  183. result.duration.tap { |duration| result_hash[:duration] = duration.nanoseconds }
  184. result_hash
  185. end
  186. def create_error_message(result)
  187. message_element = result.failed? ? result.exception : result
  188. message = "#{message_element.message} (#{message_element.class})"
  189. ([message] + message_element.backtrace).join("\n")
  190. end
  191. def encode64(data)
  192. # strip newlines from the encoded data
  193. Base64.encode64(data).delete("\n")
  194. end
  195. class Builder
  196. attr_reader :feature_hash, :background_hash, :test_case_hash
  197. def initialize(test_case, ast_lookup)
  198. @background_hash = nil
  199. uri = test_case.location.file
  200. feature = ast_lookup.gherkin_document(uri).feature
  201. feature(feature, uri)
  202. background(feature.children.first.background) unless feature.children.first.background.nil?
  203. scenario(ast_lookup.scenario_source(test_case), test_case)
  204. end
  205. def background?
  206. @background_hash != nil
  207. end
  208. def feature(feature, uri)
  209. @feature_hash = {
  210. id: create_id(feature.name),
  211. uri: uri,
  212. keyword: feature.keyword,
  213. name: feature.name,
  214. description: value_or_empty_string(feature.description),
  215. line: feature.location.line
  216. }
  217. return if feature.tags.empty?
  218. @feature_hash[:tags] = create_tags_array_from_hash_array(feature.tags)
  219. end
  220. def background(background)
  221. @background_hash = {
  222. keyword: background.keyword,
  223. name: background.name,
  224. description: value_or_empty_string(background.description),
  225. line: background.location.line,
  226. type: 'background',
  227. steps: []
  228. }
  229. end
  230. def scenario(scenario_source, test_case)
  231. scenario = scenario_source.type == :Scenario ? scenario_source.scenario : scenario_source.scenario_outline
  232. @test_case_hash = {
  233. id: "#{@feature_hash[:id]};#{create_id_from_scenario_source(scenario_source)}",
  234. keyword: scenario.keyword,
  235. name: test_case.name,
  236. description: value_or_empty_string(scenario.description),
  237. line: test_case.location.lines.max,
  238. type: 'scenario',
  239. steps: []
  240. }
  241. @test_case_hash[:tags] = create_tags_array_from_tags_array(test_case.tags) unless test_case.tags.empty?
  242. end
  243. private
  244. def value_or_empty_string(value)
  245. value.nil? ? '' : value
  246. end
  247. def create_id(name)
  248. name.downcase.tr(' ', '-')
  249. end
  250. def create_id_from_scenario_source(scenario_source)
  251. if scenario_source.type == :Scenario
  252. create_id(scenario_source.scenario.name)
  253. else
  254. scenario_outline_name = scenario_source.scenario_outline.name
  255. examples_name = scenario_source.examples.name
  256. row_number = calculate_row_number(scenario_source)
  257. "#{create_id(scenario_outline_name)};#{create_id(examples_name)};#{row_number}"
  258. end
  259. end
  260. def calculate_row_number(scenario_source)
  261. scenario_source.examples.table_body.each_with_index do |row, index|
  262. return index + 2 if row == scenario_source.row
  263. end
  264. end
  265. def create_tags_array_from_hash_array(tags)
  266. tags_array = []
  267. tags.each { |tag| tags_array << { name: tag.name, line: tag.location.line } }
  268. tags_array
  269. end
  270. def create_tags_array_from_tags_array(tags)
  271. tags_array = []
  272. tags.each { |tag| tags_array << { name: tag.name, line: tag.location.line } }
  273. tags_array
  274. end
  275. end
  276. end
  277. end
  278. end

No Description

Contributors (1)