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

Commit ab1e895

Browse files
committed
Load only files with failures when using rspec --only-failures.
Loading ALL spec files when only a handful have failures is inefficient and could make the run take significantly longer. Note that we only do this if no file or directory arg is passed to `rspec`. If the user passes anything, we load what they tell us to load.
1 parent 52d4f7c commit ab1e895

File tree

9 files changed

+163
-16
lines changed

9 files changed

+163
-16
lines changed

features/command_line/only_failures.feature

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -55,11 +55,22 @@ Feature: Only Failures
5555
end
5656
end
5757
"""
58-
And I have run `rspec` once, resulting in "7 examples, 3 failures"
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"
5969

60-
Scenario: Use just `--only-failures`
70+
Scenario: Running `rspec --only-failures` loads only spec files with failures and runs only the failures
6171
When I run `rspec --only-failures`
62-
Then the output should contain "3 examples, 3 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"
6374

6475
Scenario: Combine `--only-failures` with a file name
6576
When I run `rspec spec/array_spec.rb --only-failures`
@@ -91,4 +102,3 @@ Feature: Only Failures
91102

92103
When I run `rspec --next-failure`
93104
Then the output should contain "All examples were filtered out"
94-

lib/rspec/core/configuration.rb

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,11 @@ def deprecation_stream=(value)
159159
# `--only-failures` and `--next-failures` CLI options.
160160
add_setting :example_status_persistence_file_path
161161

162+
# @macro define_reader
163+
# Indicates if the `--only-failures` (or `--next-failure`) flag is being used.
164+
define_reader :only_failures
165+
alias_method :only_failures?, :only_failures
166+
162167
# @macro add_setting
163168
# Clean up and exit after the first failure (default: `false`).
164169
add_setting :fail_fast
@@ -310,6 +315,7 @@ def initialize
310315
@after_suite_hooks = []
311316

312317
@mock_framework = nil
318+
@files_or_directories_to_run_defaulted = false
313319
@files_or_directories_to_run = []
314320
@loaded_spec_files = Set.new
315321
@color = false
@@ -797,15 +803,29 @@ def profile_examples
797803
# @private
798804
def files_or_directories_to_run=(*files)
799805
files = files.flatten
800-
files << default_path if (command == 'rspec' || Runner.running_in_drb?) && default_path && files.empty?
806+
807+
if (command == 'rspec' || Runner.running_in_drb?) && default_path && files.empty?
808+
@files_or_directories_to_run_defaulted = true
809+
files << default_path
810+
else
811+
@files_or_directories_to_run_defaulted = false
812+
end
813+
801814
@files_or_directories_to_run = files
802815
@files_to_run = nil
803816
end
804817

805818
# The spec files RSpec will run.
806819
# @return [Array] specified files about to run
807820
def files_to_run
808-
@files_to_run ||= get_files_to_run(@files_or_directories_to_run)
821+
@files_to_run ||= begin
822+
if @files_or_directories_to_run_defaulted && only_failures?
823+
files_with_failures = RSpec.world.spec_files_with_failures.to_a
824+
@files_or_directories_to_run = files_with_failures if files_with_failures.any?
825+
end
826+
827+
get_files_to_run(@files_or_directories_to_run)
828+
end
809829
end
810830

811831
# Creates a method that delegates to `example` including the submitted

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/option_parser.rb

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -155,12 +155,12 @@ def parser(options)
155155
FILTERING
156156

157157
parser.on('--only-failures', "Filter to just the examples that failed the last time they ran.") do
158-
add_tag_filter(options, :inclusion_filter, :last_run_status, 'failed')
158+
configure_only_failures(options)
159159
end
160160

161161
parser.on("--next-failure", "Apply `--only-failures` and abort after one failure.",
162162
" (Equivalent to `--only-failures --fail-fast --order defined`)") do
163-
add_tag_filter(options, :inclusion_filter, :last_run_status, 'failed')
163+
configure_only_failures(options)
164164
set_fail_fast(options, true)
165165
set_order(options, "defined")
166166
end
@@ -252,5 +252,10 @@ def set_fail_fast(options, value)
252252
def set_order(options, value)
253253
options[:order] = value
254254
end
255+
256+
def configure_only_failures(options)
257+
options[:only_failures] = true
258+
add_tag_filter(options, :inclusion_filter, :last_run_status, 'failed')
259+
end
255260
end
256261
end

lib/rspec/core/world.rb

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,18 @@ def last_run_statuses
121121
end
122122
end
123123

124+
# @private
125+
def spec_files_with_failures
126+
@spec_files_with_failures ||=
127+
last_run_statuses.inject(Set.new) do |files, (id, status)|
128+
files << id.split(Configuration::ON_SQUARE_BRACKETS).first if status == FAILED_STATUS
129+
files
130+
end.to_a
131+
end
132+
133+
# @private
134+
FAILED_STATUS = "failed".freeze
135+
124136
# @api private
125137
#
126138
# Notify reporter of filters.

spec/rspec/core/configuration_options_spec.rb

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,13 @@
106106
opts.configure(config)
107107
end
108108

109+
it 'configures `only_failures` before `files_or_directories_to_run` since it affects loaded files' do
110+
opts = config_options_object(*%w[ --only-failures ])
111+
expect(config).to receive(:force).with(:only_failures => true).ordered
112+
expect(config).to receive(:files_or_directories_to_run=).ordered
113+
opts.configure(config)
114+
end
115+
109116
{ "pattern" => :pattern, "exclude-pattern" => :exclude_pattern }.each do |flag, attr|
110117
it "sets #{attr} before `requires` so users can check `files_to_run` in a `spec_helper` loaded by `--require`" do
111118
opts = config_options_object(*%W[--require spec_helpe --#{flag} **/*.spec])
@@ -168,6 +175,18 @@
168175
opts.configure(config)
169176
end
170177
end
178+
179+
%w[ --only-failures --next-failure ].each do |option|
180+
describe option do
181+
it "changes `config.only_failures?` to true" do
182+
opts = config_options_object(option)
183+
184+
expect {
185+
opts.configure(config)
186+
}.to change(config, :only_failures?).from(a_falsey_value).to(true)
187+
end
188+
end
189+
end
171190
end
172191

173192
describe "-c, --color, and --colour" do

spec/rspec/core/configuration_spec.rb

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -488,6 +488,52 @@ def stub_expectation_adapters
488488
end
489489
end
490490

491+
context "when only_failures is set" do
492+
around { |ex| Dir.chdir("spec/rspec/core", &ex) }
493+
let(:default_path) { "resources" }
494+
let(:files_with_failures) { ["resources/a_spec.rb"] }
495+
let(:files_loaded_via_default_path) do
496+
config = Configuration.new
497+
config.default_path = default_path
498+
config.files_or_directories_to_run = []
499+
config.files_to_run
500+
end
501+
502+
before do
503+
expect(files_loaded_via_default_path).not_to eq(files_with_failures)
504+
config.default_path = default_path
505+
allow(RSpec.world).to receive_messages(:spec_files_with_failures => files_with_failures)
506+
config.force(:only_failures => true)
507+
end
508+
509+
context "and no explicit paths have been set" do
510+
it 'loads only the files that have failures' do
511+
assign_files_or_directories_to_run
512+
expect(config.files_to_run).to eq(files_with_failures)
513+
end
514+
515+
it 'loads the default path if there are no files with failures' do
516+
allow(RSpec.world).to receive_messages(:spec_files_with_failures => [])
517+
assign_files_or_directories_to_run
518+
expect(config.files_to_run).to eq(files_loaded_via_default_path)
519+
end
520+
end
521+
522+
context "and a path has been set" do
523+
it "ignores the list of files with failures, loading the configured path instead" do
524+
assign_files_or_directories_to_run "resources/acceptance"
525+
expect(config.files_to_run).to contain_files("resources/acceptance/foo_spec.rb")
526+
end
527+
end
528+
529+
context "and the default path has been explicitly set" do
530+
it "ignores the list of files with failures, loading the configured path instead" do
531+
assign_files_or_directories_to_run default_path
532+
expect(config.files_to_run).to eq(files_loaded_via_default_path)
533+
end
534+
end
535+
end
536+
491537
context "with default pattern" do
492538
it "loads files named _spec.rb" do
493539
assign_files_or_directories_to_run "spec/rspec/core/resources"

spec/rspec/core/option_parser_spec.rb

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -128,7 +128,7 @@ def generate_help_text
128128
tag = Parser.parse(%w[ --tag last_run_status:failed ])
129129
only_failures = Parser.parse(%w[ --only-failures ])
130130

131-
expect(only_failures).to eq(tag)
131+
expect(only_failures).to include(tag)
132132
end
133133
end
134134

@@ -137,7 +137,7 @@ def generate_help_text
137137
long_form = Parser.parse(%w[ --tag last_run_status:failed --fail-fast --order defined ])
138138
next_failure = Parser.parse(%w[ --next-failure ])
139139

140-
expect(next_failure).to eq(long_form)
140+
expect(next_failure).to include(long_form)
141141
end
142142
end
143143

spec/rspec/core/world_spec.rb

Lines changed: 40 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -23,16 +23,20 @@ module RSpec::Core
2323
end
2424
end
2525

26+
def simulate_persisted_examples(*examples)
27+
configuration.example_status_persistence_file_path = "examples.txt"
28+
persister = class_double(ExampleStatusPersister).as_stubbed_const
29+
30+
allow(persister).to receive(:load_from).with("examples.txt").and_return(examples)
31+
end
32+
2633
describe "#last_run_statuses" do
2734
context "when `example_status_persistence_file_path` is configured" do
2835
it 'gets the last run statuses from the ExampleStatusPersister' do
29-
configuration.example_status_persistence_file_path = "examples.txt"
30-
persister = class_double(ExampleStatusPersister).as_stubbed_const
31-
32-
allow(persister).to receive(:load_from).with("examples.txt").and_return([
36+
simulate_persisted_examples(
3337
{ :example_id => "id_1", :status => "passed" },
3438
{ :example_id => "id_2", :status => "failed" }
35-
])
39+
)
3640

3741
expect(world.last_run_statuses).to eq(
3842
'id_1' => 'passed', 'id_2' => 'failed'
@@ -56,6 +60,37 @@ module RSpec::Core
5660
end
5761
end
5862

63+
describe "#spec_files_with_failures" do
64+
context "when `example_status_persistence_file_path` is configured" do
65+
it 'returns a memoized array of unique spec files that contain failed exaples' do
66+
simulate_persisted_examples(
67+
{ :example_id => "./spec_1.rb[1:1]", :status => "failed" },
68+
{ :example_id => "./spec_1.rb[1:2]", :status => "failed" },
69+
{ :example_id => "./spec_2.rb[1:2]", :status => "passed" },
70+
{ :example_id => "./spec_3.rb[1:2]", :status => "pending" },
71+
{ :example_id => "./spec_4.rb[1:2]", :status => "unknown" },
72+
{ :example_id => "./spec_5.rb[1:2]", :status => "failed" }
73+
)
74+
75+
expect(world.spec_files_with_failures).to(
76+
be_an(Array) &
77+
be(world.spec_files_with_failures) &
78+
contain_exactly("./spec_1.rb", "./spec_5.rb")
79+
)
80+
end
81+
end
82+
83+
context "when `example_status_persistence_file_path1` is not configured" do
84+
it "returns a memoized blank array" do
85+
configuration.example_status_persistence_file_path = nil
86+
87+
expect(world.spec_files_with_failures).to(
88+
eq([]) & be(world.spec_files_with_failures)
89+
)
90+
end
91+
end
92+
end
93+
5994
describe "#all_examples" do
6095
it "contains all examples from all levels of nesting" do
6196
RSpec.describe do

0 commit comments

Comments
 (0)