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 --only-pending CLI option #3068

Open
wants to merge 7 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 5 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
2 changes: 1 addition & 1 deletion features/command_line/only_failures.feature
Original file line number Diff line number Diff line change
Expand Up @@ -110,4 +110,4 @@ Feature: Using the `--only-failures` option
Scenario: Clear error given when using `--only-failures` without configuring `example_status_persistence_file_path`
Given I have not configured `example_status_persistence_file_path`
When I run `rspec --only-failures`
Then it should fail with "To use `--only-failures`, you must first set `config.example_status_persistence_file_path`."
Then it should fail with "To use `--only-failures` or `--only-pending`, you must first set `config.example_status_persistence_file_path`."
94 changes: 94 additions & 0 deletions features/command_line/only_pending.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
Feature: Using the `--only-pending` option

The `--only-pending` 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.

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.

Background:
Given a file named "spec/spec_helper.rb" with:
"""ruby
RSpec.configure do |c|
c.example_status_persistence_file_path = "examples.txt"
end
"""
And a file named ".rspec" with:
"""
--require spec_helper
--order random
--format documentation
"""
And a file named "spec/array_spec.rb" with:
"""ruby
RSpec.describe 'Array' do
it "checks for inclusion of 1" do
expect([1, 2]).to include(1)
end

it "checks for inclusion of 2", skip: "just not ready for this yet..." do
expect([1, 2]).to include(2)
end

it "checks for inclusion of 3" do
expect([1, 2]).to include(3) # failure
end
end
"""
And a file named "spec/string_spec.rb" with:
"""ruby
RSpec.describe 'String' do
it "checks for inclusion of 'foo'" do
expect("food").to include('foo')
end

it "checks for inclusion of 'bar'" do
expect("food").to include('bar') # failure
end

it "checks for inclusion of 'baz'" do
expect("bazzy").to include('baz')
end

it "checks for inclusion of 'foobar'" do
expect("food").to include('foobar') # failure
end

it "checks for inclusion of 'sum'", skip: "just not ready for this yet..." do
expect("lorem ipsum").to include('sum')
end

it "checks for inclusion of 'sit'", skip: "...nor am I ready for this..." do
expect("dolor sit").to include('sit')
end
end
"""
And a file named "spec/passing_spec.rb" with:
"""ruby
puts "Loading passing_spec.rb"

RSpec.describe "A passing spec" do
it "passes" do
expect(1).to eq(1)
end
end
"""
And I have run `rspec` once, resulting in "10 examples, 3 failures, 3 pending"

Scenario: Running `rspec --only-pending` loads only spec files with failures and runs only the failures
When I run `rspec --only-pending`
Then the output from "rspec --only-pending" should contain "3 examples, 0 failures, 3 pending"
And the output from "rspec --only-pending" should not contain "Loading passing_spec.rb"

Scenario: Combine `--only-pending` with a file name
When I run `rspec spec/array_spec.rb --only-pending`
Then the output should contain "1 example, 0 failures, 1 pending"
When I run `rspec spec/string_spec.rb --only-pending`
Then the output should contain "2 examples, 0 failures, 2 pending"

Scenario: Running `rspec --only-pending` with spec files that pass doesn't run anything
When I run `rspec spec/passing_spec.rb --only-pending`
Then it should pass with "0 examples, 0 failures"

Scenario: Clear error given when using `--only-pending` without configuring `example_status_persistence_file_path`
Given I have not configured `example_status_persistence_file_path`
When I run `rspec --only-pending`
Then it should fail with "To use `--only-failures` or `--only-pending`, you must first set `config.example_status_persistence_file_path`."
52 changes: 44 additions & 8 deletions lib/rspec/core/configuration.rb
Original file line number Diff line number Diff line change
Expand Up @@ -179,7 +179,7 @@ def deprecation_stream=(value)

# @macro define_reader
# The file path to use for persisting example statuses. Necessary for the
# `--only-failures` and `--next-failure` CLI options.
# `--only-failures`, `--only-pending`, and `--next-failure` CLI options.
#
# @overload example_status_persistence_file_path
# @return [String] the file path
Expand All @@ -188,7 +188,7 @@ def deprecation_stream=(value)
define_reader :example_status_persistence_file_path

# Sets the file path to use for persisting example statuses. Necessary for the
# `--only-failures` and `--next-failure` CLI options.
# `--only-failures`, `--only-pending`, and `--next-failure` CLI options.
def example_status_persistence_file_path=(value)
@example_status_persistence_file_path = value
clear_values_derived_from_example_status_persistence_file_path
Expand All @@ -199,9 +199,24 @@ def example_status_persistence_file_path=(value)
define_reader :only_failures
alias_method :only_failures?, :only_failures

# @macro define_reader
# Indicates if the `--only-pending` flag is being used.
define_reader :only_pending
alias_method :only_pending?, :only_pending

# @private
def only_flag_set?
only_failures? || only_pending?
end

# @private
def only_failures_but_not_configured?
only_failures? && !example_status_persistence_file_path
def multiple_only_flags?
only_failures && only_pending?
end

# @private
def only_flag_but_not_configured?
only_flag_set? && !example_status_persistence_file_path
end

# @macro define_reader
Expand Down Expand Up @@ -1143,6 +1158,14 @@ def spec_files_with_failures
end.to_a
end

# @private
def spec_files_with_pending
@spec_files_with_pending ||= last_run_statuses.inject(Set.new) do |files, (id, status)|
files << Example.parse_id(id).first if status == PENDING_STATUS
files
end.to_a
end

# Creates a method that delegates to `example` including the submitted
# `args`. Used internally to add variants of `example` like `pending`:
# @param name [String] example name alias
Expand Down Expand Up @@ -2196,15 +2219,27 @@ def run_suite_hooks(hook_description, hooks)
end
end

Copy link
Author

Choose a reason for hiding this comment

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

Gathering the intersection of files here is where I found it getting messy. Specifically if there were ever to be more flags similar to these set, it might be worth extracting some of this functionality.

def get_files_to_run(paths)
Copy link
Member

Choose a reason for hiding this comment

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

Why the extra method here?

Copy link
Author

Choose a reason for hiding this comment

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

@JonRowe , I was thinking the method #get_files_to_run was getting too large, so I wanted to break it up. Maybe that's premature, since nothing else calls #get_files.

files = FlatMap.flat_map(paths_to_check(paths)) do |path|
def get_files(paths)
FlatMap.flat_map(paths_to_check(paths)) do |path|
path = path.gsub(File::ALT_SEPARATOR, File::SEPARATOR) if File::ALT_SEPARATOR
File.directory?(path) ? gather_directories(path) : extract_location(path)
end.uniq
end

def get_files_to_run(paths)
files = get_files(paths)
return files unless only_flag_set?

return files unless only_failures?
relative_files = files.map { |f| Metadata.relative_path(File.expand_path f) }
intersection = (relative_files & spec_files_with_failures.to_a)

# If both are set, #fail_if_config_and_cli_options_invalid should have caught it.
case [only_failures?, only_pending?]
in [true, _]
intersection = (relative_files & spec_files_with_failures.to_a)
in [_, true]
intersection = (relative_files & spec_files_with_pending.to_a)
end

intersection.empty? ? files : intersection
end

Expand Down Expand Up @@ -2339,6 +2374,7 @@ def update_pattern_attr(name, value)
def clear_values_derived_from_example_status_persistence_file_path
@last_run_statuses = nil
@spec_files_with_failures = nil
@spec_files_with_pending = nil
end

def configure_group_with(group, module_list, application_method)
Expand Down
2 changes: 1 addition & 1 deletion lib/rspec/core/configuration_options.rb
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ def order(keys)

# `files_or_directories_to_run` uses `default_path` so it must be
# set before it.
:default_path, :only_failures,
:default_path, :only_failures, :only_pending,

# These must be set before `requires` to support checking
# `config.files_to_run` from within `spec_helper.rb` when a
Expand Down
9 changes: 9 additions & 0 deletions lib/rspec/core/option_parser.rb
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,10 @@ def parser(options)
options[:order] ||= 'defined'
end

parser.on('--only-pending', "Filter to just the examples that were pending the last time they ran.") do
configure_only_pending(options)
end

parser.on('-P', '--pattern PATTERN', 'Load files matching pattern (default: "spec/**/*_spec.rb").') do |o|
if options[:pattern]
options[:pattern] += ',' + o
Expand Down Expand Up @@ -319,5 +323,10 @@ def configure_only_failures(options)
options[:only_failures] = true
add_tag_filter(options, :inclusion_filter, :last_run_status, 'failed')
end

def configure_only_pending(options)
options[:only_pending] = true
add_tag_filter(options, :inclusion_filter, :last_run_status, 'pending')
end
Copy link
Member

Choose a reason for hiding this comment

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

These could probably be collapsed to a configure_only(options, :pending) configure_only(options, :failures) type call

Copy link
Author

Choose a reason for hiding this comment

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

@JonRowe , as a tradeoff here, I'm supplying three arguments. This accounts for the difference between 'failures' and 'failed'. If the terms were the same like 'pending', then I could just construct the symbol like

options["only_#{type}"] = true

My other thought was some sort of constant mapping.

end
end
4 changes: 2 additions & 2 deletions lib/rspec/core/project_initializer/spec/spec_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -55,8 +55,8 @@
config.filter_run_when_matching :focus

# Allows RSpec to persist some state between runs in order to support
# the `--only-failures` and `--next-failure` CLI options. We recommend
# you configure your source control system to ignore this file.
# the `--only-failures`, `--only-pending`, and `--next-failure` CLI options.
# We recommend you configure your source control system to ignore this file.
config.example_status_persistence_file_path = "spec/examples.txt"

# Limits the available syntax to the non-monkey patched syntax that is
Expand Down
22 changes: 15 additions & 7 deletions lib/rspec/core/world.rb
Original file line number Diff line number Diff line change
Expand Up @@ -183,7 +183,7 @@ def announce_filters
end
end

if @configuration.run_all_when_everything_filtered? && example_count.zero? && !@configuration.only_failures?
if @configuration.run_all_when_everything_filtered? && example_count.zero? && !@configuration.only_flag_set?
report_filter_message("#{everything_filtered_message}; ignoring #{inclusion_filter.description}")
filtered_examples.clear
inclusion_filter.clear
Expand Down Expand Up @@ -250,13 +250,21 @@ def descending_declaration_line_numbers_by_file
end

def fail_if_config_and_cli_options_invalid
return unless @configuration.only_failures_but_not_configured?
if @configuration.only_flag_but_not_configured?
reporter.abort_with(
"\nTo use `--only-failures` or `--only-pending`, you must first set " \
"`config.example_status_persistence_file_path`.",
1 # exit code
)
end

reporter.abort_with(
"\nTo use `--only-failures`, you must first set " \
"`config.example_status_persistence_file_path`.",
1 # exit code
)
if @configuration.multiple_only_flags?
reporter.abort_with(
"\nYou cannot use `--only-failures` and `--only-pending` together. " \
"Please set one or the other.",
1 # exit code
)
end
end

# @private
Expand Down
Loading