module SimpleCov

def add_not_loaded_files(result)

comments / whitespace).
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

Clear out the previously cached .result. Primarily useful in testing.
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?

Other tags:
    Api: - private — true while `SimpleCov.collate` is running its finalizer.
def collating_result?
  defined?(@collating_result) && @collating_result
end

def coverage_statistics_key(criterion)

up stats for either 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?

shelled out to the test runner. See issue #581.
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)

issue #1106.
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)

Other tags:
    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

Other tags:
    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?

(`expect(...).not_to be_external_at_exit`) accepts the result.
Coerce to a proper boolean so rspec-mocks 4's predicate matcher
def external_at_exit?
  !!@external_at_exit
end

def filtered(files)

Applies the configured filters to the given array of SimpleCov::SourceFile items
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?

Other tags:
    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?

Other tags:
    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)

"Ungrouped" bucket.
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)

Applies the profile of given name on SimpleCov configuration
def load_profile(name)
  profiles.load(name)
end

def mark_forked_subprocess!

Other tags:
    Api: - private — marked in the child immediately after a fork.
def mark_forked_subprocess!
  @forked_subprocess = true
end

def minitest_autorun_pending?

which fires after the suite completes. See issues #1099 and #1112.
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!

Other tags:
    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?

Other tags:
    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)

Other tags:
    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)

Other tags:
    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:)

so the per-process slice stays quiet to avoid one warning per worker.
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)

Other tags:
    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?

Other tags:
    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!

exit. See issue #1227.
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?

Returns nil if the result has not been computed, otherwise the 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)

the time it last changed across poll iterations.
`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)

Other tags:
    Api: - private — round down to two decimals to be extra strict.
def round_coverage(coverage)
  coverage.floor(2)
end

def run_exit_tasks!

Other tags:
    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

uniquely-named ones that pile up until merge_timeout. See issue #1171.
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

but the restriction lives in `Result#apply_cover_filters!`).
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

Other tags:
    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)

Other tags:
    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)

Other tags:
    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

Other tags:
    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

Other tags:
    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)

Other tags:
    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