simplecov 0.16.0 → 0.18.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 (39) hide show
  1. checksums.yaml +5 -5
  2. data/CHANGELOG.md +110 -1
  3. data/CODE_OF_CONDUCT.md +76 -0
  4. data/LICENSE +20 -0
  5. data/README.md +281 -112
  6. data/doc/alternate-formatters.md +10 -0
  7. data/lib/simplecov.rb +248 -63
  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 +110 -9
  15. data/lib/simplecov/coverage_statistics.rb +56 -0
  16. data/lib/simplecov/defaults.rb +6 -2
  17. data/lib/simplecov/file_list.rb +66 -13
  18. data/lib/simplecov/filter.rb +2 -1
  19. data/lib/simplecov/formatter/multi_formatter.rb +2 -2
  20. data/lib/simplecov/formatter/simple_formatter.rb +4 -4
  21. data/lib/simplecov/last_run.rb +3 -1
  22. data/lib/simplecov/lines_classifier.rb +2 -2
  23. data/lib/simplecov/profiles.rb +9 -7
  24. data/lib/simplecov/profiles/hidden_filter.rb +5 -0
  25. data/lib/simplecov/profiles/rails.rb +1 -1
  26. data/lib/simplecov/result.rb +39 -6
  27. data/lib/simplecov/result_adapter.rb +30 -0
  28. data/lib/simplecov/result_merger.rb +18 -11
  29. data/lib/simplecov/simulate_coverage.rb +29 -0
  30. data/lib/simplecov/source_file.rb +227 -126
  31. data/lib/simplecov/source_file/branch.rb +84 -0
  32. data/lib/simplecov/source_file/line.rb +72 -0
  33. data/lib/simplecov/useless_results_remover.rb +16 -0
  34. data/lib/simplecov/version.rb +1 -1
  35. metadata +32 -53
  36. data/lib/simplecov/jruby_fix.rb +0 -44
  37. data/lib/simplecov/railtie.rb +0 -9
  38. data/lib/simplecov/railties/tasks.rake +0 -13
  39. data/lib/simplecov/raw_coverage.rb +0 -41
@@ -6,12 +6,14 @@ require "pathname"
6
6
  require "simplecov/profiles/root_filter"
7
7
  require "simplecov/profiles/test_frameworks"
8
8
  require "simplecov/profiles/bundler_filter"
9
+ require "simplecov/profiles/hidden_filter"
9
10
  require "simplecov/profiles/rails"
10
11
 
11
12
  # Default configuration
12
13
  SimpleCov.configure do
13
14
  formatter SimpleCov::Formatter::HTMLFormatter
14
15
  load_profile "bundler_filter"
16
+ load_profile "hidden_filter"
15
17
  # Exclude files outside of SimpleCov.root
16
18
  load_profile "root_filter"
17
19
  end
@@ -23,7 +25,9 @@ at_exit do
23
25
  # If we are in a different process than called start, don't interfere.
24
26
  next if SimpleCov.pid != Process.pid
25
27
 
26
- SimpleCov.set_exit_exception
28
+ # If SimpleCov is no longer running then don't run exit tasks
29
+ next unless SimpleCov.running
30
+
27
31
  SimpleCov.run_exit_tasks!
28
32
  end
29
33
 
@@ -40,7 +44,7 @@ loop do
40
44
  begin
41
45
  load filename
42
46
  rescue LoadError, StandardError
43
- $stderr.puts "Warning: Error occurred while trying to load #{filename}. " \
47
+ warn "Warning: Error occurred while trying to load #{filename}. " \
44
48
  "Error message: #{$!.message}"
45
49
  end
46
50
  break
@@ -1,30 +1,53 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- # An array of SimpleCov SourceFile instances with additional collection helper
4
- # methods for calculating coverage across them etc.
5
3
  module SimpleCov
6
- class FileList < Array
4
+ # An array of SimpleCov SourceFile instances with additional collection helper
5
+ # methods for calculating coverage across them etc.
6
+ class FileList
7
+ include Enumerable
8
+ extend Forwardable
9
+
10
+ def_delegators :@files,
11
+ # For Enumerable
12
+ :each,
13
+ # also delegating methods implemented in Enumerable as they have
14
+ # custom Array implementations which are presumably better/more
15
+ # resource efficient
16
+ :size, :map, :count,
17
+ # surprisingly not in Enumerable
18
+ :empty?, :length,
19
+ # still act like we're kinda an array
20
+ :to_a, :to_ary
21
+
22
+ def initialize(files)
23
+ @files = files
24
+ end
25
+
26
+ def coverage_statistics
27
+ @coverage_statistics ||= compute_coverage_statistics
28
+ end
29
+
7
30
  # Returns the count of lines that have coverage
8
31
  def covered_lines
9
- return 0.0 if empty?
10
- map { |f| f.covered_lines.count }.inject(:+)
32
+ coverage_statistics[:line]&.covered
11
33
  end
12
34
 
13
35
  # Returns the count of lines that have been missed
14
36
  def missed_lines
15
- return 0.0 if empty?
16
- map { |f| f.missed_lines.count }.inject(:+)
37
+ coverage_statistics[:line]&.missed
17
38
  end
18
39
 
19
40
  # Returns the count of lines that are not relevant for coverage
20
41
  def never_lines
21
42
  return 0.0 if empty?
43
+
22
44
  map { |f| f.never_lines.count }.inject(:+)
23
45
  end
24
46
 
25
47
  # Returns the count of skipped lines
26
48
  def skipped_lines
27
49
  return 0.0 if empty?
50
+
28
51
  map { |f| f.skipped_lines.count }.inject(:+)
29
52
  end
30
53
 
@@ -36,26 +59,56 @@ module SimpleCov
36
59
 
37
60
  # Finds the least covered file and returns that file's name
38
61
  def least_covered_file
39
- sort_by(&:covered_percent).first.filename
62
+ min_by(&:covered_percent).filename
40
63
  end
41
64
 
42
65
  # Returns the overall amount of relevant lines of code across all files in this list
43
66
  def lines_of_code
44
- covered_lines + missed_lines
67
+ coverage_statistics[:line]&.total
45
68
  end
46
69
 
47
70
  # Computes the coverage based upon lines covered and lines missed
48
71
  # @return [Float]
49
72
  def covered_percent
50
- return 100.0 if empty? || lines_of_code.zero?
51
- Float(covered_lines * 100.0 / lines_of_code)
73
+ coverage_statistics[:line]&.percent
52
74
  end
53
75
 
54
76
  # Computes the strength (hits / line) based upon lines covered and lines missed
55
77
  # @return [Float]
56
78
  def covered_strength
57
- return 0.0 if empty? || lines_of_code.zero?
58
- Float(map { |f| f.covered_strength * f.lines_of_code }.inject(:+) / lines_of_code)
79
+ coverage_statistics[:line]&.strength
80
+ end
81
+
82
+ # Return total count of branches in all files
83
+ def total_branches
84
+ coverage_statistics[:branch]&.total
85
+ end
86
+
87
+ # Return total count of covered branches
88
+ def covered_branches
89
+ coverage_statistics[:branch]&.covered
90
+ end
91
+
92
+ # Return total count of covered branches
93
+ def missed_branches
94
+ coverage_statistics[:branch]&.missed
95
+ end
96
+
97
+ def branch_covered_percent
98
+ coverage_statistics[:branch]&.percent
99
+ end
100
+
101
+ private
102
+
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
+ end
108
+
109
+ coverage_statistics = {line: CoverageStatistics.from(total_coverage_statistics[:line])}
110
+ coverage_statistics[:branch] = CoverageStatistics.from(total_coverage_statistics[:branch]) if SimpleCov.branch_coverage?
111
+ coverage_statistics
59
112
  end
60
113
  end
61
114
  end
@@ -18,7 +18,7 @@ module SimpleCov
18
18
  @filter_argument = filter_argument
19
19
  end
20
20
 
21
- def matches?(_)
21
+ def matches?(_source_file)
22
22
  raise "The base filter class is not intended for direct use"
23
23
  end
24
24
 
@@ -29,6 +29,7 @@ module SimpleCov
29
29
 
30
30
  def self.build_filter(filter_argument)
31
31
  return filter_argument if filter_argument.is_a?(SimpleCov::Filter)
32
+
32
33
  class_for_argument(filter_argument).new(filter_argument)
33
34
  end
34
35
 
@@ -8,8 +8,8 @@ module SimpleCov
8
8
  formatters.map do |formatter|
9
9
  begin
10
10
  formatter.new.format(result)
11
- rescue => e
12
- STDERR.puts("Formatter #{formatter} failed with #{e.class}: #{e.message} (#{e.backtrace.first})")
11
+ rescue StandardError => e
12
+ warn("Formatter #{formatter} failed with #{e.class}: #{e.message} (#{e.backtrace.first})")
13
13
  nil
14
14
  end
15
15
  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,8 +8,8 @@ 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
@@ -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
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ SimpleCov.profiles.define "hidden_filter" do
4
+ add_filter %r{^/\..*}
5
+ end
@@ -7,7 +7,7 @@ SimpleCov.profiles.define "rails" do
7
7
  add_filter %r{^/db/}
8
8
 
9
9
  add_group "Controllers", "app/controllers"
10
- add_group "Channels", "app/channels" if defined?(ActionCable)
10
+ add_group "Channels", "app/channels"
11
11
  add_group "Models", "app/models"
12
12
  add_group "Mailers", "app/mailers"
13
13
  add_group "Helpers", "app/helpers"
@@ -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,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
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
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
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)
29
+ result = adapt_result(original_result)
30
+ @original_result = result.freeze
31
+ @files = SimpleCov::FileList.new(result.map do |filename, coverage|
32
+ SimpleCov::SourceFile.new(filename, JSON.parse(JSON.dump(coverage))) if File.file?(filename)
32
33
  end.compact.sort_by(&:filename))
33
34
  filter!
34
35
  end
@@ -61,13 +62,20 @@ module SimpleCov
61
62
 
62
63
  # Returns a hash representation of this Result that can be used for marshalling it into JSON
63
64
  def to_hash
64
- {command_name => {"coverage" => coverage, "timestamp" => created_at.to_i}}
65
+ {
66
+ command_name => {
67
+ "coverage" => coverage,
68
+ "timestamp" => created_at.to_i
69
+ }
70
+ }
65
71
  end
66
72
 
67
73
  # Loads a SimpleCov::Result#to_hash dump
68
74
  def self.from_hash(hash)
69
75
  command_name, data = hash.first
76
+
70
77
  result = SimpleCov::Result.new(data["coverage"])
78
+
71
79
  result.command_name = command_name
72
80
  result.created_at = Time.at(data["timestamp"])
73
81
  result
@@ -75,6 +83,31 @@ module SimpleCov
75
83
 
76
84
  private
77
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
95
+ end
96
+ end
97
+
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
110
+
78
111
  def coverage
79
112
  keys = original_result.keys & filenames
80
113
  Hash[keys.zip(original_result.values_at(*keys))]
@@ -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
@@ -27,7 +27,7 @@ module SimpleCov
27
27
  if data
28
28
  begin
29
29
  JSON.parse(data) || {}
30
- rescue
30
+ rescue StandardError
31
31
  {}
32
32
  end
33
33
  else
@@ -40,8 +40,10 @@ module SimpleCov
40
40
  def stored_data
41
41
  synchronize_resultset do
42
42
  return unless File.exist?(resultset_path)
43
+
43
44
  data = File.read(resultset_path)
44
45
  return if data.nil? || data.length < 2
46
+
45
47
  data
46
48
  end
47
49
  end
@@ -55,19 +57,24 @@ module SimpleCov
55
57
  resultset.each do |command_name, data|
56
58
  result = SimpleCov::Result.from_hash(command_name => data)
57
59
  # 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
60
+ results << result if (Time.now - result.created_at) < SimpleCov.merge_timeout
61
61
  end
62
62
  results
63
63
  end
64
64
 
65
+ def merge_and_store(*results)
66
+ result = merge_results(*results)
67
+ store_result(result) if result
68
+ result
69
+ end
70
+
65
71
  # Merge two or more SimpleCov::Results into a new one with merged
66
72
  # coverage data and the command_name for the result consisting of a join
67
73
  # on all source result's names
68
74
  def merge_results(*results)
69
- merged = SimpleCov::RawCoverage.merge_results(*results.map(&:original_result))
70
- result = SimpleCov::Result.new(merged)
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)
71
78
  # Specify the command name
72
79
  result.command_name = results.map(&:command_name).sort.join(", ")
73
80
  result