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 rspec/rails integration #2

Merged
merged 3 commits into from
Aug 21, 2024
Merged
Show file tree
Hide file tree
Changes from all 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: 2 additions & 0 deletions .rubocop.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ AllCops:
Exclude:
- 'bin/*'
- 'vendor/bundle/**/*'
- 'spec/fixtures/**/*'
NewCops: enable
SuggestExtensions: false

Expand Down Expand Up @@ -142,3 +143,4 @@ RSpec/SpecFilePathFormat:
RuboCop: rubocop
RSpec: rspec
SimpleCov: simplecov
RSpecFormatterSkipOnFailure: rspec_formatter_skip_on_failure
2 changes: 2 additions & 0 deletions lib/simplecov/inline.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
require_relative 'inline/version'
require_relative 'inline/formatter'
require_relative 'inline/rspec_formatter_skip_on_failure'
require_relative 'inline/integration'

module SimpleCov
module Inline
Expand Down
2 changes: 1 addition & 1 deletion lib/simplecov/inline/formatter.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
require 'rainbow'

module Simplecov
module SimpleCov
module Inline
class Formatter
Result = Struct.new(:file, :start_line, :end_line, :type) do
Expand Down
61 changes: 61 additions & 0 deletions lib/simplecov/inline/integration.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
module SimpleCov
module Inline
class Integration
class << self
def configure_rspec_rails(rspec: RSpec, rails: Rails)
rspec.configure do |rspec_config|
rspec_config.add_formatter SimpleCov::Inline::RSpecFormatterSkipOnFailure
rspec_config.before(:suite) do
SimpleCov::Inline::Integration.configure_formatter(rspec_config:, rails_root: rails.root.to_s)
end
end
end

def configure_formatter(rspec_config:, rails_root:)
# Restrict coverage reporting to spec files and the file they are testing.
# Note files_to_run contains all spec files when running rspec with no args,
# so in that case do not filter.
# To do so would exclude files that are covered but not directly tested.
return if running_all_specs?(rspec_config:, rails_root:)

SimpleCov::Inline::Formatter.config do |coverage_config|
if rspec_config.inclusion_filter.rules.key?(:locations)
# Skip coverage output if running rspec against a single line.
# e.g. spec/x_spec.rb:123
coverage_config.no_output!(reason: 'filtered to line of code')
else
coverage_config.files = rspec_config.files_to_run.flat_map do |spec_path|
[spec_path, rails_path_under_test(spec_path:, rails_root:)]
end.compact
end
end
end

private

def running_all_specs?(rspec_config:, rails_root:)
Dir.glob("#{rails_root}#{rspec_config.pattern}").to_set == rspec_config.files_to_run.to_set
end

def rails_path_under_test(spec_path:, rails_root:)
# Mappings
# /app/spec/lib/x_spec.rb -> /app/lib/x.rb
# /app/spec/controller/y_spec.rb -> /app/app/controllers/y.rb
raise 'Spec file must be in rails root.' unless spec_path.start_with?(rails_root)

_spec_root, spec_type, *other_directories, filename = spec_path[(rails_root.length + 1)..].split('/')

return if filename.nil?

filename_under_test = filename.gsub(/_spec.rb$/, '.rb')

if spec_type == 'lib'
[rails_root, spec_type, *other_directories, filename_under_test]
else
[rails_root, 'app', spec_type, *other_directories, filename_under_test]
end.compact.join('/')
end
end
end
end
end
28 changes: 28 additions & 0 deletions lib/simplecov/inline/rspec_formatter_skip_on_failure.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
module SimpleCov
module Inline
class RSpecFormatterSkipOnFailure
RSpec::Core::Formatters.register self, :dump_failures

def initialize(output)
@output = output
end

def dump_failures(notification)
return unless skip_reason(notification:)

SimpleCov::Inline::Formatter.config do |coverage_config|
coverage_config.no_output!(reason: skip_reason(notification:))
end
end

private

def skip_reason(notification:)
return 'no examples were run' if notification.examples.none?
return 'some specs failed' if notification.failed_examples.any?

nil
end
end
end
end
Empty file.
Empty file.
2 changes: 1 addition & 1 deletion spec/lib/simplecov/inline/formatter_spec.rb
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
RSpec.describe Simplecov::Inline::Formatter do
RSpec.describe SimpleCov::Inline::Formatter do
after { described_class.reset_config }

describe '#format' do
Expand Down
131 changes: 131 additions & 0 deletions spec/lib/simplecov/inline/integration_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
RSpec.describe SimpleCov::Inline::Integration do
after { SimpleCov::Inline::Formatter.reset_config }

describe '#configure_rspec_rails' do
subject { described_class.configure_rspec_rails(rspec:, rails:) }

let(:rspec) { class_double(RSpec) }
let(:rspec_config) { instance_double(RSpec::Core::Configuration) }

let(:rails) { double('Rails', root: :fake_root) } # rubocop:todo RSpec/VerifiedDoubles

before do
allow(rspec).to receive(:configure).and_yield(rspec_config)
allow(rspec_config).to receive(:add_formatter)
allow(rspec_config).to receive(:before)
allow(described_class).to receive(:configure_formatter)
end

it 'adds a formatter to the rspec config', :aggregate_failures do
subject

expect(rspec_config).to have_received(:add_formatter).with(SimpleCov::Inline::RSpecFormatterSkipOnFailure)
end

it 'adds a before suite hook that calls configure_formatter', :aggregate_failures do
subject

expect(rspec_config).to have_received(:before).with(:suite) do |&block|
block.call
end

expect(described_class).to have_received(:configure_formatter).with(rspec_config:, rails_root: 'fake_root')
end
end

describe '#configure_formatter' do
subject { described_class.configure_formatter(rspec_config:, rails_root:) }

let(:rspec_config) { instance_double(RSpec::Core::Configuration, pattern:, files_to_run:) }
let(:rails_root) { "#{fixture_directory}/fake_rails_root" }
let(:files_to_run) { ["#{rails_root}/spec/lib/lib_spec.rb", "#{rails_root}/spec/models/model_spec.rb"] }
let(:pattern) { '**{,/*/**}/*_spec.rb' }
let(:inclusion_rules) { instance_double(RSpec::Core::InclusionRules, rules: {}) } # no rules means run all

before { allow(rspec_config).to receive(:inclusion_filter).and_return(inclusion_rules) }

context 'running all specs' do
let(:pattern) { '**{,/*/**}/*_spec.rb' }

it 'does not filter files' do
expect { subject }.not_to change { SimpleCov::Inline::Formatter.config.files }.from(nil)
end

it 'does not supress output' do
expect { subject }.not_to change { SimpleCov::Inline::Formatter.config.no_output }.from(nil)
end
end

context 'running only a lib spec' do
let(:files_to_run) { ["#{rails_root}/spec/lib/lib_spec.rb"] }

it 'does not supress output' do
expect { subject }.not_to change { SimpleCov::Inline::Formatter.config.no_output }.from(nil)
end

it 'filters by the spec file and the file it is testing' do
expect { subject }
.to change { SimpleCov::Inline::Formatter.config.files }
.from(nil).to(["#{rails_root}/spec/lib/lib_spec.rb", "#{rails_root}/lib/lib.rb"])
end
end

context 'running only a model spec' do
let(:files_to_run) { ["#{rails_root}/spec/models/model_spec.rb"] }

it 'does not supress output' do
expect { subject }.not_to change { SimpleCov::Inline::Formatter.config.no_output }.from(nil)
end

it 'filters by the spec file and the file it is testing' do
expect { subject }
.to change { SimpleCov::Inline::Formatter.config.files }
.from(nil).to(["#{rails_root}/spec/models/model_spec.rb", "#{rails_root}/app/models/model.rb"])
end
end

context 'filtered to a line of code' do
let(:files_to_run) { ["#{rails_root}/spec/models/model_spec.rb"] }
let(:inclusion_rules) do
instance_double(
RSpec::Core::InclusionRules,
rules: {focus: true, locations: {"#{rails_root}/spec/models/model_spec.rb" => [5]}}, # 5 is the line number
)
end

it 'supresses output' do
expect { subject }
.to change { SimpleCov::Inline::Formatter.config.no_output }
.from(nil)
.to('filtered to line of code')
end

it 'does not filter files' do
expect { subject }.not_to change { SimpleCov::Inline::Formatter.config.files }.from(nil)
end
end

context 'bad spec path' do
let(:files_to_run) { ['/not/rails/root/spec/models/model_spec.rb'] }

it 'filters by the spec file and the file it is testing' do
expect { subject }.to raise_error RuntimeError, 'Spec file must be in rails root.'
end
end

context 'directory provided' do
let(:files_to_run) { ["#{rails_root}/spec/models"] }

it 'does not supress output' do
expect { subject }.not_to change { SimpleCov::Inline::Formatter.config.no_output }.from(nil)
end

it 'only filters to the spec directory and does inlcude the covered files' do
# Existing behaviour. Feels like it should suppress instead.
expect { subject }
.to change { SimpleCov::Inline::Formatter.config.files }
.from(nil).to(["#{rails_root}/spec/models"])
end
end
end
end
44 changes: 44 additions & 0 deletions spec/lib/simplecov/inline/rspec_formatter_skip_on_failure_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
RSpec.describe SimpleCov::Inline::RSpecFormatterSkipOnFailure do
after { SimpleCov::Inline::Formatter.reset_config }

describe '#dump_failures' do
subject { described_class.new(:output_arg_value_not_used).dump_failures(notification) }

let(:notification) do
instance_double(RSpec::Core::Notifications::ExamplesNotification, examples:, failed_examples:)
end

context 'no specs ran' do
let(:examples) { [] }
let(:failed_examples) { [] }

it 'supresses output' do
expect { subject }
.to change { SimpleCov::Inline::Formatter.config.no_output }
.from(nil)
.to('no examples were run')
end
end

context 'specs ran but there were failures' do
let(:examples) { ['a_spec.rb', 'b_spec.rb'] }
let(:failed_examples) { ['a_spec.rb'] }

it 'supresses output' do
expect { subject }
.to change { SimpleCov::Inline::Formatter.config.no_output }
.from(nil)
.to('some specs failed')
end
end

context 'some specs ran without error' do
let(:examples) { ['a_spec.rb', 'b_spec.rb'] }
let(:failed_examples) { [] }

it 'does not supress output' do
expect { subject }.not_to change { SimpleCov::Inline::Formatter.config.no_output }.from(nil)
end
end
end
end
Loading