coverage-reporter 0.1.0 → 0.3.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 502fcd527b9376d80aa547f7d416fc6159c610c3806aee89b1ee47bf65b0d586
4
- data.tar.gz: 90d60ffe2f23649fa8efa86603761ad3985eb5d623c81b9e80b782320682d0cc
3
+ metadata.gz: 13b5ede2585f67e28da56541940a417c9a2266f334a836d8ba4aae949f35a7aa
4
+ data.tar.gz: a23afcc6d059da70d2b0857e9a06af23177b0058690d66695596f7b7e0f774df
5
5
  SHA512:
6
- metadata.gz: 004ef1c9498a942deb8514942fc91764a49aa24c70ba3186efbceb1ed4554b935837f857735df18a6900fda0c7916fca245fd2d555002913ca17b8dd596a671e
7
- data.tar.gz: cd2007eaaa1eb78cd342c8ff43538d2878ca3fe756432b212b0534e0338fc7be02800ef26243ecf06911f3e10efebba5c85e9567bcf9b74353310032432ee157
6
+ metadata.gz: 29ba80d86581dd7f99b7141cec2e3a9a1fd81ae41dcff5d82d9305556b2a63489121946a9279cb92e905b24b983352ceb690c50ef1946f82ba7a95a2201dd25e
7
+ data.tar.gz: 592e4e220fea3f616aa7ca89938638fa463bb3a7a1ab5a3dce42ce4c0c9923baf44cba5fb2e7bfda525d5e30e524903a42c17296b0c44a547b9e02e611925cd7
data/README.md CHANGED
@@ -3,35 +3,204 @@
3
3
  [![Gem Version](https://img.shields.io/gem/v/coverage-reporter)](https://rubygems.org/gems/coverage-reporter)
4
4
  [![Gem Downloads](https://img.shields.io/gem/dt/coverage-reporter)](https://www.ruby-toolbox.com/projects/coverage-reporter)
5
5
  [![GitHub Workflow Status](https://img.shields.io/github/actions/workflow/status/gabrieltaylor/coverage-reporter/ci.yml)](https://github.com/gabrieltaylor/coverage-reporter/actions/workflows/ci.yml)
6
- [![Code Climate maintainability](https://img.shields.io/codeclimate/maintainability/gabrieltaylor/coverage-reporter)](https://codeclimate.com/github/gabrieltaylor/coverage-reporter)
7
6
 
8
- Report code coverage from SimpleCov coverage reports to a Github pull request.
7
+ Report code coverage from SimpleCov coverage reports to a GitHub pull request. This tool analyzes your test coverage data and posts detailed comments on pull requests, highlighting uncovered lines in modified code.
9
8
 
10
9
  ---
11
10
 
12
- - [Quick start](#quick-start)
11
+ - [Installation](#installation)
12
+ - [Quick Start](#quick-start)
13
+ - [Configuration](#configuration)
14
+ - [Usage Examples](#usage-examples)
15
+ - [CI/CD Integration](#cicd-integration)
16
+ - [Command Line Options](#command-line-options)
17
+ - [Environment Variables](#environment-variables)
18
+ - [How It Works](#how-it-works)
13
19
  - [License](#license)
14
- - [Code of conduct](#code-of-conduct)
15
- - [Contribution guide](#contribution-guide)
20
+ - [Code of Conduct](#code-of-conduct)
21
+ - [Contributing](#contributing)
16
22
 
17
- ## Quick start
23
+ ## Installation
18
24
 
25
+ Add this line to your application's Gemfile:
26
+
27
+ ```ruby
28
+ gem 'coverage-reporter'
29
+ ```
30
+
31
+ And then execute:
32
+
33
+ ```bash
34
+ bundle install
19
35
  ```
36
+
37
+ Or install it directly:
38
+
39
+ ```bash
20
40
  gem install coverage-reporter
21
41
  ```
22
42
 
23
- ```ruby
24
- require "coverage-reporter"
43
+ ## Quick Start
44
+
45
+ 1. **Generate a SimpleCov coverage report** in your test suite:
46
+ ```ruby
47
+ # In your test helper or spec_helper
48
+ require 'simplecov'
49
+ SimpleCov.start
50
+ ```
51
+
52
+ 2. **Set up environment variables**:
53
+ ```bash
54
+ export GITHUB_TOKEN="your_github_token_here"
55
+ export REPO="owner/repository"
56
+ export PR_NUMBER="123"
57
+ export COMMIT_SHA="abc123def456"
58
+ ```
59
+
60
+ 3. **Run the coverage reporter**:
61
+ ```bash
62
+ coverage-reporter report
63
+ ```
64
+
65
+ The tool will automatically:
66
+ - Load your coverage data from `coverage/coverage.json`
67
+ - Fetch the pull request diff from GitHub
68
+ - Identify uncovered lines in modified code
69
+ - Post inline comments on uncovered lines
70
+ - Add a global coverage summary comment
71
+
72
+ ## Configuration
73
+
74
+ ### Required Settings
75
+
76
+ - **GitHub Token**: A personal access token with `repo` permissions
77
+ - **Repository**: GitHub repository in `owner/repo` format
78
+ - **Pull Request Number**: The PR number to comment on
79
+ - **Commit SHA**: The commit SHA being analyzed
80
+
81
+ ### Optional Settings
82
+
83
+ - **Coverage Report Path**: Path to your SimpleCov coverage.json file (default: `coverage/coverage.json`)
84
+ - **Build URL**: CI build URL for linking back to your build (default: `$BUILD_URL`)
85
+
86
+ ## Usage Examples
87
+
88
+ ### Basic Usage
89
+
90
+ ```bash
91
+ coverage-reporter \
92
+ --github-token "$GITHUB_TOKEN" \
93
+ --repo "myorg/myrepo" \
94
+ --pr-number "42" \
95
+ --commit-sha "$GITHUB_SHA"
96
+ ```
97
+
98
+ ### Custom Coverage Report Path
99
+
100
+ ```bash
101
+ coverage-reporter \
102
+ --github-token "$GITHUB_TOKEN" \
103
+ --repo "myorg/myrepo" \
104
+ --pr-number "42" \
105
+ --commit-sha "$GITHUB_SHA" \
106
+ --coverage-report-path "test/coverage/coverage.json"
25
107
  ```
26
108
 
109
+ ### With Build URL
110
+
111
+ ```bash
112
+ coverage-reporter \
113
+ --github-token "$GITHUB_TOKEN" \
114
+ --repo "myorg/myrepo" \
115
+ --pr-number "42" \
116
+ --commit-sha "$GITHUB_SHA" \
117
+ --build-url "https://github.com/myorg/myrepo/actions/runs/123456"
118
+ ```
119
+
120
+ ## CI/CD Integration
121
+
122
+ ### GitHub Actions
123
+
124
+ ```yaml
125
+ name: Test Coverage
126
+ on: [pull_request]
127
+
128
+ jobs:
129
+ test:
130
+ runs-on: ubuntu-latest
131
+ steps:
132
+ - uses: actions/checkout@v4
133
+
134
+ - name: Set up Ruby
135
+ uses: ruby/setup-ruby@v1
136
+ with:
137
+ ruby-version: 3.1
138
+ bundler-cache: true
139
+
140
+ - name: Run tests with coverage
141
+ run: bundle exec rspec
142
+ env:
143
+ COVERAGE: true
144
+
145
+ - name: Report coverage
146
+ run: bundle exec coverage-reporter
147
+ env:
148
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
149
+ REPO: ${{ github.repository }}
150
+ PR_NUMBER: ${{ github.event.number }}
151
+ COMMIT_SHA: ${{ github.sha }}
152
+ BUILD_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
153
+ ```
154
+
155
+ ## Command Line Options
156
+
157
+ | Option | Description | Default | Environment Variable |
158
+ |--------|-------------|---------|---------------------|
159
+ | `--github-token TOKEN` | GitHub personal access token | `$GITHUB_TOKEN` | `GITHUB_TOKEN` |
160
+ | `--repo REPO` | GitHub repository (owner/repo) | `$REPO` | `REPO` |
161
+ | `--pr-number NUMBER` | Pull request number | `$PR_NUMBER` | `PR_NUMBER` |
162
+ | `--commit-sha SHA` | Git commit SHA | `$COMMIT_SHA` | `COMMIT_SHA` |
163
+ | `--coverage-report-path PATH` | Path to coverage.json | `coverage/coverage.json` | `COVERAGE_REPORT_PATH` |
164
+ | `--build-url URL` | CI build URL for links | `$BUILD_URL` | `BUILD_URL` |
165
+ | `--help` | Show help message | - | - |
166
+
167
+ ## Environment Variables
168
+
169
+ All command-line options can be set via environment variables:
170
+
171
+ ```bash
172
+ export GITHUB_TOKEN="ghp_xxxxxxxxxxxx"
173
+ export REPO="myorg/myrepo"
174
+ export PR_NUMBER="123"
175
+ export COMMIT_SHA="abc123def456"
176
+ export COVERAGE_REPORT_PATH="coverage/coverage.json"
177
+ export BUILD_URL="https://ci.example.com/build/123"
178
+ ```
179
+
180
+ ## How It Works
181
+
182
+ 1. **Loads Coverage Data**: Reads SimpleCov's `coverage.json` file to understand which lines are covered by tests
183
+ 2. **Fetches PR Diff**: Retrieves the pull request diff from GitHub to identify modified lines
184
+ 3. **Finds Intersections**: Identifies uncovered lines that were modified in the PR
185
+ 4. **Posts Inline Comments**: Adds comments directly on uncovered lines in the diff
186
+ 5. **Creates Summary**: Posts a global comment with overall coverage statistics
187
+
188
+ ### GitHub Token Permissions
189
+
190
+ Your GitHub token needs the following permissions:
191
+ - `repo` (Full control of private repositories)
192
+ - `public_repo` (Access public repositories)
193
+
194
+ Create a token at: https://github.com/settings/tokens
195
+
27
196
  ## License
28
197
 
29
198
  The gem is available as open source under the terms of the [MIT License](LICENSE.txt).
30
199
 
31
- ## Code of conduct
200
+ ## Code of Conduct
32
201
 
33
- Everyone interacting in this projects codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](CODE_OF_CONDUCT.md).
202
+ Everyone interacting in this project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](CODE_OF_CONDUCT.md).
34
203
 
35
- ## Contribution guide
204
+ ## Contributing
36
205
 
37
- Pull requests are welcome!
206
+ Pull requests are welcome! Please read our [contribution guidelines](CONTRIBUTING.md) for details on our code of conduct and the process for submitting pull requests.
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ $LOAD_PATH.unshift("#{__dir__}/../lib")
5
+
6
+ require "coverage_reporter"
7
+
8
+ CoverageReporter::CLI.start(ARGV)
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "optparse"
4
+
5
+ module CoverageReporter
6
+ class CLI
7
+ def self.start(argv)
8
+ case argv.first
9
+ when nil
10
+ show_usage_and_exit
11
+ when "report"
12
+ # Report command
13
+ options = Options::Report.parse(argv[1..])
14
+ Runner.new(options).run
15
+ when "collate"
16
+ collate_options = Options::Collate.parse(argv[1..])
17
+ CoverageCollator.new(collate_options).call
18
+ else
19
+ show_unknown_command_error(argv.first)
20
+ end
21
+ end
22
+
23
+ private_class_method def self.show_usage_and_exit
24
+ puts "Usage: coverage-reporter <command> [options]"
25
+ print_commands_list
26
+ exit 1
27
+ end
28
+
29
+ private_class_method def self.show_unknown_command_error(command)
30
+ puts "Unknown command: #{command}"
31
+ print_commands_list
32
+ exit 1
33
+ end
34
+
35
+ private_class_method def self.print_commands_list
36
+ puts ""
37
+ puts "Commands:"
38
+ puts " report Generate coverage report and post comments"
39
+ puts " collate Collate multiple coverage files"
40
+ puts ""
41
+ puts "Use 'coverage-reporter <command> --help' for command-specific options"
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,153 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CoverageReporter
4
+ # Analyzes coverage data against diff data to find uncovered lines in changed code
5
+ # and calculates coverage statistics
6
+ #
7
+ # @param uncovered_ranges [Hash] Uncovered data where:
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]] }
11
+ #
12
+ # @param modified_ranges [Hash] Modified data where:
13
+ # - Keys are filenames (e.g., "app/models/user.rb")
14
+ # - Values are arrays of arrays representing modified or new line ranges
15
+ # - Example: { "app/services/foo.rb" => [[100,120]] }
16
+ class CoverageAnalyzer
17
+ def initialize(uncovered_ranges:, modified_ranges:)
18
+ @uncovered_ranges = uncovered_ranges
19
+ @modified_ranges = modified_ranges
20
+ end
21
+
22
+ def call
23
+ logger.debug("Starting coverage analysis for #{@modified_ranges.size} modified files")
24
+
25
+ intersections = {}
26
+ total_modified_lines = 0
27
+ total_uncovered_modified_lines = 0
28
+
29
+ @modified_ranges.each do |file, modified_ranges|
30
+ next if modified_ranges.nil? || modified_ranges.empty?
31
+
32
+ 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]
36
+ end
37
+
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)
41
+
42
+ build_result(intersections, total_modified_lines, total_uncovered_modified_lines, coverage_percentage)
43
+ end
44
+
45
+ private
46
+
47
+ def logger
48
+ CoverageReporter.logger
49
+ end
50
+
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)
55
+
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)
59
+
60
+ intersections = {}
61
+ # Only include files with actual intersections (matching original behavior)
62
+ intersections[file] = intersecting_ranges unless intersecting_ranges.empty?
63
+
64
+ {
65
+ intersections: intersections,
66
+ modified_lines: file_modified_lines,
67
+ uncovered_lines: uncovered_modified_lines
68
+ }
69
+ end
70
+
71
+ def fibonacci(num)
72
+ return num if num <= 1
73
+
74
+ fibonacci(num - 1) + fibonacci(num - 2)
75
+ end
76
+
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
+ )
83
+ end
84
+
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
+ }
95
+ end
96
+
97
+ def count_lines_in_ranges(ranges)
98
+ ranges.sum { |range| range[1] - range[0] + 1 }
99
+ end
100
+
101
+ def count_intersecting_lines(modified_ranges, uncovered_ranges)
102
+ return 0 if uncovered_ranges.empty?
103
+
104
+ intersecting_lines = 0
105
+ i = j = 0
106
+
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]
110
+
111
+ # Find intersection
112
+ intersection_start = [modified_start, uncovered_start].max
113
+ intersection_end = [modified_end, uncovered_end].min
114
+
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
123
+ end
124
+
125
+ intersecting_lines
126
+ end
127
+
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
143
+ end
144
+ # rubocop:enable Metrics/AbcSize
145
+
146
+ def calculate_percentage(total_lines, uncovered_lines)
147
+ return 100.0 if total_lines == 0
148
+
149
+ covered_lines = total_lines - uncovered_lines
150
+ ((covered_lines.to_f / total_lines) * 100).round(2)
151
+ end
152
+ end
153
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CoverageReporter
4
+ class CoverageCollator
5
+ def initialize(options={})
6
+ @coverage_dir = options[:coverage_dir]
7
+ end
8
+
9
+ def call
10
+ require "simplecov"
11
+ require "simplecov_json_formatter"
12
+ require "coverage_reporter/simple_cov/patches/result_hash_formatter_patch"
13
+
14
+ # Collate JSON coverage reports and generate both HTML and JSON outputs
15
+ files = Dir["#{coverage_dir}/resultset-*.json"]
16
+ abort "No coverage JSON files found to collate" if files.empty?
17
+
18
+ puts "Collate coverage files: #{files.join(', ')}"
19
+
20
+ ::SimpleCov.collate(files) do
21
+ formatter ::SimpleCov::Formatter::MultiFormatter.new(
22
+ [
23
+ ::SimpleCov::Formatter::HTMLFormatter,
24
+ ::SimpleCov::Formatter::JSONFormatter
25
+ ]
26
+ )
27
+ end
28
+
29
+ puts "✅ Coverage merged and report generated."
30
+ end
31
+
32
+ private
33
+
34
+ attr_reader :coverage_dir
35
+ end
36
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+
5
+ module CoverageReporter
6
+ # Custom error classes for coverage file operations
7
+ class CoverageFileError < StandardError; end
8
+ class CoverageFileNotFoundError < CoverageFileError; end
9
+ class CoverageFileAccessError < CoverageFileError; end
10
+ class CoverageFileParseError < CoverageFileError; end
11
+
12
+ class CoverageReportLoader
13
+ def initialize(coverage_file_path)
14
+ @coverage_file_path = coverage_file_path
15
+ end
16
+
17
+ def call
18
+ content = read_file_content
19
+ parse_json_content(content)
20
+ rescue Errno::ENOENT
21
+ raise CoverageFileNotFoundError, "Coverage file not found: #{@coverage_file_path}"
22
+ rescue Errno::EACCES
23
+ raise CoverageFileAccessError, "Permission denied reading coverage file: #{@coverage_file_path}"
24
+ rescue JSON::ParserError => e
25
+ raise CoverageFileParseError, "Invalid JSON in coverage file #{@coverage_file_path}: #{e.message}"
26
+ rescue StandardError => e
27
+ raise CoverageFileError, "Unexpected error reading coverage file #{@coverage_file_path}: #{e.message}"
28
+ end
29
+
30
+ private
31
+
32
+ attr_reader :coverage_file_path
33
+
34
+ def read_file_content
35
+ File.read(@coverage_file_path)
36
+ end
37
+
38
+ def parse_json_content(content)
39
+ JSON.parse(content)
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,60 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CoverageReporter
4
+ class GlobalComment
5
+ def initialize(coverage_percentage:, commit_sha:, report_url: nil, intersections: {})
6
+ @coverage_percentage = coverage_percentage
7
+ @commit_sha = commit_sha
8
+ @report_url = report_url
9
+ @intersections = intersections
10
+ @body = build_body
11
+ end
12
+
13
+ attr_reader :coverage_percentage, :commit_sha, :report_url, :intersections, :body
14
+
15
+ private
16
+
17
+ def build_body
18
+ body = <<~MD
19
+ <!-- coverage-comment-marker -->
20
+ **Test Coverage Summary**
21
+
22
+ #{coverage_percentage < 100 ? '❌' : '✅'} **#{coverage_percentage}%** of changed lines are covered.
23
+
24
+ [View full report](#{report_url})
25
+
26
+ _Commit: #{commit_sha}_
27
+ MD
28
+
29
+ body += "\n\n#{coverage_summary_section}" if intersections.any?
30
+
31
+ body
32
+ end
33
+
34
+ def coverage_summary_section
35
+ <<~MD
36
+ **Coverage Summary**
37
+
38
+ | File | Uncovered Lines |
39
+ |------|----------------|
40
+ #{coverage_summary_table_rows}
41
+ MD
42
+ end
43
+
44
+ def coverage_summary_table_rows
45
+ intersections.map do |file, ranges|
46
+ formatted_ranges = ranges.map { |range| format_range(range) }.join(", ")
47
+ "| `#{file}` | #{formatted_ranges} |"
48
+ end.join("\n")
49
+ end
50
+
51
+ def format_range(range)
52
+ start_line, end_line = range
53
+ if start_line == end_line
54
+ start_line.to_s
55
+ else
56
+ "#{start_line}-#{end_line}"
57
+ end
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CoverageReporter
4
+ class GlobalCommentPoster
5
+ def initialize(pull_request:, global_comment:)
6
+ @pull_request = pull_request
7
+ @global_comment = global_comment
8
+ end
9
+
10
+ def call
11
+ ensure_global_comment
12
+ end
13
+
14
+ private
15
+
16
+ attr_reader :pull_request, :global_comment
17
+
18
+ def ensure_global_comment
19
+ comments = pull_request.global_comments
20
+ existing = comments.find { |c| c.body&.include?(GLOBAL_COMMENT_MARKER) }
21
+
22
+ if existing
23
+ pull_request.update_global_comment(id: existing.id, body: global_comment.body)
24
+ else
25
+ pull_request.add_global_comment(body: global_comment.body)
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CoverageReporter
4
+ class InlineComment
5
+ attr_reader :path, :start_line, :line, :body
6
+
7
+ def initialize(path:, start_line:, line:, body:)
8
+ @path = path
9
+ @start_line = start_line
10
+ @line = line
11
+ @body = body
12
+ end
13
+
14
+ def single_line?
15
+ start_line == line
16
+ end
17
+
18
+ def range?
19
+ !single_line?
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CoverageReporter
4
+ class InlineCommentFactory
5
+ def initialize(commit_sha:, intersection:)
6
+ @commit_sha = commit_sha
7
+ @intersection = intersection
8
+ end
9
+
10
+ def call
11
+ comments = []
12
+
13
+ @intersection.each do |path, ranges|
14
+ ranges.each do |start_line, line|
15
+ body = build_body(path:, start_line:, line:)
16
+
17
+ comments << InlineComment.new(
18
+ path: path,
19
+ start_line: start_line,
20
+ line: line,
21
+ body: body
22
+ )
23
+ end
24
+ end
25
+
26
+ comments
27
+ end
28
+
29
+ private
30
+
31
+ attr_reader :commit_sha, :intersection
32
+
33
+ def build_body(path:, start_line:, line:)
34
+ message = build_message(start_line, line)
35
+ "#{INLINE_COMMENT_MARKER}\n#{message}\n\n_File: #{path}, line #{start_line}_\n_Commit: #{commit_sha}_"
36
+ end
37
+
38
+ def build_message(start_line, line)
39
+ if start_line == line
40
+ "❌ Line #{start_line} is not covered by tests."
41
+ else
42
+ "❌ Lines #{start_line}–#{line} are not covered by tests."
43
+ end
44
+ end
45
+ end
46
+ end