coverage-reporter 0.3.3 → 0.3.4

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: '084368a5d340a21403c9f358fbbd599cd48dc48dd429aec44a40b6eddfec5b4f'
4
- data.tar.gz: 4f9f847624fe3f1a5692f757d316254f41eee376643bfb8f790edac4f89a228d
3
+ metadata.gz: 26f9788a49eb6c65e9b2557867211edfc74ed886c68fd785938d2590421e1136
4
+ data.tar.gz: b76e32c4a1a6d64881e31449f25325ce977d54130a02ddce7ae6a1fb5cafabcb
5
5
  SHA512:
6
- metadata.gz: e6aca8f9a84ff71d7570967e115ae2e17652e9c0514f18396f21a069222e30f7343ea7b9f11522a932f62edf1373ef081491a232ed6b8ebabe0c87fd868022cb
7
- data.tar.gz: 1ffb8d0777fce695e3d32833d9b221c6749c8d59951afb45625a167ab755377c16519848f578037fca5f5238d081dd313bbe81edd4ff52050ea55f443e57cfd0
6
+ metadata.gz: 84b0d946f58b584cc887f16078ca08f821d06674d97f5cce171bdf95336570b11d84a0829f83927ae4d7b38c3584e3b5c1432cf2cab3d742457339a78964fb8e
7
+ data.tar.gz: 395fd5968b5b5a8e9b4ce74cecb91b297463a9a31b6013593c0989d0f8e175fb27fa82800592941928d8b067d8f3b67d17531208ac4a1168ae293ef03534a3e8
@@ -6,8 +6,8 @@ module CoverageReporter
6
6
  #
7
7
  # @param uncovered_ranges [Hash] Uncovered data where:
8
8
  # - Keys are filenames (e.g., "app/models/user.rb")
9
- # - Values are arrays of ranges representing uncovered lines
10
- # - Example: { "app/models/user.rb" => [[12,14],[29,30]] }
9
+ # - Values are hashes with :actual_ranges and :display_ranges
10
+ # - Example: { "app/models/user.rb" => { actual_ranges: [[12,14],[29,30]], display_ranges: [[12,15],[29,30]] } }
11
11
  #
12
12
  # @param modified_ranges [Hash] Modified data where:
13
13
  # - Keys are filenames (e.g., "app/models/user.rb")
@@ -22,24 +22,23 @@ module CoverageReporter
22
22
  def call
23
23
  logger.debug("Starting coverage analysis for #{@modified_ranges.size} modified files")
24
24
 
25
- intersections = {}
26
- total_modified_lines = 0
27
- total_uncovered_modified_lines = 0
25
+ accumulator = initialize_accumulator
28
26
 
29
27
  @modified_ranges.each do |file, modified_ranges|
30
- next if modified_ranges.nil? || modified_ranges.empty?
28
+ next if skip_file?(modified_ranges)
29
+ next unless file_has_coverage_data?(file)
31
30
 
32
31
  file_result = process_file(file, modified_ranges)
33
- intersections.merge!(file_result[:intersections])
34
- total_modified_lines += file_result[:modified_lines]
35
- total_uncovered_modified_lines += file_result[:uncovered_lines]
32
+ merge_file_result(accumulator, file_result)
36
33
  end
37
34
 
38
- coverage_percentage = calculate_percentage(total_modified_lines, total_uncovered_modified_lines)
39
-
40
- log_results(intersections, total_modified_lines, total_uncovered_modified_lines, coverage_percentage)
35
+ coverage_percentage = calculate_percentage(
36
+ accumulator[:total_modified_lines],
37
+ accumulator[:total_uncovered_modified_lines]
38
+ )
41
39
 
42
- build_result(intersections, total_modified_lines, total_uncovered_modified_lines, coverage_percentage)
40
+ log_results(accumulator, coverage_percentage)
41
+ build_result(accumulator, coverage_percentage)
43
42
  end
44
43
 
45
44
  private
@@ -48,100 +47,155 @@ module CoverageReporter
48
47
  CoverageReporter.logger
49
48
  end
50
49
 
51
- def process_file(file, modified_ranges)
52
- # Calculate intersection for inline comments
53
- uncovered_ranges = @uncovered_ranges[file] || []
54
- intersecting_ranges = intersect_ranges(modified_ranges, uncovered_ranges)
50
+ def initialize_accumulator
51
+ {
52
+ intersections: {},
53
+ total_modified_lines: 0,
54
+ total_uncovered_modified_lines: 0
55
+ }
56
+ end
55
57
 
56
- # Calculate coverage statistics
57
- file_modified_lines = count_lines_in_ranges(modified_ranges)
58
- uncovered_modified_lines = count_intersecting_lines(modified_ranges, uncovered_ranges)
58
+ def skip_file?(modified_ranges)
59
+ modified_ranges.nil? || modified_ranges.empty?
60
+ end
61
+
62
+ def file_has_coverage_data?(file)
63
+ @uncovered_ranges.key?(file)
64
+ end
59
65
 
60
- intersections = {}
61
- # Only include files with actual intersections (matching original behavior)
62
- intersections[file] = intersecting_ranges unless intersecting_ranges.empty?
66
+ def merge_file_result(accumulator, file_result)
67
+ accumulator[:intersections].merge!(file_result[:intersections])
68
+ accumulator[:total_modified_lines] += file_result[:modified_lines]
69
+ accumulator[:total_uncovered_modified_lines] += file_result[:uncovered_lines]
70
+ end
71
+
72
+ def process_file(file, modified_ranges)
73
+ file_data = @uncovered_ranges[file] || { actual_ranges: [], display_ranges: [] }
74
+ uncovered_ranges = file_data[:actual_ranges] || []
75
+ intersecting_ranges = find_intersecting_ranges(modified_ranges, uncovered_ranges)
63
76
 
64
77
  {
65
- intersections: intersections,
66
- modified_lines: file_modified_lines,
67
- uncovered_lines: uncovered_modified_lines
78
+ intersections: build_file_intersections(file, intersecting_ranges),
79
+ modified_lines: count_lines_in_ranges(modified_ranges),
80
+ uncovered_lines: count_intersecting_lines(modified_ranges, uncovered_ranges)
68
81
  }
69
82
  end
70
83
 
71
- def fibonacci(num)
72
- return num if num <= 1
84
+ def build_file_intersections(file, intersecting_ranges)
85
+ return {} if intersecting_ranges.empty?
73
86
 
74
- fibonacci(num - 1) + fibonacci(num - 2)
87
+ { file => intersecting_ranges }
75
88
  end
76
89
 
77
- def log_results(intersections, total_modified_lines, total_uncovered_modified_lines, coverage_percentage)
78
- logger.debug("Identified modified uncovered intersection: #{intersections}")
79
- logger.debug(
80
- "Coverage calculation: #{total_modified_lines} total lines, " \
81
- "#{total_uncovered_modified_lines} uncovered, #{coverage_percentage}% covered"
82
- )
90
+ def find_intersecting_ranges(modified_ranges, uncovered_ranges)
91
+ return [] if uncovered_ranges.empty?
92
+
93
+ result = []
94
+ modified_index = 0
95
+ uncovered_index = 0
96
+
97
+ while modified_index < modified_ranges.size && uncovered_index < uncovered_ranges.size
98
+ modified_range = modified_ranges[modified_index]
99
+ uncovered_range = uncovered_ranges[uncovered_index]
100
+
101
+ intersection = calculate_range_intersection(modified_range, uncovered_range)
102
+ result << intersection if intersection
103
+
104
+ modified_index, uncovered_index = advance_indices(
105
+ modified_range,
106
+ uncovered_range,
107
+ modified_index,
108
+ uncovered_index
109
+ )
110
+ end
111
+
112
+ result
83
113
  end
84
114
 
85
- def build_result(intersections, total_modified_lines, total_uncovered_modified_lines, coverage_percentage)
86
- {
87
- intersections: intersections,
88
- coverage_stats: {
89
- total_modified_lines: total_modified_lines,
90
- uncovered_modified_lines: total_uncovered_modified_lines,
91
- covered_modified_lines: total_modified_lines - total_uncovered_modified_lines,
92
- coverage_percentage: coverage_percentage
93
- }
94
- }
115
+ def calculate_range_intersection(range1, range2)
116
+ start1, end1 = range1
117
+ start2, end2 = range2
118
+
119
+ intersection_start = [start1, start2].max
120
+ intersection_end = [end1, end2].min
121
+
122
+ return nil if intersection_start > intersection_end
123
+
124
+ [intersection_start, intersection_end]
95
125
  end
96
126
 
97
- def count_lines_in_ranges(ranges)
98
- ranges.sum { |range| range[1] - range[0] + 1 }
127
+ def advance_indices(modified_range, uncovered_range, modified_index, uncovered_index)
128
+ modified_end = modified_range[1]
129
+ uncovered_end = uncovered_range[1]
130
+
131
+ if modified_end < uncovered_end
132
+ [modified_index + 1, uncovered_index]
133
+ else
134
+ [modified_index, uncovered_index + 1]
135
+ end
99
136
  end
100
137
 
101
138
  def count_intersecting_lines(modified_ranges, uncovered_ranges)
102
139
  return 0 if uncovered_ranges.empty?
103
140
 
104
- intersecting_lines = 0
105
- i = j = 0
141
+ total_lines = 0
142
+ modified_index = 0
143
+ uncovered_index = 0
106
144
 
107
- while i < modified_ranges.size && j < uncovered_ranges.size
108
- modified_start, modified_end = modified_ranges[i]
109
- uncovered_start, uncovered_end = uncovered_ranges[j]
145
+ while modified_index < modified_ranges.size && uncovered_index < uncovered_ranges.size
146
+ modified_range = modified_ranges[modified_index]
147
+ uncovered_range = uncovered_ranges[uncovered_index]
110
148
 
111
- # Find intersection
112
- intersection_start = [modified_start, uncovered_start].max
113
- intersection_end = [modified_end, uncovered_end].min
149
+ intersection = calculate_range_intersection(modified_range, uncovered_range)
150
+ total_lines += count_lines_in_range(intersection) if intersection
114
151
 
115
- intersecting_lines += intersection_end - intersection_start + 1 if intersection_start <= intersection_end
116
-
117
- # Move to next range
118
- if modified_end < uncovered_end
119
- i += 1
120
- else
121
- j += 1
122
- end
152
+ modified_index, uncovered_index = advance_indices(
153
+ modified_range,
154
+ uncovered_range,
155
+ modified_index,
156
+ uncovered_index
157
+ )
123
158
  end
124
159
 
125
- intersecting_lines
160
+ total_lines
126
161
  end
127
162
 
128
- # rubocop:disable Metrics/AbcSize
129
- def intersect_ranges(changed, uncovered)
130
- i = j = 0
131
- result = []
132
- while i < changed.size && j < uncovered.size
133
- s = [changed[i][0], uncovered[j][0]].max
134
- e = [changed[i][1], uncovered[j][1]].min
135
- result << [s, e] if s <= e
136
- if changed[i][1] < uncovered[j][1]
137
- i += 1
138
- else
139
- j += 1
140
- end
141
- end
142
- result
163
+ def count_lines_in_ranges(ranges)
164
+ ranges.sum { |range| count_lines_in_range(range) }
165
+ end
166
+
167
+ def count_lines_in_range(range)
168
+ start_line, end_line = range
169
+ end_line - start_line + 1
170
+ end
171
+
172
+ def log_results(accumulator, coverage_percentage)
173
+ logger.debug("Identified modified uncovered intersection: #{accumulator[:intersections]}")
174
+ logger.debug(
175
+ "Coverage calculation: #{accumulator[:total_modified_lines]} total lines, " \
176
+ "#{accumulator[:total_uncovered_modified_lines]} uncovered, " \
177
+ "#{coverage_percentage}% covered"
178
+ )
179
+ end
180
+
181
+ def build_result(accumulator, coverage_percentage)
182
+ {
183
+ intersections: accumulator[:intersections],
184
+ coverage_stats: build_coverage_stats(accumulator, coverage_percentage)
185
+ }
186
+ end
187
+
188
+ def build_coverage_stats(accumulator, coverage_percentage)
189
+ total_modified_lines = accumulator[:total_modified_lines]
190
+ uncovered_modified_lines = accumulator[:total_uncovered_modified_lines]
191
+
192
+ {
193
+ total_modified_lines: total_modified_lines,
194
+ uncovered_modified_lines: uncovered_modified_lines,
195
+ covered_modified_lines: total_modified_lines - uncovered_modified_lines,
196
+ coverage_percentage: coverage_percentage
197
+ }
143
198
  end
144
- # rubocop:enable Metrics/AbcSize
145
199
 
146
200
  def calculate_percentage(total_lines, uncovered_lines)
147
201
  return 100.0 if total_lines == 0
@@ -19,16 +19,19 @@ module CoverageReporter
19
19
  coverage_files = Dir["#{coverage_dir}/resultset-*.json"]
20
20
  abort "No coverage JSON files found to collate" if coverage_files.empty?
21
21
 
22
- puts "Collate coverage files: #{coverage_files.join(', ')}"
22
+ logger.debug("Collate coverage files: #{coverage_files.join(', ')}")
23
+ logger.debug("Working directory: #{working_dir}")
24
+ logger.debug("Filenames: #{filenames}")
23
25
 
24
26
  ::SimpleCov.root(working_dir) if working_dir
25
27
 
26
28
  ::SimpleCov.collate(coverage_files) do
29
+ filters.clear
27
30
  add_filter(build_filter) if filenames.any? && working_dir
28
31
  formatter(build_formatter)
29
32
  end
30
33
 
31
- puts "✅ Coverage merged and report generated."
34
+ logger.info("✅ Coverage merged and report generated.")
32
35
  end
33
36
  # rubocop:enable Metrics/AbcSize
34
37
 
@@ -36,6 +39,10 @@ module CoverageReporter
36
39
 
37
40
  attr_reader :coverage_dir, :filenames, :working_dir
38
41
 
42
+ def logger
43
+ CoverageReporter.logger
44
+ end
45
+
39
46
  def build_formatter
40
47
  ::SimpleCov::Formatter::MultiFormatter.new(
41
48
  [
@@ -47,7 +54,7 @@ module CoverageReporter
47
54
 
48
55
  def build_filter
49
56
  lambda do |src_file|
50
- normalized_filename = src_file.filename.gsub(working_dir, "").gsub(%r{^/}, "")
57
+ normalized_filename = src_file.filename.delete_prefix(working_dir).delete_prefix("/")
51
58
  filenames.none?(normalized_filename)
52
59
  end
53
60
  end
@@ -19,7 +19,7 @@ module CoverageReporter
19
19
  <!-- coverage-comment-marker -->
20
20
  **Test Coverage Summary**
21
21
 
22
- #{coverage_percentage < 100 ? '❌' : '✅'} **#{coverage_percentage}%** of changed lines are covered.
22
+ #{coverage_percentage < 100 ? '❌' : '✅'} **#{coverage_percentage}%** of relevant modified lines are covered.
23
23
 
24
24
  [View full report](#{report_url})
25
25
 
@@ -7,15 +7,15 @@ module CoverageReporter
7
7
  end
8
8
 
9
9
  def call
10
- coverage_map = Hash.new { |h, k| h[k] = [] }
10
+ coverage_map = {}
11
11
 
12
12
  return coverage_map unless coverage
13
13
 
14
14
  coverage.each do |filename, data|
15
15
  # Remove leading slash from file paths for consistency
16
- normalized_filename = filename.start_with?("/") ? filename[1..] : filename
17
- uncovered_ranges = extract_uncovered_ranges(data["lines"])
18
- coverage_map[normalized_filename] = uncovered_ranges
16
+ normalized_filename = filename.delete_prefix("/")
17
+ ranges = extract_uncovered_ranges(data["lines"])
18
+ coverage_map[normalized_filename] = ranges
19
19
  end
20
20
 
21
21
  coverage_map
@@ -30,46 +30,60 @@ module CoverageReporter
30
30
  end
31
31
 
32
32
  def extract_uncovered_ranges(lines)
33
- return [] unless lines.is_a?(Array)
33
+ return { actual_ranges: [], display_ranges: [] } unless lines.is_a?(Array)
34
34
 
35
- uncovered_lines = []
35
+ actual_uncovered_lines = []
36
+ display_uncovered_lines = []
36
37
  i = 0
37
38
 
38
39
  while i < lines.length
39
40
  if lines[i] == 0
40
- i = process_uncovered_range(lines, uncovered_lines, i)
41
+ i = process_uncovered_range(lines, actual_uncovered_lines, display_uncovered_lines, i)
41
42
  else
42
43
  i += 1
43
44
  end
44
45
  end
45
46
 
46
- convert_to_ranges(uncovered_lines)
47
+ {
48
+ actual_ranges: convert_to_ranges(actual_uncovered_lines),
49
+ display_ranges: convert_to_ranges(display_uncovered_lines)
50
+ }
47
51
  end
48
52
 
49
- def process_uncovered_range(lines, uncovered_lines, start_index)
53
+ def process_uncovered_range(lines, actual_lines, display_lines, start_index)
50
54
  i = start_index
51
55
  # Found an uncovered line, start a range (always starts with 0)
52
- uncovered_lines << (i + 1)
56
+ line_number = i + 1
57
+ actual_lines << line_number
58
+ display_lines << line_number
53
59
  i += 1
54
60
 
55
61
  # Continue through consecutive 0s and nils
56
62
  # Include nil only if it's immediately followed by an uncovered line (0)
57
- continue_uncovered_range(lines, uncovered_lines, i)
63
+ continue_uncovered_range(lines, actual_lines, display_lines, i)
58
64
  end
59
65
 
60
- def continue_uncovered_range(lines, uncovered_lines, start_index)
66
+ def continue_uncovered_range(lines, actual_lines, display_lines, start_index)
61
67
  i = start_index
62
68
  while i < lines.length
63
- break unless should_continue_range?(lines, i)
64
-
65
- uncovered_lines << (i + 1)
66
- i += 1
69
+ line_number = i + 1
70
+ if lines[i] == 0
71
+ # Actual uncovered line - add to both
72
+ actual_lines << line_number
73
+ display_lines << line_number
74
+ i += 1
75
+ elsif lines[i].nil? && should_continue_range?(lines, i)
76
+ # Nil line that continues the range - add only to display
77
+ display_lines << line_number
78
+ i += 1
79
+ else
80
+ break
81
+ end
67
82
  end
68
83
  i
69
84
  end
70
85
 
71
86
  def should_continue_range?(lines, index)
72
- return true if lines[index] == 0
73
87
  return false unless lines[index].nil?
74
88
 
75
89
  # Include nil only if it's immediately followed by an uncovered line (0)
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module CoverageReporter
4
- VERSION = "0.3.3"
4
+ VERSION = "0.3.4"
5
5
  end
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: coverage-reporter
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.3
4
+ version: 0.3.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - Gabriel Taylor Russ
8
8
  bindir: exe
9
9
  cert_chain: []
10
- date: 2025-11-17 00:00:00.000000000 Z
10
+ date: 2025-11-25 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: octokit