simplecov 0.16.1 → 0.21.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (50) hide show
  1. checksums.yaml +5 -5
  2. data/CHANGELOG.md +107 -407
  3. data/README.md +378 -126
  4. data/doc/alternate-formatters.md +21 -1
  5. data/doc/commercial-services.md +5 -0
  6. data/lib/minitest/simplecov_plugin.rb +15 -0
  7. data/lib/simplecov/combine/branches_combiner.rb +32 -0
  8. data/lib/simplecov/combine/files_combiner.rb +24 -0
  9. data/lib/simplecov/combine/lines_combiner.rb +43 -0
  10. data/lib/simplecov/combine/results_combiner.rb +60 -0
  11. data/lib/simplecov/combine.rb +30 -0
  12. data/lib/simplecov/command_guesser.rb +6 -3
  13. data/lib/simplecov/configuration.rb +191 -15
  14. data/lib/simplecov/coverage_statistics.rb +56 -0
  15. data/lib/simplecov/default_formatter.rb +20 -0
  16. data/lib/simplecov/defaults.rb +15 -12
  17. data/lib/simplecov/exit_codes/exit_code_handling.rb +29 -0
  18. data/lib/simplecov/exit_codes/maximum_coverage_drop_check.rb +83 -0
  19. data/lib/simplecov/exit_codes/minimum_coverage_by_file_check.rb +54 -0
  20. data/lib/simplecov/exit_codes/minimum_overall_coverage_check.rb +53 -0
  21. data/lib/simplecov/exit_codes.rb +5 -0
  22. data/lib/simplecov/file_list.rb +72 -13
  23. data/lib/simplecov/filter.rb +9 -6
  24. data/lib/simplecov/formatter/multi_formatter.rb +5 -7
  25. data/lib/simplecov/formatter/simple_formatter.rb +4 -4
  26. data/lib/simplecov/formatter.rb +2 -2
  27. data/lib/simplecov/last_run.rb +3 -1
  28. data/lib/simplecov/lines_classifier.rb +5 -5
  29. data/lib/simplecov/no_defaults.rb +1 -1
  30. data/lib/simplecov/process.rb +19 -0
  31. data/lib/simplecov/profiles/hidden_filter.rb +5 -0
  32. data/lib/simplecov/profiles/rails.rb +1 -1
  33. data/lib/simplecov/profiles.rb +9 -7
  34. data/lib/simplecov/result.rb +18 -12
  35. data/lib/simplecov/result_adapter.rb +30 -0
  36. data/lib/simplecov/result_merger.rb +130 -59
  37. data/lib/simplecov/simulate_coverage.rb +29 -0
  38. data/lib/simplecov/source_file/branch.rb +84 -0
  39. data/lib/simplecov/source_file/line.rb +72 -0
  40. data/lib/simplecov/source_file.rb +273 -127
  41. data/lib/simplecov/useless_results_remover.rb +18 -0
  42. data/lib/simplecov/version.rb +1 -1
  43. data/lib/simplecov.rb +308 -121
  44. metadata +45 -47
  45. data/CONTRIBUTING.md +0 -51
  46. data/ISSUE_TEMPLATE.md +0 -23
  47. data/lib/simplecov/jruby_fix.rb +0 -44
  48. data/lib/simplecov/railtie.rb +0 -9
  49. data/lib/simplecov/railties/tasks.rake +0 -13
  50. data/lib/simplecov/raw_coverage.rb +0 -41
@@ -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
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SimpleCov
4
+ #
5
+ # Responsible for producing file coverage metrics.
6
+ #
7
+ module SimulateCoverage
8
+ module_function
9
+
10
+ #
11
+ # Simulate normal file coverage report on
12
+ # ruby 2.5 and return similar hash with lines and branches keys
13
+ #
14
+ # Happens when a file wasn't required but still tracked.
15
+ #
16
+ # @return [Hash]
17
+ #
18
+ def call(absolute_path)
19
+ lines = File.foreach(absolute_path)
20
+
21
+ {
22
+ "lines" => LinesClassifier.new.classify(lines),
23
+ # we don't want to parse branches ourselves...
24
+ # requiring files can have side effects and we don't want to trigger that
25
+ "branches" => {}
26
+ }
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,84 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SimpleCov
4
+ class SourceFile
5
+ #
6
+ # Representing single branch that has been detected in coverage report.
7
+ # Give us support methods that handle needed calculations.
8
+ class Branch
9
+ attr_reader :start_line, :end_line, :coverage, :type
10
+
11
+ # rubocop:disable Metrics/ParameterLists
12
+ def initialize(start_line:, end_line:, coverage:, inline:, type:)
13
+ @start_line = start_line
14
+ @end_line = end_line
15
+ @coverage = coverage
16
+ @inline = inline
17
+ @type = type
18
+ @skipped = false
19
+ end
20
+ # rubocop:enable Metrics/ParameterLists
21
+
22
+ def inline?
23
+ @inline
24
+ end
25
+
26
+ #
27
+ # Return true if there is relevant count defined > 0
28
+ #
29
+ # @return [Boolean]
30
+ #
31
+ def covered?
32
+ !skipped? && coverage.positive?
33
+ end
34
+
35
+ #
36
+ # Check if branche missed or not
37
+ #
38
+ # @return [Boolean]
39
+ #
40
+ def missed?
41
+ !skipped? && coverage.zero?
42
+ end
43
+
44
+ # The line on which we want to report the coverage
45
+ #
46
+ # Usually we choose the line above the start of the branch (so that it shows up
47
+ # at if/else) because that
48
+ # * highlights the condition
49
+ # * makes it distinguishable if the first line of the branch is an inline branch
50
+ # (see the nested_branches fixture)
51
+ #
52
+ def report_line
53
+ if inline?
54
+ start_line
55
+ else
56
+ start_line - 1
57
+ end
58
+ end
59
+
60
+ # Flags the branch as skipped
61
+ def skipped!
62
+ @skipped = true
63
+ end
64
+
65
+ # Returns true if the branch was marked skipped by virtue of nocov comments.
66
+ def skipped?
67
+ @skipped
68
+ end
69
+
70
+ def overlaps_with?(line_range)
71
+ start_line <= line_range.end && end_line >= line_range.begin
72
+ end
73
+
74
+ #
75
+ # Return array with coverage count and badge
76
+ #
77
+ # @return [Array]
78
+ #
79
+ def report
80
+ [type, coverage]
81
+ end
82
+ end
83
+ end
84
+ end
@@ -0,0 +1,72 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SimpleCov
4
+ class SourceFile
5
+ # Representation of a single line in a source file including
6
+ # this specific line's source code, line_number and code coverage,
7
+ # with the coverage being either nil (coverage not applicable, e.g. comment
8
+ # line), 0 (line not covered) or >1 (the amount of times the line was
9
+ # executed)
10
+ class Line
11
+ # The source code for this line. Aliased as :source
12
+ attr_reader :src
13
+ # The line number in the source file. Aliased as :line, :number
14
+ attr_reader :line_number
15
+ # The coverage data for this line: either nil (never), 0 (missed) or >=1 (times covered)
16
+ attr_reader :coverage
17
+ # Whether this line was skipped
18
+ attr_reader :skipped
19
+
20
+ # Lets grab some fancy aliases, shall we?
21
+ alias source src
22
+ alias line line_number
23
+ alias number line_number
24
+
25
+ def initialize(src, line_number, coverage)
26
+ raise ArgumentError, "Only String accepted for source" unless src.is_a?(String)
27
+ raise ArgumentError, "Only Integer accepted for line_number" unless line_number.is_a?(Integer)
28
+ raise ArgumentError, "Only Integer and nil accepted for coverage" unless coverage.is_a?(Integer) || coverage.nil?
29
+
30
+ @src = src
31
+ @line_number = line_number
32
+ @coverage = coverage
33
+ @skipped = false
34
+ end
35
+
36
+ # Returns true if this is a line that should have been covered, but was not
37
+ def missed?
38
+ !never? && !skipped? && coverage.zero?
39
+ end
40
+
41
+ # Returns true if this is a line that has been covered
42
+ def covered?
43
+ !never? && !skipped? && coverage.positive?
44
+ end
45
+
46
+ # Returns true if this line is not relevant for coverage
47
+ def never?
48
+ !skipped? && coverage.nil?
49
+ end
50
+
51
+ # Flags this line as skipped
52
+ def skipped!
53
+ @skipped = true
54
+ end
55
+
56
+ # Returns true if this line was skipped, false otherwise. Lines are skipped if they are wrapped with
57
+ # # :nocov: comment lines.
58
+ def skipped?
59
+ skipped
60
+ end
61
+
62
+ # The status of this line - either covered, missed, skipped or never. Useful i.e. for direct use
63
+ # as a css class in report generation
64
+ def status
65
+ return "skipped" if skipped?
66
+ return "never" if never?
67
+ return "missed" if missed?
68
+ return "covered" if covered?
69
+ end
70
+ end
71
+ end
72
+ end