diff --git a/features/docs/formatters/junit_formatter.feature b/features/docs/formatters/junit_formatter.feature index 2a440662ff..b4a7230e64 100644 --- a/features/docs/formatters/junit_formatter.feature +++ b/features/docs/formatters/junit_formatter.feature @@ -71,8 +71,12 @@ Feature: JUnit output formatter - - + + + + + + @@ -86,15 +90,13 @@ Feature: JUnit output formatter ./features/step_definitions/steps.rb:4:in `/^this step fails$/' features/one_passing_one_failing.feature:7:in `Given this step fails']]> - - - + """ @@ -110,8 +112,12 @@ Feature: JUnit output formatter - - + + + + + + @@ -125,15 +131,13 @@ Feature: JUnit output formatter ./features/step_definitions/steps.rb:4:in `/^this step fails$/' features/some_subdirectory/one_passing_one_failing.feature:7:in `Given this step fails']]> - - - + """ @@ -150,20 +154,22 @@ Feature: JUnit output formatter - - + + + + + + - - - + """ @@ -190,8 +196,12 @@ Feature: JUnit output formatter ./features/step_definitions/steps.rb:3:in `/^this step is pending$/' features/pending.feature:4:in `Given this step is pending']]> - - + + + + + + @@ -201,18 +211,16 @@ Feature: JUnit output formatter Message: ]]> - - - - + """ @@ -248,8 +256,12 @@ You *must* specify --out DIR for the junit formatter - - + + + + + + @@ -264,39 +276,52 @@ You *must* specify --out DIR for the junit formatter features/scenario_outline.feature:9:in `Given this step fails' features/scenario_outline.feature:4:in `Given this step ']]> - - + + + + + + + ']]> - - + + + + + + + ']]> - - - + """ @@ -312,8 +337,12 @@ You *must* specify --out DIR for the junit formatter - - + + + + + + @@ -328,8 +357,12 @@ You *must* specify --out DIR for the junit formatter features/scenario_outline.feature:9:in `Given this step fails' features/scenario_outline.feature:4:in `Given this step ']]> - - + + + + + + @@ -344,8 +377,12 @@ You *must* specify --out DIR for the junit formatter features/scenario_outline.feature:10:in `Given this step is pending' features/scenario_outline.feature:4:in `Given this step ']]> - - + + + + + + @@ -359,15 +396,13 @@ You *must* specify --out DIR for the junit formatter features/scenario_outline.feature:11:in `Given this step is undefined' features/scenario_outline.feature:4:in `Given this step ']]> - - - + """ diff --git a/lib/cucumber/formatter/junit.rb b/lib/cucumber/formatter/junit.rb index 50a0b13500..398d8d6e00 100644 --- a/lib/cucumber/formatter/junit.rb +++ b/lib/cucumber/formatter/junit.rb @@ -8,10 +8,6 @@ module Formatter # The formatter used for --format junit class Junit - # TODO: remove coupling to types - AST_SCENARIO_OUTLINE = Core::Ast::ScenarioOutline - AST_EXAMPLE_ROW = LegacyApi::Ast::ExampleTableRow - include Io class UnNamedFeatureError < StandardError @@ -20,160 +16,129 @@ def initialize(feature_file) end end - def initialize(runtime, io, options) + def initialize(_runtime, io, options) @reportdir = ensure_dir(io, "junit") @options = options end - def before_feature(feature) - @current_feature = feature - @failures = @errors = @tests = @skipped = 0 - @builder = Builder::XmlMarkup.new( :indent => 2 ) - @time = 0 + def before_test_case(test_case) + unless same_feature_as_previous_test_case?(test_case.feature) + end_feature if @current_feature + start_feature(test_case.feature) + end + @failing_step_source = nil # In order to fill out and , we need to # intercept the $stderr and $stdout @interceptedout = Interceptor::Pipe.wrap(:stdout) @interceptederr = Interceptor::Pipe.wrap(:stderr) end - def before_feature_element(feature_element) - @in_examples = AST_SCENARIO_OUTLINE === feature_element - @steps_start = Time.now - end + def after_test_step(test_step, result) + return if @failing_step_source - def after_feature(feature) - @testsuite = Builder::XmlMarkup.new( :indent => 2 ) - @testsuite.instruct! - @testsuite.testsuite( - :failures => @failures, - :errors => @errors, - :skipped => @skipped, - :tests => @tests, - :time => "%.6f" % @time, - :name => @feature_name ) do - @testsuite << @builder.target! - @testsuite.tag!('system-out') do - @testsuite.cdata! strip_control_chars(@interceptedout.buffer.join) - end - @testsuite.tag!('system-err') do - @testsuite.cdata! strip_control_chars(@interceptederr.buffer.join) - end - end + @failing_step_source = test_step.source.last unless result.ok?(@options[:strict]) + end - write_file(feature_result_filename(feature.file), @testsuite.target!) + def after_test_case(test_case, result) + test_case_name = NameBuilder.new(test_case) + scenario = test_case_name.scenario_name + scenario_designation = "#{scenario}#{test_case_name.name_suffix}" + output = create_output_string(test_case, scenario, result, test_case_name.row_name) + build_testcase(result, scenario_designation, output) Interceptor::Pipe.unwrap! :stdout Interceptor::Pipe.unwrap! :stderr end - def before_background(*args) - @in_background = true - end - - def after_background(*args) - @in_background = false + def done + end_feature if @current_feature end - def feature_name(keyword, name) - raise UnNamedFeatureError.new(@current_feature.file) if name.empty? - lines = name.split(/\r?\n/) - @feature_name = lines[0] - end + private - def scenario_name(keyword, name, file_colon_line, source_indent) - @scenario = (name.nil? || name == "") ? "Unnamed scenario" : name.split("\n")[0] - @output = "#{keyword}: #{@scenario}\n\n" + def same_feature_as_previous_test_case?(feature) + @current_feature && @current_feature.file == feature.file && @current_feature.location == feature.location end - def before_steps(steps) + def start_feature(feature) + raise UnNamedFeatureError.new(feature.file) if feature.name.empty? + @current_feature = feature + @failures = @errors = @tests = @skipped = 0 + @builder = Builder::XmlMarkup.new(:indent => 2) + @time = 0 end - def after_steps(steps) - return if @in_background || @in_examples - - duration = Time.now - @steps_start - if steps.failed? - steps.each { |step| @output += "#{step.keyword}#{step.name}\n" } - @output += "\nMessage:\n" + def end_feature + @testsuite = Builder::XmlMarkup.new(:indent => 2) + @testsuite.instruct! + @testsuite.testsuite( + :failures => @failures, + :errors => @errors, + :skipped => @skipped, + :tests => @tests, + :time => "%.6f" % @time, + :name => @current_feature.name ) do + @testsuite << @builder.target! end - build_testcase(duration, steps.status, steps.exception) - end - - def before_examples(*args) - @header_row = true - @in_examples = true - end - def after_examples(*args) - @in_examples = false + write_file(feature_result_filename(@current_feature.file), @testsuite.target!) end - def before_table_row(table_row) - return unless @in_examples - - @table_start = Time.now - end - - def after_table_row(table_row) - return unless @in_examples and AST_EXAMPLE_ROW === table_row - duration = Time.now - @table_start - unless @header_row - name_suffix = " (outline example : #{table_row.name})" - if table_row.failed? - @output += "Example row: #{table_row.name}\n" - @output += "\nMessage:\n" - end - build_testcase(duration, table_row.status, table_row.exception, name_suffix) + def create_output_string(test_case, scenario, result, row_name) + output = "#{test_case.keyword}: #{scenario}\n\n" + return output if result.ok?(@options[:strict]) + if test_case.keyword == "Scenario" + output += "#{@failing_step_source.keyword}" unless hook?(@failing_step_source) + output += "#{@failing_step_source.name}\n" + else + output += "Example row: #{row_name}\n" end - - @header_row = false if @header_row + output + "\nMessage:\n" end - def after_test_case(test_case, result) - if @options[:expand] and test_case.keyword == "Scenario Outline" - test_case_name = NameBuilder.new(test_case) - @scenario = test_case_name.outline_name - @output = "#{test_case.keyword}: #{@scenario}\n\n" - @exception = nil - if result.failed? or (@options[:strict] and (result.pending? or result.undefined?)) - if result.failed? - @exception = result.exception - elsif result.backtrace - @exception = result - end - @output += "Example row: #{test_case_name.row_name}\n" - @output += "\nMessage:\n" - end - test_case_result = ResultBuilder.new(result) - build_testcase(test_case_result.test_case_duration, test_case_result.status, @exception, test_case_name.name_suffix) - end + def hook?(step) + ["Before hook", "After hook", "AfterStep hook"].include? step.name end - private - - def build_testcase(duration, status, exception = nil, suffix = "") + def build_testcase(result, scenario_designation, output) + duration = ResultBuilder.new(result).test_case_duration @time += duration - classname = @feature_name - name = "#{@scenario}#{suffix}" - pending = [:pending, :undefined].include?(status) && (!@options[:strict]) + classname = @current_feature.name + name = scenario_designation @builder.testcase(:classname => classname, :name => name, :time => "%.6f" % duration) do - if status == :skipped || pending + if !result.passed? && result.ok?(@options[:strict]) @builder.skipped @skipped += 1 - elsif status != :passed - @builder.failure(:message => "#{status.to_s} #{name}", :type => status.to_s) do - @builder.cdata! @output + elsif !result.passed? + status = result.to_sym + exception = get_backtrace_object(result) + @builder.failure(:message => "#{status} #{name}", :type => status) do + @builder.cdata! output @builder.cdata!(format_exception(exception)) if exception end @failures += 1 end - @builder.tag!('system-out') - @builder.tag!('system-err') + @builder.tag!('system-out') do + @builder.cdata! strip_control_chars(@interceptedout.buffer.join) + end + @builder.tag!('system-err') do + @builder.cdata! strip_control_chars(@interceptederr.buffer.join) + end end @tests += 1 end + def get_backtrace_object(result) + if result.failed? + return result.exception + elsif result.backtrace + return result + else + return nil + end + end + def format_exception(exception) (["#{exception.message} (#{exception.class})"] + exception.backtrace).join("\n") end @@ -194,13 +159,15 @@ def write_file(feature_filename, data) def strip_control_chars(cdata) cdata.scan(/[[:print:]\t\n\r]/).join end - + end class NameBuilder - attr_reader :outline_name, :name_suffix, :row_name + attr_reader :scenario_name, :name_suffix, :row_name def initialize(test_case) + @name_suffix = "" + @row_name = "" test_case.describe_source_to self end @@ -208,12 +175,13 @@ def feature(*) self end - def scenario(*) + def scenario(scenario) + @scenario_name = (scenario.name.nil? || scenario.name == "") ? "Unnamed scenario" : scenario.name self end def scenario_outline(outline) - @outline_name = outline.name + @scenario_name = (outline.name.nil? || outline.name == "") ? "Unnamed scenario outline" : outline.name self end @@ -229,37 +197,26 @@ def examples_table_row(row) end class ResultBuilder - attr_reader :status, :test_case_duration + attr_reader :test_case_duration def initialize(result) @test_case_duration = 0 result.describe_to(self) end - def passed - @status = :passed - end + def passed(*) end - def failed - @status = :failed - end + def failed(*) end - def undefined - @status = :undefined - end + def undefined(*) end - def skipped - @status = :skipped - end + def skipped(*) end - def pending(*) - @status = :pending - end + def pending(*) end - def exception(*) - end + def exception(*) end def duration(duration, *) - duration.tap { |duration| @test_case_duration = duration.nanoseconds / 10 ** 9.0 } + duration.tap { |duration| @test_case_duration = duration.nanoseconds / 10**9.0 } end end diff --git a/spec/cucumber/formatter/junit_spec.rb b/spec/cucumber/formatter/junit_spec.rb index 8d3f46e6b8..5a44562c9d 100644 --- a/spec/cucumber/formatter/junit_spec.rb +++ b/spec/cucumber/formatter/junit_spec.rb @@ -59,7 +59,7 @@ def after_step(step) end end - it { expect(@doc.xpath('//testsuite/system-out').first.content).to match(/\s+boo boo\s+/) } + it { expect(@doc.xpath('//testsuite/testcase/system-out').first.content).to match(/\s+boo boo\s+/) } end describe "a feature with no name" do @@ -92,12 +92,12 @@ def after_step(step) it { expect(@doc.to_s).to match /One passing scenario, one failing scenario/ } - it 'has a root system-out node' do - expect(@doc.xpath('//testsuite/system-out').size).to eq 1 + it 'has not a root system-out node' do + expect(@doc.xpath('//testsuite/system-out').size).to eq 0 end - it 'has a root system-err node' do - expect(@doc.xpath('//testsuite/system-err').size).to eq 1 + it 'has not a root system-err node' do + expect(@doc.xpath('//testsuite/system-err').size).to eq 0 end it 'has a system-out node under ' do