Skip to content

Instantly share code, notes, and snippets.

@palkan
Created March 28, 2017 16:54
Show Gist options
  • Star 3 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save palkan/ac4eb92f08d0b33f41a9330445e76fbd to your computer and use it in GitHub Desktop.
Save palkan/ac4eb92f08d0b33f41a9330445e76fbd to your computer and use it in GitHub Desktop.
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
@ramhoj
Copy link

ramhoj commented Sep 4, 2019

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:

  1. Change https://gist.github.com/palkan/ac4eb92f08d0b33f41a9330445e76fbd#file-aggregate_failures-rb-L18 to
add_offense(
  node,
  location: :expression,
  message: "Use :aggregate_failures instead of several one-liners."
)
  1. Change all calls to autocorrect_source by removing the first argument cop and joining the array with join("\n").

@palkan
Copy link
Author

palkan commented Sep 4, 2019

@ramhoj
Copy link

ramhoj commented Sep 6, 2019

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