polyrun 2.1.3 → 2.2.0

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 (43) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +10 -1
  3. data/CONTRIBUTING.md +22 -0
  4. data/docs/SETUP_PROFILE.md +1 -1
  5. data/ext/polyrun_coverage_merge/coverage_merge.c +492 -0
  6. data/ext/polyrun_coverage_merge/extconf.rb +3 -0
  7. data/lib/polyrun/benchmark/profile.rb +191 -0
  8. data/lib/polyrun/benchmark/report.rb +87 -0
  9. data/lib/polyrun/cli/benchmark_commands.rb +66 -0
  10. data/lib/polyrun/cli/coverage_commands.rb +4 -4
  11. data/lib/polyrun/cli/coverage_merge_io.rb +30 -4
  12. data/lib/polyrun/cli/failure_commands.rb +10 -3
  13. data/lib/polyrun/cli/help.rb +12 -8
  14. data/lib/polyrun/cli/report_commands.rb +25 -11
  15. data/lib/polyrun/cli/run_shards_parallel_children.rb +1 -1
  16. data/lib/polyrun/cli/run_shards_plan_options.rb +1 -1
  17. data/lib/polyrun/cli/spec_quality_commands.rb +16 -7
  18. data/lib/polyrun/cli.rb +8 -1
  19. data/lib/polyrun/coverage/example_diff.rb +127 -60
  20. data/lib/polyrun/coverage/example_diff_snapshot.rb +44 -0
  21. data/lib/polyrun/coverage/example_diff_track_filter.rb +51 -0
  22. data/lib/polyrun/coverage/file_stats_report.rb +36 -0
  23. data/lib/polyrun/coverage/formatter.rb +22 -1
  24. data/lib/polyrun/coverage/merge/formatters.rb +11 -3
  25. data/lib/polyrun/coverage/merge_merge_two.rb +112 -63
  26. data/lib/polyrun/coverage/merge_native.rb +37 -0
  27. data/lib/polyrun/coverage/reporting.rb +1 -1
  28. data/lib/polyrun/export/csv.rb +25 -0
  29. data/lib/polyrun/export/markdown.rb +47 -0
  30. data/lib/polyrun/hooks/shell_runner.rb +33 -0
  31. data/lib/polyrun/hooks.rb +4 -7
  32. data/lib/polyrun/reporting/failure_merge.rb +48 -12
  33. data/lib/polyrun/reporting/junit.rb +8 -7
  34. data/lib/polyrun/reporting/junit_report.rb +87 -0
  35. data/lib/polyrun/rspec/example_debug.rb +1 -1
  36. data/lib/polyrun/rspec/sharded_formatter_compat.rb +30 -7
  37. data/lib/polyrun/spec_quality/profile.rb +24 -12
  38. data/lib/polyrun/spec_quality/report.rb +93 -0
  39. data/lib/polyrun/spec_quality.rb +19 -11
  40. data/lib/polyrun/timing/summary.rb +55 -5
  41. data/lib/polyrun/version.rb +1 -1
  42. data/polyrun.gemspec +8 -1
  43. metadata +58 -2
@@ -43,10 +43,11 @@ module Polyrun
43
43
  config_path = nil
44
44
  strict = false
45
45
  json_out = false
46
+ report_format = nil
46
47
  plan_paths = []
47
48
 
48
49
  OptionParser.new do |opts|
49
- opts.banner = "usage: polyrun report-spec-quality -i FILE [-o PATH] [--top N] [--profile LIST] [--strict] [--json]"
50
+ opts.banner = "usage: polyrun report-spec-quality -i FILE [-o PATH] [--top N] [--profile LIST] [--strict] [--format text|json|csv|markdown] [--json]"
50
51
  opts.on("-i", "--input PATH", "Merged polyrun-spec-quality.json") { |v| input = v }
51
52
  opts.on("-o", "--output PATH", "Write report to file instead of stdout") { |v| out_file = v }
52
53
  opts.on("--top N", Integer) { |v| top = v }
@@ -54,6 +55,7 @@ module Polyrun
54
55
  opts.on("-c", "--config PATH", "polyrun_spec_quality.yml path") { |v| config_path = v }
55
56
  opts.on("--plan PATH", "Partition plan JSON (repeatable; polyrun plan output per shard)") { |v| plan_paths << v }
56
57
  opts.on("--strict", "Exit 1 when gate thresholds fail") { strict = true }
58
+ opts.on("--format VAL", "text (default), json, csv, or markdown") { |v| report_format = v }
57
59
  opts.on("--json", "Write analysis JSON instead of text report") { json_out = true }
58
60
  end.parse!(argv)
59
61
  input ||= argv.first
@@ -67,13 +69,20 @@ module Polyrun
67
69
  cfg = load_spec_quality_config(config_path)
68
70
  strict = true if cfg["strict"] || strict
69
71
  plan_shards = Polyrun::SpecQuality::PlanLoader.load_shards(plan_paths)
70
-
71
- text = if json_out
72
- JSON.pretty_generate(Polyrun::SpecQuality::Report.analyze(merged, cfg, plan_shards: plan_shards))
73
- else
74
- Polyrun::SpecQuality::Report.format_report(
75
- merged, cfg: cfg, top: top, profile: profile, plan_shards: plan_shards
72
+ resolved_format = report_format || (json_out ? "json" : "text")
73
+
74
+ begin
75
+ text = Polyrun::SpecQuality::Report.render(
76
+ merged,
77
+ format: resolved_format,
78
+ cfg: cfg,
79
+ top: top,
80
+ profile: profile,
81
+ plan_shards: plan_shards
76
82
  )
83
+ rescue Polyrun::Error => e
84
+ Polyrun::Log.warn e.message.to_s
85
+ return 2
77
86
  end
78
87
 
79
88
  if out_file
data/lib/polyrun/cli.rb CHANGED
@@ -14,6 +14,7 @@ require_relative "cli/run_queue_command"
14
14
  require_relative "cli/queue_command"
15
15
  require_relative "cli/timing_command"
16
16
  require_relative "cli/spec_quality_commands"
17
+ require_relative "cli/benchmark_commands"
17
18
  require_relative "cli/init_command"
18
19
  require_relative "cli/quick_command"
19
20
  require_relative "cli/ci_shard_run_parse"
@@ -32,7 +33,7 @@ module Polyrun
32
33
 
33
34
  # Keep in sync with +dispatch_cli_command_subcommands+ (+when+ branches). Used for implicit path routing.
34
35
  DISPATCH_SUBCOMMAND_NAMES = %w[
35
- plan prepare merge-coverage merge-failures merge-spec-quality report-coverage report-junit report-timing report-spec-quality
36
+ plan prepare merge-coverage merge-failures merge-spec-quality report-coverage report-junit report-timing report-spec-quality report-benchmark bench
36
37
  env config merge-timing db:setup-template db:setup-shard db:clone-shards
37
38
  run-shards parallel-rspec start build-paths init queue run-queue quick hook
38
39
  ].freeze
@@ -55,6 +56,7 @@ module Polyrun
55
56
  include QueueCommand
56
57
  include TimingCommand
57
58
  include SpecQualityCommands
59
+ include BenchmarkCommands
58
60
  include InitCommand
59
61
  include QuickCommand
60
62
  include CiShardRunParse
@@ -120,6 +122,7 @@ module Polyrun
120
122
  config_path = argv.shift or break
121
123
  when "-v", "--verbose"
122
124
  @verbose = true
125
+ ENV["POLYRUN_VERBOSE"] = "1"
123
126
  argv.shift
124
127
  when "-h", "--help"
125
128
  print_help
@@ -170,6 +173,10 @@ module Polyrun
170
173
  cmd_merge_spec_quality(argv)
171
174
  when "report-spec-quality"
172
175
  cmd_report_spec_quality(argv)
176
+ when "report-benchmark"
177
+ cmd_report_benchmark(argv)
178
+ when "bench"
179
+ cmd_bench(argv)
173
180
  when "db:setup-template"
174
181
  cmd_db_setup_template(argv, config_path)
175
182
  when "db:setup-shard"
@@ -1,63 +1,124 @@
1
+ require_relative "example_diff_track_filter"
2
+ require_relative "example_diff_snapshot"
3
+
1
4
  module Polyrun
2
5
  module Coverage
3
6
  # Per-example line hit deltas from stdlib +Coverage.peek_result+ snapshots.
4
7
  module ExampleDiff
5
8
  module_function
6
9
 
7
- # @return [Hash{String=>Hash}] path => +{"lines"=>[...]}+
8
- def peek_blob
10
+ # @return [Hash{String=>Hash}] path => sparse line snapshot
11
+ def peek_blob(root: nil, track_under: nil, ignore_paths: nil)
9
12
  return {} unless coverage_active?
10
13
 
11
- normalize_peek(::Coverage.peek_result)
14
+ snapshot_peek(
15
+ ::Coverage.peek_result,
16
+ root: root,
17
+ track_under: track_under,
18
+ ignore_paths: ignore_paths
19
+ )
12
20
  end
13
21
 
14
- def coverage_active?
15
- defined?(::Coverage) && ::Coverage.respond_to?(:running?) && ::Coverage.running?
16
- end
22
+ # Frozen snapshot for storage between peeks (stdlib mutates line arrays in place).
23
+ def snapshot_peek(raw, root: nil, track_under: nil, ignore_paths: nil)
24
+ return {} if raw.nil? || raw.empty?
17
25
 
18
- def normalize_peek(raw)
26
+ filter = track_filter_for(root: root, track_under: track_under, ignore_paths: ignore_paths)
19
27
  out = {}
20
- raw.each do |path, cov|
21
- next unless cov.is_a?(Hash)
28
+ raw.each do |path, coverage_entry|
29
+ next unless coverage_entry.is_a?(Hash)
30
+ next if filter && !filter.include_path?(path.to_s)
22
31
 
23
- lines = cov[:lines] || cov["lines"]
32
+ lines = coverage_entry[:lines] || coverage_entry["lines"]
24
33
  next unless lines.is_a?(Array)
25
34
 
26
- out[path.to_s] = {"lines" => lines.map { |x| x }}
35
+ out[path.to_s] = Snapshot.sparse_snapshot_lines(lines)
27
36
  end
28
37
  out
29
38
  end
30
39
 
31
- # @return [Hash] +:unique_lines+, +:line_churn+, +:max_line_churn+, +:lines+ (compact triples)
32
- # rubocop:disable Metrics/AbcSize -- per-file coverage delta walk
33
- def diff(before_blob, after_blob)
40
+ # +after_source+ may be a normalized blob or a raw +Coverage.peek_result+ hash.
41
+ def diff(before_blob, after_source, root: nil, track_under: nil, ignore_paths: nil)
42
+ filter = track_filter_for(root: root, track_under: track_under, ignore_paths: ignore_paths)
43
+ if raw_peek_result?(after_source)
44
+ diff_against_peek(before_blob, after_source, filter: filter)
45
+ else
46
+ diff_blobs(before_blob, after_source, filter: filter)
47
+ end
48
+ end
49
+
50
+ def coverage_active?
51
+ defined?(::Coverage) && ::Coverage.respond_to?(:running?) && ::Coverage.running?
52
+ end
53
+
54
+ def diff_blobs(before_blob, after_blob, filter: nil)
34
55
  before_blob ||= {}
35
56
  after_blob ||= {}
36
- files = before_blob.keys | after_blob.keys
57
+ paths = collect_diff_paths(before_blob, after_blob, filter: filter)
58
+ diff_paths(paths, before_blob, after_blob, raw_after: false)
59
+ end
60
+
61
+ def diff_against_peek(before_blob, after_raw, filter: nil)
62
+ before_blob ||= {}
63
+ after_raw ||= {}
64
+ paths = collect_diff_paths(before_blob, after_raw, filter: filter)
65
+ diff_paths(paths, before_blob, after_raw, raw_after: true)
66
+ end
67
+
68
+ def filter_lines(lines, root:, track_under:, ignore_paths: [])
69
+ filter = TrackFilter.new(root: root, track_under: track_under, ignore_paths: ignore_paths)
70
+ lines.select { |path, _line, _delta| filter.include_path?(path) }
71
+ end
72
+
73
+ def apply_track_under(delta, root:, track_under:, ignore_paths: [])
74
+ filtered = filter_lines(delta[:lines] || [], root: root, track_under: track_under, ignore_paths: ignore_paths)
75
+ unique = filtered.size
76
+ churn = filtered.sum { |(_path, _line, hit_delta)| hit_delta }
77
+ max_churn = filtered.map { |(_path, _line, hit_delta)| hit_delta }.max || 0
78
+ {
79
+ unique_lines: unique,
80
+ line_churn: churn,
81
+ max_line_churn: max_churn,
82
+ lines: filtered
83
+ }
84
+ end
85
+
86
+ def line_array(entry)
87
+ return [] unless entry.is_a?(Hash)
88
+ return [] if entry["sparse"]
37
89
 
90
+ arr = entry["lines"] || entry[:lines]
91
+ arr.is_a?(Array) ? arr : []
92
+ end
93
+
94
+ def raw_peek_result?(source)
95
+ return false unless source.is_a?(Hash)
96
+ return false if source.empty?
97
+
98
+ entry = source.values.first
99
+ entry.is_a?(Hash) && (entry.key?(:lines) || entry.key?(:branches))
100
+ end
101
+
102
+ def diff_paths(paths, before_blob, after_blob, raw_after:)
38
103
  line_entries = []
39
104
  unique = 0
40
105
  churn = 0
41
106
  max_churn = 0
42
107
 
43
- files.each do |path|
44
- b_lines = line_array(before_blob[path])
45
- a_lines = line_array(after_blob[path])
46
- max_len = [b_lines.size, a_lines.size].max
47
-
48
- (0...max_len).each do |i|
49
- b = b_lines[i]
50
- a = a_lines[i]
51
- next if a.nil? && b.nil?
108
+ paths.each do |path|
109
+ before_entry = before_blob[path]
110
+ after_lines =
111
+ if raw_after
112
+ lines_from_peek_entry(path, after_blob)
113
+ else
114
+ line_array(after_blob[path])
115
+ end
52
116
 
53
- delta = (integer_hit(a) - integer_hit(b))
54
- next unless delta.positive?
55
-
56
- line_no = i + 1
57
- line_entries << [path, line_no, delta]
117
+ accumulate_line_delta(before_entry, after_lines) do |delta, line_no|
58
118
  unique += 1
59
119
  churn += delta
60
120
  max_churn = delta if delta > max_churn
121
+ line_entries << [path, line_no, delta]
61
122
  end
62
123
  end
63
124
 
@@ -68,55 +129,61 @@ module Polyrun
68
129
  lines: line_entries
69
130
  }
70
131
  end
71
- # rubocop:enable Metrics/AbcSize
72
-
73
- def filter_lines(lines, root:, track_under:, ignore_paths: [])
74
- root = File.expand_path(root)
75
- prefixes = Array(track_under).map { |d| File.join(root, d.to_s) }
76
- ignore = Array(ignore_paths).map(&:to_s).reject(&:empty?)
77
-
78
- lines.select do |path, _line, _delta|
79
- p = File.expand_path(path.to_s, root)
80
- next false if ignore.any? { |pat| path_matches_ignore?(p, pat) }
81
132
 
82
- prefixes.any? { |pre| p == pre || p.start_with?(pre + "/") }
133
+ def collect_diff_paths(before_blob, after_blob, filter: nil)
134
+ keys = before_blob.keys.dup
135
+ after_blob.each_key do |path|
136
+ string_path = path.to_s
137
+ keys << string_path unless keys.include?(string_path)
83
138
  end
84
- end
139
+ return keys unless filter
85
140
 
86
- def apply_track_under(delta, root:, track_under:, ignore_paths: [])
87
- filtered = filter_lines(delta[:lines] || [], root: root, track_under: track_under, ignore_paths: ignore_paths)
88
- unique = filtered.size
89
- churn = filtered.sum { |(_p, _l, d)| d }
90
- max_churn = filtered.map { |(_p, _l, d)| d }.max || 0
91
- {
92
- unique_lines: unique,
93
- line_churn: churn,
94
- max_line_churn: max_churn,
95
- lines: filtered
96
- }
141
+ keys.select { |path| filter.include_path?(path) }
97
142
  end
98
143
 
99
- def path_matches_ignore?(path, pattern)
100
- return path.include?(pattern) if pattern.is_a?(String) && !pattern.start_with?("/")
144
+ def accumulate_line_delta(before_entry, after_lines)
145
+ before_hits = Snapshot.hits_map(before_entry)
146
+ before_max = before_hits.empty? ? 0 : before_hits.keys.max + 1
147
+ max_len = [after_lines.size, before_max].max
101
148
 
102
- path.match?(Regexp.new(pattern))
103
- rescue RegexpError
104
- path.include?(pattern.to_s)
149
+ (0...max_len).each do |index|
150
+ before_hit = before_hits[index]
151
+ after_hit = after_lines[index]
152
+ next if after_hit.nil? && before_hit.nil?
153
+
154
+ delta = integer_hit(after_hit) - integer_hit(before_hit)
155
+ next unless delta.positive?
156
+
157
+ yield(delta, index + 1)
158
+ end
105
159
  end
106
160
 
107
- def line_array(entry)
161
+ def lines_from_peek_entry(path, after_raw)
162
+ entry = after_raw[path] || after_raw[path.to_s]
108
163
  return [] unless entry.is_a?(Hash)
109
164
 
110
- arr = entry["lines"] || entry[:lines]
165
+ arr = entry[:lines] || entry["lines"]
111
166
  arr.is_a?(Array) ? arr : []
112
167
  end
113
168
 
169
+ def track_filter_for(root:, track_under:, ignore_paths:)
170
+ return nil if track_under.nil? && ignore_paths.nil?
171
+
172
+ TrackFilter.new(
173
+ root: root || Dir.pwd,
174
+ track_under: track_under,
175
+ ignore_paths: ignore_paths || []
176
+ )
177
+ end
178
+
114
179
  def integer_hit(value)
115
180
  return 0 if value.nil?
181
+ return 0 if value == "ignored"
116
182
 
117
183
  value.is_a?(Integer) ? value : value.to_i
118
184
  end
119
- private_class_method :line_array, :integer_hit
185
+ private_class_method :diff_paths, :collect_diff_paths, :accumulate_line_delta,
186
+ :lines_from_peek_entry, :track_filter_for, :integer_hit, :raw_peek_result?
120
187
  end
121
188
  end
122
189
  end
@@ -0,0 +1,44 @@
1
+ module Polyrun
2
+ module Coverage
3
+ module ExampleDiff
4
+ # Sparse line-hit snapshots for per-example diff storage.
5
+ module Snapshot
6
+ module_function
7
+
8
+ def sparse_snapshot_lines(lines)
9
+ {"sparse" => true, "hits" => dense_hits_map(lines)}
10
+ end
11
+
12
+ def hits_map(entry)
13
+ return {} if entry.nil?
14
+
15
+ if entry.is_a?(Hash) && entry["sparse"]
16
+ entry["hits"] || {}
17
+ else
18
+ dense_hits_map(ExampleDiff.line_array(entry))
19
+ end
20
+ end
21
+
22
+ def dense_hits_map(lines)
23
+ hits = {}
24
+ lines.each_with_index do |value, index|
25
+ stored = snapshot_line_value(value)
26
+ hits[index] = stored unless stored.nil?
27
+ end
28
+ hits
29
+ end
30
+
31
+ def snapshot_line_value(value)
32
+ case value
33
+ when nil then nil
34
+ when "ignored" then "ignored"
35
+ when Integer then value
36
+ else
37
+ parsed = Integer(value, exception: false)
38
+ parsed.nil? ? value : parsed
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,51 @@
1
+ module Polyrun
2
+ module Coverage
3
+ module ExampleDiff
4
+ # Path inclusion for scoped snapshots and track-aware diffs.
5
+ class TrackFilter
6
+ def initialize(root:, track_under: nil, ignore_paths: [])
7
+ @root = root ? File.expand_path(root) : nil
8
+ @track_under = Array(track_under).map(&:to_s).reject(&:empty?)
9
+ @ignore = Array(ignore_paths).map(&:to_s).reject(&:empty?)
10
+ @prefixes =
11
+ if @root && !@track_under.empty?
12
+ @track_under.map { |directory| File.join(@root, directory) }
13
+ else
14
+ []
15
+ end
16
+ end
17
+
18
+ def include_path?(path)
19
+ absolute = absolute_path(path)
20
+ return false if ignored?(absolute)
21
+
22
+ return true if @prefixes.empty?
23
+
24
+ @prefixes.any? { |prefix| absolute == prefix || absolute.start_with?(prefix + "/") }
25
+ end
26
+
27
+ private
28
+
29
+ def absolute_path(path)
30
+ if @root
31
+ File.expand_path(path.to_s, @root)
32
+ else
33
+ File.expand_path(path.to_s)
34
+ end
35
+ end
36
+
37
+ def ignored?(absolute)
38
+ @ignore.any? { |pattern| path_matches_ignore?(absolute, pattern) }
39
+ end
40
+
41
+ def path_matches_ignore?(path, pattern)
42
+ return path.include?(pattern) if !pattern.start_with?("/")
43
+
44
+ path.match?(Regexp.new(pattern))
45
+ rescue RegexpError
46
+ path.include?(pattern)
47
+ end
48
+ end
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,36 @@
1
+ require_relative "../export/csv"
2
+ require_relative "../export/markdown"
3
+
4
+ module Polyrun
5
+ module Coverage
6
+ # Per-file line coverage tables for CSV and Markdown exports.
7
+ module FileStatsReport
8
+ HEADERS = %w[path line_percent lines_covered lines_relevant].freeze
9
+
10
+ module_function
11
+
12
+ def file_rows(coverage_blob)
13
+ coverage_blob.map do |path, file_entry|
14
+ line_percent, lines_relevant, lines_covered = Merge.file_line_stats(file_entry)
15
+ [path, format("%.2f", line_percent), lines_covered, lines_relevant]
16
+ end.sort_by { |row| row[1].to_f }
17
+ end
18
+
19
+ def emit_csv(coverage_blob)
20
+ Export::Csv.generate(HEADERS, file_rows(coverage_blob))
21
+ end
22
+
23
+ def emit_markdown(coverage_blob, title: "Polyrun coverage report")
24
+ summary = Merge.console_summary(coverage_blob)
25
+ summary_line = Merge.format_console_summary(summary).strip
26
+ Export::Markdown.document(
27
+ title,
28
+ [
29
+ {body: summary_line},
30
+ {heading: "Per-file coverage", headers: HEADERS, rows: file_rows(coverage_blob)}
31
+ ]
32
+ )
33
+ end
34
+ end
35
+ end
36
+ end
@@ -3,6 +3,7 @@ require "json"
3
3
 
4
4
  require_relative "merge"
5
5
  require_relative "result"
6
+ require_relative "file_stats_report"
6
7
 
7
8
  module Polyrun
8
9
  module Coverage
@@ -26,8 +27,11 @@ module Polyrun
26
27
  when :cobertura then CoberturaFormatter
27
28
  when :console then ConsoleFormatter
28
29
  when :html then HtmlFormatter
30
+ when :csv then CsvFormatter
31
+ when :markdown then MarkdownFormatter
29
32
  else
30
- raise ArgumentError, "unknown coverage format: #{name.inspect} (expected :json, :lcov, :cobertura, :console, :html)"
33
+ raise ArgumentError,
34
+ "unknown coverage format: #{name.inspect} (expected :json, :lcov, :cobertura, :console, :html, :csv, :markdown)"
31
35
  end
32
36
  end
33
37
 
@@ -111,6 +115,23 @@ module Polyrun
111
115
  {html: path}
112
116
  end
113
117
  end
118
+
119
+ class CsvFormatter < Base
120
+ def write_files(result, output_dir, basename)
121
+ path = File.join(output_dir, "#{basename}.csv")
122
+ File.write(path, FileStatsReport.emit_csv(result.coverage_blob))
123
+ {csv: path}
124
+ end
125
+ end
126
+
127
+ class MarkdownFormatter < Base
128
+ def write_files(result, output_dir, basename)
129
+ path = File.join(output_dir, "#{basename}.md")
130
+ title = (result.meta && result.meta["title"]) || (result.meta && result.meta[:title]) || "Polyrun coverage report"
131
+ File.write(path, FileStatsReport.emit_markdown(result.coverage_blob, title: title))
132
+ {markdown: path}
133
+ end
134
+ end
114
135
  end
115
136
  end
116
137
  end
@@ -153,16 +153,24 @@ module Polyrun
153
153
  # Per-file line stats for HTML and other formatters.
154
154
  # Integer line counts for one file entry (for O(files x groups) group aggregation).
155
155
  def line_counts(file_entry)
156
+ if native_acceleration?
157
+ MergeNative.line_counts(file_entry)
158
+ else
159
+ line_counts_ruby(file_entry)
160
+ end
161
+ end
162
+
163
+ def line_counts_ruby(file_entry)
156
164
  line_arr = line_array_from_file_entry(file_entry)
157
165
  return {relevant: 0, covered: 0} unless line_arr.is_a?(Array)
158
166
 
159
167
  relevant = 0
160
168
  covered = 0
161
- line_arr.each do |h|
162
- next if h.nil? || h == "ignored"
169
+ line_arr.each do |hit|
170
+ next if hit.nil? || hit == "ignored"
163
171
 
164
172
  relevant += 1
165
- covered += 1 if h.to_i > 0
173
+ covered += 1 if hit.to_i > 0
166
174
  end
167
175
  {relevant: relevant, covered: covered}
168
176
  end