simplecov 0.17.1 → 0.21.1

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