# frozen_string_literal: true require 'forwardable' require 'delegate' require 'cucumber/errors' require 'cucumber/multiline_argument' require 'cucumber/formatter/backtrace_filter' require 'cucumber/formatter/legacy_api/ast' module Cucumber module Formatter module LegacyApi Adapter = Struct.new(:formatter, :results, :config) do extend Forwardable def initialize(*) super emit_deprecation_warning @matches = collect_matches config.on_event(:test_case_started) do |event| formatter.before_test_case(event.test_case) printer.before_test_case(event.test_case) end config.on_event(:test_step_started) do |event| formatter.before_test_step(event.test_step) printer.before_test_step(event.test_step) end config.on_event(:test_step_finished) do |event| test_step, result = *event.attributes printer.after_test_step(test_step, result) formatter.after_test_step(test_step, result.with_filtered_backtrace(Cucumber::Formatter::BacktraceFilter)) end config.on_event(:test_case_finished) do |event| test_case, result = *event.attributes record_test_case_result(test_case, result) printer.after_test_case(test_case, result) formatter.after_test_case(test_case, result.with_filtered_backtrace(Cucumber::Formatter::BacktraceFilter)) end config.on_event(:test_run_finished) do printer.after formatter.done end end def_delegators :formatter, :ask def_delegators :printer, :embed def puts(*messages) printer.puts(messages) end private def emit_deprecation_warning parent_name = formatter_class_name =~ /::[^:]+\Z/ ? $`.freeze : nil return if parent_name == 'Cucumber::Formatter' return if !config.out_stream # some unit tests don't set it config.out_stream.puts "WARNING: The formatter #{formatter.class.name} is using the deprecated formatter API which will be removed in v4.0 of Cucumber." config.out_stream.puts end def formatter_class_name formatter.class.name rescue NoMethodError # when we use the Fanout, things get gnarly formatter.class[0].class.name end def printer @printer ||= FeaturesPrinter.new(formatter, results, config, @matches).before end def record_test_case_result(test_case, result) scenario = LegacyResultBuilder.new(result).scenario("#{test_case.keyword}: #{test_case.name}", test_case.location) results.scenario_visited(scenario) end def collect_matches result = {} config.on_event(:step_activated) do |event| test_step, step_match = *event.attributes result[test_step.source.last] = step_match end result end require 'cucumber/core/test/timer' FeaturesPrinter = Struct.new(:formatter, :results, :config, :matches) do extend Forwardable def before timer.start formatter.before_features(nil) self end def before_test_case(test_case) test_case.describe_source_to(self) @child.before_test_case(test_case) end def before_test_step(*args) @child.before_test_step(*args) end def after_test_step(test_step, result) @child.after_test_step(test_step, result) end def after_test_case(*args) @child.after_test_case(*args) end def feature(node, *) if node != @current_feature @child.after if @child @child = FeaturePrinter.new(formatter, results, matches, config, node).before @current_feature = node end end def scenario(node, *) end def scenario_outline(node, *) end def examples_table(node, *) end def examples_table_row(node, *) end def after @child.after if @child formatter.after_features Ast::Features.new(timer.sec) self end def puts(messages) @child.puts(messages) end def embed(src, mime_type, label) @child.embed(src, mime_type, label) end private def timer @timer ||= Cucumber::Core::Test::Timer.new end end module TestCaseSource def self.for(test_case, result) collector = Collector.new test_case.describe_source_to collector, result collector.result.freeze end class Collector attr_reader :result def initialize @result = CaseSource.new end def method_missing(name, node, _test_case_result, *_args) result.send "#{name}=", node end end require 'ostruct' class CaseSource < OpenStruct end end module TestStepSource def self.for(test_step, result) collector = Collector.new test_step.describe_source_to collector, result collector.result.freeze end class Collector attr_reader :result def initialize @result = StepSource.new end def method_missing(name, node, step_result, *_args) result.send "#{name}=", node result.send "#{name}_result=", LegacyResultBuilder.new(step_result) end end require 'ostruct' class StepSource < OpenStruct def build_step_invocation(indent, matches, config, messages, embeddings) step_result.step_invocation( matches.fetch(step) { NoStepMatch.new(step, step.text) }, step, indent, background, config, messages, embeddings ) end end end Embedding = Struct.new(:src, :mime_type, :label) do def send_to_formatter(formatter) formatter.embed(src, mime_type, label) end end FeaturePrinter = Struct.new(:formatter, :results, :matches, :config, :node) do def before formatter.before_feature(node) language_comment = node.language.iso_code != 'en' ? ["# language: #{node.language.iso_code}"] : [] Ast::Comments.new(language_comment + node.comments).accept(formatter) Ast::Tags.new(node.tags).accept(formatter) formatter.feature_name node.keyword, node.legacy_conflated_name_and_description @delayed_messages = [] @delayed_embeddings = [] self end attr_reader :current_test_step_source def before_test_case(_test_case) @before_hook_results = Ast::HookResultCollection.new @test_step_results = [] end def before_test_step(test_step) end def after_test_step(test_step, result) @current_test_step_source = TestStepSource.for(test_step, result) # TODO: stop calling self, and describe source to another object test_step.describe_source_to(self, result) print_step @test_step_results << result end def after_test_case(test_case, test_case_result) if current_test_step_source && current_test_step_source.step_result.nil? switch_step_container end if test_case_result.failed? && !any_test_steps_failed? # around hook must have failed. Print the error. switch_step_container(TestCaseSource.for(test_case, test_case_result)) LegacyResultBuilder.new(test_case_result).describe_exception_to formatter end # messages and embedding should already have been handled, but just in case... @delayed_messages.each { |message| formatter.puts(message) } @delayed_embeddings.each { |embedding| embedding.send_to_formatter(formatter) } @delayed_messages = [] @delayed_embeddings = [] @child.after_test_case(test_case, test_case_result) if @child @previous_test_case_background = @current_test_case_background @previous_test_case_scenario_outline = current_test_step_source && current_test_step_source.scenario_outline end def before_hook(_location, result) @before_hook_results << Ast::HookResult.new(LegacyResultBuilder.new(result), @delayed_messages, @delayed_embeddings) @delayed_messages = [] @delayed_embeddings = [] end def after_hook(_location, result) # if the scenario has no steps, we can hit this before we've created the scenario printer # ideally we should call switch_step_container in before_step_step switch_step_container if !@child @child.after_hook Ast::HookResult.new(LegacyResultBuilder.new(result), @delayed_messages, @delayed_embeddings) @delayed_messages = [] @delayed_embeddings = [] end def after_step_hook(_hook, result) p current_test_step_source if current_test_step_source.step.nil? line = current_test_step_source.step.backtrace_line @child.after_step_hook Ast::HookResult.new(LegacyResultBuilder.new(result). append_to_exception_backtrace(line), @delayed_messages, @delayed_embeddings) @delayed_messages = [] @delayed_embeddings = [] end def background(node, *) @current_test_case_background = node end def puts(messages) @delayed_messages.push *messages end def embed(src, mime_type, label) @delayed_embeddings.push Embedding.new(src, mime_type, label) end def step(*);end def scenario(*);end def scenario_outline(*);end def examples_table(*);end def examples_table_row(*);end def feature(*);end def after @child.after if @child formatter.after_feature(node) self end private attr_reader :before_hook_results private :before_hook_results def any_test_steps_failed? @test_step_results.any? &:failed? end def switch_step_container(source = current_test_step_source) switch_to_child select_step_container(source), source end def select_step_container(source) if source.background if same_background_as_previous_test_case?(source) HiddenBackgroundPrinter.new(formatter, source.background) else BackgroundPrinter.new(formatter, node, source.background, before_hook_results) end elsif source.scenario ScenarioPrinter.new(formatter, source.scenario, before_hook_results) elsif source.scenario_outline if same_scenario_outline_as_previous_test_case?(source) && @previous_outline_child @previous_outline_child else ScenarioOutlinePrinter.new(formatter, config, source.scenario_outline) end else raise 'unknown step container' end end def same_background_as_previous_test_case?(source) source.background == @previous_test_case_background end def same_scenario_outline_as_previous_test_case?(source) source.scenario_outline == @previous_test_case_scenario_outline end def print_step return unless current_test_step_source.step_result switch_step_container if current_test_step_source.scenario_outline @child.examples_table(current_test_step_source.examples_table) @child.examples_table_row(current_test_step_source.examples_table_row, before_hook_results) end if @failed_hidden_background_step indent = Indent.new(@child.node) step_invocation = @failed_hidden_background_step.build_step_invocation(indent, matches, config, [], []) @child.step_invocation(step_invocation, @failed_hidden_background_step) @failed_hidden_background_step = nil end unless @last_step == current_test_step_source.step indent ||= Indent.new(@child.node) step_invocation = current_test_step_source.build_step_invocation(indent, matches, config, @delayed_messages, @delayed_embeddings) results.step_visited step_invocation @child.step_invocation(step_invocation, current_test_step_source) @last_step = current_test_step_source.step end @delayed_messages = [] @delayed_embeddings = [] end def switch_to_child(child, source) return if @child == child if @child if from_first_background(@child) @first_background_failed = @child.failed? elsif from_hidden_background(@child) if not @first_background_failed @failed_hidden_background_step = @child.get_failed_step_source end if @previous_outline_child @previous_outline_child.after unless same_scenario_outline_as_previous_test_case?(source) end end if from_scenario_outline_to_hidden_background(@child, child) @previous_outline_child = @child else @child.after @previous_outline_child = nil end end child.before unless to_scenario_outline(child) && same_scenario_outline_as_previous_test_case?(source) @child = child end def from_scenario_outline_to_hidden_background(from, to) from.class.name == ScenarioOutlinePrinter.name && to.class.name == HiddenBackgroundPrinter.name end def from_first_background(from) from.class.name == BackgroundPrinter.name end def from_hidden_background(from) from.class.name == HiddenBackgroundPrinter.name end def to_scenario_outline(to) to.class.name == ScenarioOutlinePrinter.name end end module PrintsAfterHooks def after_hook_results @after_hook_results ||= Ast::HookResultCollection.new end def after_hook(result) after_hook_results << result end end # Basic printer used by default class AfterHookPrinter attr_reader :formatter def initialize(formatter) @formatter = formatter end include PrintsAfterHooks def after after_hook_results.accept(formatter) end end BackgroundPrinter = Struct.new(:formatter, :feature, :node, :before_hook_results) do def after_test_case(*) end def after_hook(*) end def before formatter.before_background Ast::Background.new(feature, node) Ast::Comments.new(node.comments).accept(formatter) formatter.background_name node.keyword, node.legacy_conflated_name_and_description, node.location.to_s, indent.of(node) before_hook_results.accept(formatter) self end def after_step_hook(result) result.accept formatter end def step_invocation(step_invocation, source) @child ||= StepsPrinter.new(formatter).before @child.step_invocation step_invocation if source.step_result.status == :failed @failed = true end end def after @child.after if @child formatter.after_background(Ast::Background.new(feature, node)) self end def failed? @failed end private def indent @indent ||= Indent.new(node) end end # Printer to handle background steps for anything but the first scenario in a # feature. These steps should not be printed. class HiddenBackgroundPrinter < Struct.new(:formatter, :node) def get_failed_step_source return @source_of_failed_step end def step_invocation(_step_invocation, source) if source.step_result.status == :failed @source_of_failed_step = source end end def before;self;end def after;self;end def before_hook(*);end def after_hook(*);end def after_step_hook(*);end def examples_table(*);end def after_test_case(*);end end ScenarioPrinter = Struct.new(:formatter, :node, :before_hook_results) do include PrintsAfterHooks def before formatter.before_feature_element(node) Ast::Comments.new(node.comments).accept(formatter) Ast::Tags.new(node.tags).accept(formatter) formatter.scenario_name node.keyword, node.legacy_conflated_name_and_description, node.location.to_s, indent.of(node) before_hook_results.accept(formatter) self end def step_invocation(step_invocation, _source) @child ||= StepsPrinter.new(formatter).before @child.step_invocation step_invocation end def after_step_hook(result) result.accept formatter end def after_test_case(_test_case, result) @test_case_result = result after end def after return if @done @child.after if @child scenario = LegacyResultBuilder.new(@test_case_result).scenario(node.name, node.location) after_hook_results.accept(formatter) formatter.after_feature_element(scenario) @done = true self end private def indent @indent ||= Indent.new(node) end end StepsPrinter = Struct.new(:formatter) do def before formatter.before_steps(nil) self end def step_invocation(step_invocation) steps << step_invocation step_invocation.accept(formatter) self end def after formatter.after_steps(steps) self end private def steps @steps ||= Ast::StepInvocations.new end end ScenarioOutlinePrinter = Struct.new(:formatter, :configuration, :node) do extend Forwardable def_delegators :@child, :after_hook, :after_step_hook def before formatter.before_feature_element(node) Ast::Comments.new(node.comments).accept(formatter) Ast::Tags.new(node.tags).accept(formatter) formatter.scenario_name node.keyword, node.legacy_conflated_name_and_description, node.location.to_s, indent.of(node) OutlineStepsPrinter.new(formatter, configuration, indent).print(node) self end def step_invocation(step_invocation, source) _node, result = source.step, source.step_result @last_step_result = result @child.step_invocation(step_invocation, source) end def examples_table(examples_table) @child ||= ExamplesArrayPrinter.new(formatter, configuration).before @child.examples_table(examples_table) end def examples_table_row(node, before_hook_results) @child.examples_table_row(node, before_hook_results) end def after_test_case(test_case, result) @child.after_test_case(test_case, result) end def after @child.after if @child # TODO: the last step result might not accurately reflect the # overall scenario result. scenario_outline = last_step_result.scenario_outline(node.name, node.location) formatter.after_feature_element(scenario_outline) self end private def last_step_result @last_step_result || Core::Test::Result::Unknown.new end def indent @indent ||= Indent.new(node) end end OutlineStepsPrinter = Struct.new(:formatter, :configuration, :indent, :outline) do def print(node) node.describe_to self steps_printer.after end def scenario_outline(_node, &descend) descend.call(self) end def outline_step(step) step_match = NoStepMatch.new(step, step.text) step_invocation = LegacyResultBuilder.new(Core::Test::Result::Skipped.new). step_invocation(step_match, step, indent, nil, configuration, [], []) steps_printer.step_invocation step_invocation end def examples_table(*);end private def steps_printer @steps_printer ||= StepsPrinter.new(formatter).before end end ExamplesArrayPrinter = Struct.new(:formatter, :configuration) do extend Forwardable def_delegators :@child, :step_invocation, :after_hook, :after_step_hook, :after_test_case, :examples_table_row def before formatter.before_examples_array(:examples_array) self end def examples_table(examples_table) return if examples_table == @current @child.after if @child @child = ExamplesTablePrinter.new(formatter, configuration, examples_table).before @current = examples_table end def after @child.after if @child formatter.after_examples_array self end end ExamplesTablePrinter = Struct.new(:formatter, :configuration, :node) do extend Forwardable def_delegators :@child, :step_invocation, :after_hook, :after_step_hook, :after_test_case def before formatter.before_examples(node) Ast::Comments.new(node.comments).accept(formatter) Ast::Tags.new(node.tags).accept(formatter) formatter.examples_name(node.keyword, node.legacy_conflated_name_and_description) formatter.before_outline_table(legacy_table) if !configuration.expand? HeaderTableRowPrinter.new(formatter, ExampleTableRow.new(node.header), Ast::Node.new).before.after end self end def examples_table_row(examples_table_row, before_hook_results) return if examples_table_row == @current @child.after if @child row = ExampleTableRow.new(examples_table_row) @child = if !configuration.expand? TableRowPrinter.new(formatter, row, before_hook_results).before else ExpandTableRowPrinter.new(formatter, row, before_hook_results).before end @current = examples_table_row end def after_test_case(*args) @child.after_test_case(*args) end def after @child.after if @child formatter.after_outline_table(node) formatter.after_examples(node) self end private def legacy_table LegacyTable.new(node) end class ExampleTableRow < SimpleDelegator def dom_id file_colon_line.gsub(/[\/\.:]/, '_') end end LegacyTable = Struct.new(:node) do def col_width(index) max_width = FindMaxWidth.new(index) node.describe_to max_width max_width.result end require 'cucumber/gherkin/formatter/escaping' FindMaxWidth = Struct.new(:index) do include ::Cucumber::Gherkin::Formatter::Escaping def examples_table(table, &descend) @result = char_length_of(table.header.values[index]) descend.call(self) end def examples_table_row(row, &_descend) width = char_length_of(row.values[index]) @result = width if width > result end def result @result ||= 0 end private def char_length_of(cell) escape_cell(cell).unpack('U*').length end end end end class TableRowPrinterBase < Struct.new(:formatter, :node, :before_hook_results) include PrintsAfterHooks def after_step_hook(result) @after_step_hook_result ||= Ast::HookResultCollection.new @after_step_hook_result << result end def after_test_case(*_args) after end private def indent :not_needed end def legacy_table_row Ast::ExampleTableRow.new(exception, @status, node.values, node.location, node.language) end def exception return nil unless @failed_step @failed_step.exception end end class HeaderTableRowPrinter < TableRowPrinterBase def legacy_table_row Ast::ExampleTableRow.new(exception, @status, node.values, node.location, Ast::NullLanguage.new) end def before Ast::Comments.new(node.comments).accept(formatter) formatter.before_table_row(node) self end def after node.values.each do |value| formatter.before_table_cell(value) formatter.table_cell_value(value, :skipped_param) formatter.after_table_cell(value) end formatter.after_table_row(legacy_table_row) self end end class TableRowPrinter < TableRowPrinterBase def before before_hook_results.accept(formatter) Ast::Comments.new(node.comments).accept(formatter) formatter.before_table_row(node) self end def step_invocation(step_invocation, source) result = source.step_result step_invocation.messages.each { |message| formatter.puts(message) } step_invocation.embeddings.each { |embedding| embedding.send_to_formatter(formatter) } @failed_step = step_invocation if result.status == :failed @status = step_invocation.status unless already_failed? end def after return if @done @child.after if @child node.values.each do |value| formatter.before_table_cell(value) formatter.table_cell_value(value, @status || :skipped) formatter.after_table_cell(value) end @after_step_hook_result.send_output_to(formatter) if @after_step_hook_result after_hook_results.send_output_to(formatter) formatter.after_table_row(legacy_table_row) @after_step_hook_result.describe_exception_to(formatter) if @after_step_hook_result after_hook_results.describe_exception_to(formatter) @done = true self end private def already_failed? @status == :failed || @status == :undefined || @status == :pending end end class ExpandTableRowPrinter < TableRowPrinterBase def before before_hook_results.accept(formatter) self end def step_invocation(step_invocation, source) result = source.step_result @table_row ||= legacy_table_row step_invocation.indent.record_width_of(@table_row) if !@scenario_name_printed print_scenario_name(step_invocation, @table_row) @scenario_name_printed = true end step_invocation.accept(formatter) @failed_step = step_invocation if result.status == :failed @status = step_invocation.status unless @status == :failed end def after return if @done @child.after if @child @after_step_hook_result.accept(formatter) if @after_step_hook_result after_hook_results.accept(formatter) @done = true self end private def print_scenario_name(step_invocation, table_row) formatter.scenario_name table_row.keyword, table_row.name, node.location.to_s, step_invocation.indent.of(table_row) end end class Indent def initialize(node) @widths = [] node.describe_to(self) end [:background, :scenario, :scenario_outline].each do |node_name| define_method(node_name) do |node, &descend| record_width_of node descend.call(self) end end [:step, :outline_step].each do |node_name| define_method(node_name) do |node| record_width_of node end end def examples_table(*); end def examples_table_row(*); end def of(node) # The length of the instantiated steps in --expand mode are currently # not included in the calculation of max => make sure to return >= 1 [1, max - node.to_s.length - node.keyword.length].max end def record_width_of(node) @widths << node.keyword.length + node.to_s.length + 1 end private def max @widths.max end end class LegacyResultBuilder attr_reader :status def initialize(result) @result = result @result.describe_to(self) end def passed @status = :passed end def failed @status = :failed end def undefined @status = :undefined end def skipped @status = :skipped end def pending(exception, *) @exception = exception @status = :pending end def exception(exception, *) @exception = exception end def append_to_exception_backtrace(line) @exception.set_backtrace(@exception.backtrace + [line.to_s]) if @exception return self end def duration(duration, *) @duration = duration end def step_invocation(step_match, step, indent, background, configuration, messages, embeddings) Ast::StepInvocation.new(step_match, @status, @duration, step_exception(step, configuration), indent, background, step, messages, embeddings) end def scenario(name, location) Ast::Scenario.new(@status, name, location) end def scenario_outline(name, location) Ast::ScenarioOutline.new(@status, name, location) end def describe_exception_to(formatter) formatter.exception(filtered_exception, @status) if @exception end private def step_exception(step, configuration) return filtered_step_exception(step) if @exception return nil unless @status == :undefined && configuration.strict.strict?(:undefined) @exception = Cucumber::Undefined.from(@result, step.text) @exception.backtrace << step.backtrace_line filtered_step_exception(step) end def filtered_exception Cucumber::Formatter::BacktraceFilter.new(@exception.dup).exception end def filtered_step_exception(_step) exception = filtered_exception return Cucumber::Formatter::BacktraceFilter.new(exception).exception end end end end end end