simplecov 0.14.1 → 0.15.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +5 -5
- data/.rubocop.yml +6 -2
- data/.travis.yml +4 -4
- data/CHANGELOG.md +15 -0
- data/Gemfile +2 -2
- data/MIT-LICENSE +1 -1
- data/README.md +51 -5
- data/doc/editor-integration.md +6 -1
- data/features/config_tracked_files.feature +1 -1
- data/features/config_tracked_files_relevant_lines.feature +31 -0
- data/features/step_definitions/transformers.rb +1 -1
- data/features/support/aruba_freedom_patch.rb +53 -0
- data/features/support/env.rb +5 -5
- data/lib/simplecov.rb +16 -10
- data/lib/simplecov/configuration.rb +5 -9
- data/lib/simplecov/defaults.rb +4 -4
- data/lib/simplecov/file_list.rb +5 -5
- data/lib/simplecov/filter.rb +39 -4
- data/lib/simplecov/formatter/simple_formatter.rb +1 -1
- data/lib/simplecov/lines_classifier.rb +32 -0
- data/lib/simplecov/result_merger.rb +42 -13
- data/lib/simplecov/source_file.rb +6 -1
- data/lib/simplecov/version.rb +1 -23
- data/spec/filters_spec.rb +74 -0
- data/spec/lines_classifier_spec.rb +103 -0
- data/spec/result_merger_spec.rb +89 -6
- data/spec/simplecov_spec.rb +109 -0
- data/spec/source_file_spec.rb +4 -0
- metadata +20 -15
data/lib/simplecov/defaults.rb
CHANGED
@@ -7,7 +7,7 @@ SimpleCov.profiles.define "root_filter" do
|
|
7
7
|
root_filter = nil
|
8
8
|
add_filter do |src|
|
9
9
|
root_filter ||= /\A#{Regexp.escape(SimpleCov.root)}/io
|
10
|
-
|
10
|
+
src.filename !~ root_filter
|
11
11
|
end
|
12
12
|
end
|
13
13
|
|
@@ -25,15 +25,15 @@ end
|
|
25
25
|
SimpleCov.profiles.define "rails" do
|
26
26
|
load_profile "test_frameworks"
|
27
27
|
|
28
|
-
add_filter
|
29
|
-
add_filter
|
28
|
+
add_filter %r{^/config/}
|
29
|
+
add_filter %r{/^/db/}
|
30
30
|
|
31
31
|
add_group "Controllers", "app/controllers"
|
32
32
|
add_group "Channels", "app/channels" if defined?(ActionCable)
|
33
33
|
add_group "Models", "app/models"
|
34
34
|
add_group "Mailers", "app/mailers"
|
35
35
|
add_group "Helpers", "app/helpers"
|
36
|
-
add_group "Jobs", %w
|
36
|
+
add_group "Jobs", %w[app/jobs app/workers]
|
37
37
|
add_group "Libraries", "lib"
|
38
38
|
|
39
39
|
track_files "{app,lib}/**/*.rb"
|
data/lib/simplecov/file_list.rb
CHANGED
@@ -5,25 +5,25 @@ module SimpleCov
|
|
5
5
|
# Returns the count of lines that have coverage
|
6
6
|
def covered_lines
|
7
7
|
return 0.0 if empty?
|
8
|
-
map { |f| f.covered_lines.count }.inject(
|
8
|
+
map { |f| f.covered_lines.count }.inject(:+)
|
9
9
|
end
|
10
10
|
|
11
11
|
# Returns the count of lines that have been missed
|
12
12
|
def missed_lines
|
13
13
|
return 0.0 if empty?
|
14
|
-
map { |f| f.missed_lines.count }.inject(
|
14
|
+
map { |f| f.missed_lines.count }.inject(:+)
|
15
15
|
end
|
16
16
|
|
17
17
|
# Returns the count of lines that are not relevant for coverage
|
18
18
|
def never_lines
|
19
19
|
return 0.0 if empty?
|
20
|
-
map { |f| f.never_lines.count }.inject(
|
20
|
+
map { |f| f.never_lines.count }.inject(:+)
|
21
21
|
end
|
22
22
|
|
23
23
|
# Returns the count of skipped lines
|
24
24
|
def skipped_lines
|
25
25
|
return 0.0 if empty?
|
26
|
-
map { |f| f.skipped_lines.count }.inject(
|
26
|
+
map { |f| f.skipped_lines.count }.inject(:+)
|
27
27
|
end
|
28
28
|
|
29
29
|
# Computes the coverage based upon lines covered and lines missed for each file
|
@@ -53,7 +53,7 @@ module SimpleCov
|
|
53
53
|
# @return [Float]
|
54
54
|
def covered_strength
|
55
55
|
return 0.0 if empty? || lines_of_code.zero?
|
56
|
-
Float(map { |f| f.covered_strength * f.lines_of_code }.inject(
|
56
|
+
Float(map { |f| f.covered_strength * f.lines_of_code }.inject(:+) / lines_of_code)
|
57
57
|
end
|
58
58
|
end
|
59
59
|
end
|
data/lib/simplecov/filter.rb
CHANGED
@@ -24,13 +24,40 @@ module SimpleCov
|
|
24
24
|
warn "#{Kernel.caller.first}: [DEPRECATION] #passes? is deprecated. Use #matches? instead."
|
25
25
|
matches?(source_file)
|
26
26
|
end
|
27
|
+
|
28
|
+
def self.build_filter(filter_argument)
|
29
|
+
return filter_argument if filter_argument.is_a?(SimpleCov::Filter)
|
30
|
+
class_for_argument(filter_argument).new(filter_argument)
|
31
|
+
end
|
32
|
+
|
33
|
+
def self.class_for_argument(filter_argument)
|
34
|
+
if filter_argument.is_a?(String)
|
35
|
+
SimpleCov::StringFilter
|
36
|
+
elsif filter_argument.is_a?(Regexp)
|
37
|
+
SimpleCov::RegexFilter
|
38
|
+
elsif filter_argument.is_a?(Array)
|
39
|
+
SimpleCov::ArrayFilter
|
40
|
+
elsif filter_argument.is_a?(Proc)
|
41
|
+
SimpleCov::BlockFilter
|
42
|
+
else
|
43
|
+
raise ArgumentError, "You have provided an unrecognized filter type"
|
44
|
+
end
|
45
|
+
end
|
27
46
|
end
|
28
47
|
|
29
48
|
class StringFilter < SimpleCov::Filter
|
30
49
|
# Returns true when the given source file's filename matches the
|
31
50
|
# string configured when initializing this Filter with StringFilter.new('somestring)
|
32
51
|
def matches?(source_file)
|
33
|
-
(source_file.
|
52
|
+
(source_file.project_filename =~ /#{filter_argument}/)
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
class RegexFilter < SimpleCov::Filter
|
57
|
+
# Returns true when the given source file's filename matches the
|
58
|
+
# regex configured when initializing this Filter with RegexFilter.new(/someregex/)
|
59
|
+
def matches?(source_file)
|
60
|
+
(source_file.project_filename =~ filter_argument)
|
34
61
|
end
|
35
62
|
end
|
36
63
|
|
@@ -43,11 +70,19 @@ module SimpleCov
|
|
43
70
|
end
|
44
71
|
|
45
72
|
class ArrayFilter < SimpleCov::Filter
|
46
|
-
|
47
|
-
|
73
|
+
def initialize(filter_argument)
|
74
|
+
filter_objects = filter_argument.map do |arg|
|
75
|
+
Filter.build_filter(arg)
|
76
|
+
end
|
77
|
+
|
78
|
+
super(filter_objects)
|
79
|
+
end
|
80
|
+
|
81
|
+
# Returns true if any of the filters in the array match the given source file.
|
82
|
+
# Configure this Filter like StringFilter.new(['some/path', /^some_regex/, Proc.new {|src_file| ... }])
|
48
83
|
def matches?(source_files_list)
|
49
84
|
filter_argument.any? do |arg|
|
50
|
-
source_files_list
|
85
|
+
arg.matches?(source_files_list)
|
51
86
|
end
|
52
87
|
end
|
53
88
|
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
module SimpleCov
|
2
|
+
# Classifies whether lines are relevant for code coverage analysis.
|
3
|
+
# Comments & whitespace lines, and :nocov: token blocks, are considered not relevant.
|
4
|
+
|
5
|
+
class LinesClassifier
|
6
|
+
RELEVANT = 0
|
7
|
+
NOT_RELEVANT = nil
|
8
|
+
|
9
|
+
WHITESPACE_LINE = /^\s*$/
|
10
|
+
COMMENT_LINE = /^\s*#/
|
11
|
+
WHITESPACE_OR_COMMENT_LINE = Regexp.union(WHITESPACE_LINE, COMMENT_LINE)
|
12
|
+
|
13
|
+
def self.no_cov_line
|
14
|
+
/^(\s*)#(\s*)(\:#{SimpleCov.nocov_token}\:)/
|
15
|
+
end
|
16
|
+
|
17
|
+
def classify(lines)
|
18
|
+
skipping = false
|
19
|
+
|
20
|
+
lines.map do |line|
|
21
|
+
if line =~ self.class.no_cov_line
|
22
|
+
skipping = !skipping
|
23
|
+
NOT_RELEVANT
|
24
|
+
elsif skipping || line =~ WHITESPACE_OR_COMMENT_LINE
|
25
|
+
NOT_RELEVANT
|
26
|
+
else
|
27
|
+
RELEVANT
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -17,25 +17,31 @@ module SimpleCov
|
|
17
17
|
File.join(SimpleCov.coverage_path, ".resultset.json.lock")
|
18
18
|
end
|
19
19
|
|
20
|
-
# Loads the cached resultset from JSON and returns it as a Hash
|
20
|
+
# Loads the cached resultset from JSON and returns it as a Hash,
|
21
|
+
# caching it for subsequent accesses.
|
21
22
|
def resultset
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
23
|
+
@resultset ||= begin
|
24
|
+
data = stored_data
|
25
|
+
if data
|
26
|
+
begin
|
27
|
+
JSON.parse(data) || {}
|
28
|
+
rescue
|
29
|
+
{}
|
30
|
+
end
|
31
|
+
else
|
26
32
|
{}
|
27
33
|
end
|
28
|
-
else
|
29
|
-
{}
|
30
34
|
end
|
31
35
|
end
|
32
36
|
|
33
37
|
# Returns the contents of the resultset cache as a string or if the file is missing or empty nil
|
34
38
|
def stored_data
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
+
synchronize_resultset do
|
40
|
+
return unless File.exist?(resultset_path)
|
41
|
+
data = File.read(resultset_path)
|
42
|
+
return if data.nil? || data.length < 2
|
43
|
+
data
|
44
|
+
end
|
39
45
|
end
|
40
46
|
|
41
47
|
# Gets the resultset hash and re-creates all included instances
|
@@ -76,8 +82,9 @@ module SimpleCov
|
|
76
82
|
|
77
83
|
# Saves the given SimpleCov::Result in the resultset cache
|
78
84
|
def store_result(result)
|
79
|
-
|
80
|
-
|
85
|
+
synchronize_resultset do
|
86
|
+
# Ensure we have the latest, in case it was already cached
|
87
|
+
clear_resultset
|
81
88
|
new_set = resultset
|
82
89
|
command_name, data = result.to_hash.first
|
83
90
|
new_set[command_name] = data
|
@@ -87,6 +94,28 @@ module SimpleCov
|
|
87
94
|
end
|
88
95
|
true
|
89
96
|
end
|
97
|
+
|
98
|
+
# Ensure only one process is reading or writing the resultset at any
|
99
|
+
# given time
|
100
|
+
def synchronize_resultset
|
101
|
+
# make it reentrant
|
102
|
+
return yield if defined?(@resultset_locked) && @resultset_locked
|
103
|
+
|
104
|
+
begin
|
105
|
+
@resultset_locked = true
|
106
|
+
File.open(resultset_writelock, "w+") do |f|
|
107
|
+
f.flock(File::LOCK_EX)
|
108
|
+
yield
|
109
|
+
end
|
110
|
+
ensure
|
111
|
+
@resultset_locked = false
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
# Clear out the previously cached .resultset
|
116
|
+
def clear_resultset
|
117
|
+
@resultset = nil
|
118
|
+
end
|
90
119
|
end
|
91
120
|
end
|
92
121
|
end
|
@@ -80,6 +80,11 @@ module SimpleCov
|
|
80
80
|
@coverage = coverage
|
81
81
|
end
|
82
82
|
|
83
|
+
# The path to this source file relative to the projects directory
|
84
|
+
def project_filename
|
85
|
+
@filename.sub(/^#{SimpleCov.root}/, "")
|
86
|
+
end
|
87
|
+
|
83
88
|
# The source code for this file. Aliased as :source
|
84
89
|
def src
|
85
90
|
# We intentionally read source code lazily to
|
@@ -175,7 +180,7 @@ module SimpleCov
|
|
175
180
|
skipping = false
|
176
181
|
|
177
182
|
lines.each do |line|
|
178
|
-
if line.src =~
|
183
|
+
if line.src =~ SimpleCov::LinesClassifier.no_cov_line
|
179
184
|
skipping = !skipping
|
180
185
|
line.skipped!
|
181
186
|
elsif skipping
|
data/lib/simplecov/version.rb
CHANGED
@@ -1,25 +1,3 @@
|
|
1
1
|
module SimpleCov
|
2
|
-
|
3
|
-
|
4
|
-
def version.to_a
|
5
|
-
split(".").map(&:to_i)
|
6
|
-
end
|
7
|
-
|
8
|
-
def version.major
|
9
|
-
to_a[0]
|
10
|
-
end
|
11
|
-
|
12
|
-
def version.minor
|
13
|
-
to_a[1]
|
14
|
-
end
|
15
|
-
|
16
|
-
def version.patch
|
17
|
-
to_a[2]
|
18
|
-
end
|
19
|
-
|
20
|
-
def version.pre
|
21
|
-
to_a[3]
|
22
|
-
end
|
23
|
-
|
24
|
-
VERSION = version
|
2
|
+
VERSION = "0.15.0".freeze
|
25
3
|
end
|
data/spec/filters_spec.rb
CHANGED
@@ -26,6 +26,27 @@ if SimpleCov.usable?
|
|
26
26
|
expect(SimpleCov::StringFilter.new("sample.rb")).to be_matches subject
|
27
27
|
end
|
28
28
|
|
29
|
+
it "doesn't match a parent directory with a new SimpleCov::StringFilter" do
|
30
|
+
parent_dir_name = File.basename(File.expand_path("..", File.dirname(__FILE__)))
|
31
|
+
expect(SimpleCov::StringFilter.new(parent_dir_name)).not_to be_matches subject
|
32
|
+
end
|
33
|
+
|
34
|
+
it "matches a new SimpleCov::StringFilter '/fixtures/'" do
|
35
|
+
expect(SimpleCov::StringFilter.new("sample.rb")).to be_matches subject
|
36
|
+
end
|
37
|
+
|
38
|
+
it "matches a new SimpleCov::RegexFilter /\/fixtures\//" do
|
39
|
+
expect(SimpleCov::RegexFilter.new(/\/fixtures\//)).to be_matches subject
|
40
|
+
end
|
41
|
+
|
42
|
+
it "doesn't match a new SimpleCov::RegexFilter /^\/fixtures\//" do
|
43
|
+
expect(SimpleCov::RegexFilter.new(/^\/fixtures\//)).not_to be_matches subject
|
44
|
+
end
|
45
|
+
|
46
|
+
it "matches a new SimpleCov::RegexFilter /^\/spec\//" do
|
47
|
+
expect(SimpleCov::RegexFilter.new(/^\/spec\//)).to be_matches subject
|
48
|
+
end
|
49
|
+
|
29
50
|
it "doesn't match a new SimpleCov::BlockFilter that is not applicable" do
|
30
51
|
expect(SimpleCov::BlockFilter.new(proc { |s| File.basename(s.filename) == "foo.rb" })).not_to be_matches subject
|
31
52
|
end
|
@@ -46,6 +67,45 @@ if SimpleCov.usable?
|
|
46
67
|
expect(SimpleCov::ArrayFilter.new(["sample.rb", "other_file.rb"])).to be_matches subject
|
47
68
|
end
|
48
69
|
|
70
|
+
it "doesn't match a parent directory with a new SimpleCov::ArrayFilter" do
|
71
|
+
parent_dir_name = File.basename(File.expand_path("..", File.dirname(__FILE__)))
|
72
|
+
expect(SimpleCov::ArrayFilter.new([parent_dir_name])).not_to be_matches subject
|
73
|
+
end
|
74
|
+
|
75
|
+
it "matches a new SimpleCov::ArrayFilter when /sample.rb/ is passed as array" do
|
76
|
+
expect(SimpleCov::ArrayFilter.new([/sample.rb/])).to be_matches subject
|
77
|
+
end
|
78
|
+
|
79
|
+
it "doesn't match a new SimpleCov::ArrayFilter when a file path different than /sample.rb/ is passed as array" do
|
80
|
+
expect(SimpleCov::ArrayFilter.new([/other_file.rb/])).not_to be_matches subject
|
81
|
+
end
|
82
|
+
|
83
|
+
it "matches a new SimpleCov::ArrayFilter when a block is passed as array and returns true" do
|
84
|
+
expect(SimpleCov::ArrayFilter.new([proc { true }])).to be_matches subject
|
85
|
+
end
|
86
|
+
|
87
|
+
it "doesn't match a new SimpleCov::ArrayFilter when a block that returns false is passed as array" do
|
88
|
+
expect(SimpleCov::ArrayFilter.new([proc { false }])).not_to be_matches subject
|
89
|
+
end
|
90
|
+
|
91
|
+
it "matches a new SimpleCov::ArrayFilter when a custom class that returns true is passed as array" do
|
92
|
+
filter = Class.new(SimpleCov::Filter) do
|
93
|
+
def matches?(_)
|
94
|
+
true
|
95
|
+
end
|
96
|
+
end.new(nil)
|
97
|
+
expect(SimpleCov::ArrayFilter.new([filter])).to be_matches subject
|
98
|
+
end
|
99
|
+
|
100
|
+
it "doesn't match a new SimpleCov::ArrayFilter when a custom class that returns false is passed as array" do
|
101
|
+
filter = Class.new(SimpleCov::Filter) do
|
102
|
+
def matches?(_)
|
103
|
+
false
|
104
|
+
end
|
105
|
+
end.new(nil)
|
106
|
+
expect(SimpleCov::ArrayFilter.new([filter])).not_to be_matches subject
|
107
|
+
end
|
108
|
+
|
49
109
|
context "with no filters set up and a basic source file in an array" do
|
50
110
|
before do
|
51
111
|
@prev_filters = SimpleCov.filters
|
@@ -94,5 +154,19 @@ if SimpleCov.usable?
|
|
94
154
|
expect(SimpleCov.filtered(subject)).to be_a SimpleCov::FileList
|
95
155
|
end
|
96
156
|
end
|
157
|
+
|
158
|
+
describe ".class_for_argument" do
|
159
|
+
it "returns SimpleCov::StringFilter for a string" do
|
160
|
+
expect(SimpleCov::Filter.class_for_argument("filestring")).to eq(SimpleCov::StringFilter)
|
161
|
+
end
|
162
|
+
|
163
|
+
it "returns SimpleCov::RegexFilter for a string" do
|
164
|
+
expect(SimpleCov::Filter.class_for_argument(/regex/)).to eq(SimpleCov::RegexFilter)
|
165
|
+
end
|
166
|
+
|
167
|
+
it "returns SimpleCov::RegexFilter for a string" do
|
168
|
+
expect(SimpleCov::Filter.class_for_argument(%w[file1 file2])).to eq(SimpleCov::ArrayFilter)
|
169
|
+
end
|
170
|
+
end
|
97
171
|
end
|
98
172
|
end
|
@@ -0,0 +1,103 @@
|
|
1
|
+
require "helper"
|
2
|
+
require "simplecov/lines_classifier"
|
3
|
+
|
4
|
+
describe SimpleCov::LinesClassifier do
|
5
|
+
describe "#classify" do
|
6
|
+
describe "relevant lines" do
|
7
|
+
it "determines code as relevant" do
|
8
|
+
classified_lines = subject.classify [
|
9
|
+
"module Foo",
|
10
|
+
" class Baz",
|
11
|
+
" def Bar",
|
12
|
+
" puts 'hi'",
|
13
|
+
" end",
|
14
|
+
" end",
|
15
|
+
"end",
|
16
|
+
]
|
17
|
+
|
18
|
+
expect(classified_lines.length).to eq 7
|
19
|
+
expect(classified_lines).to all be_relevant
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
describe "not-relevant lines" do
|
24
|
+
it "determines whitespace is not-relevant" do
|
25
|
+
classified_lines = subject.classify [
|
26
|
+
"",
|
27
|
+
" ",
|
28
|
+
"\t\t",
|
29
|
+
]
|
30
|
+
|
31
|
+
expect(classified_lines.length).to eq 3
|
32
|
+
expect(classified_lines).to all be_irrelevant
|
33
|
+
end
|
34
|
+
|
35
|
+
describe "comments" do
|
36
|
+
it "determines comments are not-relevant" do
|
37
|
+
classified_lines = subject.classify [
|
38
|
+
"#Comment",
|
39
|
+
" # Leading space comment",
|
40
|
+
"\t# Leading tab comment",
|
41
|
+
]
|
42
|
+
|
43
|
+
expect(classified_lines.length).to eq 3
|
44
|
+
expect(classified_lines).to all be_irrelevant
|
45
|
+
end
|
46
|
+
|
47
|
+
it "doesn't mistake interpolation as a comment" do
|
48
|
+
classified_lines = subject.classify [
|
49
|
+
'puts "#{var}"',
|
50
|
+
]
|
51
|
+
|
52
|
+
expect(classified_lines.length).to eq 1
|
53
|
+
expect(classified_lines).to all be_relevant
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
describe ":nocov: blocks" do
|
58
|
+
it "determines :nocov: blocks are not-relevant" do
|
59
|
+
classified_lines = subject.classify [
|
60
|
+
"# :nocov:",
|
61
|
+
"def hi",
|
62
|
+
"end",
|
63
|
+
"# :nocov:",
|
64
|
+
]
|
65
|
+
|
66
|
+
expect(classified_lines.length).to eq 4
|
67
|
+
expect(classified_lines).to all be_irrelevant
|
68
|
+
end
|
69
|
+
|
70
|
+
it "determines all lines after a non-closing :nocov: as not-relevant" do
|
71
|
+
classified_lines = subject.classify [
|
72
|
+
"# :nocov:",
|
73
|
+
"puts 'Not relevant'",
|
74
|
+
"# :nocov:",
|
75
|
+
"puts 'Relevant again'",
|
76
|
+
"puts 'Still relevant'",
|
77
|
+
"# :nocov:",
|
78
|
+
"puts 'Not relevant till the end'",
|
79
|
+
"puts 'Ditto'",
|
80
|
+
]
|
81
|
+
|
82
|
+
expect(classified_lines.length).to eq 8
|
83
|
+
|
84
|
+
expect(classified_lines[0..2]).to all be_irrelevant
|
85
|
+
expect(classified_lines[3..4]).to all be_relevant
|
86
|
+
expect(classified_lines[5..7]).to all be_irrelevant
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
RSpec::Matchers.define :be_relevant do
|
93
|
+
match do |actual|
|
94
|
+
actual == SimpleCov::LinesClassifier::RELEVANT
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
RSpec::Matchers.define :be_irrelevant do
|
99
|
+
match do |actual|
|
100
|
+
actual == SimpleCov::LinesClassifier::NOT_RELEVANT
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|