simplecov 0.14.1 → 0.15.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
- !(src.filename =~ root_filter)
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 "/config/"
29
- add_filter "/db/"
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(app/jobs app/workers)
36
+ add_group "Jobs", %w[app/jobs app/workers]
37
37
  add_group "Libraries", "lib"
38
38
 
39
39
  track_files "{app,lib}/**/*.rb"
@@ -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(&:+) / lines_of_code)
56
+ Float(map { |f| f.covered_strength * f.lines_of_code }.inject(:+) / lines_of_code)
57
57
  end
58
58
  end
59
59
  end
@@ -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.filename =~ /#{filter_argument}/)
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
- # Returns true if any of the file paths passed in the given array matches the string
47
- # configured when initializing this Filter with StringFilter.new(['some/path', 'other/path'])
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.filename =~ /#{arg}/
85
+ arg.matches?(source_files_list)
51
86
  end
52
87
  end
53
88
  end
@@ -6,7 +6,7 @@ module SimpleCov
6
6
  class SimpleFormatter
7
7
  # Takes a SimpleCov::Result and generates a string out of it
8
8
  def format(result)
9
- output = ""
9
+ output = "".dup
10
10
  result.groups.each do |name, files|
11
11
  output << "Group: #{name}\n"
12
12
  output << "=" * 40
@@ -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
- if stored_data
23
- begin
24
- JSON.parse(stored_data)
25
- rescue
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
- return unless File.exist?(resultset_path)
36
- data = File.read(resultset_path)
37
- return if data.nil? || data.length < 2
38
- data
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
- File.open(resultset_writelock, "w+") do |f|
80
- f.flock(File::LOCK_EX)
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 =~ /^([\s]*)#([\s]*)(\:#{SimpleCov.nocov_token}\:)/
183
+ if line.src =~ SimpleCov::LinesClassifier.no_cov_line
179
184
  skipping = !skipping
180
185
  line.skipped!
181
186
  elsif skipping
@@ -1,25 +1,3 @@
1
1
  module SimpleCov
2
- version = "0.14.1"
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
@@ -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