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)


75
76
77
# File 'lib/polyrun/coverage/collector.rb', line 75

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

.build_meta(cfg) ⇒ Object



120
121
122
123
124
125
126
127
128
129
130
131
132
133
# File 'lib/polyrun/coverage/collector.rb', line 120

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
  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)


91
92
93
94
95
96
97
98
99
100
101
# File 'lib/polyrun/coverage/collector.rb', line 91

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)


79
80
81
82
# File 'lib/polyrun/coverage/collector.rb', line 79

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
# 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"],
    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



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

def self.finish_debug_time_label
  if ENV["POLYRUN_SHARD_TOTAL"].to_i > 1
    "worker pid=#{$$} shard=#{ENV.fetch("POLYRUN_SHARD_INDEX", "?")} Coverage::Collector.finish (write fragment)"
  else
    "Coverage::Collector.finish (write fragment)"
  end
end

.keep_under_root(blob, root, track_under) ⇒ Object



168
169
170
171
172
173
174
175
176
177
178
179
# File 'lib/polyrun/coverage/collector.rb', line 168

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



152
153
154
155
156
157
# File 'lib/polyrun/coverage/collector.rb', line 152

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



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

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).



136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
# File 'lib/polyrun/coverage/collector.rb', line 136

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)


106
107
108
109
110
# File 'lib/polyrun/coverage/collector.rb', line 106

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

Parameters:

  • root (String)

    project root (absolute or relative)

  • reject_patterns (Array<String>) (defaults to: [])

    path substrings to drop (like SimpleCov add_filter)

  • output_path (String, nil) (defaults to: nil)

    default coverage/polyrun-fragment-<shard>.json

  • minimum_line_percent (Float, nil) (defaults to: nil)

    exit 1 if below (when strict)

  • strict (Boolean) (defaults to: nil)

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

  • track_under (Array<String>) (defaults to: ["lib"])

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

  • track_files (String, Array<String>, nil) (defaults to: 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}) (defaults to: nil)

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

  • meta (Hash) (defaults to: {})

    extra keys under merged JSON meta

  • formatter (Object, nil) (defaults to: nil)

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

  • report_output_dir (String, nil) (defaults to: nil)

    directory for formatter outputs (default coverage/ under root)

  • report_basename (String) (defaults to: "polyrun-coverage")

    file prefix for formatter outputs (default polyrun-coverage)



37
38
39
40
41
42
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
# File 'lib/polyrun/coverage/collector.rb', line 37

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)
  shard = ENV.fetch("POLYRUN_SHARD_INDEX", "0")
  output_path ||= File.join(root, "coverage", "polyrun-fragment-#{shard}.json")
  strict = if minimum_line_percent.nil?
    false
  else
    strict.nil? || strict
  end

  @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
  }

  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)


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

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