Skip to content
This repository was archived by the owner on Nov 30, 2024. It is now read-only.

Commit 4a7e748

Browse files
committed
Merge pull request #1888 from rspec/rerun-failures
Add new --only-failures CLI option
2 parents 1e483f6 + af8628f commit 4a7e748

31 files changed

+1367
-61
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,3 +19,4 @@ Gemfile-custom
1919
.idea
2020
bundle
2121
.rspec-local
22+
spec/examples.txt

Changelog.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,15 @@ Enhancements:
1515
where the location isn't unique. (Myron Marston, #1884)
1616
* Use the example id in the rerun command printed for failed examples
1717
when the location is not unique. (Myron Marston, #1884)
18+
* Add `config.example_status_persistence_file_path` option, which is
19+
used to persist the last run status of each example. (Myron Marston, #1888)
20+
* Add `:last_run_status` metadata to each example, which indicates what
21+
happened the last time an example ran. (Myron Marston, #1888)
22+
* Add `--only-failures` CLI option which filters to only the examples
23+
that failed the last time they ran. (Myron Marston, #1888)
24+
* Add `--next-failure` CLI option which allows you to repeatedly focus
25+
on just one of the currently failing examples, then move on to the
26+
next failure, etc. (Myron Marston, #1888)
1827

1928
Bug Fixes:
2029

appveyor.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ install:
2525
- cinst ansicon
2626

2727
test_script:
28-
- bundle exec rspec
28+
- bundle exec rspec --backtrace
2929

3030
environment:
3131
matrix:
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
Feature: Only Failures
2+
3+
The `--only-failures` option filters what examples are run so that only those that failed the last time they ran are executed. To use this option, you first have to configure `config.example_status_persistence_file_path`, which RSpec will use to store the status of each example the last time it ran.
4+
5+
There's also a `--next-failure` option, which is shorthand for `--only-failures --fail-fast --order defined`. It allows you to repeatedly focus on just one of the currently failing examples, then move on to the next failure, etc.
6+
7+
Either of these options can be combined with another a directory or file name; RSpec will run just the failures from the set of loaded examples.
8+
9+
Background:
10+
Given a file named "spec/spec_helper.rb" with:
11+
"""ruby
12+
RSpec.configure do |c|
13+
c.example_status_persistence_file_path = "examples.txt"
14+
end
15+
"""
16+
And a file named ".rspec" with:
17+
"""
18+
--require spec_helper
19+
--order random
20+
--format documentation
21+
"""
22+
And a file named "spec/array_spec.rb" with:
23+
"""ruby
24+
RSpec.describe 'Array' do
25+
it "checks for inclusion of 1" do
26+
expect([1, 2]).to include(1)
27+
end
28+
29+
it "checks for inclusion of 2" do
30+
expect([1, 2]).to include(2)
31+
end
32+
33+
it "checks for inclusion of 3" do
34+
expect([1, 2]).to include(3) # failure
35+
end
36+
end
37+
"""
38+
And a file named "spec/string_spec.rb" with:
39+
"""ruby
40+
RSpec.describe 'String' do
41+
it "checks for inclusion of 'foo'" do
42+
expect("food").to include('foo')
43+
end
44+
45+
it "checks for inclusion of 'bar'" do
46+
expect("food").to include('bar') # failure
47+
end
48+
49+
it "checks for inclusion of 'baz'" do
50+
expect("bazzy").to include('baz')
51+
end
52+
53+
it "checks for inclusion of 'foobar'" do
54+
expect("food").to include('foobar') # failure
55+
end
56+
end
57+
"""
58+
And a file named "spec/passing_spec.rb" with:
59+
"""ruby
60+
puts "Loading passing_spec.rb"
61+
62+
RSpec.describe "A passing spec" do
63+
it "passes" do
64+
expect(1).to eq(1)
65+
end
66+
end
67+
"""
68+
And I have run `rspec` once, resulting in "8 examples, 3 failures"
69+
70+
Scenario: Running `rspec --only-failures` loads only spec files with failures and runs only the failures
71+
When I run `rspec --only-failures`
72+
Then the output from "rspec --only-failures" should contain "3 examples, 3 failures"
73+
And the output from "rspec --only-failures" should not contain "Loading passing_spec.rb"
74+
75+
Scenario: Combine `--only-failures` with a file name
76+
When I run `rspec spec/array_spec.rb --only-failures`
77+
Then the output should contain "1 example, 1 failure"
78+
When I run `rspec spec/string_spec.rb --only-failures`
79+
Then the output should contain "2 examples, 2 failures"
80+
81+
Scenario: Use `--next-failure` to repeatedly run a single failure
82+
When I run `rspec --next-failure`
83+
Then the output should contain "1 example, 1 failure"
84+
And the output should contain "checks for inclusion of 3"
85+
86+
When I fix "spec/array_spec.rb" by replacing "to include(3)" with "not_to include(3)"
87+
And I run `rspec --next-failure`
88+
Then the output should contain "2 examples, 1 failure"
89+
And the output should contain "checks for inclusion of 3"
90+
And the output should contain "checks for inclusion of 'bar'"
91+
92+
When I fix "spec/string_spec.rb" by replacing "to include('bar')" with "not_to include('bar')"
93+
And I run `rspec --next-failure`
94+
Then the output should contain "2 examples, 1 failure"
95+
And the output should contain "checks for inclusion of 'bar'"
96+
And the output should contain "checks for inclusion of 'foobar'"
97+
98+
When I fix "spec/string_spec.rb" by replacing "to include('foobar')" with "not_to include('foobar')"
99+
And I run `rspec --next-failure`
100+
Then the output should contain "1 example, 0 failures"
101+
And the output should contain "checks for inclusion of 'foobar'"
102+
103+
When I run `rspec --next-failure`
104+
Then the output should contain "All examples were filtered out"
105+
106+
Scenario: Clear error given when using `--only-failures` without configuring `example_status_persistence_file_path`
107+
Given I have not configured `example_status_persistence_file_path`
108+
When I run `rspec --only-failures`
109+
Then it should fail with "To use `--only-failures`, you must first set `config.example_status_persistence_file_path`."

features/step_definitions/additional_cli_steps.rb

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,3 +124,29 @@
124124
When /^I create "([^"]*)" with the following content:$/ do |file_name, content|
125125
write_file(file_name, content)
126126
end
127+
128+
Given(/^I have run `([^`]*)` once, resulting in "([^"]*)"$/) do |command, output_snippet|
129+
step %Q{I run `#{command}`}
130+
step %Q{the output from "#{command}" should contain "#{output_snippet}"}
131+
end
132+
133+
When(/^I fix "(.*?)" by replacing "(.*?)" with "(.*?)"$/) do |file_name, original, replacement|
134+
in_current_dir do
135+
contents = File.read(file_name)
136+
expect(contents).to include(original)
137+
fixed = contents.sub(original, replacement)
138+
File.open(file_name, "w") { |f| f.write(fixed) }
139+
end
140+
end
141+
142+
Then(/^it should fail with "(.*?)"$/) do |snippet|
143+
assert_failing_with(snippet)
144+
end
145+
146+
Given(/^I have not configured `example_status_persistence_file_path`$/) do
147+
in_current_dir do
148+
return unless File.exist?("spec/spec_helper.rb")
149+
return unless File.read("spec/spec_helper.rb").include?("example_status_persistence_file_path")
150+
File.open("spec/spec_helper.rb", "w") { |f| f.write("") }
151+
end
152+
end

lib/rspec/core.rb

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,8 @@ def self.world
146146

147147
# Namespace for the rspec-core code.
148148
module Core
149+
autoload :ExampleStatusPersister, "rspec/core/example_status_persister"
150+
149151
# @private
150152
# This avoids issues with reporting time caused by examples that
151153
# change the value/meaning of Time.now without properly restoring

lib/rspec/core/configuration.rb

Lines changed: 80 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,33 @@ def deprecation_stream=(value)
154154
end
155155
end
156156

157+
# @macro define_reader
158+
# The file path to use for persisting example statuses. Necessary for the
159+
# `--only-failures` and `--next-failures` CLI options.
160+
#
161+
# @overload example_status_persistence_file_path
162+
# @return [String] the file path
163+
# @overload example_status_persistence_file_path=(value)
164+
# @param value [String] the file path
165+
define_reader :example_status_persistence_file_path
166+
167+
# Sets the file path to use for persisting example statuses. Necessary for the
168+
# `--only-failures` and `--next-failures` CLI options.
169+
def example_status_persistence_file_path=(value)
170+
@example_status_persistence_file_path = value
171+
clear_values_derived_from_example_status_persistence_file_path
172+
end
173+
174+
# @macro define_reader
175+
# Indicates if the `--only-failures` (or `--next-failure`) flag is being used.
176+
define_reader :only_failures
177+
alias_method :only_failures?, :only_failures
178+
179+
# @private
180+
def only_failures_but_not_configured?
181+
only_failures? && !example_status_persistence_file_path
182+
end
183+
157184
# @macro add_setting
158185
# Clean up and exit after the first failure (default: `false`).
159186
add_setting :fail_fast
@@ -342,6 +369,9 @@ def initialize
342369
def force(hash)
343370
ordering_manager.force(hash)
344371
@preferred_options.merge!(hash)
372+
373+
return unless hash.key?(:example_status_persistence_file_path)
374+
clear_values_derived_from_example_status_persistence_file_path
345375
end
346376

347377
# @private
@@ -792,7 +822,11 @@ def profile_examples
792822
# @private
793823
def files_or_directories_to_run=(*files)
794824
files = files.flatten
795-
files << default_path if (command == 'rspec' || Runner.running_in_drb?) && default_path && files.empty?
825+
826+
if (command == 'rspec' || Runner.running_in_drb?) && default_path && files.empty?
827+
files << default_path
828+
end
829+
796830
@files_or_directories_to_run = files
797831
@files_to_run = nil
798832
end
@@ -803,6 +837,40 @@ def files_to_run
803837
@files_to_run ||= get_files_to_run(@files_or_directories_to_run)
804838
end
805839

840+
# @private
841+
def last_run_statuses
842+
@last_run_statuses ||= Hash.new(UNKNOWN_STATUS).tap do |statuses|
843+
if (path = example_status_persistence_file_path)
844+
begin
845+
ExampleStatusPersister.load_from(path).inject(statuses) do |hash, example|
846+
hash[example.fetch(:example_id)] = example.fetch(:status)
847+
hash
848+
end
849+
rescue SystemCallError => e
850+
RSpec.warning "Could not read from #{path.inspect} (configured as " \
851+
"`config.example_status_persistence_file_path`) due " \
852+
"to a system error: #{e.inspect}. Please check that " \
853+
"the config option is set to an accessible, valid " \
854+
"file path", :call_site => nil
855+
end
856+
end
857+
end
858+
end
859+
860+
# @private
861+
UNKNOWN_STATUS = "unknown".freeze
862+
863+
# @private
864+
FAILED_STATUS = "failed".freeze
865+
866+
# @private
867+
def spec_files_with_failures
868+
@spec_files_with_failures ||= last_run_statuses.inject(Set.new) do |files, (id, status)|
869+
files << id.split(ON_SQUARE_BRACKETS).first if status == FAILED_STATUS
870+
files
871+
end.to_a
872+
end
873+
806874
# Creates a method that delegates to `example` including the submitted
807875
# `args`. Used internally to add variants of `example` like `pending`:
808876
# @param name [String] example name alias
@@ -1560,10 +1628,15 @@ def run_hooks_with(hooks, hook_context)
15601628
end
15611629

15621630
def get_files_to_run(paths)
1563-
FlatMap.flat_map(paths_to_check(paths)) do |path|
1631+
files = FlatMap.flat_map(paths_to_check(paths)) do |path|
15641632
path = path.gsub(File::ALT_SEPARATOR, File::SEPARATOR) if File::ALT_SEPARATOR
15651633
File.directory?(path) ? gather_directories(path) : extract_location(path)
15661634
end.sort.uniq
1635+
1636+
return files unless only_failures?
1637+
relative_files = files.map { |f| Metadata.relative_path(File.expand_path f) }
1638+
intersection = (relative_files & spec_files_with_failures.to_a)
1639+
intersection.empty? ? files : intersection
15671640
end
15681641

15691642
def paths_to_check(paths)
@@ -1678,6 +1751,11 @@ def update_pattern_attr(name, value)
16781751
instance_variable_set(:"@#{name}", value)
16791752
@files_to_run = nil
16801753
end
1754+
1755+
def clear_values_derived_from_example_status_persistence_file_path
1756+
@last_run_statuses = nil
1757+
@spec_files_with_failures = nil
1758+
end
16811759
end
16821760
# rubocop:enable Style/ClassLength
16831761
end

lib/rspec/core/configuration_options.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ def order(keys)
8181

8282
# `files_or_directories_to_run` uses `default_path` so it must be
8383
# set before it.
84-
:default_path,
84+
:default_path, :only_failures,
8585

8686
# These must be set before `requires` to support checking
8787
# `config.files_to_run` from within `spec_helper.rb` when a

lib/rspec/core/example.rb

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -115,7 +115,7 @@ def rerun_argument
115115
# @return [String] the unique id of this example. Pass
116116
# this at the command line to re-run this exact example.
117117
def id
118-
Metadata.id_from(metadata)
118+
@id ||= Metadata.id_from(metadata)
119119
end
120120

121121
# @attr_reader
@@ -160,6 +160,11 @@ def initialize(example_group_class, description, user_metadata, example_block=ni
160160
description, example_block
161161
)
162162

163+
# This should perhaps be done in `Metadata::ExampleHash.create`,
164+
# but the logic there has no knowledge of `RSpec.world` and we
165+
# want to keep it that way. It's easier to just assign it here.
166+
@metadata[:last_run_status] = RSpec.configuration.last_run_statuses[id]
167+
163168
@example_group_instance = @exception = nil
164169
@clock = RSpec::Core::Time
165170
@reporter = RSpec::Core::NullReporter

0 commit comments

Comments
 (0)