Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Summary formatter #999

Merged
merged 4 commits into from
Aug 5, 2016
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The public methods of the Console module are part of the public API (see #893), so this PR introduces breaking changes (which is OK since we are aiming at 3.0.0 - but the PR is miss labeled).

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fair point Bjorn. I'd like to get rid of that module altogether really, or at least slim down its interface a lot. Using composition (e.g. ConsoleIssues) is the way forward.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK I've updated the labelling on the PR.

I regret that I didn't tease apart the refactoring from the development of the new formatter, but it was late. I guess I still could if you think that would make it easier to review / merge?

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would have been nice for this change to be documented in the changelog 🍻

@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