simplecov 0.22.0 → 1.0.0.rc1

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 (113) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +81 -1
  3. data/LICENSE +1 -1
  4. data/README.md +1009 -511
  5. data/doc/alternate-formatters.md +0 -5
  6. data/doc/commercial-services.md +5 -5
  7. data/exe/simplecov +11 -0
  8. data/lib/minitest/simplecov_plugin.rb +13 -5
  9. data/lib/simplecov/autostart.rb +11 -0
  10. data/lib/simplecov/cli/clean.rb +47 -0
  11. data/lib/simplecov/cli/coverage.rb +91 -0
  12. data/lib/simplecov/cli/diff.rb +151 -0
  13. data/lib/simplecov/cli/dotfile.rb +100 -0
  14. data/lib/simplecov/cli/merge.rb +116 -0
  15. data/lib/simplecov/cli/open.rb +50 -0
  16. data/lib/simplecov/cli/report.rb +84 -0
  17. data/lib/simplecov/cli/run.rb +36 -0
  18. data/lib/simplecov/cli/serve.rb +139 -0
  19. data/lib/simplecov/cli/uncovered.rb +107 -0
  20. data/lib/simplecov/cli.rb +150 -0
  21. data/lib/simplecov/color.rb +74 -0
  22. data/lib/simplecov/combine/branches_combiner.rb +3 -2
  23. data/lib/simplecov/combine/files_combiner.rb +7 -1
  24. data/lib/simplecov/combine/lines_combiner.rb +19 -17
  25. data/lib/simplecov/combine/methods_combiner.rb +26 -0
  26. data/lib/simplecov/combine/results_combiner.rb +5 -4
  27. data/lib/simplecov/command_guesser.rb +46 -32
  28. data/lib/simplecov/configuration/coverage.rb +171 -0
  29. data/lib/simplecov/configuration/coverage_criteria.rb +156 -0
  30. data/lib/simplecov/configuration/filters.rb +195 -0
  31. data/lib/simplecov/configuration/formatting.rb +119 -0
  32. data/lib/simplecov/configuration/ignored_entries.rb +63 -0
  33. data/lib/simplecov/configuration/merging.rb +74 -0
  34. data/lib/simplecov/configuration/thresholds.rb +174 -0
  35. data/lib/simplecov/configuration.rb +79 -405
  36. data/lib/simplecov/coverage_statistics.rb +12 -9
  37. data/lib/simplecov/coverage_violations.rb +148 -0
  38. data/lib/simplecov/defaults.rb +27 -20
  39. data/lib/simplecov/directive.rb +162 -0
  40. data/lib/simplecov/exit_codes/exit_code_handling.rb +8 -2
  41. data/lib/simplecov/exit_codes/maximum_coverage_drop_check.rb +19 -57
  42. data/lib/simplecov/exit_codes/maximum_overall_coverage_check.rb +45 -0
  43. data/lib/simplecov/exit_codes/minimum_coverage_by_file_check.rb +17 -27
  44. data/lib/simplecov/exit_codes/minimum_coverage_by_group_check.rb +41 -0
  45. data/lib/simplecov/exit_codes/minimum_overall_coverage_check.rb +38 -21
  46. data/lib/simplecov/exit_codes.rb +3 -0
  47. data/lib/simplecov/exit_handling.rb +158 -0
  48. data/lib/simplecov/file_list.rb +61 -17
  49. data/lib/simplecov/filter.rb +69 -24
  50. data/lib/simplecov/formatter/base.rb +101 -0
  51. data/lib/simplecov/formatter/html_formatter/public/application.css +1 -0
  52. data/lib/simplecov/formatter/html_formatter/public/application.js +18 -0
  53. data/lib/simplecov/formatter/html_formatter/public/favicon_green.png +0 -0
  54. data/lib/simplecov/formatter/html_formatter/public/favicon_red.png +0 -0
  55. data/lib/simplecov/formatter/html_formatter/public/favicon_yellow.png +0 -0
  56. data/lib/simplecov/formatter/html_formatter/public/index.html +56 -0
  57. data/lib/simplecov/formatter/html_formatter.rb +79 -0
  58. data/lib/simplecov/formatter/json_formatter/errors_formatter.rb +84 -0
  59. data/lib/simplecov/formatter/json_formatter/result_hash_formatter.rb +127 -0
  60. data/lib/simplecov/formatter/json_formatter/source_file_formatter.rb +99 -0
  61. data/lib/simplecov/formatter/json_formatter.rb +77 -0
  62. data/lib/simplecov/formatter/multi_formatter.rb +4 -5
  63. data/lib/simplecov/formatter/simple_formatter.rb +9 -11
  64. data/lib/simplecov/formatter.rb +4 -0
  65. data/lib/simplecov/last_run.rb +10 -3
  66. data/lib/simplecov/lines_classifier.rb +26 -13
  67. data/lib/simplecov/load_global_config.rb +9 -4
  68. data/lib/simplecov/parallel_adapters/base.rb +51 -0
  69. data/lib/simplecov/parallel_adapters/generic.rb +42 -0
  70. data/lib/simplecov/parallel_adapters/parallel_tests.rb +77 -0
  71. data/lib/simplecov/parallel_adapters.rb +83 -0
  72. data/lib/simplecov/parallel_coordination.rb +95 -0
  73. data/lib/simplecov/process.rb +20 -14
  74. data/lib/simplecov/profiles/bundler_filter.rb +1 -1
  75. data/lib/simplecov/profiles/hidden_filter.rb +1 -1
  76. data/lib/simplecov/profiles/rails.rb +24 -10
  77. data/lib/simplecov/profiles/root_filter.rb +6 -5
  78. data/lib/simplecov/profiles/strict.rb +32 -0
  79. data/lib/simplecov/profiles/test_frameworks.rb +1 -4
  80. data/lib/simplecov/profiles.rb +32 -3
  81. data/lib/simplecov/result/missing_source_files_reporter.rb +49 -0
  82. data/lib/simplecov/result/source_file_builder.rb +51 -0
  83. data/lib/simplecov/result.rb +97 -19
  84. data/lib/simplecov/result_adapter.rb +68 -6
  85. data/lib/simplecov/result_merger/legacy_format_adapter.rb +28 -0
  86. data/lib/simplecov/result_merger/resultset_file.rb +38 -0
  87. data/lib/simplecov/result_merger/resultset_store.rb +50 -0
  88. data/lib/simplecov/result_merger.rb +46 -90
  89. data/lib/simplecov/result_processing.rb +162 -0
  90. data/lib/simplecov/simulate_coverage.rb +54 -8
  91. data/lib/simplecov/source_file/branch.rb +1 -3
  92. data/lib/simplecov/source_file/branch_builder.rb +114 -0
  93. data/lib/simplecov/source_file/builder_context.rb +28 -0
  94. data/lib/simplecov/source_file/line.rb +7 -2
  95. data/lib/simplecov/source_file/line_builder.rb +43 -0
  96. data/lib/simplecov/source_file/method.rb +52 -0
  97. data/lib/simplecov/source_file/method_builder.rb +58 -0
  98. data/lib/simplecov/source_file/ruby_data_parser.rb +88 -0
  99. data/lib/simplecov/source_file/skip_chunks.rb +77 -0
  100. data/lib/simplecov/source_file/source_loader.rb +63 -0
  101. data/lib/simplecov/source_file/statistics.rb +57 -0
  102. data/lib/simplecov/source_file.rb +66 -232
  103. data/lib/simplecov/static_coverage_extractor/visitor.rb +193 -0
  104. data/lib/simplecov/static_coverage_extractor.rb +111 -0
  105. data/lib/simplecov/useless_results_remover.rb +16 -7
  106. data/lib/simplecov/version.rb +1 -1
  107. data/lib/simplecov-html.rb +4 -0
  108. data/lib/simplecov.rb +131 -377
  109. data/lib/simplecov_json_formatter.rb +4 -0
  110. data/schemas/coverage-v1.0.schema.json +300 -0
  111. data/schemas/coverage.schema.json +300 -0
  112. metadata +88 -56
  113. data/lib/simplecov/default_formatter.rb +0 -20
@@ -3,16 +3,30 @@
3
3
  SimpleCov.profiles.define "rails" do
4
4
  load_profile "test_frameworks"
5
5
 
6
- add_filter %r{^/config/}
7
- add_filter %r{^/db/}
6
+ skip %r{\Aconfig/}
7
+ skip %r{\Adb/}
8
8
 
9
- add_group "Controllers", "app/controllers"
10
- add_group "Channels", "app/channels"
11
- add_group "Models", "app/models"
12
- add_group "Mailers", "app/mailers"
13
- add_group "Helpers", "app/helpers"
14
- add_group "Jobs", %w[app/jobs app/workers]
15
- add_group "Libraries", "lib/"
9
+ group "Controllers", "app/controllers"
10
+ group "Channels", "app/channels"
11
+ group "Models", "app/models"
12
+ group "Mailers", "app/mailers"
13
+ group "Helpers", "app/helpers"
14
+ group "Jobs", %w[app/jobs app/workers]
15
+ group "Libraries", "lib/"
16
16
 
17
- track_files "{app,lib}/**/*.rb"
17
+ # Preserve the legacy `track_files` semantics (additive disk-discovery
18
+ # without restricting the report's universe): write the ivar directly
19
+ # so loading the profile doesn't emit the public-API deprecation. Users
20
+ # migrating their own configs should prefer `cover "{app,lib}/**/*.rb"`,
21
+ # which both injects unloaded files and scopes the report to the match
22
+ # set — usually the intended behavior for a Rails project.
23
+ @tracked_files = "{app,lib}/**/*.rb"
24
+
25
+ # `parallelize(workers: ...)` forks worker processes that each run a
26
+ # slice of the suite. Without subprocess support, the workers' coverage
27
+ # is dropped on the floor and the parent records 0% for everything they
28
+ # touched. Hooking `Process._fork` makes each worker re-call
29
+ # `SimpleCov.start` with a unique command_name so the resultsets merge
30
+ # correctly.
31
+ merge_subprocesses true
18
32
  end
@@ -1,10 +1,11 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  SimpleCov.profiles.define "root_filter" do
4
- # Exclude all files outside of simplecov root
5
- root_filter = nil
6
- add_filter do |src|
7
- root_filter ||= /\A#{Regexp.escape(SimpleCov.root + File::SEPARATOR)}/io
8
- src.filename !~ root_filter
4
+ # Exclude all files outside of simplecov root. Shares the regex with
5
+ # SimpleCov::UselessResultsRemover so the root-prefix logic lives in one
6
+ # place; this profile is the user-facing entry point that tools like
7
+ # `SimpleCov.filtered` apply.
8
+ skip do |src|
9
+ src.filename !~ SimpleCov::UselessResultsRemover.root_regex
9
10
  end
10
11
  end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ # A "no shortfalls allowed" profile: every coverage criterion is
4
+ # enabled and held to 100%. Per
5
+ # https://github.com/simplecov-ruby/simplecov/issues/1061, this lives as
6
+ # an opt-in profile rather than the default — most projects can't
7
+ # realistically start at 100% on day one — so a team that has paid down
8
+ # the debt can flip the strict switch without re-typing the threshold
9
+ # trio every time.
10
+ #
11
+ # JRuby gracefully degrades: `enable_coverage :branch` / `:method` are
12
+ # accepted (they just add to the configured criteria set), but the
13
+ # runtime can't actually measure them, so the corresponding stats never
14
+ # materialize. `CoverageViolations` skips threshold lookups for
15
+ # criteria not in the stats, so the `:branch` / `:method` clauses below
16
+ # silently no-op on JRuby and only `:line` is enforced.
17
+ #
18
+ # `:eval` widens the universe of code held to 100% to include strings
19
+ # passed through `Kernel#eval` (typically ERB templates, when the user
20
+ # sets `ERB#filename=`). Guarded on `coverage_for_eval_supported?` so
21
+ # the profile stays quiet on Ruby < 3.2, where `enable_coverage :eval`
22
+ # would otherwise warn about the missing runtime support every time
23
+ # the profile loads.
24
+ SimpleCov.profiles.define "strict" do
25
+ enable_coverage :branch
26
+ enable_coverage :method
27
+ # simplecov:disable branch — dogfood runs on Ruby >= 3.2 only, so
28
+ # the else arm (eval coverage not supported) is unreachable from CI.
29
+ enable_coverage :eval if coverage_for_eval_supported?
30
+ # simplecov:enable
31
+ minimum_coverage line: 100, branch: 100, method: 100
32
+ end
@@ -1,8 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  SimpleCov.profiles.define "test_frameworks" do
4
- add_filter "/test/"
5
- add_filter "/features/"
6
- add_filter "/spec/"
7
- add_filter "/autotest/"
4
+ skip %r{\A(test|features|spec|autotest)/}
8
5
  end
@@ -17,7 +17,7 @@ module SimpleCov
17
17
  #
18
18
  def define(name, &blk)
19
19
  name = name.to_sym
20
- raise "SimpleCov Profile '#{name}' is already defined" unless self[name].nil?
20
+ raise SimpleCov::ConfigurationError, "SimpleCov Profile '#{name}' is already defined" unless self[name].nil?
21
21
 
22
22
  self[name] = blk
23
23
  end
@@ -26,10 +26,39 @@ module SimpleCov
26
26
  # Applies the profile of given name on SimpleCov.configure
27
27
  #
28
28
  def load(name)
29
+ SimpleCov.configure(&fetch_proc(name))
30
+ end
31
+
32
+ #
33
+ # Returns the proc registered for the given profile name, autoloading
34
+ # bundled or plugin-gem profiles on first lookup. Raises if the profile
35
+ # cannot be located.
36
+ #
37
+ # Lookup order:
38
+ # 1. already registered via #define
39
+ # 2. require "simplecov/profiles/<name>" (bundled profiles)
40
+ # 3. require "simplecov-profile-<name>" (third-party plugin gems)
41
+ #
42
+ def fetch_proc(name)
29
43
  name = name.to_sym
30
- raise "Could not find SimpleCov Profile called '#{name}'" unless key?(name)
44
+ autoload_profile(name) unless key?(name)
45
+ return self[name] if key?(name)
46
+
47
+ raise SimpleCov::ConfigurationError, "Could not find SimpleCov Profile called '#{name}'"
48
+ end
49
+
50
+ private
31
51
 
32
- SimpleCov.configure(&self[name])
52
+ def autoload_profile(name)
53
+ require "simplecov/profiles/#{name}"
54
+ rescue LoadError
55
+ begin
56
+ # simplecov:disable — third-party gem fallback (no such gem in test env)
57
+ require "simplecov-profile-#{name}"
58
+ # simplecov:enable
59
+ rescue LoadError
60
+ # fall through; #fetch_proc raises the user-facing error
61
+ end
33
62
  end
34
63
  end
35
64
  end
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SimpleCov
4
+ class Result
5
+ # When a resultset references source files that don't exist on the local
6
+ # filesystem they're silently dropped — which produces an empty `0 / 0
7
+ # (100.00%)` report that looks like success but isn't. Emit a single
8
+ # warning summarizing the drop and, when every entry was lost, point at
9
+ # the typical cause (`SimpleCov.collate` invoked from a machine or path
10
+ # different from where the resultsets were generated). See #980.
11
+ class MissingSourceFilesReporter
12
+ def initialize(missing_paths, input_size:, every_entry_dropped:)
13
+ @missing_paths = missing_paths
14
+ @input_size = input_size
15
+ @every_entry_dropped = every_entry_dropped
16
+ end
17
+
18
+ def warn!
19
+ warn SimpleCov::Color.colorize(message, :yellow)
20
+ end
21
+
22
+ def message
23
+ @every_entry_dropped ? all_missing_warning : partial_missing_warning
24
+ end
25
+
26
+ private
27
+
28
+ def all_missing_warning
29
+ "SimpleCov dropped all #{@missing_paths.size} source file(s) from the result — " \
30
+ "none of the paths in the resultset exist on this filesystem: " \
31
+ "#{summary}. If you're running `SimpleCov.collate`, the source " \
32
+ "files must be available at the same absolute paths as when the individual resultsets " \
33
+ "were generated."
34
+ end
35
+
36
+ def partial_missing_warning
37
+ "SimpleCov dropped #{@missing_paths.size} source file(s) from the result because " \
38
+ "they don't exist on this filesystem: #{summary}. They were " \
39
+ "tracked in the resultset but have since moved or been removed."
40
+ end
41
+
42
+ def summary
43
+ sample = @missing_paths.first(5).join(", ")
44
+ remaining = @missing_paths.size - 5
45
+ remaining.positive? ? "#{sample} (+#{remaining} more)" : sample
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SimpleCov
4
+ class Result
5
+ # Constructs `SimpleCov::SourceFile` instances from a raw coverage
6
+ # hash, sorts them by filename, and surfaces filenames whose source
7
+ # is no longer present on disk so the caller can warn about the
8
+ # silent drop (see #980).
9
+ class SourceFileBuilder
10
+ attr_reader :missing_source_files
11
+
12
+ def initialize(original_result, not_loaded_files:)
13
+ @original_result = original_result
14
+ @not_loaded_files = not_loaded_files
15
+ @missing_source_files = []
16
+ end
17
+
18
+ def call
19
+ SimpleCov::FileList.new(
20
+ @original_result
21
+ .filter_map { |filename, coverage| build_source_file(filename, coverage) }
22
+ .sort_by(&:filename)
23
+ )
24
+ end
25
+
26
+ private
27
+
28
+ def build_source_file(filename, coverage)
29
+ unless File.file?(filename)
30
+ @missing_source_files << filename
31
+ return
32
+ end
33
+
34
+ SimpleCov::SourceFile.new(
35
+ filename,
36
+ stringify_outer_keys(coverage),
37
+ loaded: !@not_loaded_files.include?(filename)
38
+ )
39
+ end
40
+
41
+ # `Coverage.result` returns symbol keys (`:lines`, `:branches`,
42
+ # `:methods`); resultsets loaded from disk are already string-keyed.
43
+ # SourceFile reads with strings, and handles both Array and
44
+ # stringified-Array branch/method keys via `restore_ruby_data_structure`,
45
+ # so only the outer hash needs normalizing.
46
+ def stringify_outer_keys(coverage)
47
+ coverage.transform_keys(&:to_s)
48
+ end
49
+ end
50
+ end
51
+ end
@@ -2,6 +2,8 @@
2
2
 
3
3
  require "digest/sha1"
4
4
  require "forwardable"
5
+ require_relative "result/missing_source_files_reporter"
6
+ require_relative "result/source_file_builder"
5
7
 
6
8
  module SimpleCov
7
9
  #
@@ -10,9 +12,11 @@ module SimpleCov
10
12
  #
11
13
  class Result
12
14
  extend Forwardable
15
+
13
16
  # Returns the original Coverage.result used for this instance of SimpleCov::Result
14
17
  attr_reader :original_result
15
- # Returns all files that are applicable to this result (sans filters!) as instances of SimpleCov::SourceFile. Aliased as :source_files
18
+ # Returns all files that are applicable to this result (sans filters!) as instances of
19
+ # SimpleCov::SourceFile. Aliased as :source_files
16
20
  attr_reader :files
17
21
  alias source_files files
18
22
  # Explicitly set the Time this result has been created
@@ -20,20 +24,47 @@ module SimpleCov
20
24
  # Explicitly set the command name that was used for this coverage result. Defaults to SimpleCov.command_name
21
25
  attr_writer :command_name
22
26
 
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
27
+ def_delegators :files, :covered_percent, :covered_percentages, :least_covered_file, :covered_strength,
28
+ :covered_lines, :missed_lines,
29
+ :total_branches, :covered_branches, :missed_branches,
30
+ :total_methods, :covered_methods, :missed_methods,
31
+ :coverage_statistics, :coverage_statistics_by_file
24
32
  def_delegator :files, :lines_of_code, :total_lines
25
33
 
34
+ # Bundles the filter and grouping configuration a Result applies to its
35
+ # source files after building them. Each field defaults to the SimpleCov
36
+ # singleton's configuration, so ordinary callers never construct one;
37
+ # tests pass a custom instance to opt out of (or extend) the project's
38
+ # filters or groups (e.g. `filters: []` to keep every file). Grouping the
39
+ # three together keeps Result#initialize's parameter list small.
40
+ class FilterConfig
41
+ attr_reader :filters, :cover_filters, :groups
42
+
43
+ def initialize(filters: SimpleCov.filters, cover_filters: SimpleCov.cover_filters, groups: SimpleCov.groups)
44
+ @filters = filters
45
+ @cover_filters = cover_filters
46
+ @groups = groups
47
+ end
48
+ end
49
+
26
50
  # Initialize a new SimpleCov::Result from given Coverage.result (a Hash of filenames each containing an array of
27
- # coverage data)
28
- def initialize(original_result, command_name: nil, created_at: nil)
29
- result = original_result
30
- @original_result = result.freeze
51
+ # coverage data).
52
+ #
53
+ # `filter_config` defaults to the SimpleCov singleton's filter / group
54
+ # configuration so existing call sites are unchanged. Pass a custom
55
+ # FilterConfig to opt out — useful for tests that build synthetic Results
56
+ # and don't want the project's filters or groups applied.
57
+ def initialize(original_result, command_name: nil, created_at: nil, not_loaded_files: Set.new,
58
+ filter_config: FilterConfig.new)
59
+ @original_result = original_result.freeze
31
60
  @command_name = command_name
32
61
  @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)
35
- end.compact.sort_by(&:filename))
36
- filter!
62
+ @groups_config = filter_config.groups
63
+ builder = SourceFileBuilder.new(original_result, not_loaded_files: not_loaded_files)
64
+ @files = builder.call
65
+ warn_about_missing_source_files(builder.missing_source_files, original_result.size)
66
+ apply_cover_filters!(filter_config.cover_filters)
67
+ apply_filters!(filter_config.filters)
37
68
  end
38
69
 
39
70
  # Returns all filenames for source files contained in this result
@@ -41,14 +72,37 @@ module SimpleCov
41
72
  files.map(&:filename)
42
73
  end
43
74
 
44
- # Returns a Hash of groups for this result. Define groups using SimpleCov.add_group 'Models', 'app/models'
75
+ # Returns the SimpleCov::SourceFile for the given path, or nil if no
76
+ # matching file is in this result. The path is resolved against
77
+ # SimpleCov.root, so callers can pass either an absolute path or a
78
+ # project-relative one.
79
+ def source_file_for(path)
80
+ target = File.expand_path(path, SimpleCov.root)
81
+ files.find { |file| file.filename == target }
82
+ end
83
+
84
+ # Returns the {line:/branch:/method:} coverage_statistics hash for the
85
+ # given file path, or nil if no matching source file is in this
86
+ # result. See SimpleCov::Result#source_file_for for path resolution.
87
+ def coverage_for(path)
88
+ source_file_for(path)&.coverage_statistics
89
+ end
90
+
91
+ # Returns a Hash of groups for this result. Define groups using SimpleCov.group 'Models', 'app/models'
45
92
  def groups
46
- @groups ||= SimpleCov.grouped(files)
93
+ @groups ||= SimpleCov.grouped(files, groups: @groups_config)
47
94
  end
48
95
 
49
- # Applies the configured SimpleCov.formatter on this result
96
+ # Applies the configured SimpleCov.formatter on this result. Returns
97
+ # nil if formatting has been opted out of (`SimpleCov.formatter false`
98
+ # / `SimpleCov.formatters []`) — the cheap path for non-final
99
+ # processes in a parallel CI run, which only need their
100
+ # `.resultset.json` on disk. See #964.
50
101
  def format!
51
- SimpleCov.formatter.new.format(self)
102
+ formatter = SimpleCov.formatter
103
+ return nil if formatter.nil?
104
+
105
+ formatter.new.format(self)
52
106
  end
53
107
 
54
108
  # Defines when this result has been created. Defaults to Time.now
@@ -81,14 +135,38 @@ module SimpleCov
81
135
 
82
136
  private
83
137
 
138
+ def warn_about_missing_source_files(missing, input_size)
139
+ return if missing.empty?
140
+
141
+ MissingSourceFilesReporter.new(
142
+ missing,
143
+ input_size: input_size,
144
+ every_entry_dropped: @files.empty? && missing.size == input_size
145
+ ).warn!
146
+ end
147
+
84
148
  def coverage
85
- keys = original_result.keys & filenames
86
- Hash[keys.zip(original_result.values_at(*keys))]
149
+ original_result.slice(*filenames)
87
150
  end
88
151
 
89
- # Applies all configured SimpleCov filters on this result's source files
90
- def filter!
91
- @files = SimpleCov.filtered(files)
152
+ # Applies the given filter chain to `@files`, dropping each source
153
+ # file that any filter matches.
154
+ def apply_filters!(filters)
155
+ filters.each do |filter|
156
+ @files = SimpleCov::FileList.new(@files.reject { |source_file| filter.matches?(source_file) })
157
+ end
158
+ end
159
+
160
+ # When any `cover` matcher is configured, restrict `@files` to source
161
+ # files matching at least one of them. With no cover matchers configured
162
+ # this is a no-op, preserving the historical "everything required, then
163
+ # filtered" universe.
164
+ def apply_cover_filters!(cover_filters)
165
+ return if cover_filters.empty?
166
+
167
+ @files = SimpleCov::FileList.new(
168
+ @files.select { |source_file| cover_filters.any? { |filter| filter.matches?(source_file) } }
169
+ )
92
170
  end
93
171
  end
94
172
  end
@@ -18,13 +18,75 @@ module SimpleCov
18
18
  def adapt
19
19
  return unless result
20
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
21
+ result.to_h do |file_name, cover_statistic|
22
+ [file_name, adapt_one(file_name, cover_statistic)]
27
23
  end
28
24
  end
25
+
26
+ private
27
+
28
+ # Pre-0.18 resultsets pointed each filename straight at a line-coverage
29
+ # array; everything since uses the `{lines:, branches:, methods:}`
30
+ # shape. Newer entries also need their methods table massaged before
31
+ # downstream code merges across processes.
32
+ def adapt_one(file_name, cover_statistic)
33
+ return {"lines" => cover_statistic} if cover_statistic.is_a?(Array)
34
+
35
+ adapt_oneshot_lines_if_needed(file_name, cover_statistic)
36
+ normalize_method_keys(cover_statistic)
37
+ cover_statistic
38
+ end
39
+
40
+ # Normalize memory addresses in method coverage keys so that results
41
+ # from different processes can be merged. Anonymous class names like
42
+ # "#<Class:0x00007ff19ab24790>" get inconsistent addresses across runs.
43
+ # Address widths vary by runtime (32-bit hosts: 8 hex chars; 64-bit
44
+ # CRuby: 16; some JVM/TruffleRuby formats may differ), so match any
45
+ # length of hex digits and collapse to a single placeholder.
46
+ ADDRESS_PATTERN = /0x\h+/
47
+ private_constant :ADDRESS_PATTERN
48
+
49
+ ADDRESS_PLACEHOLDER = "0x0"
50
+ private_constant :ADDRESS_PLACEHOLDER
51
+
52
+ # Strip the `#<Class:Foo>` wrapper Ruby's Coverage adds to singleton-class
53
+ # method keys. `module_function` and class methods get recorded both as
54
+ # singleton (`[#<Class:Foo>, :m, …]`) and instance/module (`[Foo, :m, …]`)
55
+ # entries pointing at the same source location; only one of the two is
56
+ # ever reachable at runtime, so we merge them. Only applies to named
57
+ # constants — anonymous-class addresses like `#<Class:0x0>` are left
58
+ # alone (handled by ADDRESS_PATTERN above).
59
+ SINGLETON_WRAPPER_PATTERN = /\A#<Class:([A-Z_][\w:]*)>\z/
60
+ private_constant :SINGLETON_WRAPPER_PATTERN
61
+
62
+ def normalize_method_keys(cover_statistic)
63
+ methods = cover_statistic[:methods]
64
+ return unless methods
65
+
66
+ cover_statistic[:methods] = methods.each_with_object({}) do |(key, count), normalized|
67
+ normalized_key = key.dup
68
+ normalized_key[0] = key[0].to_s
69
+ .gsub(ADDRESS_PATTERN, ADDRESS_PLACEHOLDER)
70
+ .sub(SINGLETON_WRAPPER_PATTERN, '\1')
71
+ # Keys may collide after normalization (anonymous classes sharing a
72
+ # method name, or singleton + instance forms of a module_function method).
73
+ normalized[normalized_key] = normalized.fetch(normalized_key, 0) + count
74
+ end
75
+ end
76
+
77
+ def adapt_oneshot_lines_if_needed(file_name, cover_statistic)
78
+ return unless cover_statistic.key?(:oneshot_lines)
79
+
80
+ oneshot_lines = cover_statistic.delete(:oneshot_lines)
81
+ line_stub = build_line_stub(file_name, oneshot_lines)
82
+ oneshot_lines.each { |covered_line| line_stub[covered_line - 1] = 1 }
83
+ cover_statistic[:lines] = line_stub
84
+ end
85
+
86
+ def build_line_stub(file_name, oneshot_lines)
87
+ Coverage.line_stub(file_name)
88
+ rescue Errno::ENOENT, SyntaxError
89
+ Array.new(oneshot_lines.max || 0, nil)
90
+ end
29
91
  end
30
92
  end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SimpleCov
4
+ module ResultMerger
5
+ # We changed the format of the raw result data in simplecov, as people
6
+ # are likely to have "old" resultsets lying around (but not too old so
7
+ # that they're still considered we can adapt them). See
8
+ # https://github.com/simplecov-ruby/simplecov/pull/824#issuecomment-576049747
9
+ module LegacyFormatAdapter
10
+ module_function
11
+
12
+ def call(result)
13
+ pre_0_18?(result) ? upgrade(result) : result
14
+ end
15
+
16
+ # Pre-0.18 coverage data pointed from file directly to an array of
17
+ # line coverage rather than a `{"lines" => [...]}` hash.
18
+ def pre_0_18?(result)
19
+ _key, data = result.first
20
+ data.is_a?(Array)
21
+ end
22
+
23
+ def upgrade(result)
24
+ result.transform_values { |line_coverage_data| {"lines" => line_coverage_data} }
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+
5
+ module SimpleCov
6
+ module ResultMerger
7
+ # Read + parse a `.resultset.json` file with the same tolerance the
8
+ # historical `ResultMerger` had: missing file returns `{}`, an empty
9
+ # or unparseable file warns and returns `{}`, parse success returns
10
+ # the decoded Hash.
11
+ module ResultsetFile
12
+ module_function
13
+
14
+ def parse(path)
15
+ data = read(path)
16
+ decode(data)
17
+ end
18
+
19
+ def read(path)
20
+ return unless File.exist?(path)
21
+
22
+ data = File.read(path)
23
+ return if data.nil? || data.length < 2
24
+
25
+ data
26
+ end
27
+
28
+ def decode(content)
29
+ return {} unless content
30
+
31
+ JSON.parse(content) || {}
32
+ rescue StandardError
33
+ warn "[SimpleCov]: Warning! Parsing JSON content of resultset file failed"
34
+ {}
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "fileutils"
4
+ require "json"
5
+
6
+ module SimpleCov
7
+ module ResultMerger
8
+ # Reads and writes the persistent `.resultset.json` cache, including
9
+ # file-lock synchronization between processes and atomic temp-file
10
+ # renames so concurrent readers don't observe a truncated file.
11
+ module ResultsetStore
12
+ module_function
13
+
14
+ def resultset_path
15
+ File.join(SimpleCov.coverage_path, ".resultset.json")
16
+ end
17
+
18
+ def writelock_path
19
+ File.join(SimpleCov.coverage_path, ".resultset.json.lock")
20
+ end
21
+
22
+ def write(resultset)
23
+ FileUtils.mkdir_p(SimpleCov.coverage_path)
24
+ temp_path = "#{resultset_path}.#{Process.pid}.tmp"
25
+ File.open(temp_path, "w") { |f| f.puts JSON.pretty_generate(resultset) }
26
+ File.rename(temp_path, resultset_path)
27
+ end
28
+
29
+ # Ensure only one process is reading or writing the resultset at
30
+ # any given time. Reentrant: the lock is acquired once per outer
31
+ # call no matter how deeply nested.
32
+ def synchronize(&)
33
+ return yield if @locked
34
+
35
+ @locked = true
36
+ with_flock(&)
37
+ ensure
38
+ @locked = false
39
+ end
40
+
41
+ def with_flock
42
+ FileUtils.mkdir_p(SimpleCov.coverage_path)
43
+ File.open(writelock_path, "w+") do |f|
44
+ f.flock(File::LOCK_EX)
45
+ yield
46
+ end
47
+ end
48
+ end
49
+ end
50
+ end