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.
@@ -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