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 +4 -4
- data/lib/coverage_reporter/coverage_analyzer.rb +134 -80
- data/lib/coverage_reporter/coverage_collator.rb +10 -3
- data/lib/coverage_reporter/global_comment.rb +1 -1
- data/lib/coverage_reporter/uncovered_ranges_extractor.rb +31 -17
- data/lib/coverage_reporter/version.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 26f9788a49eb6c65e9b2557867211edfc74ed886c68fd785938d2590421e1136
|
|
4
|
+
data.tar.gz: b76e32c4a1a6d64881e31449f25325ce977d54130a02ddce7ae6a1fb5cafabcb
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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(
|
|
39
|
-
|
|
40
|
-
|
|
35
|
+
coverage_percentage = calculate_percentage(
|
|
36
|
+
accumulator[:total_modified_lines],
|
|
37
|
+
accumulator[:total_uncovered_modified_lines]
|
|
38
|
+
)
|
|
41
39
|
|
|
42
|
-
|
|
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
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
50
|
+
def initialize_accumulator
|
|
51
|
+
{
|
|
52
|
+
intersections: {},
|
|
53
|
+
total_modified_lines: 0,
|
|
54
|
+
total_uncovered_modified_lines: 0
|
|
55
|
+
}
|
|
56
|
+
end
|
|
55
57
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
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
|
-
|
|
61
|
-
|
|
62
|
-
|
|
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:
|
|
66
|
-
modified_lines:
|
|
67
|
-
uncovered_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
|
|
72
|
-
return
|
|
84
|
+
def build_file_intersections(file, intersecting_ranges)
|
|
85
|
+
return {} if intersecting_ranges.empty?
|
|
73
86
|
|
|
74
|
-
|
|
87
|
+
{ file => intersecting_ranges }
|
|
75
88
|
end
|
|
76
89
|
|
|
77
|
-
def
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
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
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
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
|
|
98
|
-
|
|
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
|
-
|
|
105
|
-
|
|
141
|
+
total_lines = 0
|
|
142
|
+
modified_index = 0
|
|
143
|
+
uncovered_index = 0
|
|
106
144
|
|
|
107
|
-
while
|
|
108
|
-
|
|
109
|
-
|
|
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
|
-
|
|
112
|
-
|
|
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
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
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
|
-
|
|
160
|
+
total_lines
|
|
126
161
|
end
|
|
127
162
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
|
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 =
|
|
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.
|
|
17
|
-
|
|
18
|
-
coverage_map[normalized_filename] =
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
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,
|
|
63
|
+
continue_uncovered_range(lines, actual_lines, display_lines, i)
|
|
58
64
|
end
|
|
59
65
|
|
|
60
|
-
def continue_uncovered_range(lines,
|
|
66
|
+
def continue_uncovered_range(lines, actual_lines, display_lines, start_index)
|
|
61
67
|
i = start_index
|
|
62
68
|
while i < lines.length
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
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)
|
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.
|
|
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-
|
|
10
|
+
date: 2025-11-25 00:00:00.000000000 Z
|
|
11
11
|
dependencies:
|
|
12
12
|
- !ruby/object:Gem::Dependency
|
|
13
13
|
name: octokit
|