Rubocop: RSpec/AggregateFailures
require 'rubocop/rspec/language' | |
module RuboCop | |
module Cop | |
module RSpec | |
class AggregateFailures < RuboCop::Cop::Cop | |
GROUP_BLOCKS = RuboCop::RSpec::Language::ExampleGroups::ALL | |
EXAMPLE_BLOCKS = RuboCop::RSpec::Language::Examples::ALL | |
def on_block(node) | |
method, _args, body = *node | |
return unless body&.begin_type? | |
_receiver, method_name, _object = *method | |
return unless GROUP_BLOCKS.include?(method_name) | |
unless check_node(body) | |
add_offense( | |
node, | |
:expression, | |
'Use :aggregate_failures instead of several one-liners.' | |
) | |
end | |
end | |
def autocorrect(node) | |
_method, _args, body = *node | |
iter = body.children.each | |
first_example = loop do | |
child = iter.next | |
break child if oneliner?(child) | |
end | |
base_indent = " " * first_example.source_range.column | |
replacements = [ | |
header_from(first_example), | |
body_from(first_example, base_indent) | |
] | |
last_example = nil | |
loop do | |
child = iter.next | |
break unless oneliner?(child) | |
last_example = child | |
replacements << body_from(child, base_indent) | |
end | |
replacements << "#{base_indent}end" | |
range = first_example.source_range.begin.join( | |
last_example.source_range.end | |
) | |
replacement = replacements.join("\n") | |
lambda do |corrector| | |
corrector.replace(range, replacement) | |
end | |
end | |
private | |
def check_node(node) | |
offenders = 0 | |
node.children.each do |child| | |
if oneliner?(child) | |
offenders += 1 | |
elsif example_node?(child) | |
break if offenders > 1 | |
offenders = 0 | |
end | |
end | |
offenders < 2 | |
end | |
def oneliner?(node) | |
node&.block_type? && | |
(node.source.lines.size == 1) && | |
example_node?(node) | |
end | |
def example_node?(node) | |
method, _args, _body = *node | |
_receiver, method_name, _object = *method | |
EXAMPLE_BLOCKS.include?(method_name) | |
end | |
def header_from(node) | |
method, _args, _body = *node | |
_receiver, method_name, _object = *method | |
%(#{method_name} "works", :aggregate_failures do) | |
end | |
def body_from(node, base_indent = '') | |
_method, _args, body = *node | |
"#{base_indent}#{indent}#{body.source}" | |
end | |
def indent | |
@indent ||= " " * (config.for_cop('IndentationWidth')['Width'] || 2) | |
end | |
end | |
end | |
end | |
end |
require 'rubocop' | |
require 'rubocop/rspec/support' | |
require 'rubocop/cop/rspec/aggregate_failures' | |
describe RuboCop::Cop::RSpec::AggregateFailures, :config do | |
subject(:cop) { described_class.new(config) } | |
it 'rejects two one-liners in a row' do | |
inspect_source(cop, ['context "request" do', | |
' it { is_expected.to be_success }', | |
' it { expect(response.body).to eq "OK" }', | |
'end']) | |
expect(cop.offenses.size).to eq(1) | |
expect(cop.messages.first).to eq('Use :aggregate_failures instead of several one-liners.') | |
end | |
it 'rejects two one-liners when blank lines and non-example blocks' do | |
inspect_source(cop, ['context "request" do', | |
' let(:user) { create(:user) } ', | |
' before { get "/" }', | |
'', | |
' it { is_expected.to be_success }', | |
' ', | |
' ', | |
' it { expect(response.body).to eq "OK" }', | |
'end']) | |
expect(cop.offenses.size).to eq(1) | |
expect(cop.messages.first).to eq('Use :aggregate_failures instead of several one-liners.') | |
end | |
it 'rejects one-liners with nested context' do | |
inspect_source(cop, ['context "request" do', | |
' it { is_expected.to be_success }', | |
' it "works" do', | |
' expect(subject).to be_ok', | |
' end', | |
' it { expect(response.body).to eq "OK" }', | |
'', | |
' context "sub-request" do', | |
' let(:params) { "?q=1" }', | |
' it { is_expected.to be_valid }', | |
' end', | |
'end']) | |
expect(cop.offenses).to be_empty | |
end | |
it 'accepts single one-liner' do | |
inspect_source(cop, ['context "request" do', | |
' it { is_expected.to be_success }', | |
'end']) | |
expect(cop.offenses).to be_empty | |
end | |
it 'accepts one-liners separated by multiliners' do | |
inspect_source(cop, ['context "request" do', | |
' it { is_expected.to be_success }', | |
' it "works" do', | |
' expect(subject).to be_ok', | |
' end', | |
' it { expect(response.body).to eq "OK" }', | |
'end']) | |
expect(cop.offenses).to be_empty | |
end | |
it 'handles edge cases' do | |
inspect_source(cop, ['context "request" do', | |
' include_examples "edges"', | |
' xdescribe "POST #create" do', | |
' end', | |
'', | |
' pending {}', | |
'end']) | |
expect(cop.offenses).to be_empty | |
end | |
it 'handles edge cases 2' do | |
inspect_source(cop, ['context "request" do', | |
' include_examples "edges"', | |
'end']) | |
expect(cop.offenses).to be_empty | |
end | |
describe "#autocorrect" do | |
it "corrects two one-liners" do | |
new_source = autocorrect_source( | |
cop, | |
['context "request" do', | |
' it { is_expected.to be_success }', | |
' it { expect(response.body).to eq "OK" }', | |
'end'] | |
) | |
expect(new_source).to eq( | |
['context "request" do', | |
' it "works", :aggregate_failures do', | |
' is_expected.to be_success', | |
' expect(response.body).to eq "OK"', | |
' end', | |
'end'].join("\n") | |
) | |
end | |
it 'corrects indented one-liners when blank lines and non-example blocks' do | |
new_source = autocorrect_source( | |
cop, | |
['describe "GET #index" do', | |
' context "request" do', | |
' let(:user) { create(:user) } ', | |
' before { get "/" }', | |
'', | |
' it { is_expected.to be_success }', | |
' ', | |
' ', | |
' it { expect(response.body).to eq "OK" }', | |
' end', | |
'end'] | |
) | |
expect(new_source).to eq( | |
['describe "GET #index" do', | |
' context "request" do', | |
' let(:user) { create(:user) } ', | |
' before { get "/" }', | |
'', | |
' it "works", :aggregate_failures do', | |
' is_expected.to be_success', | |
' expect(response.body).to eq "OK"', | |
' end', | |
' end', | |
'end'].join("\n") | |
) | |
end | |
it "corrects several groups" do | |
new_source = autocorrect_source( | |
cop, | |
[ | |
'describe "GET #index" do', | |
' context "request" do', | |
' let(:user) { create(:user) } ', | |
' before { get "/" }', | |
'', | |
' it { is_expected.to be_success }', | |
' ', | |
' ', | |
' it { expect(response.body).to eq "OK" }', | |
'', | |
' context "sub-request", :invalid do', | |
' it { is_expected.not_to be_success }', | |
' it { expect(response.body).to eq "FAILED" }', | |
' end', | |
' end', | |
'end' | |
] | |
) | |
expect(new_source).to eq( | |
['describe "GET #index" do', | |
' context "request" do', | |
' let(:user) { create(:user) } ', | |
' before { get "/" }', | |
'', | |
' it "works", :aggregate_failures do', | |
' is_expected.to be_success', | |
' expect(response.body).to eq "OK"', | |
' end', | |
'', | |
' context "sub-request", :invalid do', | |
' it "works", :aggregate_failures do', | |
' is_expected.not_to be_success', | |
' expect(response.body).to eq "FAILED"', | |
' end', | |
' end', | |
' end', | |
'end'].join("\n") | |
) | |
end | |
end | |
end |
This comment has been minimized.
This comment has been minimized.
This cop is now a part of the TestProf project: https://test-prof.evilmartians.io/#/rubocop?id=rspecaggregatefailures And it seems to be fixed: https://github.com/palkan/test-prof/blob/master/lib/test_prof/cops/rspec/aggregate_failures.rb#L64 |
This comment has been minimized.
This comment has been minimized.
Thank you @palkan. I Googled this gist after watching your conference talk on youtube and completely missed the gem until later. It's been a real pleasure following your guides and using the tools in test-prof. Much appreciated! |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
This comment has been minimized.
Thank you for sharing this!
I had to make a few small adjustments to get it to run at present date. Have a look if you too are having problems:
autocorrect_source
by removing the first argumentcop
and joining the array withjoin("\n")
.