Skip to content

Commit

Permalink
Summary formatter (#999)
Browse files Browse the repository at this point in the history
Add a summary formatter
  • Loading branch information
mattwynne committed Aug 5, 2016
1 parent e25b9cc commit 4154c8c
Show file tree
Hide file tree
Showing 10 changed files with 205 additions and 95 deletions.
34 changes: 34 additions & 0 deletions features/docs/formatters/summary_formatter.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
Feature: Spec formatter

This formatter mimics the output from tools like RSpec or Mocha, giving an
overview of each feature and scenario but omitting the steps.

Background:
Given the standard step definitions

Scenario: A couple of scenarios
Given a file named "features/test.feature" with:
"""
Feature: Test
Scenario: Passing
Given this step passes
Scenario: Failing
Given this step fails
"""
When I run `cucumber --format summary`
Then it should fail with exactly:
"""
Test
Passing ✓
Failing ✗
Failing Scenarios:
cucumber features/test.feature:5 # Scenario: Failing
2 scenarios (1 failed, 1 passed)
2 steps (1 failed, 1 passed)
0m0.012s
"""

3 changes: 2 additions & 1 deletion lib/cucumber/cli/options.rb
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,8 @@ class Options
'junit' => ['Cucumber::Formatter::Junit', 'Generates a report similar to Ant+JUnit.'],
'json' => ['Cucumber::Formatter::Json', 'Prints the feature as JSON'],
'json_pretty' => ['Cucumber::Formatter::JsonPretty', 'Prints the feature as prettified JSON'],
'debug' => ['Cucumber::Formatter::Debug', 'For developing formatters - prints the calls made to the listeners.']
'debug' => ['Cucumber::Formatter::Debug', 'For developing formatters - prints the calls made to the listeners.'],
'summary' => ['Cucumber::Formatter::Summary', 'Summary output of feature and scenarios']
}
max = BUILTIN_FORMATS.keys.map{|s| s.length}.max
FORMAT_HELP_MSG = ["Use --format rerun --out rerun.txt to write out failing",
Expand Down
46 changes: 8 additions & 38 deletions lib/cucumber/formatter/console.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
require 'cucumber/formatter/ansicolor'
require 'cucumber/formatter/duration'
require 'cucumber/formatter/summary'
require 'cucumber/gherkin/i18n'

module Cucumber
Expand Down Expand Up @@ -29,7 +28,6 @@ module Formatter
module Console
extend ANSIColor
include Duration
include Summary

def format_step(keyword, step_match, status, source_indent)
comment = if source_indent
Expand Down Expand Up @@ -80,51 +78,23 @@ def print_element_messages(element_messages, status, kind)
end
end

def print_stats(features, options)
duration = features ? features.duration : nil
print_statistics(duration, options)
end

def print_statistics(duration, options)
failures = collect_failing_scenarios(runtime)
if !failures.empty?
print_failing_scenarios(failures, options.custom_profiles, options[:source])
def print_statistics(duration, config, counts, issues)
if issues.any?
@io.puts issues.to_s
@io.puts
end

@io.puts scenario_summary(runtime) {|status_count, status| format_string(status_count, status)}
@io.puts step_summary(runtime) {|status_count, status| format_string(status_count, status)}
@io.puts(format_duration(duration)) if duration && options[:duration]
@io.puts counts.to_s
@io.puts(format_duration(duration)) if duration && config.duration?

if runtime.configuration.randomize?
if config.randomize?
@io.puts
@io.puts "Randomized with seed #{runtime.configuration.seed}"
@io.puts "Randomized with seed #{config.seed}"
end

@io.flush
end

def collect_failing_scenarios(runtime)
# TODO: brittle - stop coupling to types
scenario_class = LegacyApi::Ast::Scenario
example_table_class = Core::Ast::ExamplesTable

runtime.scenarios(:failed).select do |s|
[scenario_class, example_table_class].include?(s.class)
end.map do |s|
s.is_a?(example_table_class)? s.scenario_outline : s
end
end

def print_failing_scenarios(failures, custom_profiles, given_source)
@io.puts format_string("Failing Scenarios:", :failed)
failures.each do |failure|
profiles_string = custom_profiles.empty? ? '' : (custom_profiles.map{|profile| "-p #{profile}" }).join(' ') + ' '
source = given_source ? format_string(" # " + failure.name, :comment) : ''
@io.puts format_string("cucumber #{profiles_string}" + failure.location, :failed) + source
end
@io.puts
end

def print_exception(e, status, indent)
string = exception_message_string(e, indent)
@io.puts(format_string(string, status))
Expand Down
57 changes: 57 additions & 0 deletions lib/cucumber/formatter/console_counts.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
require 'cucumber/formatter/console'

module Cucumber
module Formatter
class ConsoleCounts
include Console

def initialize(config)
@test_case_summary = Core::Test::Result::Summary.new
@test_step_summary = Core::Test::Result::Summary.new

config.on_event :test_case_finished do |event|
event.result.describe_to @test_case_summary
end

config.on_event :test_step_finished do |event|
event.result.describe_to @test_step_summary if from_gherkin?(event.test_step)
end
end

def to_s
[
[scenario_count, status_counts(@test_case_summary)].compact.join(" "),
[step_count, status_counts(@test_step_summary)].compact.join(" ")
].join("\n")
end

private

def from_gherkin?(test_step)
test_step.source.last.location.file.match(/\.feature$/)
end

def scenario_count
count = @test_case_summary.total
"#{count} scenario" + (count == 1 ? "" : "s")
end

def step_count
count = @test_step_summary.total
"#{count} step" + (count == 1 ? "" : "s")
end

def status_counts(summary)
counts = [:failed, :skipped, :undefined, :pending, :passed].map { |status|
count = summary.total(status)
[status, count]
}.select { |status, count|
count > 0
}.map { |status, count|
format_string("#{count} #{status}", status)
}
"(#{counts.join(", ")})" if counts.any?
end
end
end
end
37 changes: 37 additions & 0 deletions lib/cucumber/formatter/console_issues.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
require 'cucumber/formatter/console'

module Cucumber
module Formatter
class ConsoleIssues
include Console

def initialize(config)
@failures = []
@config = config
@config.on_event(:test_case_finished) do |event|
@failures << event.test_case if event.result.failed?
end
end

def to_s
return if @failures.empty?
result = [ format_string("Failing Scenarios:", :failed) ] + @failures.map { |failure|
source = @config.source? ? format_string(" # #{failure.keyword}: #{failure.name}", :comment) : ''
format_string("cucumber #{profiles_string}" + failure.location, :failed) + source
}
result.join("\n")
end

def any?
@failures.any?
end

private

def profiles_string
return if @config.custom_profiles.empty?
@config.custom_profiles.map { |profile| "-p #{profile}" }.join(' ') + ' '
end
end
end
end
7 changes: 6 additions & 1 deletion lib/cucumber/formatter/pretty.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
require 'cucumber/formatter/console'
require 'cucumber/formatter/io'
require 'cucumber/gherkin/formatter/escaping'
require 'cucumber/formatter/console_counts'
require 'cucumber/formatter/console_issues'

module Cucumber
module Formatter
Expand All @@ -22,12 +24,15 @@ class Pretty

def initialize(runtime, path_or_io, options)
@runtime, @io, @options = runtime, ensure_io(path_or_io), options
@config = runtime.configuration
@exceptions = []
@indent = 0
@prefixes = options[:prefixes] || {}
@delayed_messages = []
@previous_step_keyword = nil
@snippets_input = []
@counts = ConsoleCounts.new(runtime.configuration)
@issues = ConsoleIssues.new(runtime.configuration)
end

def before_features(features)
Expand Down Expand Up @@ -238,7 +243,7 @@ def cell_prefix(status)
end

def print_summary(features)
print_stats(features, @options)
print_statistics(features.duration, @config, @counts, @issues)
print_snippets(@options)
print_passing_wip(@options)
end
Expand Down
28 changes: 5 additions & 23 deletions lib/cucumber/formatter/progress.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
require 'cucumber/core/report/summary'
require 'cucumber/formatter/backtrace_filter'
require 'cucumber/formatter/console'
require 'cucumber/formatter/console_counts'
require 'cucumber/formatter/console_issues'
require 'cucumber/formatter/io'
require 'cucumber/formatter/duration_extractor'
require 'cucumber/formatter/hook_query_visitor'
Expand All @@ -25,6 +27,8 @@ def initialize(config)
@failed_results = []
@failed_test_cases = []
@passed_test_cases = []
@counts = ConsoleCounts.new(config)
@issues = ConsoleIssues.new(config)
config.on_event :step_match, &method(:on_step_match)
config.on_event :test_case_starting, &method(:on_test_case_starting)
config.on_event :test_step_finished, &method(:on_test_step_finished)
Expand Down Expand Up @@ -74,7 +78,7 @@ def on_test_run_finished(_event)
def print_summary
print_elements(@pending_step_matches, :pending, 'steps')
print_elements(@failed_results, :failed, 'steps')
print_statistics_local(@total_duration)
print_statistics(@total_duration, @config, @counts, @issues)
snippet_text_proc = lambda { |step_keyword, step_name, multiline_arg|
snippet_text(step_keyword, step_name, multiline_arg)
}
Expand All @@ -87,28 +91,6 @@ def print_summary
end
end

def print_statistics_local(duration)
if @failed_test_cases.any?
failed_test_cases_data = @failed_test_cases.map do |test_case|
TestCaseData.new(name="#{test_case.keyword}: #{test_case.name}", location=test_case.location)
end
print_failing_scenarios(failed_test_cases_data, config.custom_profiles, config.source?)
end

scenarios_proc = lambda{|status| summary.test_cases.total(status)}
@io.puts dump_summary_counts(summary.test_cases.total, scenarios_proc, "scenario") {|status_count, status| format_string(status_count, status)}
steps_proc = lambda{|status| summary.test_steps.total(status)}
@io.puts dump_summary_counts(summary.test_steps.total, steps_proc, "step") {|status_count, status| format_string(status_count, status)}
@io.puts(format_duration(duration)) if duration && config.duration?

if config.randomize?
@io.puts
@io.puts "Randomized with seed #{config.seed}"
end

@io.flush
end

CHARS = {
:passed => '.',
:failed => 'F',
Expand Down
71 changes: 40 additions & 31 deletions lib/cucumber/formatter/summary.rb
Original file line number Diff line number Diff line change
@@ -1,48 +1,57 @@
require 'cucumber/formatter/io'
require 'cucumber/formatter/console'
require 'cucumber/formatter/console_counts'
require 'cucumber/formatter/console_issues'
require 'cucumber/core/test/result'

module Cucumber
module Formatter
module Summary

def scenario_summary(runtime, &block)
scenarios_proc = lambda{|status| elements = runtime.scenarios(status)}
scenario_count_proc = element_count_proc(scenarios_proc)
dump_summary_counts(runtime.scenarios.length, scenario_count_proc, "scenario", &block)
end
# Summary formatter, outputting only feature / scenario titles
class Summary
include Io
include Console

def step_summary(runtime, &block)
steps_proc = lambda{|status| runtime.steps(status)}
step_count_proc = element_count_proc(steps_proc)
dump_summary_counts(runtime.steps.length, step_count_proc, "step", &block)
end
def initialize(config)
@config, @io = config, ensure_io(config.out_stream)
@counts = ConsoleCounts.new(@config)
@issues = ConsoleIssues.new(@config)
@start_time = Time.now

def dump_summary_counts(total_count, status_proc, what, &block)
dump_count(total_count, what) + dump_status_counts(status_proc, &block)
@config.on_event :test_case_starting do |event|
print_feature event.test_case
print_test_case event.test_case
end

@config.on_event :test_case_finished do |event|
print_result event.result
end

@config.on_event :test_run_finished do |event|
duration = Time.now - @start_time
@io.puts
print_statistics(duration, @config, @counts, @issues)
end
end

private

def element_count_proc(find_elements_proc)
lambda { |status|
elements = find_elements_proc.call(status)
elements.any? ? elements.length : 0
}
def print_feature(test_case)
feature = test_case.feature
return if @current_feature == feature
@io.puts unless @current_feature.nil?
@io.puts feature
@current_feature = feature
end

def dump_status_counts(element_count_proc)
counts = [:failed, :skipped, :undefined, :pending, :passed].map do |status|
count = element_count_proc.call(status)
count != 0 ? yield("#{count} #{status.to_s}", status) : nil
end.compact
if counts.any?
" (#{counts.join(', ')})"
else
""
end
def print_test_case(test_case)
@io.print " #{test_case.name} "
end

def dump_count(count, what, state=nil)
[count, state, "#{what}#{count == 1 ? '' : 's'}"].compact.join(" ")
def print_result(result)
@io.puts format_string(result, result.to_sym)
end

end
end
end

Loading

0 comments on commit 4154c8c

Please sign in to comment.