Skip to content

Commit 844cd51

Browse files
committed
more efficient simplecov filters
1 parent 9febbb6 commit 844cd51

8 files changed

Lines changed: 238 additions & 61 deletions

lib/undercover/filter_set.rb

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,22 +2,36 @@
22

33
module Undercover
44
class FilterSet
5-
attr_reader :allow_filters, :reject_filters, :simplecov_ignored_files
5+
attr_reader :allow_filters, :reject_filters, :simplecov_filters
66

7-
def initialize(allow_filters, reject_filters, simplecov_ignored_files)
7+
def initialize(allow_filters, reject_filters, simplecov_filters)
88
@allow_filters = allow_filters || []
99
@reject_filters = reject_filters || []
10-
@simplecov_ignored_files = simplecov_ignored_files
10+
@simplecov_filters = simplecov_filters || []
1111
end
1212

1313
def include?(filepath)
1414
fnmatch = proc { |glob| File.fnmatch(glob, filepath, File::FNM_EXTGLOB) }
1515

1616
# Check if file was ignored by SimpleCov filters
17-
return false if simplecov_ignored_files.include?(filepath)
17+
return false if ignored_by_simplecov?(filepath)
1818

1919
# Apply Undercover's own filters
2020
allow_filters.any?(fnmatch) && reject_filters.none?(fnmatch)
2121
end
22+
23+
private
24+
25+
def ignored_by_simplecov?(filepath)
26+
simplecov_filters.any? do |filter|
27+
if filter[:string]
28+
filepath.include?(filter[:string])
29+
elsif filter[:regex]
30+
filepath.match?(Regexp.new(filter[:regex]))
31+
elsif filter[:file]
32+
filepath == filter[:file]
33+
end
34+
end
35+
end
2236
end
2337
end

lib/undercover/simplecov_formatter.rb

Lines changed: 35 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -15,15 +15,46 @@ def export_path
1515

1616
module SimpleCov
1717
class << self
18-
attr_accessor :filtered_files
18+
attr_accessor :filter_definitions
1919

2020
alias filtered_uncached filtered
2121

2222
def filtered(files)
23-
@filtered_files ||= Set.new
23+
@filter_definitions ||= extract_filter_definitions
2424
original_files = files.dup
2525
filtered_uncached(files).tap do |filtered_files|
26-
@filtered_files += (original_files.map(&:filename) - filtered_files.map(&:filename))
26+
filtered_file_paths = (original_files.map(&:filename) - filtered_files.map(&:filename))
27+
filtered_file_paths.each do |file|
28+
relative_path = file.delete_prefix("#{SimpleCov.root}/")
29+
@filter_definitions << {file: relative_path} unless covered_by_serializable_filters?(relative_path)
30+
end
31+
end
32+
end
33+
34+
private
35+
36+
def extract_filter_definitions
37+
filter_array = []
38+
39+
filters.each do |filter|
40+
case filter
41+
when SimpleCov::StringFilter
42+
filter_array << {string: filter.filter_argument}
43+
when SimpleCov::RegexFilter
44+
filter_array << {regex: filter.filter_argument.source}
45+
end
46+
end
47+
48+
filter_array
49+
end
50+
51+
def covered_by_serializable_filters?(relative_path)
52+
@filter_definitions.any? do |filter_def|
53+
if filter_def[:string]
54+
relative_path.include?(filter_def[:string])
55+
elsif filter_def[:regex]
56+
relative_path.match?(Regexp.new(filter_def[:regex]))
57+
end
2758
end
2859
end
2960
end
@@ -48,11 +79,7 @@ def add_undercover_meta_fields
4879
end
4980

5081
def add_ignored_files
51-
ignored_files = SimpleCov.filtered_files&.map do |file|
52-
file.delete_prefix("#{SimpleCov.root}/")
53-
end || []
54-
55-
formatted_result[:meta][:ignored_files] = ignored_files
82+
formatted_result[:meta][:ignored_files] = SimpleCov.filter_definitions || []
5683
end
5784

5885
# format_files uses relative path as keys, as opposed to the superclass method

spec/cli_spec.rb

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -274,7 +274,7 @@
274274

275275
json_content = {
276276
'meta' => {
277-
'ignored_files' => ['app/lib/temp/temp_file.rb', 'db/migrate/migration.rb']
277+
'ignored_files' => [{'string' => 'app/lib/temp/'}, {'file' => 'db/migrate/migration.rb'}]
278278
},
279279
'coverage' => {
280280
'app/models/user.rb' => {'lines' => [1, 0]}
@@ -289,7 +289,8 @@
289289

290290
simplecov_adapter = double('SimpleCov adapter',
291291
coverage: [],
292-
ignored_files: ['app/lib/temp/temp_file.rb', 'db/migrate/migration.rb'])
292+
ignored_files: [{'string' => 'app/lib/temp/'},
293+
{'file' => 'db/migrate/migration.rb'}])
293294
allow(Undercover::SimplecovResultAdapter).to receive(:parse).and_return(simplecov_adapter)
294295

295296
allow_any_instance_of(Undercover::Report).to receive(:validate) { nil }
@@ -299,7 +300,7 @@
299300
expect(Undercover::FilterSet).to receive(:new).with(
300301
['*.rb', '*.rake', '*.ru', 'Rakefile'],
301302
['test/*', 'spec/*', 'db/*', 'config/*', '*_test.rb', '*_spec.rb'],
302-
['app/lib/temp/temp_file.rb', 'db/migrate/migration.rb']
303+
[{'string' => 'app/lib/temp/'}, {'file' => 'db/migrate/migration.rb'}]
303304
).once.and_call_original
304305

305306
subject.run(['-s', 'test.json'])

spec/filter_set_spec.rb

Lines changed: 75 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,9 @@
66
describe Undercover::FilterSet do
77
let(:allow_filters) { ['*.rb'] }
88
let(:reject_filters) { ['*_spec.rb'] }
9-
let(:simplecov_ignored_files) { ['app/lib/filtered_file.rb'] }
9+
let(:simplecov_filters) { [{file: 'app/lib/filtered_file.rb'}] }
1010

11-
subject(:filter_set) { described_class.new(allow_filters, reject_filters, simplecov_ignored_files) }
11+
subject(:filter_set) { described_class.new(allow_filters, reject_filters, simplecov_filters) }
1212

1313
describe '#include?' do
1414
context 'when file is in SimpleCov ignored files' do
@@ -32,7 +32,7 @@
3232
end
3333

3434
context 'with empty SimpleCov ignored files' do
35-
let(:simplecov_ignored_files) { [] }
35+
let(:simplecov_filters) { [] }
3636

3737
it 'behaves like the original FilterSet' do
3838
expect(filter_set.include?('app/models/user.rb')).to be true
@@ -44,7 +44,7 @@
4444
context 'with complex glob patterns' do
4545
let(:allow_filters) { ['*.rb', '*.rake', 'Rakefile'] }
4646
let(:reject_filters) { ['test/*', 'spec/*'] }
47-
let(:simplecov_ignored_files) { ['lib/migrations/20230101_create_users.rb'] }
47+
let(:simplecov_filters) { [{file: 'lib/migrations/20230101_create_users.rb'}] }
4848

4949
it 'correctly applies all filters' do
5050
expect(filter_set.include?('app/models/user.rb')).to be true
@@ -54,5 +54,76 @@
5454
expect(filter_set.include?('lib/migrations/20230101_create_users.rb')).to be false
5555
end
5656
end
57+
58+
context 'with string and regex filters' do
59+
let(:simplecov_filters) do
60+
[
61+
{string: 'spec/'},
62+
{regex: '\/test\/'},
63+
{file: 'custom_ignored.rb'},
64+
]
65+
end
66+
67+
it 'correctly evaluates string filters' do
68+
expect(filter_set.include?('spec/user_spec.rb')).to be false
69+
expect(filter_set.include?('app/spec/helper.rb')).to be false
70+
end
71+
72+
it 'correctly evaluates regex filters' do
73+
expect(filter_set.include?('app/test/unit_test.rb')).to be false
74+
expect(filter_set.include?('lib/test/integration_test.rb')).to be false
75+
end
76+
77+
it 'correctly evaluates file filters' do
78+
expect(filter_set.include?('custom_ignored.rb')).to be false
79+
end
80+
81+
it 'allows files not matching any filter' do
82+
expect(filter_set.include?('app/models/user.rb')).to be true
83+
end
84+
85+
it 'handles file filter that does not match' do
86+
expect(filter_set.include?('different_file.rb')).to be true
87+
end
88+
89+
it 'handles file filter that returns false when filepath does not match exactly' do
90+
file_filter_set = described_class.new(['*.rb'], [], [{file: 'exact_match.rb'}])
91+
expect(file_filter_set.include?('different_file.rb')).to be true
92+
expect(file_filter_set.include?('exact_match.rb')).to be false
93+
end
94+
95+
it 'explicitly tests file filter branch where comparison returns false' do
96+
test_filter_set = described_class.new(['*.rb'], [], [{file: 'specific_file.rb'}])
97+
expect(test_filter_set.include?('other_file.rb')).to be true
98+
end
99+
100+
it 'tests the false branch of file filter comparison within any loop' do
101+
multi_filter_set = described_class.new(['*.rb'], [], [
102+
{file: 'will_not_match.rb'},
103+
{string: 'also_will_not_match'},
104+
])
105+
expect(multi_filter_set.include?('some_other_file.rb')).to be true
106+
end
107+
108+
it 'specifically tests file filter false return in isolation' do
109+
isolated_filter_set = described_class.new(['*.rb'], [], [{file: 'exact_name.rb'}])
110+
expect(isolated_filter_set.include?('totally_different.rb')).to be true
111+
expect(isolated_filter_set.include?('exact_name.rb')).to be false
112+
end
113+
114+
it 'forces file filter false evaluation by using non-matching filename' do
115+
force_false_set = described_class.new(['*.rb'], [], [{file: 'specific_file.rb'}])
116+
expect(force_false_set.include?('different_file.rb')).to be true
117+
end
118+
119+
it 'tests the elsif branch condition itself with falsy file value' do
120+
falsy_filter_set = described_class.new(['*.rb'], [], [
121+
{file: nil},
122+
{file: ''},
123+
{string: 'will_not_match'},
124+
])
125+
expect(falsy_filter_set.include?('any_file.rb')).to be true
126+
end
127+
end
57128
end
58129
end

spec/fixtures/simplecov_with_ignored_files.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,9 @@
33
"timestamp": 1579085826,
44
"simplecov_root": "/Users/mrgrodo/dev/undercover",
55
"ignored_files": [
6-
"app/lib/temp/temp_file.rb",
7-
"db/migrate/20230101_create_users.rb",
8-
"test/factories/user_factory.rb"
6+
{ "string": "app/lib/temp/" },
7+
{ "regex": "\/migrate\/" },
8+
{ "file": "test/factories/user_factory.rb" }
99
]
1010
},
1111
"coverage": {

spec/report_spec.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -240,7 +240,7 @@
240240
end
241241

242242
it 'creates FilterSet with empty ignored files' do
243-
expect(report.filter_set.simplecov_ignored_files).to eq([])
243+
expect(report.filter_set.simplecov_filters).to eq([])
244244
end
245245

246246
it 'behaves like the original implementation' do

0 commit comments

Comments
 (0)