simplecov 0.14.1 → 0.15.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|