module SimpleCov
def add_not_loaded_files(result)
initializes their line-by-line coverage to zero (or nil for
Finds files that were to be tracked but were not loaded, and
def add_not_loaded_files(result) globs = unloaded_file_discovery_globs return [result, Set.new] if globs.empty? inject_unloaded_files(result.dup, discover_unloaded_paths(globs)) end
def at_exit_behavior
def at_exit_behavior # If we are in a different process than called start, don't interfere. return if SimpleCov.pid != Process.pid # If Coverage is no longer running (e.g. someone manually stopped it # or a test consumed the result) then don't run exit tasks. return unless Coverage.running? # Stand down when we'd only clobber a fresher report. See # `defer_to_existing_report?` and issue #581. return if defer_to_existing_report? SimpleCov.run_exit_tasks! end
def build_coverage_limits
def build_coverage_limits CoverageLimits.new( minimum_coverage: minimum_coverage, minimum_coverage_by_file: minimum_coverage_by_file, minimum_coverage_by_file_overrides: minimum_coverage_by_file_overrides, minimum_coverage_by_group: minimum_coverage_by_group, maximum_coverage: maximum_coverage, maximum_coverage_drop: maximum_coverage_drop ) end
def clear_result
def clear_result @result = nil end
def collate(result_filenames, profile = nil, ignore_timeout: true, &)
`ignore_timeout: false` to honor it.
so all results in all files specified will be merged. Pass
See README for usage. By default `collate` ignores the merge_timeout
Collate a series of SimpleCov result files into a single SimpleCov output.
def collate(result_filenames, profile = nil, ignore_timeout: true, &) raise ArgumentError, "There are no reports to be merged" if result_filenames.empty? initial_setup(profile, &) # Use the ResultMerger to produce a single, merged result, ready to use. @result = ResultMerger.merge_and_store(*result_filenames, ignore_timeout: ignore_timeout) @collating_result = true run_exit_tasks! ensure @collating_result = false end
def collating_result?
- Api: - private — true while `SimpleCov.collate` is running its finalizer.
def collating_result? defined?(@collating_result) && @collating_result end
def coverage_statistics_key(criterion)
`coverage_statistics` by `ResultAdapter`, so use `:line` to look
`:oneshot_line` data is folded into the `:line` bucket of
def coverage_statistics_key(criterion) criterion == :oneshot_line ? :line : criterion end
def defer_to_existing_report?
process — e.g. a Rakefile or Rails' `Bundler.require` — that
disk. Typically fires when `SimpleCov.start` ran in a parent
(after the resultset merge) and a newer report already exists on
Returns true when our process has no coverage data to contribute
def defer_to_existing_report? return false unless existing_report_newer_than_us? res = result empty = res.nil? || res.files.empty? warn_about_deferred_report if empty empty end
def defer_to_minitest_after_run
def defer_to_minitest_after_run self.external_at_exit = true Minitest.after_run { SimpleCov.at_exit_behavior } end
def discover_unloaded_paths(globs)
injection and produce a different file set per environment. See
from a subdir) would otherwise silently miss the unloaded-file
test runners that chdir (or CI scripts that invoke the suite
Expand the given globs relative to SimpleCov.root, not Dir.pwd —
def discover_unloaded_paths(globs) globs.flat_map { |glob| Dir.glob(glob, base: root) }.uniq end
def existing_report_newer_than_us?
def existing_report_newer_than_us? return false unless process_start_time last_run_path = File.join(coverage_path, ".last_run.json") File.exist?(last_run_path) && File.mtime(last_run_path) > process_start_time end
def exit_and_report_previous_error(exit_status)
- Api: - private
def exit_and_report_previous_error(exit_status) if print_errors ExitCodes.print_error SimpleCov::Color.colorize( "Stopped processing SimpleCov as a previous error not related to SimpleCov has been detected", :yellow ) end Kernel.exit(exit_status) end
def exit_status_from_exception
- Api: - private — returns the exit status from the exit exception.
def exit_status_from_exception @exit_exception = $ERROR_INFO return nil unless @exit_exception if @exit_exception.is_a?(SystemExit) @exit_exception.status else SimpleCov::ExitCodes::EXCEPTION end end
def external_at_exit?
Coerce to a proper boolean so rspec-mocks 4's predicate matcher
def external_at_exit? !!@external_at_exit end
def filtered(files)
def filtered(files) result = files.to_a.dup filters.each do |filter| result = result.reject { |source_file| filter.matches?(source_file) } end SimpleCov::FileList.new result end
def final_result_process?
- Api: - private
def final_result_process? adapter = SimpleCov::ParallelAdapters.current # No recognized parallel-test adapter. A subprocess forked while # coverage was running is never the final reporter — the process that # spawned it merges every slice and produces the report. Without this, # fork-based runners that don't set TEST_ENV_NUMBER (e.g. Minitest's # `parallelize`) have every worker produce the final report and its # warnings. See issue #1171. return !forked_subprocess? unless adapter adapter.first_worker? end
def forked_subprocess?
- Api: - private — true in a process that was forked while coverage was
def forked_subprocess? !!(defined?(@forked_subprocess) && @forked_subprocess) end
def grouped(files, groups: SimpleCov.groups)
construction). Files matched by no group fall into the implicit
different group config (e.g., the snapshot a Result captured at
`SimpleCov.groups`; pass a Hash explicitly to bin against a
Bin the given source files by group filter. `groups:` defaults to
def grouped(files, groups: SimpleCov.groups) return {} if groups.empty? grouped = groups.transform_values do |filter| SimpleCov::FileList.new(files.select { |source_file| filter.matches?(source_file) }) end in_group = grouped_file_set(grouped) ungrouped = files.reject { |source_file| in_group.include?(source_file) } grouped["Ungrouped"] = SimpleCov::FileList.new(ungrouped) if ungrouped.any? grouped end
def grouped_file_set(grouped)
def grouped_file_set(grouped) grouped.values.each_with_object(Set.new) { |file_list, set| set.merge(file_list) } end
def initial_setup(profile, &block)
def initial_setup(profile, &block) load_profile(profile) if profile configure(&block) if block end
def inject_unloaded_files(result, candidate_paths)
def inject_unloaded_files(result, candidate_paths) not_loaded_files = candidate_paths.each_with_object(Set.new) do |file, set| absolute_path = File.expand_path(file, root) next if result.key?(absolute_path) result[absolute_path] = SimulateCoverage.call(absolute_path) set << absolute_path end [result, not_loaded_files] end
def install_at_exit_hook
using `start_tracking` directly instead of `start`.
pipeline themselves (e.g., dogfood test setups) can skip it by
safe to call multiple times. Callers that drive the formatting
checks. `SimpleCov.start` calls this automatically. Idempotent —
Install the at_exit hook that formats results and runs exit-code
def install_at_exit_hook return if @at_exit_hook_installed @at_exit_hook_installed = true # Never defer in a forked child: Minitest pins its after_run at_exit # to the pid that armed autorun, so the deferral target can't fire # there and the child's resultset would be silently dropped. See # issue #1227. defer_to_minitest_after_run if minitest_autorun_pending? && !forked_subprocess? Kernel.at_exit do next if SimpleCov.external_at_exit? SimpleCov.at_exit_behavior end end
def load_profile(name)
def load_profile(name) profiles.load(name) end
def mark_forked_subprocess!
- Api: - private — marked in the child immediately after a fork.
def mark_forked_subprocess! @forked_subprocess = true end
def minitest_autorun_pending?
is armed, route the report through `Minitest.after_run` instead,
resultset. When we can see that Minitest is loaded and its autorun
Minitest gets a chance to invoke the tests — and format an empty
at_exit fires LIFO, SimpleCov's hook would otherwise run *before*
which means Minitest's at_exit registers before SimpleCov's. Since
`Rake::TestTask` runs `ruby -e 'require "minitest/autorun"; ...'`,
def minitest_autorun_pending? return false unless defined?(Minitest) && Minitest.respond_to?(:after_run) return false unless Minitest.class_variable_defined?(:@@installed_at_exit) Minitest.class_variable_get(:@@installed_at_exit) end
def monotonic_time
def monotonic_time Process.clock_gettime(Process::CLOCK_MONOTONIC) end
def next_subprocess_serial!
- Api: - private — bump the serial in the parent before a fork so the
def next_subprocess_serial! @subprocess_serial = subprocess_serial + 1 end
def parallel_results_complete?
- Api: - private — true when every sibling reported its resultset
def parallel_results_complete? defined?(@parallel_results_complete) ? @parallel_results_complete : true end
def parallel_wait_timed_out?(deadline, expected, seen)
- Api: - private — true once the wait deadline has passed; warns on
def parallel_wait_timed_out?(deadline, expected, seen) return false unless monotonic_time > deadline warn_about_incomplete_parallel_results(expected, seen) true end
def previous_error?(error_exit_status)
- Api: - private — strict boolean so rspec-mocks 4's predicate matcher
def previous_error?(error_exit_status) !!(error_exit_status && error_exit_status != SimpleCov::ExitCodes::SUCCESS) end
def process_coverage_result(report:)
off); with merging on the merged result reports dropped source files,
`report:` is true only when this slice is the final result (merging
Run all the steps that handle processing the raw coverage result.
def process_coverage_result(report:) raw = SimpleCov::UselessResultsRemover.call(Coverage.result) adapted = SimpleCov::ResultAdapter.call(raw) result, not_loaded_files = add_not_loaded_files(adapted) @result = SimpleCov::Result.new(result, not_loaded_files: not_loaded_files, report: report) end
def process_result(result)
- Api: - private — `exit_status = SimpleCov.process_result(SimpleCov.result)`.
def process_result(result) result_exit_status = result_exit_status(result) write_last_run(result) if result_exit_status == SimpleCov::ExitCodes::SUCCESS result_exit_status end
def process_results_and_report_error
def process_results_and_report_error exit_status = process_result(result) # Force exit with stored status (see github issue #5) return unless exit_status.positive? if print_errors ExitCodes.print_error SimpleCov::Color.colorize( "SimpleCov failed with exit #{exit_status} due to a coverage related error", :red ) end Kernel.exit exit_status end
def ready_to_process_results?
- Api: - private — the process that owns final merge processing is the
def ready_to_process_results? merge_finalization_owner? && final_result_process? && result? && (collating_result? || parallel_results_complete?) end
def reset_inherited_at_exit_state!
SimpleCov.start installs a fresh hook that actually fires at child
pid-pinned to the parent. Reset both so the at_fork proc's
external_at_exit may point at a Minitest.after_run deferral that is
parent's at_exit, after SimpleCov's own hook has fired), and
consumed before forking (Minitest autorun runs the suite inside the
@at_exit_hook_installed may describe a hook the parent already
Forked children inherit at_exit state that is wrong for them:
def reset_inherited_at_exit_state! @at_exit_hook_installed = false self.external_at_exit = false end
def result
from cache using SimpleCov::ResultMerger if use_merging is activated (default)
Returns the result for the current coverage run, merging it across test suites
def result return @result if result? use_merging = merging # Collect our coverage result. When merging is off there is no merge # step, so this per-process result is the final one and reports any # dropped source files; otherwise the merged result does the reporting. process_coverage_result(report: !use_merging) if defined?(Coverage) && Coverage.running? # If we're using merging of results, store the current result # first (if there is one), then merge the results and return those if use_merging SimpleCov::ResultMerger.store_result(@result) if result? return @result unless finalize_merge? wait_for_other_processes @result = SimpleCov::ResultMerger.merged_result end @result end
def result?
def result? defined?(@result) && @result end
def result_exit_status(result)
def result_exit_status(result) ExitCodes::ExitCodeHandling.call(result, coverage_limits: build_coverage_limits) end
def resultset_count_settled?(tracker, count)
`PARALLEL_RESULTS_SETTLE` seconds. `tracker` carries the last count and
Track whether the resultset count has held steady (and positive) for
def resultset_count_settled?(tracker, count) if count > tracker[:count] tracker[:count] = count tracker[:since] = monotonic_time return false end count.positive? && (monotonic_time - tracker[:since]) >= PARALLEL_RESULTS_SETTLE end
def round_coverage(coverage)
- Api: - private — round down to two decimals to be extra strict.
def round_coverage(coverage) coverage.floor(2) end
def run_exit_tasks!
- Api: - private — called from the at_exit block.
def run_exit_tasks! error_exit_status = exit_status_from_exception at_exit.call exit_and_report_previous_error(error_exit_status) if previous_error?(error_exit_status) process_results_and_report_error if ready_to_process_results? end
def start(profile = nil, &)
SimpleCov.start { add_filter 'test' } # with a config block
SimpleCov.start 'rails' # using a profile
SimpleCov.start
the full DSL, or:
Sets up SimpleCov to run against your project. See README for
def start(profile = nil, &) warn_about_start_in_dot_simplecov if @autoloading_dot_simplecov initial_setup(profile, &) start_tracking install_at_exit_hook end
def start_coverage_measurement
criteria-hash form, so no compatibility fallback is needed.
runtime (CRuby >= 3.2, JRuby >= 10, TruffleRuby >= 22) accepts the
Trigger Coverage.start with the configured criteria. Every supported
def start_coverage_measurement start_arguments = coverage_criteria.to_h do |criterion| [CRITERION_TO_RUBY_COVERAGE.fetch(criterion), true] end start_arguments[:eval] = true if coverage_for_eval_enabled? Coverage.start(**start_arguments) unless Coverage.running? end
def start_tracking
process_start_time / pid / fork-hook bookkeeping.
`Coverage` itself before requiring simplecov, but still wants the
the two — for example a dogfood test that has already started
`SimpleCov.configure { ... }` for callers that want to separate
Begin coverage tracking without applying configuration. Pairs with
def start_tracking require "coverage" warn_if_jruby_full_trace_disabled validate_coverage_criteria! # simplecov:disable — fork-hook is enabled via SimpleCov.enable_for_subprocesses, off by default require_relative "simplecov/process" if SimpleCov.enabled_for_subprocesses? && ::Process.respond_to?(:_fork) # simplecov:enable # Trigger adapter selection now so the (possibly lazy) parallel_tests # gem load happens at start_tracking time rather than mid-suite. # `current` is memoized; subsequent calls are cheap. SimpleCov::ParallelAdapters.current @result = nil self.pid = Process.pid self.process_start_time = Time.now start_coverage_measurement end
def subprocess_serial
overwrites the previous run's resultset entries instead of writing
the serial sequence is the same from one run to the next, so a re-run
builds the worker's command_name from this rather than the OS pid:
subprocess (see SimpleCov::ProcessForkHook). The default `at_fork`
A monotonically increasing serial the parent assigns to each forked
def subprocess_serial @subprocess_serial ||= 0 end
def unloaded_file_discovery_globs
with every string glob declared via `cover` (also restrictive,
result. Combines the legacy `track_files` glob (additive only)
Globs to expand on disk when injecting unloaded files into the
def unloaded_file_discovery_globs [tracked_files, *cover_globs].compact end
def wait_for_other_processes
- Api: - private
def wait_for_other_processes adapter = SimpleCov::ParallelAdapters.current return unless adapter && final_result_process? # Native synchronization first (adapters that wrap a runner with a # real "wait" primitive — parallel_tests' # `wait_for_other_processes_to_finish` — implement this; adapters # without a native API no-op and rely on the polling fallback below). adapter.wait_for_siblings # The native wait can return before sibling at_exit handlers finish # writing resultsets, and adapters without a native wait have # nothing else. Either way, poll the resultset cache until all # expected workers have reported or a timeout is reached. Capture # the outcome so `ready_to_process_results?` can suppress min/max # threshold checks against a partial total. @parallel_results_complete = wait_for_parallel_results(adapter.expected_worker_count, native_wait: adapter.native_wait?) end
def wait_for_parallel_results(expected, native_wait: false)
- Api: - private — returns true when the reporting worker has every
def wait_for_parallel_results(expected, native_wait: false) return true unless expected > 1 # simplecov:disable branch — only false in real parallel runs deadline = monotonic_time + parallel_wait_timeout tracker = {count: 0, since: monotonic_time} loop do seen = SimpleCov::ResultMerger.read_resultset.size return true if seen >= expected return true if native_wait && resultset_count_settled?(tracker, seen) return false if parallel_wait_timed_out?(deadline, expected, seen) sleep 0.1 end end
def warn_about_deferred_report
def warn_about_deferred_report return unless print_errors ExitCodes.print_error SimpleCov::Color.colorize( "Skipping SimpleCov report — this process tracked no application code and a newer " \ "report already exists at #{coverage_path}. This usually means SimpleCov.start ran in a " \ "parent process (e.g. a Rakefile or Rails' Bundler.require) that shelled out to the test " \ "runner. See https://github.com/simplecov-ruby/simplecov/issues/581.", :yellow ) end
def warn_about_incomplete_parallel_results(expected, seen)
- Api: - private
def warn_about_incomplete_parallel_results(expected, seen) return unless print_errors warn SimpleCov::Color.colorize( "Only #{seen} of #{expected} parallel-test workers reported within " \ "#{parallel_wait_timeout}s, so coverage totals are partial and minimum / " \ "maximum coverage checks are skipped for this run. Increase " \ "SimpleCov.parallel_wait_timeout if a worker routinely needs longer.", :yellow ) end
def warn_about_start_in_dot_simplecov
def warn_about_start_in_dot_simplecov return if @dot_simplecov_start_warned @dot_simplecov_start_warned = true warn "[DEPRECATION] Calling `SimpleCov.start` from `.simplecov` is deprecated and will " \ "be removed in a future release. `.simplecov` should contain configuration only; " \ "move the `SimpleCov.start` call into your `spec_helper.rb` / `test_helper.rb`. " \ "Coverage tracking still begins for backward compatibility, but a future release " \ "will require the explicit `SimpleCov.start` from a test helper. " \ "See https://github.com/simplecov-ruby/simplecov/issues/581." end
def warn_if_jruby_full_trace_disabled
- See: https://github.com/simplecov-ruby/simplecov/issues/86 -
See: https://github.com/simplecov-ruby/simplecov/issues/420 -
See: https://github.com/jruby/jruby/issues/1196 -
def warn_if_jruby_full_trace_disabled return unless defined?(JRUBY_VERSION) && defined?(JRuby) # simplecov:disable — JRuby-only branch # simplecov:disable — JRuby-only branches; unreachable from CRuby # `org` is JRuby's Java-package entry point; it does not exist on # CRuby, so no RBS declaration can be truthful here. return if org.jruby.RubyInstanceConfig.FULL_TRACE_ENABLED # steep:ignore NoMethod warn 'Coverage may be inaccurate; set the "--debug" command line option, ' \ 'or do JRUBY_OPTS="--debug" ' \ 'or set the "debug.fullTrace=true" option in your .jrubyrc' # simplecov:enable end
def with_dot_simplecov_autoload
- Api: - private
def with_dot_simplecov_autoload # Read in the ensure clause, where flow analysis cannot see the # assignment above; anchor the type here. previous = @autoloading_dot_simplecov # : bool? @autoloading_dot_simplecov = true yield ensure # @type var previous: bool? @autoloading_dot_simplecov = previous end
def write_last_run(result)
- Api: - private — persist the per-criterion coverage percentages
def write_last_run(result) SimpleCov::LastRun.write( result: result.coverage_statistics.transform_values { |stats| round_coverage(stats.percent) } ) end