simplecov 0.14.1 → 0.15.1

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.
@@ -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.1".freeze
25
3
  end
data/lib/simplecov.rb CHANGED
@@ -55,7 +55,7 @@ module SimpleCov
55
55
 
56
56
  #
57
57
  # Finds files that were to be tracked but were not loaded and initializes
58
- # their coverage to zero.
58
+ # the line-by-line coverage to zero (if relevant) or nil (comments / whitespace etc).
59
59
  #
60
60
  def add_not_loaded_files(result)
61
61
  if tracked_files
@@ -63,7 +63,7 @@ module SimpleCov
63
63
  Dir[tracked_files].each do |file|
64
64
  absolute = File.expand_path(file)
65
65
 
66
- result[absolute] ||= [0] * File.foreach(absolute).count
66
+ result[absolute] ||= LinesClassifier.new.classify(File.foreach(absolute))
67
67
  end
68
68
  end
69
69
 
@@ -75,23 +75,21 @@ module SimpleCov
75
75
  # from cache using SimpleCov::ResultMerger if use_merging is activated (default)
76
76
  #
77
77
  def result
78
- # Ensure the variable is defined to avoid ruby warnings
79
- @result = nil unless defined?(@result)
78
+ return @result if result?
80
79
 
81
80
  # Collect our coverage result
82
- if running && !result?
81
+ if running
83
82
  @result = SimpleCov::Result.new add_not_loaded_files(Coverage.result)
84
83
  end
85
84
 
86
85
  # If we're using merging of results, store the current result
87
- # first, then merge the results and return those
86
+ # first (if there is one), then merge the results and return those
88
87
  if use_merging
89
88
  SimpleCov::ResultMerger.store_result(@result) if result?
90
-
91
- SimpleCov::ResultMerger.merged_result
92
- else
93
- @result
89
+ @result = SimpleCov::ResultMerger.merged_result
94
90
  end
91
+
92
+ @result
95
93
  ensure
96
94
  self.running = false
97
95
  end
@@ -158,6 +156,13 @@ module SimpleCov
158
156
  false
159
157
  end
160
158
  end
159
+
160
+ #
161
+ # Clear out the previously cached .result. Primarily useful in testing
162
+ #
163
+ def clear_result
164
+ @result = nil
165
+ end
161
166
  end
162
167
  end
163
168
 
@@ -172,6 +177,7 @@ require "simplecov/result"
172
177
  require "simplecov/filter"
173
178
  require "simplecov/formatter"
174
179
  require "simplecov/last_run"
180
+ require "simplecov/lines_classifier"
175
181
  require "simplecov/raw_coverage"
176
182
  require "simplecov/result_merger"
177
183
  require "simplecov/command_guesser"
@@ -0,0 +1,41 @@
1
+ require "helper"
2
+
3
+ if SimpleCov.usable?
4
+ describe SimpleCov do
5
+ skip "requires the default configuration" if ENV["SIMPLECOV_NO_DEFAULTS"]
6
+
7
+ context "profiles" do
8
+ let(:config_class) do
9
+ Class.new do
10
+ include SimpleCov::Configuration
11
+
12
+ def load_profile(name)
13
+ configure(&SimpleCov.profiles[name.to_sym])
14
+ end
15
+ end
16
+ end
17
+
18
+ let(:config) { config_class.new }
19
+
20
+ def filtered?(config, filename)
21
+ path = File.join(SimpleCov.root, filename)
22
+ file = SimpleCov::SourceFile.new(path, [nil, 1, 1, 1, nil, nil, 1, 0, nil, nil])
23
+ config.filters.any? { |filter| filter.matches?(file) }
24
+ end
25
+
26
+ it "provides a sensible test_frameworks profile" do
27
+ config.load_profile(:test_frameworks)
28
+ expect(filtered?(config, "foo.rb")).not_to be
29
+ expect(filtered?(config, "test/foo.rb")).to be
30
+ expect(filtered?(config, "spec/bar.rb")).to be
31
+ end
32
+
33
+ it "provides a sensible rails profile" do
34
+ config.load_profile(:rails)
35
+ expect(filtered?(config, "app/models/user.rb")).not_to be
36
+ expect(filtered?(config, "db/schema.rb")).to be
37
+ expect(filtered?(config, "config/environment.rb")).to be
38
+ end
39
+ end
40
+ end
41
+ 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,49 @@ if SimpleCov.usable?
94
154
  expect(SimpleCov.filtered(subject)).to be_a SimpleCov::FileList
95
155
  end
96
156
  end
157
+
158
+ context "with the default configuration" do
159
+ skip "requires the default configuration" if ENV["SIMPLECOV_NO_DEFAULTS"]
160
+
161
+ def a_file(path)
162
+ path = File.join(SimpleCov.root, path) unless path.start_with?("/")
163
+ SimpleCov::SourceFile.new(path, [nil, 1, 1, 1, nil, nil, 1, 0, nil, nil])
164
+ end
165
+
166
+ context "inside the project" do
167
+ it "doesn't filter" do
168
+ expect(SimpleCov.filtered([a_file("foo.rb")]).count).to eq(1)
169
+ end
170
+
171
+ it "filters vendor/bundle" do
172
+ expect(SimpleCov.filtered([a_file("vendor/bundle/foo.rb")]).count).to eq(0)
173
+ end
174
+ end
175
+
176
+ context "outside the project" do
177
+ it "filters" do
178
+ expect(SimpleCov.filtered([a_file("/other/path/foo.rb")]).count).to eq(0)
179
+ end
180
+
181
+ it "filters even if the sibling directory has SimpleCov.root as a prefix" do
182
+ sibling_dir = SimpleCov.root + "_cache"
183
+ expect(SimpleCov.filtered([a_file(sibling_dir + "/foo.rb")]).count).to eq(0)
184
+ end
185
+ end
186
+ end
187
+
188
+ describe ".class_for_argument" do
189
+ it "returns SimpleCov::StringFilter for a string" do
190
+ expect(SimpleCov::Filter.class_for_argument("filestring")).to eq(SimpleCov::StringFilter)
191
+ end
192
+
193
+ it "returns SimpleCov::RegexFilter for a string" do
194
+ expect(SimpleCov::Filter.class_for_argument(/regex/)).to eq(SimpleCov::RegexFilter)
195
+ end
196
+
197
+ it "returns SimpleCov::RegexFilter for a string" do
198
+ expect(SimpleCov::Filter.class_for_argument(%w[file1 file2])).to eq(SimpleCov::ArrayFilter)
199
+ end
200
+ end
97
201
  end
98
202
  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
@@ -1,10 +1,16 @@
1
1
  require "helper"
2
+ require "tempfile"
3
+ require "timeout"
2
4
 
3
5
  if SimpleCov.usable?
4
6
  describe SimpleCov::ResultMerger do
7
+ before do
8
+ SimpleCov::ResultMerger.clear_resultset
9
+ File.delete(SimpleCov::ResultMerger.resultset_path) if File.exist?(SimpleCov::ResultMerger.resultset_path)
10
+ end
11
+
5
12
  describe "with two faked coverage resultsets" do
6
13
  before do
7
- SimpleCov.use_merging true
8
14
  @resultset1 = {
9
15
  source_fixture("sample.rb") => [nil, 1, 1, 1, nil, nil, 1, 1, nil, nil],
10
16
  source_fixture("app/models/user.rb") => [nil, 1, 1, 1, nil, nil, 1, 0, nil, nil],
@@ -56,7 +62,7 @@ if SimpleCov.usable?
56
62
  end
57
63
 
58
64
  it "returns a hash containing keys ['result1' and 'result2'] for resultset" do
59
- expect(SimpleCov::ResultMerger.resultset.keys.sort).to eq %w(result1 result2)
65
+ expect(SimpleCov::ResultMerger.resultset.keys.sort).to eq %w[result1 result2]
60
66
  end
61
67
 
62
68
  it "returns proper values for merged_result" do
@@ -73,15 +79,92 @@ if SimpleCov.usable?
73
79
  expect(SimpleCov::ResultMerger.results.length).to eq(1)
74
80
  end
75
81
  end
82
+ end
83
+ end
84
+ end
85
+
86
+ describe ".store_result" do
87
+ it "refreshes the resultset" do
88
+ set = SimpleCov::ResultMerger.resultset
89
+ SimpleCov::ResultMerger.store_result({})
90
+ new_set = SimpleCov::ResultMerger.resultset
91
+ expect(new_set).not_to be(set)
92
+ end
76
93
 
77
- context "with merging disabled" do
78
- before { SimpleCov.use_merging false }
94
+ it "persists to disk" do
95
+ SimpleCov::ResultMerger.store_result("a" => [1])
96
+ SimpleCov::ResultMerger.clear_resultset
97
+ new_set = SimpleCov::ResultMerger.resultset
98
+ expect(new_set).to eq("a" => [1])
99
+ end
100
+
101
+ it "synchronizes writes" do
102
+ expect(SimpleCov::ResultMerger).to receive(:synchronize_resultset)
103
+ SimpleCov::ResultMerger.store_result({})
104
+ end
105
+ end
79
106
 
80
- it "returns nil for SimpleCov.result" do
81
- expect(SimpleCov.result).to be_nil
107
+ describe ".resultset" do
108
+ it "caches" do
109
+ set = SimpleCov::ResultMerger.resultset
110
+ new_set = SimpleCov::ResultMerger.resultset
111
+ expect(new_set).to be(set)
112
+ end
113
+
114
+ it "synchronizes reads" do
115
+ expect(SimpleCov::ResultMerger).to receive(:synchronize_resultset)
116
+ SimpleCov::ResultMerger.resultset
117
+ end
118
+ end
119
+
120
+ describe ".synchronize_resultset" do
121
+ it "is reentrant (i.e. doesn't block its own process)" do
122
+ # without @resultset_locked, this spec would fail and
123
+ # `.store_result` wouldn't work
124
+ expect do
125
+ Timeout.timeout(1) do
126
+ SimpleCov::ResultMerger.synchronize_resultset do
127
+ SimpleCov::ResultMerger.synchronize_resultset do
128
+ end
82
129
  end
83
130
  end
131
+ end.not_to raise_error
132
+ end
133
+
134
+ it "blocks other processes" do
135
+ file = Tempfile.new("foo")
136
+
137
+ other_process = open("|ruby -e " + Shellwords.escape(<<-CODE) + " 2>/dev/null")
138
+ require "simplecov"
139
+ SimpleCov.coverage_dir(#{SimpleCov.coverage_dir.inspect})
140
+
141
+ # ensure the parent process has enough time to get a
142
+ # lock before we do
143
+ sleep 0.5
144
+
145
+ $stdout.sync = true
146
+ puts "running" # see `sleep`s in parent process
147
+
148
+ SimpleCov::ResultMerger.synchronize_resultset do
149
+ File.open(#{file.path.inspect}, "a") { |f| f.write("process 2\n") }
150
+ end
151
+ CODE
152
+
153
+ SimpleCov::ResultMerger.synchronize_resultset do
154
+ # wait until the child process is going, and then wait some more
155
+ # so we can be sure it blocks on the lock we already have.
156
+ sleep 0.1 until other_process.gets == "running\n"
157
+ sleep 1
158
+
159
+ # despite the sleeps, this will be written first since we got
160
+ # the first lock
161
+ File.open(file.path, "a") { |f| f.write("process 1\n") }
84
162
  end
163
+
164
+ # wait for it to finish
165
+ other_process.gets
166
+
167
+ expect(file.read).to eq("process 1\nprocess 2\n")
85
168
  end
86
169
  end
87
170
  end