Module: Polyrun::Coverage::Collector

Defined in:
lib/polyrun/coverage/collector.rb,
lib/polyrun/coverage/collector_finish.rb

Overview

Stdlib Coverage → SimpleCov-compatible JSON for merge-coverage / report-coverage. No SimpleCov gem. Enable with POLYRUN_COVERAGE=1 or call start! from spec_helper.

Disable with POLYRUN_COVERAGE_DISABLE=1 or SIMPLECOV_DISABLE=1 (migration alias).

Branch coverage: set POLYRUN_COVERAGE_BRANCHES=1 so stdlib Coverage.start records branches; merge-coverage merges branch keys when present in fragments.

Class Method Summary collapse

Class Method Details

.branch_coverage_enabled?Boolean

Returns:

  • (Boolean)


84
85
86
# File 'lib/polyrun/coverage/collector.rb', line 84

def branch_coverage_enabled?
  %w[1 true yes].include?(ENV["POLYRUN_COVERAGE_BRANCHES"]&.downcase)
end

.build_meta(cfg) ⇒ Object



125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
# File 'lib/polyrun/coverage/collector.rb', line 125

def build_meta(cfg)
  m = (cfg[:meta] || {}).transform_keys(&:to_s)
  m["polyrun_version"] = Polyrun::VERSION
  m["timestamp"] ||= Time.now.to_i
  m["command_name"] ||= "rspec"
  m["polyrun_coverage_root"] = cfg[:root].to_s
  CollectorFragmentMeta.merge_fragment_meta!(m, cfg[:fragment_meta])
  if cfg[:groups]
    m["polyrun_coverage_groups"] = cfg[:groups].transform_keys(&:to_s).transform_values(&:to_s)
  end
  if cfg[:track_files]
    m["polyrun_track_files"] = cfg[:track_files]
  end
  m
end

.coverage_requested_for_quick?(root = Dir.pwd) ⇒ Boolean

Whether polyrun quick should call Rails.start! before loading quick files: not disabled, and (+POLYRUN_COVERAGE=1+ or (config/polyrun_coverage.yml exists and POLYRUN_QUICK_COVERAGE=1)).

Returns:

  • (Boolean)


100
101
102
103
104
105
106
107
108
109
110
# File 'lib/polyrun/coverage/collector.rb', line 100

def self.coverage_requested_for_quick?(root = Dir.pwd)
  return false if disabled?
  return true if %w[1 true yes].include?(ENV["POLYRUN_COVERAGE"]&.to_s&.downcase)

  path = File.join(File.expand_path(root), "config", "polyrun_coverage.yml")
  return false unless File.file?(path)

  # Config file alone is for merge/report defaults; opt-in so test suites that only
  # keep polyrun_coverage.yml for gates do not start Collector during `polyrun quick`.
  %w[1 true yes].include?(ENV["POLYRUN_QUICK_COVERAGE"]&.to_s&.downcase)
end

.disabled?Boolean

Returns:

  • (Boolean)


88
89
90
91
# File 'lib/polyrun/coverage/collector.rb', line 88

def disabled?
  %w[1 true yes].include?(ENV["POLYRUN_COVERAGE_DISABLE"]&.downcase) ||
    %w[1 true yes].include?(ENV["SIMPLECOV_DISABLE"]&.downcase)
end

.finishObject



9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
# File 'lib/polyrun/coverage/collector_finish.rb', line 9

def finish
  cfg = @config || return # rubocop:disable ThreadSafety/ClassInstanceVariable -- Collector stores @config from start! (same process)
  Polyrun::Debug.log_worker_kv(
    collector_finish: "start",
    polyrun_shard_index: ENV["POLYRUN_SHARD_INDEX"],
    polyrun_shard_total: ENV["POLYRUN_SHARD_TOTAL"],
    polyrun_shard_matrix_index: ENV["POLYRUN_SHARD_MATRIX_INDEX"],
    polyrun_shard_matrix_total: ENV["POLYRUN_SHARD_MATRIX_TOTAL"],
    output_path: cfg[:output_path]
  )
  Polyrun::Debug.time(Collector.finish_debug_time_label) do
    blob = Collector.send(:prepare_finish_blob, cfg)
    summary = Merge.console_summary(blob)
    group_payload = cfg[:groups] ? TrackFiles.group_summaries(blob, cfg[:root], cfg[:groups]) : nil

    Collector.send(:exit_if_below_minimum_line_percent, cfg, summary)

    Collector.send(:write_finish_fragment!, cfg, blob, group_payload)
    Collector.send(:run_finish_formatter!, cfg, blob, group_payload)
    Polyrun::Log.warn Merge.format_console_summary(summary) if ENV["POLYRUN_COVERAGE_VERBOSE"]
  end
end

.finish_debug_time_labelObject



121
122
123
# File 'lib/polyrun/coverage/collector.rb', line 121

def self.finish_debug_time_label
  CollectorFragmentMeta.finish_debug_time_label
end

.fragment_default_basename_from_env(env = ENV) ⇒ Object

Parameters:

  • root (String)

    project root (absolute or relative)

  • reject_patterns (Array<String>)

    path substrings to drop (like SimpleCov add_filter)

  • output_path (String, nil)
  • minimum_line_percent (Float, nil)

    exit 1 if below (when strict)

  • strict (Boolean)

    whether to exit non-zero on threshold failure (default true when minimum set)

  • track_under (Array<String>)

    when track_files is nil, only keep coverage keys under these dirs relative to root. Default [“lib”].

  • track_files (String, Array<String>, nil)

    glob(s) relative to root (e.g. “{lib,app}/*/.rb”). Adds never-loaded files with simulated lines, like SimpleCov track_files.

  • groups (Hash{String=>String})

    group name => glob relative to root (SimpleCov add_group); JSON gets lines.covered_percent per group and optional “Ungrouped”.

  • meta (Hash)

    extra keys under merged JSON meta

  • formatter (Object, nil)

    Object responding to format(result, output_dir:, basename:) like SimpleCov formatters (e.g. Formatter.multi or Formatter::MultiFormatter)

  • report_output_dir (String, nil)

    directory for formatter outputs (default coverage/ under root)

  • report_basename (String)

    file prefix for formatter outputs (default polyrun-coverage)



39
40
41
# File 'lib/polyrun/coverage/collector.rb', line 39

def self.fragment_default_basename_from_env(env = ENV)
  CollectorFragmentMeta.fragment_default_basename_from_env(env)
end

.keep_under_root(blob, root, track_under) ⇒ Object



174
175
176
177
178
179
180
181
182
183
184
185
# File 'lib/polyrun/coverage/collector.rb', line 174

def keep_under_root(blob, root, track_under)
  return blob if track_under.nil? || track_under.empty?

  root = File.expand_path(root)
  prefixes = track_under.map { |d| File.join(root, d) }
  blob.each_with_object({}) do |(path, entry), acc|
    p = path.to_s
    next unless prefixes.any? { |pre| p == pre || p.start_with?(pre + "/") }

    acc[path] = entry
  end
end

.normalize_blob_paths(blob, root) ⇒ Object



158
159
160
161
162
163
# File 'lib/polyrun/coverage/collector.rb', line 158

def normalize_blob_paths(blob, root)
  root = File.expand_path(root)
  blob.each_with_object({}) do |(path, entry), acc|
    acc[File.expand_path(path.to_s, root)] = entry
  end
end

.normalize_groups(groups) ⇒ Object



165
166
167
168
169
170
171
172
# File 'lib/polyrun/coverage/collector.rb', line 165

def normalize_groups(groups)
  return nil if groups.nil?

  h = groups.is_a?(Hash) ? groups : {}
  return nil if h.empty?

  h.transform_keys(&:to_s).transform_values(&:to_s)
end

.result_to_blob(raw) ⇒ Object

Normalizes stdlib Coverage.result to merge-compatible file entries (lines; branches when collected).



142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
# File 'lib/polyrun/coverage/collector.rb', line 142

def result_to_blob(raw)
  out = {}
  raw.each do |path, cov|
    next unless cov.is_a?(Hash)

    lines = cov[:lines] || cov["lines"]
    next unless lines.is_a?(Array)

    entry = {"lines" => lines.map { |x| x }}
    br = cov[:branches] || cov["branches"]
    entry["branches"] = br if br
    out[path.to_s] = entry
  end
  out
end

.run_formatter_per_worker?Boolean

When POLYRUN_SHARD_TOTAL > 1, each worker only writes the JSON fragment; merged reports (merge-coverage / report-coverage) are authoritative. Set POLYRUN_COVERAGE_WORKER_FORMATS=1 to force per-worker formatter output (debug only; duplicates work N times).

Returns:

  • (Boolean)


115
116
117
118
119
# File 'lib/polyrun/coverage/collector.rb', line 115

def run_formatter_per_worker?
  return true if ENV["POLYRUN_COVERAGE_WORKER_FORMATS"] == "1"

  ENV["POLYRUN_SHARD_TOTAL"].to_i <= 1
end

.start!(root:, reject_patterns: [], track_under: ["lib"], track_files: nil, groups: nil, output_path: nil, minimum_line_percent: nil, strict: nil, meta: {}, formatter: nil, report_output_dir: nil, report_basename: "polyrun-coverage") ⇒ Object



43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
# File 'lib/polyrun/coverage/collector.rb', line 43

def start!(root:, reject_patterns: [], track_under: ["lib"], track_files: nil, groups: nil, output_path: nil, minimum_line_percent: nil, strict: nil, meta: {}, formatter: nil, report_output_dir: nil, report_basename: "polyrun-coverage")
  return if disabled?

  root = File.expand_path(root)
  basename = fragment_default_basename_from_env
  output_path ||= File.join(root, "coverage", "polyrun-fragment-#{basename}.json")
  strict = if minimum_line_percent.nil?
    false
  else
    strict.nil? || strict
  end

  fragment_meta = CollectorFragmentMeta.fragment_meta_from_env(basename)

  @config = {
    root: root,
    track_under: Array(track_under).map(&:to_s),
    track_files: track_files,
    groups: normalize_groups(groups),
    reject_patterns: reject_patterns,
    output_path: output_path,
    minimum_line_percent: minimum_line_percent,
    strict: strict,
    meta: meta,
    formatter: formatter,
    report_output_dir: report_output_dir,
    report_basename: report_basename,
    shard_total_at_start: ENV["POLYRUN_SHARD_TOTAL"].to_i,
    fragment_meta: fragment_meta
  }

  unless ::Coverage.running?
    ::Coverage.start(lines: true, branches: branch_coverage_enabled?)
  end
  unless instance_variable_defined?(:@collector_finish_at_exit_registered)
    @collector_finish_at_exit_registered = true
    at_exit { finish }
  end
  nil
end

.started?Boolean

True after a successful start! in this process (stdlib Coverage is active).

Returns:

  • (Boolean)


94
95
96
# File 'lib/polyrun/coverage/collector.rb', line 94

def self.started?
  instance_variable_defined?(:@config) && @config
end