coverage-reporter 0.3.1 → 0.3.3

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: 4396828c1c0a640f1edd7a8ff354033e238d2eac7994e0e48de6090a97266665
4
- data.tar.gz: f5c3048e767f25993afb0968995d9cde0511035354a2e75b8ab2abc0b171d89f
3
+ metadata.gz: '084368a5d340a21403c9f358fbbd599cd48dc48dd429aec44a40b6eddfec5b4f'
4
+ data.tar.gz: 4f9f847624fe3f1a5692f757d316254f41eee376643bfb8f790edac4f89a228d
5
5
  SHA512:
6
- metadata.gz: 80129c5c2e16aa7667cfd6af3b49ed6f1691a75bcd3e5b9025ec660ae9eef6db35adb9de552e2ac2d68886cebbca79c6e05f0be8a3d88ab61bbb2f8a198f6f87
7
- data.tar.gz: d451ca6c1f47e18d856c42e4466afd8c7776040113d09346d1c8fb003e72d57a69368b86bcdb88ebb610219a823db6930de2b42437b45352b8aaf0bf968d15d9
6
+ metadata.gz: e6aca8f9a84ff71d7570967e115ae2e17652e9c0514f18396f21a069222e30f7343ea7b9f11522a932f62edf1373ef081491a232ed6b8ebabe0c87fd868022cb
7
+ data.tar.gz: 1ffb8d0777fce695e3d32833d9b221c6749c8d59951afb45625a167ab755377c16519848f578037fca5f5238d081dd313bbe81edd4ff52050ea55f443e57cfd0
@@ -9,12 +9,11 @@ module CoverageReporter
9
9
  when nil
10
10
  show_usage_and_exit
11
11
  when "report"
12
- # Report command
13
- options = Options::Report.parse(argv[1..])
14
- Runner.new(options).run
12
+ report_options = Options::Report.parse(argv[1..])
13
+ ReportRunner.new(report_options).run
15
14
  when "collate"
16
15
  collate_options = Options::Collate.parse(argv[1..])
17
- CoverageCollator.new(collate_options).call
16
+ CollateRunner.new(collate_options).run
18
17
  else
19
18
  show_unknown_command_error(argv.first)
20
19
  end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CoverageReporter
4
+ class CollateRunner
5
+ def initialize(options)
6
+ @coverage_dir = options[:coverage_dir]
7
+ @modified_only = options[:modified_only]
8
+ @github_token = options[:github_token]
9
+ @repo = options[:repo]
10
+ @pr_number = options[:pr_number]
11
+ @working_dir = options[:working_dir]
12
+ end
13
+
14
+ def run
15
+ pull_request = PullRequest.new(github_token:, repo:, pr_number:)
16
+ filenames = modified_only ? ModifiedFilesExtractor.new(pull_request.diff).call : []
17
+ CoverageCollator.new(coverage_dir:, filenames:, working_dir:).call
18
+ end
19
+
20
+ private
21
+
22
+ attr_reader :coverage_dir, :modified_only, :github_token, :repo, :pr_number, :working_dir
23
+ end
24
+ end
@@ -4,8 +4,11 @@ module CoverageReporter
4
4
  class CoverageCollator
5
5
  def initialize(options={})
6
6
  @coverage_dir = options[:coverage_dir]
7
+ @filenames = options[:filenames]
8
+ @working_dir = options[:working_dir]
7
9
  end
8
10
 
11
+ # rubocop:disable Metrics/AbcSize
9
12
  def call
10
13
  require "simplecov"
11
14
  require "simplecov_json_formatter"
@@ -13,25 +16,40 @@ module CoverageReporter
13
16
  require "coverage_reporter/simple_cov/patches/result_hash_formatter_patch"
14
17
 
15
18
  # Collate JSON coverage reports and generate both HTML and JSON outputs
16
- files = Dir["#{coverage_dir}/resultset-*.json"]
17
- abort "No coverage JSON files found to collate" if files.empty?
18
-
19
- puts "Collate coverage files: #{files.join(', ')}"
20
-
21
- ::SimpleCov.collate(files) do
22
- formatter ::SimpleCov::Formatter::MultiFormatter.new(
23
- [
24
- ::SimpleCov::Formatter::HypertextFormatter,
25
- ::SimpleCov::Formatter::JSONFormatter
26
- ]
27
- )
19
+ coverage_files = Dir["#{coverage_dir}/resultset-*.json"]
20
+ abort "No coverage JSON files found to collate" if coverage_files.empty?
21
+
22
+ puts "Collate coverage files: #{coverage_files.join(', ')}"
23
+
24
+ ::SimpleCov.root(working_dir) if working_dir
25
+
26
+ ::SimpleCov.collate(coverage_files) do
27
+ add_filter(build_filter) if filenames.any? && working_dir
28
+ formatter(build_formatter)
28
29
  end
29
30
 
30
31
  puts "✅ Coverage merged and report generated."
31
32
  end
33
+ # rubocop:enable Metrics/AbcSize
32
34
 
33
35
  private
34
36
 
35
- attr_reader :coverage_dir
37
+ attr_reader :coverage_dir, :filenames, :working_dir
38
+
39
+ def build_formatter
40
+ ::SimpleCov::Formatter::MultiFormatter.new(
41
+ [
42
+ ::SimpleCov::Formatter::JSONFormatter,
43
+ ::SimpleCov::Formatter::HypertextFormatter
44
+ ]
45
+ )
46
+ end
47
+
48
+ def build_filter
49
+ lambda do |src_file|
50
+ normalized_filename = src_file.filename.gsub(working_dir, "").gsub(%r{^/}, "")
51
+ filenames.none?(normalized_filename)
52
+ end
53
+ end
36
54
  end
37
55
  end
@@ -80,12 +80,59 @@ module CoverageReporter
80
80
 
81
81
  def existing_comment_for_path_and_lines(path, start_line, line)
82
82
  existing_coverage_comments.values.find do |comment|
83
- if line == start_line
84
- comment.path == path && comment.line == line
85
- else
86
- comment.path == path && comment.start_line == start_line && comment.line == line
87
- end
83
+ next false unless comment.path == path
84
+
85
+ # Check if line numbers match
86
+ line_numbers_match = if line == start_line
87
+ comment.line == line
88
+ else
89
+ comment.start_line == start_line && comment.line == line
90
+ end
91
+
92
+ next false unless line_numbers_match
93
+
94
+ # Check if the content of the lines has changed
95
+ content_matches?(comment, path, start_line, line)
88
96
  end
89
97
  end
98
+
99
+ def content_matches?(existing_comment, path, start_line, line)
100
+ # Extract commit SHA from existing comment body
101
+ existing_commit_sha = extract_commit_sha_from_comment(existing_comment.body)
102
+ return false unless existing_commit_sha
103
+
104
+ # If commit SHA matches current commit, content is the same
105
+ return true if existing_commit_sha == commit_sha
106
+
107
+ # Get file content at both commits
108
+ existing_content = pull_request.file_content(path: path, commit_sha: existing_commit_sha)
109
+ current_content = pull_request.file_content(path: path, commit_sha: commit_sha)
110
+
111
+ return false unless existing_content && current_content
112
+
113
+ # Compare the lines at the specified range
114
+ existing_lines = extract_lines(existing_content, start_line, line)
115
+ current_lines = extract_lines(current_content, start_line, line)
116
+
117
+ existing_lines == current_lines
118
+ end
119
+
120
+ def extract_commit_sha_from_comment(comment_body)
121
+ return nil unless comment_body
122
+
123
+ # Extract commit SHA from format: "_Commit: abc123_"
124
+ match = comment_body.match(/_Commit:\s*([a-f0-9]+)_/i)
125
+ match ? match[1] : nil
126
+ end
127
+
128
+ def extract_lines(content, start_line, line)
129
+ lines = content.lines
130
+ # Convert to 0-based index
131
+ start_idx = start_line - 1
132
+ end_idx = line - 1
133
+
134
+ # Return the lines in the range (inclusive)
135
+ lines[start_idx..end_idx] || []
136
+ end
90
137
  end
91
138
  end
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "set"
4
+
5
+ module CoverageReporter
6
+ # Extracts a list of modified files from diff text
7
+ class ModifiedFilesExtractor
8
+ def initialize(diff_text)
9
+ @diff_text = diff_text
10
+ end
11
+
12
+ def call
13
+ return [] unless @diff_text
14
+
15
+ parse_diff(@diff_text)
16
+ rescue StandardError => e
17
+ puts "Warning: Could not parse diff text: #{e.message}"
18
+ []
19
+ end
20
+
21
+ private
22
+
23
+ def parse_diff(text)
24
+ modified_files = Set.new
25
+
26
+ text.each_line do |line|
27
+ if file_header_line?(line)
28
+ file_path = parse_file_path(line)
29
+ modified_files.add(file_path) if file_path
30
+ end
31
+ end
32
+
33
+ modified_files.to_a.sort
34
+ end
35
+
36
+ def file_header_line?(line)
37
+ line.start_with?("+++ ")
38
+ end
39
+
40
+ def parse_file_path(line)
41
+ return nil if line.end_with?(File::NULL)
42
+
43
+ line = line.chomp
44
+ if (m = line.match(%r{\A\+\+\+\s[wb]/(.+)\z}))
45
+ m[1]
46
+ end
47
+ end
48
+ end
49
+ end
@@ -7,26 +7,58 @@ module CoverageReporter
7
7
  class Collate < Base
8
8
  def self.defaults
9
9
  {
10
- coverage_dir: "coverage"
10
+ coverage_dir: "coverage",
11
+ modified_only: false,
12
+ github_token: ENV.fetch("GITHUB_TOKEN", nil),
13
+ repo: normalize_repo(ENV.fetch("REPO", nil)),
14
+ pr_number: ENV.fetch("PR_NUMBER", nil),
15
+ working_dir: nil
11
16
  }
12
17
  end
13
18
 
14
19
  def self.parse(argv)
15
20
  opts = defaults.dup
21
+ parser = build_parser(opts)
22
+ parser.parse!(argv)
23
+ opts
24
+ end
16
25
 
17
- parser = OptionParser.new do |o|
26
+ # rubocop:disable Metrics/MethodLength, Metrics/AbcSize
27
+ def self.build_parser(opts)
28
+ OptionParser.new do |o|
18
29
  o.banner = "Usage: coverage-reporter collate [options]"
19
30
  o.on("--coverage-dir DIR", "Directory containing coverage files (default: coverage)") do |v|
20
31
  opts[:coverage_dir] = v
21
32
  end
33
+ o.on("--modified-only", "Filter to only modified files") do
34
+ opts[:modified_only] = true
35
+ end
36
+ o.on("--github-token TOKEN", "GitHub token") do |v|
37
+ opts[:github_token] = v
38
+ end
39
+ o.on("--repo REPO", "Repository") do |v|
40
+ opts[:repo] = v
41
+ end
42
+ o.on("--pr-number PR_NUMBER", "Pull request number") do |v|
43
+ opts[:pr_number] = v
44
+ end
45
+ o.on("--working-dir DIR", "Working directory for coverage files") do |v|
46
+ opts[:working_dir] = v
47
+ end
22
48
  o.on_tail("-h", "--help", "Show help") do
23
49
  puts o
24
50
  exit 0
25
51
  end
26
52
  end
53
+ end
54
+ # rubocop:enable Metrics/MethodLength, Metrics/AbcSize
27
55
 
28
- parser.parse!(argv)
29
- opts
56
+ def self.normalize_repo(repo)
57
+ return repo if repo.nil? || repo.strip.empty?
58
+
59
+ repo.strip
60
+ .gsub(%r{^(https://github\.com/|git@github\.com:)}, "")
61
+ .gsub(/\.git$/, "")
30
62
  end
31
63
  end
32
64
  end
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "base64"
4
+
3
5
  module CoverageReporter
4
6
  class PullRequest
5
7
  def initialize(github_token:, repo:, pr_number:)
@@ -59,6 +61,18 @@ module CoverageReporter
59
61
  @diff ||= client.pull_request(repo, pr_number, accept: "application/vnd.github.v3.diff")
60
62
  end
61
63
 
64
+ def file_content(path:, commit_sha:)
65
+ content = client.contents(repo, path: path, ref: commit_sha)
66
+ # GitHub API returns file content as base64-encoded string
67
+ if content.encoding == "base64" && content.content
68
+ Base64.decode64(content.content)
69
+ elsif content.content
70
+ content.content
71
+ end
72
+ rescue Octokit::NotFound, Octokit::Error
73
+ nil
74
+ end
75
+
62
76
  private
63
77
 
64
78
  attr_reader :client, :repo, :pr_number
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module CoverageReporter
4
- class Runner
4
+ class ReportRunner
5
5
  def initialize(options)
6
6
  @commit_sha = options[:commit_sha]
7
7
  @coverage_report_path = options[:coverage_report_path]
@@ -33,14 +33,49 @@ module CoverageReporter
33
33
  return [] unless lines.is_a?(Array)
34
34
 
35
35
  uncovered_lines = []
36
- lines.each_with_index do |count, idx|
37
- # Only lines with 0 count are considered uncovered
38
- # null values are not relevant for coverage
39
- uncovered_lines << (idx + 1) if count == 0
36
+ i = 0
37
+
38
+ while i < lines.length
39
+ if lines[i] == 0
40
+ i = process_uncovered_range(lines, uncovered_lines, i)
41
+ else
42
+ i += 1
43
+ end
40
44
  end
45
+
41
46
  convert_to_ranges(uncovered_lines)
42
47
  end
43
48
 
49
+ def process_uncovered_range(lines, uncovered_lines, start_index)
50
+ i = start_index
51
+ # Found an uncovered line, start a range (always starts with 0)
52
+ uncovered_lines << (i + 1)
53
+ i += 1
54
+
55
+ # Continue through consecutive 0s and nils
56
+ # Include nil only if it's immediately followed by an uncovered line (0)
57
+ continue_uncovered_range(lines, uncovered_lines, i)
58
+ end
59
+
60
+ def continue_uncovered_range(lines, uncovered_lines, start_index)
61
+ i = start_index
62
+ while i < lines.length
63
+ break unless should_continue_range?(lines, i)
64
+
65
+ uncovered_lines << (i + 1)
66
+ i += 1
67
+ end
68
+ i
69
+ end
70
+
71
+ def should_continue_range?(lines, index)
72
+ return true if lines[index] == 0
73
+ return false unless lines[index].nil?
74
+
75
+ # Include nil only if it's immediately followed by an uncovered line (0)
76
+ index + 1 < lines.length && lines[index + 1] == 0
77
+ end
78
+
44
79
  def convert_to_ranges(lines)
45
80
  return [] if lines.empty?
46
81
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module CoverageReporter
4
- VERSION = "0.3.1"
4
+ VERSION = "0.3.3"
5
5
  end
@@ -41,12 +41,14 @@ module CoverageReporter
41
41
  require_relative "coverage_reporter/inline_comment"
42
42
  require_relative "coverage_reporter/inline_comment_factory"
43
43
  require_relative "coverage_reporter/inline_comment_poster"
44
+ require_relative "coverage_reporter/modified_files_extractor"
44
45
  require_relative "coverage_reporter/modified_ranges_extractor"
45
46
  require_relative "coverage_reporter/options/base"
46
47
  require_relative "coverage_reporter/options/collate"
47
48
  require_relative "coverage_reporter/options/report"
48
49
  require_relative "coverage_reporter/pull_request"
49
- require_relative "coverage_reporter/runner"
50
+ require_relative "coverage_reporter/report_runner"
51
+ require_relative "coverage_reporter/collate_runner"
50
52
  require_relative "coverage_reporter/uncovered_ranges_extractor"
51
53
  require_relative "coverage_reporter/simple_cov/patches/result_hash_formatter_patch"
52
54
  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.1
4
+ version: 0.3.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Gabriel Taylor Russ
8
8
  bindir: exe
9
9
  cert_chain: []
10
- date: 2025-10-26 00:00:00.000000000 Z
10
+ date: 2025-11-17 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: octokit
@@ -77,6 +77,7 @@ files:
77
77
  - exe/coverage-reporter
78
78
  - lib/coverage_reporter.rb
79
79
  - lib/coverage_reporter/cli.rb
80
+ - lib/coverage_reporter/collate_runner.rb
80
81
  - lib/coverage_reporter/coverage_analyzer.rb
81
82
  - lib/coverage_reporter/coverage_collator.rb
82
83
  - lib/coverage_reporter/coverage_report_loader.rb
@@ -85,12 +86,13 @@ files:
85
86
  - lib/coverage_reporter/inline_comment.rb
86
87
  - lib/coverage_reporter/inline_comment_factory.rb
87
88
  - lib/coverage_reporter/inline_comment_poster.rb
89
+ - lib/coverage_reporter/modified_files_extractor.rb
88
90
  - lib/coverage_reporter/modified_ranges_extractor.rb
89
91
  - lib/coverage_reporter/options/base.rb
90
92
  - lib/coverage_reporter/options/collate.rb
91
93
  - lib/coverage_reporter/options/report.rb
92
94
  - lib/coverage_reporter/pull_request.rb
93
- - lib/coverage_reporter/runner.rb
95
+ - lib/coverage_reporter/report_runner.rb
94
96
  - lib/coverage_reporter/simple_cov/patches/result_hash_formatter_patch.rb
95
97
  - lib/coverage_reporter/uncovered_ranges_extractor.rb
96
98
  - lib/coverage_reporter/version.rb