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

Add --retry option to retry failed tests as part of the same run #920

Merged
merged 15 commits into from May 17, 2016
Merged
7 changes: 2 additions & 5 deletions .travis.yml
Expand Up @@ -12,14 +12,11 @@ rvm:
branches:
only:
- master
- resolve-issue-882
- v1.3.x-bugfix

before_install:
- gem update bundler

notifications:
email:
- cukes-devs@googlegroups.com
webhooks:
urls: # gitter
- https://webhooks.gitter.im/e/dc010332f9d40fcc21c4
email: false
32 changes: 32 additions & 0 deletions features/docs/cli/retry_failing_tests.feature
@@ -0,0 +1,32 @@
@wip
Feature: Retry failing tests

Retry gives you a way to get through flaky tests that usually pass after a few runs.
This gives a development team a way forward other than disabling a valuable test.

- Specify max retry count in option
- Output information to the screen
- Output retry information in test report

Questions:
use a tag for flaky tests? Global option to retry any test that fails?

Background:
Given a scenario "Flakey" that fails once, then passes
And a scenario "Shakey" that fails twice, then passes
And a scenario "Solid" that passes
And a scenario "No Dice" that fails

Scenario:
When I run `cucumber -q --retry 1`
Then it should fail with:
"""
4 scenarios (2 passed, 2 failed)
"""

Scenario:
When I run `cucumber -q --retry 2`
Then it should pass with:
"""
4 scenarios (3 passed, 1 failed)
"""
28 changes: 28 additions & 0 deletions features/lib/step_definitions/cucumber_steps.rb
Expand Up @@ -32,6 +32,34 @@
create_step_definition { string }
end

Given /^a scenario "([^\"]*)" that passes$/ do |name|
write_file "features/#{name}.feature",
<<-FEATURE
Feature: #{name}
Scenario: #{name}
Given it passes
FEATURE

write_file "features/step_definitions/#{name}_steps.rb",
<<-STEPS
Given(/^it passes$/) { expect(true).to be true }
STEPS
end

Given /^a scenario "([^\"]*)" that fails$/ do |name|
write_file "features/#{name}.feature",
<<-FEATURE
Feature: #{name}
Scenario: #{name}
Given it fails
FEATURE

write_file "features/step_definitions/#{name}_steps.rb",
<<-STEPS
Given(/^it fails$/) { expect(false).to be true }
STEPS
end

When /^I run the feature with the (\w+) formatter$/ do |formatter|
expect(features.length).to eq 1
run_feature features.first, formatter
Expand Down
35 changes: 35 additions & 0 deletions features/lib/step_definitions/retry_steps.rb
@@ -0,0 +1,35 @@
Given /^a scenario "([^\"]*)" that fails once, then passes$/ do |name|
write_file "features/#{name}.feature",
<<-FEATURE
Feature: #{name}
Scenario: #{name}
Given it fails once, then passes
FEATURE

write_file "features/step_defnitions/#{name}_steps.rb",
<<-STEPS
Given(/^it fails once, then passes$/) do
$#{name.downcase} ||= 0
$#{name.downcase} += 1
expect($#{name.downcase}).to eql 2
end
STEPS
end

Given /^a scenario "([^\"]*)" that fails twice, then passes$/ do |name|
write_file "features/#{name}.feature",
<<-FEATURE
Feature: #{name}
Scenario: #{name}
Given it fails twice, then passes
FEATURE

write_file "features/step_definitions/#{name}_steps.rb",
<<-STEPS
Given(/^it fails twice, then passes$/) do
$#{name.downcase} ||= 0
$#{name.downcase} += 1
expect($#{name.downcase}).to eql 3
end
STEPS
end
4 changes: 4 additions & 0 deletions lib/cucumber/cli/configuration.rb
Expand Up @@ -67,6 +67,10 @@ def fail_fast?
!!@options[:fail_fast]
end

def retry_attempts
@options[:retry]
end

def snippet_type
@options[:snippet_type] || :regexp
end
Expand Down
9 changes: 7 additions & 2 deletions lib/cucumber/cli/options.rb
Expand Up @@ -44,9 +44,10 @@ class Options
PROFILE_LONG_FLAG = '--profile'
NO_PROFILE_LONG_FLAG = '--no-profile'
FAIL_FAST_FLAG = '--fail-fast'
RETRY_FLAG = '--retry'
OPTIONS_WITH_ARGS = ['-r', '--require', '--i18n', '-f', '--format', '-o', '--out',
'-t', '--tags', '-n', '--name', '-e', '--exclude',
PROFILE_SHORT_FLAG, PROFILE_LONG_FLAG,
PROFILE_SHORT_FLAG, PROFILE_LONG_FLAG, RETRY_FLAG,
'-l', '--lines', '--port',
'-I', '--snippet-type']
ORDER_TYPES = %w{defined random}
Expand Down Expand Up @@ -188,6 +189,9 @@ def parse!(args)
"Disables all profile loading to avoid using the 'default' profile.") do |v|
@disable_profile_loading = true
end
opts.on("#{RETRY_FLAG} ATTEMPTS", "Specify the number of times to retry failing tests (default: 0)") do |v|
@options[:retry] = v.to_i
end
opts.on("-c", "--[no-]color",
"Whether or not to use ANSI color in the output. Cucumber decides",
"based on your platform and the output destination if not specified.") do |v|
Expand Down Expand Up @@ -450,7 +454,8 @@ def default_options
:diff_enabled => true,
:snippets => true,
:source => true,
:duration => true
:duration => true,
:retry => 0
}
end
end
Expand Down
4 changes: 4 additions & 0 deletions lib/cucumber/configuration.rb
Expand Up @@ -65,6 +65,10 @@ def fail_fast?
@options[:fail_fast]
end

def retry_attempts
@options[:retry]
end

def guess?
@options[:guess]
end
Expand Down
32 changes: 32 additions & 0 deletions lib/cucumber/filters/retry.rb
@@ -0,0 +1,32 @@
require 'cucumber/core/filter'
require 'cucumber/running_test_case'
require 'cucumber/events/bus'
require 'cucumber/events/after_test_case'

module Cucumber
module Filters
class Retry < Core::Filter.new(:configuration)

def test_case(test_case)
configuration.on_event(:after_test_case) do |event|
Copy link
Member

Choose a reason for hiding this comment

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

I'd have thought you'd need to register the event listener earlier than this, like maybe in the filter's constructor. Does it work?

next unless retry_required?(test_case, event)

test_case_counts[test_case] += 1
event.test_case.describe_to(receiver)
end

super
end

private

def retry_required?(test_case, event)
event.test_case == test_case && event.result.failed? && test_case_counts[test_case] < configuration.retry_attempts
Copy link
Contributor

Choose a reason for hiding this comment

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

This does not work, since the test_case you pass on is never the same that are in the after_test_case event. Filters like activate_steps call Case#with_steps which creates a new test case object.

Copy link
Member

Choose a reason for hiding this comment

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

I've resolved this with cucumber/cucumber-ruby-core#111 which makes equality for Test::Case objects work as you would expect it to.

end

def test_case_counts
@test_case_counts ||= Hash.new {|h,k| h[k] = 0 }
end
end
end
end
7 changes: 7 additions & 0 deletions spec/cucumber/cli/configuration_spec.rb
Expand Up @@ -423,6 +423,13 @@ def reset_config
expect(config.snippet_type).to eq :regexp
end
end

describe "#retry_attempts" do
it "returns the specified number of retries" do
config.parse!(['--retry=3'])
expect(config.retry_attempts).to eql 3
end
end
end
end
end
14 changes: 14 additions & 0 deletions spec/cucumber/cli/options_spec.rb
Expand Up @@ -348,6 +348,20 @@ def after_parsing(args)
end
end

context '--retry ATTEMPTS' do
it 'is 0 by default' do
after_parsing("") do
expect(options[:retry]).to eql 0
end
end

it 'sets the options[:retry] value' do
after_parsing("--retry 4") do
expect(options[:retry]).to eql 4
end
end
end

it "assigns any extra arguments as paths to features" do
after_parsing('-f pretty my_feature.feature my_other_features') do
expect(options[:paths]).to eq ['my_feature.feature', 'my_other_features']
Expand Down
79 changes: 79 additions & 0 deletions spec/cucumber/filters/retry_spec.rb
@@ -0,0 +1,79 @@
require 'cucumber'
require 'cucumber/filters/retry'
require 'cucumber/core/gherkin/writer'
require 'cucumber/configuration'
require 'cucumber/core/test/case'
require 'cucumber/core'
require 'cucumber/events'

describe Cucumber::Filters::Retry do
include Cucumber::Core::Gherkin::Writer
include Cucumber::Core
include Cucumber::Events

let(:configuration) { Cucumber::Configuration.new(:retry => 2) }
let(:test_case) { Cucumber::Core::Test::Case.new([double('test steps')], double('source').as_null_object) }
let(:receiver) { double('receiver').as_null_object }
let(:filter) { Cucumber::Filters::Retry.new(configuration, receiver) }
let(:fail) { Cucumber::Events::AfterTestCase.new(test_case, double('result', :failed? => true, :ok? => false)) }
let(:pass) { Cucumber::Events::AfterTestCase.new(test_case, double('result', :failed? => false, :ok? => true)) }

it { is_expected.to respond_to(:test_case) }
it { is_expected.to respond_to(:with_receiver) }
it { is_expected.to respond_to(:done) }

context "general" do
before(:each) do
filter.with_receiver(receiver)
end

it "registers the :after_test_case event" do
expect(configuration).to receive(:on_event).with(:after_test_case)
filter.test_case(test_case)
end
end

context "passing test case" do
it "describes the test case once" do
expect(test_case).to receive(:describe_to).with(receiver)
filter.test_case(test_case)
configuration.notify(pass)
end
end

context "failing test case" do
it "describes the test case the specified number of times" do
expect(receiver).to receive(:test_case) {|test_case|
configuration.notify(fail)
}.exactly(3).times

filter.test_case(test_case)
end
end

context "flaky test cases" do

context "a little flaky" do
it "describes the test case twice" do
results = [fail, pass]
expect(receiver).to receive(:test_case) {|test_case|
configuration.notify(results.shift)
}.exactly(2).times

filter.test_case(test_case)
end
end

context "really flaky" do
it "describes the test case 3 times" do
results = [fail, fail, pass]

expect(receiver).to receive(:test_case) {|test_case|
configuration.notify(results.shift)
}.exactly(3).times

filter.test_case(test_case)
end
end
end
end