simplecov 0.17.1 → 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.
Files changed (48) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +100 -434
  3. data/README.md +375 -93
  4. data/doc/alternate-formatters.md +16 -1
  5. data/doc/commercial-services.md +5 -0
  6. data/lib/minitest/simplecov_plugin.rb +15 -0
  7. data/lib/simplecov.rb +294 -128
  8. data/lib/simplecov/combine.rb +30 -0
  9. data/lib/simplecov/combine/branches_combiner.rb +32 -0
  10. data/lib/simplecov/combine/files_combiner.rb +24 -0
  11. data/lib/simplecov/combine/lines_combiner.rb +43 -0
  12. data/lib/simplecov/combine/results_combiner.rb +60 -0
  13. data/lib/simplecov/command_guesser.rb +6 -3
  14. data/lib/simplecov/configuration.rb +191 -15
  15. data/lib/simplecov/coverage_statistics.rb +56 -0
  16. data/lib/simplecov/default_formatter.rb +20 -0
  17. data/lib/simplecov/defaults.rb +14 -13
  18. data/lib/simplecov/exit_codes.rb +5 -0
  19. data/lib/simplecov/exit_codes/exit_code_handling.rb +29 -0
  20. data/lib/simplecov/exit_codes/maximum_coverage_drop_check.rb +73 -0
  21. data/lib/simplecov/exit_codes/minimum_coverage_by_file_check.rb +54 -0
  22. data/lib/simplecov/exit_codes/minimum_overall_coverage_check.rb +53 -0
  23. data/lib/simplecov/file_list.rb +72 -13
  24. data/lib/simplecov/filter.rb +9 -6
  25. data/lib/simplecov/formatter.rb +2 -2
  26. data/lib/simplecov/formatter/multi_formatter.rb +5 -7
  27. data/lib/simplecov/formatter/simple_formatter.rb +4 -4
  28. data/lib/simplecov/last_run.rb +3 -1
  29. data/lib/simplecov/lines_classifier.rb +5 -5
  30. data/lib/simplecov/no_defaults.rb +1 -1
  31. data/lib/simplecov/process.rb +19 -0
  32. data/lib/simplecov/profiles.rb +9 -7
  33. data/lib/simplecov/result.rb +18 -12
  34. data/lib/simplecov/result_adapter.rb +30 -0
  35. data/lib/simplecov/result_merger.rb +130 -59
  36. data/lib/simplecov/simulate_coverage.rb +29 -0
  37. data/lib/simplecov/source_file.rb +272 -126
  38. data/lib/simplecov/source_file/branch.rb +84 -0
  39. data/lib/simplecov/source_file/line.rb +72 -0
  40. data/lib/simplecov/useless_results_remover.rb +18 -0
  41. data/lib/simplecov/version.rb +1 -1
  42. metadata +44 -158
  43. data/CONTRIBUTING.md +0 -51
  44. data/ISSUE_TEMPLATE.md +0 -23
  45. data/lib/simplecov/jruby_fix.rb +0 -44
  46. data/lib/simplecov/railtie.rb +0 -9
  47. data/lib/simplecov/railties/tasks.rake +0 -13
  48. data/lib/simplecov/raw_coverage.rb +0 -41
@@ -14,11 +14,12 @@ 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
20
21
 
21
- def matches?(_)
22
+ def matches?(_source_file)
22
23
  raise "The base filter class is not intended for direct use"
23
24
  end
24
25
 
@@ -29,17 +30,19 @@ module SimpleCov
29
30
 
30
31
  def self.build_filter(filter_argument)
31
32
  return filter_argument if filter_argument.is_a?(SimpleCov::Filter)
33
+
32
34
  class_for_argument(filter_argument).new(filter_argument)
33
35
  end
34
36
 
35
37
  def self.class_for_argument(filter_argument)
36
- if filter_argument.is_a?(String)
38
+ case filter_argument
39
+ when String
37
40
  SimpleCov::StringFilter
38
- elsif filter_argument.is_a?(Regexp)
41
+ when Regexp
39
42
  SimpleCov::RegexFilter
40
- elsif filter_argument.is_a?(Array)
43
+ when Array
41
44
  SimpleCov::ArrayFilter
42
- elsif filter_argument.is_a?(Proc)
45
+ when Proc
43
46
  SimpleCov::BlockFilter
44
47
  else
45
48
  raise ArgumentError, "You have provided an unrecognized filter type"
@@ -49,7 +52,7 @@ module SimpleCov
49
52
 
50
53
  class StringFilter < SimpleCov::Filter
51
54
  # Returns true when the given source file's filename matches the
52
- # string configured when initializing this Filter with StringFilter.new('somestring)
55
+ # string configured when initializing this Filter with StringFilter.new('somestring')
53
56
  def matches?(source_file)
54
57
  source_file.project_filename.include?(filter_argument)
55
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 => e
12
- STDERR.puts("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
@@ -1,14 +1,14 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- #
4
- # A ridiculously simple formatter for SimpleCov results.
5
- #
6
3
  module SimpleCov
7
4
  module Formatter
5
+ #
6
+ # A ridiculously simple formatter for SimpleCov results.
7
+ #
8
8
  class SimpleFormatter
9
9
  # Takes a SimpleCov::Result and generates a string out of it
10
10
  def format(result)
11
- output = "".dup
11
+ output = +""
12
12
  result.groups.each do |name, files|
13
13
  output << "Group: #{name}\n"
14
14
  output << "=" * 40
@@ -11,9 +11,11 @@ module SimpleCov
11
11
 
12
12
  def read
13
13
  return nil unless File.exist?(last_run_path)
14
+
14
15
  json = File.read(last_run_path)
15
16
  return nil if json.strip.empty?
16
- JSON.parse(json)
17
+
18
+ JSON.parse(json, symbolize_names: true)
17
19
  end
18
20
 
19
21
  def write(json)
@@ -8,23 +8,23 @@ module SimpleCov
8
8
  RELEVANT = 0
9
9
  NOT_RELEVANT = nil
10
10
 
11
- WHITESPACE_LINE = /^\s*$/
12
- COMMENT_LINE = /^\s*#/
11
+ WHITESPACE_LINE = /^\s*$/.freeze
12
+ COMMENT_LINE = /^\s*#/.freeze
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
@@ -1,13 +1,13 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- #
4
- # Profiles are SimpleCov configuration procs that can be easily
5
- # loaded using SimpleCov.start :rails and defined using
6
- # SimpleCov.profiles.define :foo do
7
- # # SimpleCov configuration here, same as in SimpleCov.configure
8
- # end
9
- #
10
3
  module SimpleCov
4
+ #
5
+ # Profiles are SimpleCov configuration procs that can be easily
6
+ # loaded using SimpleCov.start :rails and defined using
7
+ # SimpleCov.profiles.define :foo do
8
+ # # SimpleCov configuration here, same as in SimpleCov.configure
9
+ # end
10
+ #
11
11
  class Profiles < Hash
12
12
  #
13
13
  # Define a SimpleCov profile:
@@ -18,6 +18,7 @@ module SimpleCov
18
18
  def define(name, &blk)
19
19
  name = name.to_sym
20
20
  raise "SimpleCov Profile '#{name}' is already defined" unless self[name].nil?
21
+
21
22
  self[name] = blk
22
23
  end
23
24
 
@@ -27,6 +28,7 @@ module SimpleCov
27
28
  def load(name)
28
29
  name = name.to_sym
29
30
  raise "Could not find SimpleCov Profile called '#{name}'" unless key?(name)
31
+
30
32
  SimpleCov.configure(&self[name])
31
33
  end
32
34
  end
@@ -5,7 +5,7 @@ require "forwardable"
5
5
 
6
6
  module SimpleCov
7
7
  #
8
- # A simplecov code coverage result, initialized from the Hash Ruby 1.9's built-in coverage
8
+ # A simplecov code coverage result, initialized from the Hash Ruby's built-in coverage
9
9
  # library generates (Coverage.result).
10
10
  #
11
11
  class Result
@@ -20,15 +20,18 @@ 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
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
- @original_result = original_result.freeze
30
- @files = SimpleCov::FileList.new(original_result.map do |filename, coverage|
31
- SimpleCov::SourceFile.new(filename, coverage) if File.file?(filename)
28
+ def initialize(original_result, command_name: nil, created_at: nil)
29
+ result = original_result
30
+ @original_result = result.freeze
31
+ @command_name = command_name
32
+ @created_at = created_at
33
+ @files = SimpleCov::FileList.new(result.map do |filename, coverage|
34
+ SimpleCov::SourceFile.new(filename, JSON.parse(JSON.dump(coverage))) if File.file?(filename)
32
35
  end.compact.sort_by(&:filename))
33
36
  filter!
34
37
  end
@@ -61,16 +64,19 @@ module SimpleCov
61
64
 
62
65
  # Returns a hash representation of this Result that can be used for marshalling it into JSON
63
66
  def to_hash
64
- {command_name => {"coverage" => coverage, "timestamp" => created_at.to_i}}
67
+ {
68
+ command_name => {
69
+ "coverage" => coverage,
70
+ "timestamp" => created_at.to_i
71
+ }
72
+ }
65
73
  end
66
74
 
67
75
  # Loads a SimpleCov::Result#to_hash dump
68
76
  def self.from_hash(hash)
69
- command_name, data = hash.first
70
- result = SimpleCov::Result.new(data["coverage"])
71
- result.command_name = command_name
72
- result.created_at = Time.at(data["timestamp"])
73
- result
77
+ hash.map do |command_name, data|
78
+ new(data.fetch("coverage"), command_name: command_name, created_at: Time.at(data["timestamp"]))
79
+ end
74
80
  end
75
81
 
76
82
  private
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SimpleCov
4
+ #
5
+ # Responsible for adapting the format of the coverage result whether it's default or with statistics
6
+ #
7
+ class ResultAdapter
8
+ attr_reader :result
9
+
10
+ def initialize(result)
11
+ @result = result
12
+ end
13
+
14
+ def self.call(*args)
15
+ new(*args).adapt
16
+ end
17
+
18
+ def adapt
19
+ return unless result
20
+
21
+ result.each_with_object({}) do |(file_name, cover_statistic), adapted_result|
22
+ if cover_statistic.is_a?(Array)
23
+ adapted_result.merge!(file_name => {"lines" => cover_statistic})
24
+ else
25
+ adapted_result.merge!(file_name => cover_statistic)
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
@@ -2,12 +2,12 @@
2
2
 
3
3
  require "json"
4
4
 
5
- #
6
- # Singleton that is responsible for caching, loading and merging
7
- # SimpleCov::Results into a single result for coverage analysis based
8
- # upon multiple test suites.
9
- #
10
5
  module SimpleCov
6
+ #
7
+ # Singleton that is responsible for caching, loading and merging
8
+ # SimpleCov::Results into a single result for coverage analysis based
9
+ # upon multiple test suites.
10
+ #
11
11
  module ResultMerger
12
12
  class << self
13
13
  # The path to the .resultset.json cache file
@@ -19,79 +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
31
- {}
32
- end
33
- else
34
- {}
35
- end
36
- 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
37
26
  end
38
27
 
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)
43
- data = File.read(resultset_path)
44
- return if data.nil? || data.length < 2
45
- data
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))
46
41
  end
42
+
43
+ create_result(command_names, coverage)
47
44
  end
48
45
 
49
- # Gets the resultset hash and re-creates all included instances
50
- # of SimpleCov::Result from that.
51
- # All results that are above the SimpleCov.merge_timeout will be
52
- # dropped. Returns an array of SimpleCov::Result items.
53
- def results
54
- results = []
55
- resultset.each do |command_name, data|
56
- result = SimpleCov::Result.from_hash(command_name => data)
57
- # Only add result if the timeout is above the configured threshold
58
- if (Time.now - result.created_at) < SimpleCov.merge_timeout
59
- results << result
60
- end
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
50
+
51
+ def parse_file(path)
52
+ data = read_file(path)
53
+ parse_json(data)
54
+ end
55
+
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
+ {}
72
+ end
73
+
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
- # Merge two or more SimpleCov::Results into a new one with merged
66
- # coverage data and the command_name for the result consisting of a join
67
- # on all source result's names
68
- def merge_results(*results)
69
- merged = SimpleCov::RawCoverage.merge_results(*results.map(&:original_result))
70
- result = SimpleCov::Result.new(merged)
71
- # Specify the command name
72
- result.command_name = results.map(&:command_name).sort.join(", ")
73
- result
85
+ def within_merge_timeout?(data)
86
+ time_since_result_creation(data) < SimpleCov.merge_timeout
87
+ end
88
+
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
74
111
  end
75
112
 
76
113
  #
77
- # 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
78
115
  # SimpleCov::Result with merged coverage data and the command_name
79
116
  # for the result consisting of a join on all source result's names
80
- #
81
117
  def merged_result
82
- 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)
83
133
  end
84
134
 
85
135
  # Saves the given SimpleCov::Result in the resultset cache
86
136
  def store_result(result)
87
137
  synchronize_resultset do
88
138
  # Ensure we have the latest, in case it was already cached
89
- clear_resultset
90
- new_set = resultset
139
+ new_resultset = read_resultset
140
+
141
+ # A single result only ever has one command_name, see `SimpleCov::Result#to_hash`
91
142
  command_name, data = result.to_hash.first
92
- new_set[command_name] = data
143
+ new_resultset[command_name] = data
93
144
  File.open(resultset_path, "w+") do |f_|
94
- f_.puts JSON.pretty_generate(new_set)
145
+ f_.puts JSON.pretty_generate(new_resultset)
95
146
  end
96
147
  end
97
148
  true
@@ -114,9 +165,29 @@ module SimpleCov
114
165
  end
115
166
  end
116
167
 
117
- # Clear out the previously cached .resultset
118
- def clear_resultset
119
- @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
120
191
  end
121
192
  end
122
193
  end