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.

pretty.rb 18 kB

2 years ago
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471
  1. # frozen_string_literal: true
  2. require 'fileutils'
  3. require 'gherkin/dialect'
  4. require 'cucumber/formatter/console'
  5. require 'cucumber/formatter/io'
  6. require 'cucumber/gherkin/formatter/escaping'
  7. require 'cucumber/formatter/console_counts'
  8. require 'cucumber/formatter/console_issues'
  9. require 'cucumber/formatter/duration_extractor'
  10. require 'cucumber/formatter/backtrace_filter'
  11. require 'cucumber/formatter/ast_lookup'
  12. module Cucumber
  13. module Formatter
  14. # The formatter used for <tt>--format pretty</tt> (the default formatter).
  15. #
  16. # This formatter prints the result of the feature executions to plain text - exactly how they were parsed.
  17. #
  18. # If the output is STDOUT (and not a file), there are bright colours to watch too.
  19. #
  20. class Pretty # rubocop:disable Metrics/ClassLength
  21. include FileUtils
  22. include Console
  23. include Io
  24. include Cucumber::Gherkin::Formatter::Escaping
  25. attr_reader :config, :options, :current_feature_uri, :current_scenario_outline, :current_examples, :current_test_case, :in_scenario_outline, :print_background_steps
  26. private :config, :options
  27. private :current_feature_uri, :current_scenario_outline, :current_examples, :current_test_case
  28. private :in_scenario_outline, :print_background_steps
  29. def initialize(config)
  30. @io = ensure_io(config.out_stream, config.error_stream)
  31. @config = config
  32. @options = config.to_hash
  33. @snippets_input = []
  34. @undefined_parameter_types = []
  35. @total_duration = 0
  36. @exceptions = []
  37. @gherkin_sources = {}
  38. @step_matches = {}
  39. @ast_lookup = AstLookup.new(config)
  40. @counts = ConsoleCounts.new(config)
  41. @issues = ConsoleIssues.new(config, @ast_lookup)
  42. @first_feature = true
  43. @current_feature_uri = ''
  44. @current_scenario_outline = nil
  45. @current_examples = nil
  46. @current_test_case = nil
  47. @in_scenario_outline = false
  48. @print_background_steps = false
  49. @test_step_output = []
  50. @passed_test_cases = []
  51. @source_indent = 0
  52. @next_comment_to_be_printed = 0
  53. bind_events(config)
  54. end
  55. def bind_events(config)
  56. config.on_event :gherkin_source_read, &method(:on_gherkin_source_read)
  57. config.on_event :step_activated, &method(:on_step_activated)
  58. config.on_event :test_case_started, &method(:on_test_case_started)
  59. config.on_event :test_step_started, &method(:on_test_step_started)
  60. config.on_event :test_step_finished, &method(:on_test_step_finished)
  61. config.on_event :test_case_finished, &method(:on_test_case_finished)
  62. config.on_event :test_run_finished, &method(:on_test_run_finished)
  63. config.on_event :undefined_parameter_type, &method(:collect_undefined_parameter_type_names)
  64. end
  65. def on_gherkin_source_read(event)
  66. @gherkin_sources[event.path] = event.body
  67. end
  68. def on_step_activated(event)
  69. test_step, step_match = *event.attributes
  70. @step_matches[test_step.to_s] = step_match
  71. end
  72. def on_test_case_started(event)
  73. if !same_feature_as_previous_test_case?(event.test_case.location)
  74. if first_feature?
  75. @first_feature = false
  76. print_profile_information
  77. else
  78. print_comments(gherkin_source.split("\n").length, 0)
  79. @io.puts
  80. end
  81. @current_feature_uri = event.test_case.location.file
  82. @exceptions = []
  83. print_feature_data
  84. if feature_has_background?
  85. print_background_data
  86. @print_background_steps = true
  87. @in_scenario_outline = false
  88. end
  89. else
  90. @print_background_steps = false
  91. end
  92. @current_test_case = event.test_case
  93. print_step_header(current_test_case) unless print_background_steps
  94. end
  95. def on_test_step_started(event)
  96. return if event.test_step.hook?
  97. print_step_header(current_test_case) if first_step_after_printing_background_steps?(event.test_step)
  98. end
  99. def on_test_step_finished(event)
  100. collect_snippet_data(event.test_step, @ast_lookup) if event.result.undefined?
  101. return if in_scenario_outline && !options[:expand]
  102. exception_to_be_printed = find_exception_to_be_printed(event.result)
  103. print_step_data(event.test_step, event.result) if print_step_data?(event, exception_to_be_printed)
  104. print_step_output
  105. return unless exception_to_be_printed
  106. print_exception(exception_to_be_printed, event.result.to_sym, 6)
  107. @exceptions << exception_to_be_printed
  108. end
  109. def on_test_case_finished(event)
  110. @total_duration += DurationExtractor.new(event.result).result_duration
  111. @passed_test_cases << event.test_case if config.wip? && event.result.passed?
  112. if in_scenario_outline && !options[:expand]
  113. print_row_data(event.test_case, event.result)
  114. else
  115. exception_to_be_printed = find_exception_to_be_printed(event.result)
  116. return unless exception_to_be_printed
  117. print_exception(exception_to_be_printed, event.result.to_sym, 6)
  118. @exceptions << exception_to_be_printed
  119. end
  120. end
  121. def on_test_run_finished(_event)
  122. print_comments(gherkin_source.split("\n").length, 0) unless current_feature_uri.empty?
  123. @io.puts
  124. print_summary
  125. end
  126. def attach(src, media_type)
  127. return unless media_type == 'text/x.cucumber.log+plain'
  128. @test_step_output.push src
  129. end
  130. private
  131. def find_exception_to_be_printed(result)
  132. return nil if result.ok?(options[:strict])
  133. result = result.with_filtered_backtrace(Cucumber::Formatter::BacktraceFilter)
  134. exception = result.failed? ? result.exception : result
  135. return nil if @exceptions.include?(exception)
  136. exception
  137. end
  138. def calculate_source_indent(test_case)
  139. scenario = scenario_source(test_case).scenario
  140. @source_indent = calculate_source_indent_for_ast_node(scenario)
  141. end
  142. def calculate_source_indent_for_ast_node(ast_node)
  143. indent = 4 + ast_node.keyword.length
  144. indent += 1 + ast_node.name.length
  145. ast_node.steps.each do |step|
  146. step_indent = 5 + step.keyword.length + step.text.length
  147. indent = step_indent if step_indent > indent
  148. end
  149. indent
  150. end
  151. def calculate_source_indent_for_expanded_test_case(test_case, scenario_keyword, expanded_name)
  152. indent = 7 + scenario_keyword.length
  153. indent += 2 + expanded_name.length
  154. test_case.test_steps.each do |step|
  155. if !step.hook? && step.location.lines.max >= test_case.location.lines.max
  156. step_indent = 9 + test_step_keyword(step).length + step.text.length
  157. indent = step_indent if step_indent > indent
  158. end
  159. end
  160. indent
  161. end
  162. def print_step_output
  163. @test_step_output.each { |message| @io.puts(indent(format_string(message, :tag), 6)) }
  164. @test_step_output = []
  165. end
  166. def first_feature?
  167. @first_feature
  168. end
  169. def same_feature_as_previous_test_case?(location)
  170. location.file == current_feature_uri
  171. end
  172. def feature_has_background?
  173. feature_children = gherkin_document.feature.children
  174. return false if feature_children.empty?
  175. !feature_children.first.background.nil?
  176. end
  177. def print_step_header(test_case)
  178. if from_scenario_outline?(test_case)
  179. @in_scenario_outline = true
  180. unless same_outline_as_previous_test_case?(test_case)
  181. @current_scenario_outline = scenario_source(test_case).scenario_outline
  182. @io.puts
  183. print_outline_data(current_scenario_outline)
  184. end
  185. unless same_examples_as_previous_test_case?(test_case)
  186. @current_examples = scenario_source(test_case).examples
  187. @io.puts
  188. print_examples_data(current_examples)
  189. end
  190. print_expanded_row_data(current_test_case) if options[:expand]
  191. else
  192. @in_scenario_outline = false
  193. @current_scenario_outline = nil
  194. @current_examples = nil
  195. @io.puts
  196. @source_indent = calculate_source_indent(current_test_case)
  197. print_scenario_data(test_case)
  198. end
  199. end
  200. def same_outline_as_previous_test_case?(test_case)
  201. scenario_source(test_case).scenario_outline == current_scenario_outline
  202. end
  203. def same_examples_as_previous_test_case?(test_case)
  204. scenario_source(test_case).examples == current_examples
  205. end
  206. def from_scenario_outline?(test_case)
  207. scenario = scenario_source(test_case)
  208. scenario.type != :Scenario
  209. end
  210. def first_step_after_printing_background_steps?(test_step)
  211. return false unless print_background_steps
  212. return false unless test_step.location.lines.max >= current_test_case.location.lines.max
  213. @print_background_steps = false
  214. true
  215. end
  216. def print_feature_data
  217. feature = gherkin_document.feature
  218. print_language_comment(feature.location.line)
  219. print_comments(feature.location.line, 0)
  220. print_tags(feature.tags, 0)
  221. print_feature_line(feature)
  222. print_description(feature.description)
  223. @io.flush
  224. end
  225. def print_language_comment(feature_line)
  226. gherkin_source.split("\n")[0..feature_line].each do |line|
  227. @io.puts(format_string(line, :comment)) if /# *language *:/ =~ line
  228. end
  229. end
  230. def print_comments(up_to_line, indent_amount)
  231. comments = gherkin_document.comments
  232. return if comments.empty? || comments.length <= @next_comment_to_be_printed
  233. comments[@next_comment_to_be_printed..].each do |comment|
  234. if comment.location.line <= up_to_line
  235. @io.puts(indent(format_string(comment.text.strip, :comment), indent_amount))
  236. @next_comment_to_be_printed += 1
  237. end
  238. break if @next_comment_to_be_printed >= comments.length
  239. end
  240. end
  241. def print_tags(tags, indent_amount)
  242. return if !tags || tags.empty?
  243. @io.puts(indent(tags.map { |tag| format_string(tag.name, :tag) }.join(' '), indent_amount))
  244. end
  245. def print_feature_line(feature)
  246. print_keyword_name(feature.keyword, feature.name, 0)
  247. end
  248. def print_keyword_name(keyword, name, indent_amount, location = nil)
  249. line = "#{keyword}:"
  250. line += " #{name}"
  251. @io.print(indent(line, indent_amount))
  252. if location && options[:source]
  253. line_comment = indent(format_string("# #{location}", :comment), @source_indent - line.length - indent_amount)
  254. @io.print(line_comment)
  255. end
  256. @io.puts
  257. end
  258. def print_description(description)
  259. return unless description
  260. description.split("\n").each do |line|
  261. @io.puts(line)
  262. end
  263. end
  264. def print_background_data
  265. @io.puts
  266. background = gherkin_document.feature.children.first.background
  267. @source_indent = calculate_source_indent_for_ast_node(background) if options[:source]
  268. print_comments(background.location.line, 2)
  269. print_background_line(background)
  270. print_description(background.description)
  271. @io.flush
  272. end
  273. def print_background_line(background)
  274. print_keyword_name(background.keyword, background.name, 2, "#{current_feature_uri}:#{background.location.line}")
  275. end
  276. def print_scenario_data(test_case)
  277. scenario = scenario_source(test_case).scenario
  278. print_comments(scenario.location.line, 2)
  279. print_tags(scenario.tags, 2)
  280. print_scenario_line(scenario, test_case.location)
  281. print_description(scenario.description)
  282. @io.flush
  283. end
  284. def print_scenario_line(scenario, location = nil)
  285. print_keyword_name(scenario.keyword, scenario.name, 2, location)
  286. end
  287. def print_step_data?(event, exception_to_be_printed)
  288. !event.test_step.hook? && (
  289. print_background_steps ||
  290. event.test_step.location.lines.max >= current_test_case.location.lines.max ||
  291. exception_to_be_printed
  292. )
  293. end
  294. def print_step_data(test_step, result)
  295. base_indent = options[:expand] && in_scenario_outline ? 8 : 4
  296. step_keyword = test_step_keyword(test_step)
  297. indent = options[:source] ? @source_indent - step_keyword.length - test_step.text.length - base_indent : nil
  298. print_comments(test_step.location.lines.max, base_indent)
  299. name_to_report = format_step(step_keyword, @step_matches.fetch(test_step.to_s) { NoStepMatch.new(test_step, test_step.text) }, result.to_sym, indent)
  300. @io.puts(indent(name_to_report, base_indent))
  301. print_multiline_argument(test_step, result, base_indent + 2) unless options[:no_multiline]
  302. @io.flush
  303. end
  304. def test_step_keyword(test_step)
  305. step = step_source(test_step).step
  306. step.keyword
  307. end
  308. def step_source(test_step)
  309. @ast_lookup.step_source(test_step)
  310. end
  311. def scenario_source(test_case)
  312. @ast_lookup.scenario_source(test_case)
  313. end
  314. def gherkin_source
  315. @gherkin_sources[current_feature_uri]
  316. end
  317. def gherkin_document
  318. @ast_lookup.gherkin_document(current_feature_uri)
  319. end
  320. def print_multiline_argument(test_step, result, indent)
  321. step = step_source(test_step).step
  322. if !step.doc_string.nil?
  323. print_doc_string(step.doc_string.content, result.to_sym, indent)
  324. elsif !step.data_table.nil?
  325. print_data_table(step.data_table, result.to_sym, indent)
  326. end
  327. end
  328. def print_data_table(data_table, status, indent_amount)
  329. data_table.rows.each do |row|
  330. print_comments(row.location.line, indent_amount)
  331. @io.puts indent(format_string(gherkin_source.split("\n")[row.location.line - 1].strip, status), indent_amount)
  332. end
  333. end
  334. def print_outline_data(scenario_outline) # rubocop:disable Metrics/AbcSize
  335. print_comments(scenario_outline.location.line, 2)
  336. print_tags(scenario_outline.tags, 2)
  337. @source_indent = calculate_source_indent_for_ast_node(scenario_outline) if options[:source]
  338. print_scenario_line(scenario_outline, "#{current_feature_uri}:#{scenario_outline.location.line}")
  339. print_description(scenario_outline.description)
  340. scenario_outline.steps.each do |step|
  341. print_comments(step.location.line, 4)
  342. step_line = " #{step.keyword}#{step.text}"
  343. @io.print(format_string(step_line, :skipped))
  344. if options[:source]
  345. comment_line = format_string("# #{current_feature_uri}:#{step.location.line}", :comment)
  346. @io.print(indent(comment_line, @source_indent - step_line.length))
  347. end
  348. @io.puts
  349. next if options[:no_multiline]
  350. print_doc_string(step.doc_string.content, :skipped, 6) unless step.doc_string.nil?
  351. print_data_table(step.data_table, :skipped, 6) unless step.data_table.nil?
  352. end
  353. @io.flush
  354. end
  355. def print_doc_string(content, status, indent_amount)
  356. s = indent(%("""\n#{content}\n"""), indent_amount)
  357. s = s.split("\n").map { |l| l =~ /^\s+$/ ? '' : l }.join("\n")
  358. @io.puts(format_string(s, status))
  359. end
  360. def print_examples_data(examples)
  361. print_comments(examples.location.line, 4)
  362. print_tags(examples.tags, 4)
  363. print_keyword_name(examples.keyword, examples.name, 4)
  364. print_description(examples.description)
  365. unless options[:expand]
  366. print_comments(examples.table_header.location.line, 6)
  367. @io.puts(indent(gherkin_source.split("\n")[examples.table_header.location.line - 1].strip, 6))
  368. end
  369. @io.flush
  370. end
  371. def print_row_data(test_case, result)
  372. print_comments(test_case.location.lines.max, 6)
  373. @io.print(indent(format_string(gherkin_source.split("\n")[test_case.location.lines.max - 1].strip, result.to_sym), 6))
  374. @io.print(indent(format_string(@test_step_output.join(', '), :tag), 2)) unless @test_step_output.empty?
  375. @test_step_output = []
  376. @io.puts
  377. if result.failed? || result.pending?
  378. result = result.with_filtered_backtrace(Cucumber::Formatter::BacktraceFilter)
  379. exception = result.failed? ? result.exception : result
  380. unless @exceptions.include?(exception)
  381. print_exception(exception, result.to_sym, 6)
  382. @exceptions << exception
  383. end
  384. end
  385. @io.flush
  386. end
  387. def print_expanded_row_data(test_case)
  388. feature = gherkin_document.feature
  389. language_code = feature.language || 'en'
  390. language = ::Gherkin::Dialect.for(language_code)
  391. scenario_keyword = language.scenario_keywords[0]
  392. row = scenario_source(test_case).row
  393. expanded_name = "| #{row.cells.map(&:value).join(' | ')} |"
  394. @source_indent = calculate_source_indent_for_expanded_test_case(test_case, scenario_keyword, expanded_name)
  395. @io.puts
  396. print_keyword_name(scenario_keyword, expanded_name, 6, test_case.location)
  397. end
  398. def print_summary
  399. print_statistics(@total_duration, config, @counts, @issues)
  400. print_snippets(options)
  401. print_passing_wip(config, @passed_test_cases, @ast_lookup)
  402. end
  403. end
  404. end
  405. end

No Description

Contributors (1)