From db76e2667fb0410f5d72b23c6cbe99c61c37a10f Mon Sep 17 00:00:00 2001 From: tris Date: Wed, 21 Aug 2024 12:51:22 +0800 Subject: [PATCH 1/3] Fix namespace case in formatter Module is Simplecov but should be SimpleCov --- lib/simplecov/inline/formatter.rb | 2 +- spec/lib/simplecov/inline/formatter_spec.rb | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/simplecov/inline/formatter.rb b/lib/simplecov/inline/formatter.rb index 7eca6a5..aca750b 100644 --- a/lib/simplecov/inline/formatter.rb +++ b/lib/simplecov/inline/formatter.rb @@ -1,6 +1,6 @@ require 'rainbow' -module Simplecov +module SimpleCov module Inline class Formatter Result = Struct.new(:file, :start_line, :end_line, :type) do diff --git a/spec/lib/simplecov/inline/formatter_spec.rb b/spec/lib/simplecov/inline/formatter_spec.rb index 16c614f..13a9e66 100644 --- a/spec/lib/simplecov/inline/formatter_spec.rb +++ b/spec/lib/simplecov/inline/formatter_spec.rb @@ -1,4 +1,4 @@ -RSpec.describe Simplecov::Inline::Formatter do +RSpec.describe SimpleCov::Inline::Formatter do after { described_class.reset_config } describe '#format' do From 78542553da727c76ab975c5ab1154cccd13bf568 Mon Sep 17 00:00:00 2001 From: tris Date: Wed, 21 Aug 2024 13:25:13 +0800 Subject: [PATCH 2/3] Add formatter that watches for spec results --- .rubocop.yml | 1 + lib/simplecov/inline.rb | 1 + .../inline/rspec_formatter_skip_on_failure.rb | 28 ++++++++++++ .../rspec_formatter_skip_on_failure_spec.rb | 44 +++++++++++++++++++ 4 files changed, 74 insertions(+) create mode 100644 lib/simplecov/inline/rspec_formatter_skip_on_failure.rb create mode 100644 spec/lib/simplecov/inline/rspec_formatter_skip_on_failure_spec.rb diff --git a/.rubocop.yml b/.rubocop.yml index 47a0700..1ce3d8f 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -142,3 +142,4 @@ RSpec/SpecFilePathFormat: RuboCop: rubocop RSpec: rspec SimpleCov: simplecov + RSpecFormatterSkipOnFailure: rspec_formatter_skip_on_failure diff --git a/lib/simplecov/inline.rb b/lib/simplecov/inline.rb index 71cbb1f..d62444c 100644 --- a/lib/simplecov/inline.rb +++ b/lib/simplecov/inline.rb @@ -1,5 +1,6 @@ require_relative 'inline/version' require_relative 'inline/formatter' +require_relative 'inline/rspec_formatter_skip_on_failure' module SimpleCov module Inline diff --git a/lib/simplecov/inline/rspec_formatter_skip_on_failure.rb b/lib/simplecov/inline/rspec_formatter_skip_on_failure.rb new file mode 100644 index 0000000..52045e7 --- /dev/null +++ b/lib/simplecov/inline/rspec_formatter_skip_on_failure.rb @@ -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 diff --git a/spec/lib/simplecov/inline/rspec_formatter_skip_on_failure_spec.rb b/spec/lib/simplecov/inline/rspec_formatter_skip_on_failure_spec.rb new file mode 100644 index 0000000..b60e5d1 --- /dev/null +++ b/spec/lib/simplecov/inline/rspec_formatter_skip_on_failure_spec.rb @@ -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 From 157948013c0805dc7ab0fb8d346255f0e229913a Mon Sep 17 00:00:00 2001 From: tris Date: Wed, 21 Aug 2024 13:25:37 +0800 Subject: [PATCH 3/3] Add class to configure rspec and rails --- .rubocop.yml | 1 + lib/simplecov/inline.rb | 1 + lib/simplecov/inline/integration.rb | 61 ++++++++ .../fake_rails_root/spec/lib/lib_spec.rb | 0 .../fake_rails_root/spec/models/model_spec.rb | 0 spec/lib/simplecov/inline/integration_spec.rb | 131 ++++++++++++++++++ 6 files changed, 194 insertions(+) create mode 100644 lib/simplecov/inline/integration.rb create mode 100644 spec/fixtures/fake_rails_root/spec/lib/lib_spec.rb create mode 100644 spec/fixtures/fake_rails_root/spec/models/model_spec.rb create mode 100644 spec/lib/simplecov/inline/integration_spec.rb diff --git a/.rubocop.yml b/.rubocop.yml index 1ce3d8f..3b7bbd6 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -6,6 +6,7 @@ AllCops: Exclude: - 'bin/*' - 'vendor/bundle/**/*' + - 'spec/fixtures/**/*' NewCops: enable SuggestExtensions: false diff --git a/lib/simplecov/inline.rb b/lib/simplecov/inline.rb index d62444c..b183514 100644 --- a/lib/simplecov/inline.rb +++ b/lib/simplecov/inline.rb @@ -1,6 +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 diff --git a/lib/simplecov/inline/integration.rb b/lib/simplecov/inline/integration.rb new file mode 100644 index 0000000..fd75053 --- /dev/null +++ b/lib/simplecov/inline/integration.rb @@ -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 diff --git a/spec/fixtures/fake_rails_root/spec/lib/lib_spec.rb b/spec/fixtures/fake_rails_root/spec/lib/lib_spec.rb new file mode 100644 index 0000000..e69de29 diff --git a/spec/fixtures/fake_rails_root/spec/models/model_spec.rb b/spec/fixtures/fake_rails_root/spec/models/model_spec.rb new file mode 100644 index 0000000..e69de29 diff --git a/spec/lib/simplecov/inline/integration_spec.rb b/spec/lib/simplecov/inline/integration_spec.rb new file mode 100644 index 0000000..5a1545d --- /dev/null +++ b/spec/lib/simplecov/inline/integration_spec.rb @@ -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