simplecov 0.18.5 → 0.21.2

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