simplecov 0.18.5 → 0.21.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.
@@ -27,6 +27,10 @@ module SimpleCov
27
27
  @coverage_statistics ||= compute_coverage_statistics
28
28
  end
29
29
 
30
+ def coverage_statistics_by_file
31
+ @coverage_statistics_by_file ||= compute_coverage_statistics_by_file
32
+ end
33
+
30
34
  # Returns the count of lines that have coverage
31
35
  def covered_lines
32
36
  coverage_statistics[:line]&.covered
@@ -100,14 +104,16 @@ module SimpleCov
100
104
 
101
105
  private
102
106
 
103
- def compute_coverage_statistics
104
- total_coverage_statistics = @files.each_with_object(line: [], branch: []) do |file, together|
105
- together[:line] << file.coverage_statistics[:line]
106
- together[:branch] << file.coverage_statistics[:branch] if SimpleCov.branch_coverage?
107
+ def compute_coverage_statistics_by_file
108
+ @files.each_with_object(line: [], branch: []) do |file, together|
109
+ together[:line] << file.coverage_statistics.fetch(:line)
110
+ together[:branch] << file.coverage_statistics.fetch(:branch) if SimpleCov.branch_coverage?
107
111
  end
112
+ end
108
113
 
109
- coverage_statistics = {line: CoverageStatistics.from(total_coverage_statistics[:line])}
110
- coverage_statistics[:branch] = CoverageStatistics.from(total_coverage_statistics[:branch]) if SimpleCov.branch_coverage?
114
+ def compute_coverage_statistics
115
+ coverage_statistics = {line: CoverageStatistics.from(coverage_statistics_by_file[:line])}
116
+ coverage_statistics[:branch] = CoverageStatistics.from(coverage_statistics_by_file[:branch]) if SimpleCov.branch_coverage?
111
117
  coverage_statistics
112
118
  end
113
119
  end
@@ -14,6 +14,7 @@ module SimpleCov
14
14
  #
15
15
  class Filter
16
16
  attr_reader :filter_argument
17
+
17
18
  def initialize(filter_argument)
18
19
  @filter_argument = filter_argument
19
20
  end
@@ -34,13 +35,14 @@ module SimpleCov
34
35
  end
35
36
 
36
37
  def self.class_for_argument(filter_argument)
37
- if filter_argument.is_a?(String)
38
+ case filter_argument
39
+ when String
38
40
  SimpleCov::StringFilter
39
- elsif filter_argument.is_a?(Regexp)
41
+ when Regexp
40
42
  SimpleCov::RegexFilter
41
- elsif filter_argument.is_a?(Array)
43
+ when Array
42
44
  SimpleCov::ArrayFilter
43
- elsif filter_argument.is_a?(Proc)
45
+ when Proc
44
46
  SimpleCov::BlockFilter
45
47
  else
46
48
  raise ArgumentError, "You have provided an unrecognized filter type"
@@ -50,7 +52,7 @@ module SimpleCov
50
52
 
51
53
  class StringFilter < SimpleCov::Filter
52
54
  # Returns true when the given source file's filename matches the
53
- # string configured when initializing this Filter with StringFilter.new('somestring)
55
+ # string configured when initializing this Filter with StringFilter.new('somestring')
54
56
  def matches?(source_file)
55
57
  source_file.project_filename.include?(filter_argument)
56
58
  end
@@ -6,5 +6,5 @@ module SimpleCov
6
6
  end
7
7
  end
8
8
 
9
- require "simplecov/formatter/simple_formatter"
10
- require "simplecov/formatter/multi_formatter"
9
+ require_relative "formatter/simple_formatter"
10
+ require_relative "formatter/multi_formatter"
@@ -6,12 +6,10 @@ module SimpleCov
6
6
  module InstanceMethods
7
7
  def format(result)
8
8
  formatters.map do |formatter|
9
- begin
10
- formatter.new.format(result)
11
- rescue StandardError => e
12
- warn("Formatter #{formatter} failed with #{e.class}: #{e.message} (#{e.backtrace.first})")
13
- nil
14
- end
9
+ formatter.new.format(result)
10
+ rescue StandardError => e
11
+ warn("Formatter #{formatter} failed with #{e.class}: #{e.message} (#{e.backtrace.first})")
12
+ nil
15
13
  end
16
14
  end
17
15
  end
@@ -27,7 +25,7 @@ module SimpleCov
27
25
 
28
26
  def self.[](*args)
29
27
  warn "#{Kernel.caller.first}: [DEPRECATION] ::[] is deprecated. Use ::new instead."
30
- new(Array([*args]))
28
+ new(Array(args))
31
29
  end
32
30
  end
33
31
  end
@@ -13,18 +13,18 @@ module SimpleCov
13
13
  WHITESPACE_OR_COMMENT_LINE = Regexp.union(WHITESPACE_LINE, COMMENT_LINE)
14
14
 
15
15
  def self.no_cov_line
16
- /^(\s*)#(\s*)(\:#{SimpleCov.nocov_token}\:)/o
16
+ /^(\s*)#(\s*)(:#{SimpleCov.nocov_token}:)/o
17
17
  end
18
18
 
19
19
  def self.no_cov_line?(line)
20
- line =~ no_cov_line
20
+ no_cov_line.match?(line)
21
21
  rescue ArgumentError
22
22
  # E.g., line contains an invalid byte sequence in UTF-8
23
23
  false
24
24
  end
25
25
 
26
26
  def self.whitespace_line?(line)
27
- line =~ WHITESPACE_OR_COMMENT_LINE
27
+ WHITESPACE_OR_COMMENT_LINE.match?(line)
28
28
  rescue ArgumentError
29
29
  # E.g., line contains an invalid byte sequence in UTF-8
30
30
  false
@@ -1,4 +1,4 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  ENV["SIMPLECOV_NO_DEFAULTS"] = "yes, no defaults"
4
- require "simplecov"
4
+ require_relative "../simplecov"
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Process
4
+ class << self
5
+ def fork_with_simplecov(&block)
6
+ if defined?(SimpleCov) && SimpleCov.running
7
+ fork_without_simplecov do
8
+ SimpleCov.at_fork.call(Process.pid)
9
+ block.call if block_given?
10
+ end
11
+ else
12
+ fork_without_simplecov(&block)
13
+ end
14
+ end
15
+
16
+ alias fork_without_simplecov fork
17
+ alias fork fork_with_simplecov
18
+ end
19
+ end
@@ -20,14 +20,16 @@ module SimpleCov
20
20
  # Explicitly set the command name that was used for this coverage result. Defaults to SimpleCov.command_name
21
21
  attr_writer :command_name
22
22
 
23
- def_delegators :files, :covered_percent, :covered_percentages, :least_covered_file, :covered_strength, :covered_lines, :missed_lines, :total_branches, :covered_branches, :missed_branches, :coverage_statistics
23
+ def_delegators :files, :covered_percent, :covered_percentages, :least_covered_file, :covered_strength, :covered_lines, :missed_lines, :total_branches, :covered_branches, :missed_branches, :coverage_statistics, :coverage_statistics_by_file
24
24
  def_delegator :files, :lines_of_code, :total_lines
25
25
 
26
26
  # Initialize a new SimpleCov::Result from given Coverage.result (a Hash of filenames each containing an array of
27
27
  # coverage data)
28
- def initialize(original_result)
29
- result = adapt_result(original_result)
28
+ def initialize(original_result, command_name: nil, created_at: nil)
29
+ result = original_result
30
30
  @original_result = result.freeze
31
+ @command_name = command_name
32
+ @created_at = created_at
31
33
  @files = SimpleCov::FileList.new(result.map do |filename, coverage|
32
34
  SimpleCov::SourceFile.new(filename, JSON.parse(JSON.dump(coverage))) if File.file?(filename)
33
35
  end.compact.sort_by(&:filename))
@@ -72,41 +74,12 @@ module SimpleCov
72
74
 
73
75
  # Loads a SimpleCov::Result#to_hash dump
74
76
  def self.from_hash(hash)
75
- command_name, data = hash.first
76
-
77
- result = SimpleCov::Result.new(data["coverage"])
78
-
79
- result.command_name = command_name
80
- result.created_at = Time.at(data["timestamp"])
81
- result
82
- end
83
-
84
- private
85
-
86
- # We changed the format of the raw result data in simplecov, as people are likely
87
- # to have "old" resultsets lying around (but not too old so that they're still
88
- # considered we can adapt them).
89
- # See https://github.com/colszowka/simplecov/pull/824#issuecomment-576049747
90
- def adapt_result(result)
91
- if pre_simplecov_0_18_result?(result)
92
- adapt_pre_simplecov_0_18_result(result)
93
- else
94
- result
77
+ hash.map do |command_name, data|
78
+ new(data.fetch("coverage"), command_name: command_name, created_at: Time.at(data["timestamp"]))
95
79
  end
96
80
  end
97
81
 
98
- # pre 0.18 coverage data pointed from file directly to an array of line coverage
99
- def pre_simplecov_0_18_result?(result)
100
- _key, data = result.first
101
-
102
- data.is_a?(Array)
103
- end
104
-
105
- def adapt_pre_simplecov_0_18_result(result)
106
- result.map do |file_path, line_coverage_data|
107
- [file_path, {"lines" => line_coverage_data}]
108
- end.to_h
109
- end
82
+ private
110
83
 
111
84
  def coverage
112
85
  keys = original_result.keys & filenames
@@ -19,86 +19,130 @@ module SimpleCov
19
19
  File.join(SimpleCov.coverage_path, ".resultset.json.lock")
20
20
  end
21
21
 
22
- # Loads the cached resultset from JSON and returns it as a Hash,
23
- # caching it for subsequent accesses.
24
- def resultset
25
- @resultset ||= begin
26
- data = stored_data
27
- if data
28
- begin
29
- JSON.parse(data) || {}
30
- rescue StandardError
31
- {}
32
- end
33
- else
34
- {}
35
- end
22
+ def merge_and_store(*file_paths, ignore_timeout: false)
23
+ result = merge_results(*file_paths, ignore_timeout: ignore_timeout)
24
+ store_result(result) if result
25
+ result
26
+ end
27
+
28
+ def merge_results(*file_paths, ignore_timeout: false)
29
+ # It is intentional here that files are only read in and parsed one at a time.
30
+ #
31
+ # In big CI setups you might deal with 100s of CI jobs and each one producing Megabytes
32
+ # of data. Reading them all in easily produces Gigabytes of memory consumption which
33
+ # we want to avoid.
34
+ #
35
+ # For similar reasons a SimpleCov::Result is only created in the end as that'd create
36
+ # even more data especially when it also reads in all source files.
37
+ initial_memo = valid_results(file_paths.shift, ignore_timeout: ignore_timeout)
38
+
39
+ command_names, coverage = file_paths.reduce(initial_memo) do |memo, file_path|
40
+ merge_coverage(memo, valid_results(file_path, ignore_timeout: ignore_timeout))
36
41
  end
42
+
43
+ create_result(command_names, coverage)
37
44
  end
38
45
 
39
- # Returns the contents of the resultset cache as a string or if the file is missing or empty nil
40
- def stored_data
41
- synchronize_resultset do
42
- return unless File.exist?(resultset_path)
46
+ def valid_results(file_path, ignore_timeout: false)
47
+ results = parse_file(file_path)
48
+ merge_valid_results(results, ignore_timeout: ignore_timeout)
49
+ end
43
50
 
44
- data = File.read(resultset_path)
45
- return if data.nil? || data.length < 2
51
+ def parse_file(path)
52
+ data = read_file(path)
53
+ parse_json(data)
54
+ end
46
55
 
47
- data
48
- end
56
+ def read_file(path)
57
+ return unless File.exist?(path)
58
+
59
+ data = File.read(path)
60
+ return if data.nil? || data.length < 2
61
+
62
+ data
63
+ end
64
+
65
+ def parse_json(content)
66
+ return {} unless content
67
+
68
+ JSON.parse(content) || {}
69
+ rescue StandardError
70
+ warn "[SimpleCov]: Warning! Parsing JSON content of resultset file failed"
71
+ {}
49
72
  end
50
73
 
51
- # Gets the resultset hash and re-creates all included instances
52
- # of SimpleCov::Result from that.
53
- # All results that are above the SimpleCov.merge_timeout will be
54
- # dropped. Returns an array of SimpleCov::Result items.
55
- def results
56
- results = []
57
- resultset.each do |command_name, data|
58
- result = SimpleCov::Result.from_hash(command_name => data)
59
- # Only add result if the timeout is above the configured threshold
60
- results << result if (Time.now - result.created_at) < SimpleCov.merge_timeout
74
+ def merge_valid_results(results, ignore_timeout: false)
75
+ results = results.select { |_command_name, data| within_merge_timeout?(data) } unless ignore_timeout
76
+
77
+ command_plus_coverage = results.map do |command_name, data|
78
+ [[command_name], adapt_result(data.fetch("coverage"))]
61
79
  end
62
- results
80
+
81
+ # one file itself _might_ include multiple test runs
82
+ merge_coverage(*command_plus_coverage)
63
83
  end
64
84
 
65
- def merge_and_store(*results)
66
- result = merge_results(*results)
67
- store_result(result) if result
68
- result
85
+ def within_merge_timeout?(data)
86
+ time_since_result_creation(data) < SimpleCov.merge_timeout
69
87
  end
70
88
 
71
- # Merge two or more SimpleCov::Results into a new one with merged
72
- # coverage data and the command_name for the result consisting of a join
73
- # on all source result's names
74
- def merge_results(*results)
75
- parsed_results = JSON.parse(JSON.dump(results.map(&:original_result)))
76
- combined_result = SimpleCov::Combine::ResultsCombiner.combine(*parsed_results)
77
- result = SimpleCov::Result.new(combined_result)
78
- # Specify the command name
79
- result.command_name = results.map(&:command_name).sort.join(", ")
80
- result
89
+ def time_since_result_creation(data)
90
+ Time.now - Time.at(data.fetch("timestamp"))
91
+ end
92
+
93
+ def create_result(command_names, coverage)
94
+ return nil unless coverage
95
+
96
+ command_name = command_names.reject(&:empty?).sort.join(", ")
97
+ SimpleCov::Result.new(coverage, command_name: command_name)
98
+ end
99
+
100
+ def merge_coverage(*results)
101
+ return [[""], nil] if results.empty?
102
+ return results.first if results.size == 1
103
+
104
+ results.reduce do |(memo_command, memo_coverage), (command, coverage)|
105
+ # timestamp is dropped here, which is intentional (we merge it, it gets a new time stamp as of now)
106
+ merged_coverage = Combine.combine(Combine::ResultsCombiner, memo_coverage, coverage)
107
+ merged_command = memo_command + command
108
+
109
+ [merged_command, merged_coverage]
110
+ end
81
111
  end
82
112
 
83
113
  #
84
- # Gets all SimpleCov::Results from cache, merges them and produces a new
114
+ # Gets all SimpleCov::Results stored in resultset, merges them and produces a new
85
115
  # SimpleCov::Result with merged coverage data and the command_name
86
116
  # for the result consisting of a join on all source result's names
87
- #
88
117
  def merged_result
89
- merge_results(*results)
118
+ # conceptually this is just doing `merge_results(resultset_path)`
119
+ # it's more involved to make syre `synchronize_resultset` is only used around reading
120
+ resultset_hash = read_resultset
121
+ command_names, coverage = merge_valid_results(resultset_hash)
122
+
123
+ create_result(command_names, coverage)
124
+ end
125
+
126
+ def read_resultset
127
+ resultset_content =
128
+ synchronize_resultset do
129
+ read_file(resultset_path)
130
+ end
131
+
132
+ parse_json(resultset_content)
90
133
  end
91
134
 
92
135
  # Saves the given SimpleCov::Result in the resultset cache
93
136
  def store_result(result)
94
137
  synchronize_resultset do
95
138
  # Ensure we have the latest, in case it was already cached
96
- clear_resultset
97
- new_set = resultset
139
+ new_resultset = read_resultset
140
+
141
+ # A single result only ever has one command_name, see `SimpleCov::Result#to_hash`
98
142
  command_name, data = result.to_hash.first
99
- new_set[command_name] = data
143
+ new_resultset[command_name] = data
100
144
  File.open(resultset_path, "w+") do |f_|
101
- f_.puts JSON.pretty_generate(new_set)
145
+ f_.puts JSON.pretty_generate(new_resultset)
102
146
  end
103
147
  end
104
148
  true
@@ -121,9 +165,29 @@ module SimpleCov
121
165
  end
122
166
  end
123
167
 
124
- # Clear out the previously cached .resultset
125
- def clear_resultset
126
- @resultset = nil
168
+ # We changed the format of the raw result data in simplecov, as people are likely
169
+ # to have "old" resultsets lying around (but not too old so that they're still
170
+ # considered we can adapt them).
171
+ # See https://github.com/simplecov-ruby/simplecov/pull/824#issuecomment-576049747
172
+ def adapt_result(result)
173
+ if pre_simplecov_0_18_result?(result)
174
+ adapt_pre_simplecov_0_18_result(result)
175
+ else
176
+ result
177
+ end
178
+ end
179
+
180
+ # pre 0.18 coverage data pointed from file directly to an array of line coverage
181
+ def pre_simplecov_0_18_result?(result)
182
+ _key, data = result.first
183
+
184
+ data.is_a?(Array)
185
+ end
186
+
187
+ def adapt_pre_simplecov_0_18_result(result)
188
+ result.transform_values do |line_coverage_data|
189
+ {"lines" => line_coverage_data}
190
+ end
127
191
  end
128
192
  end
129
193
  end
@@ -56,7 +56,7 @@ module SimpleCov
56
56
  # Returns true if this line was skipped, false otherwise. Lines are skipped if they are wrapped with
57
57
  # # :nocov: comment lines.
58
58
  def skipped?
59
- !!skipped
59
+ skipped
60
60
  end
61
61
 
62
62
  # The status of this line - either covered, missed, skipped or never. Useful i.e. for direct use
@@ -5,12 +5,14 @@ module SimpleCov
5
5
  # Select the files that related to working scope directory of SimpleCov
6
6
  #
7
7
  module UselessResultsRemover
8
- ROOT_REGX = /\A#{Regexp.escape(SimpleCov.root + File::SEPARATOR)}/io.freeze
9
-
10
8
  def self.call(coverage_result)
11
9
  coverage_result.select do |path, _coverage|
12
- path =~ ROOT_REGX
10
+ path =~ root_regx
13
11
  end
14
12
  end
13
+
14
+ def self.root_regx
15
+ @root_regx ||= /\A#{Regexp.escape(SimpleCov.root + File::SEPARATOR)}/i.freeze
16
+ end
15
17
  end
16
18
  end