diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index f5623d2..ccb761c 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -10,15 +10,15 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Check out the repo
- uses: actions/checkout@v2
+ uses: actions/checkout@v4
- name: Docker Login
- uses: docker/login-action@v1
+ uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Docker Build and Push
id: docker_build
- uses: docker/build-push-action@v2
+ uses: docker/build-push-action@v6
with:
push: true
tags: |
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index a781a06..0e269b3 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -6,12 +6,12 @@ jobs:
build:
runs-on: ubuntu-latest
steps:
- - uses: actions/checkout@v2
+ - uses: actions/checkout@v4
- name: Set up Go
- uses: actions/setup-go@v2
+ uses: actions/setup-go@v5
with:
- go-version: 1.17
+ go-version: '1.26.3'
- name: Build
run: go build -v ./...
diff --git a/Dockerfile b/Dockerfile
index aa84fe8..729b319 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,4 +1,4 @@
-FROM golang:1.17-alpine as builder
+FROM golang:1.26.3-alpine as builder
WORKDIR /go/src/github.com/target/pull-request-code-coverage
COPY . .
ENV GO111MODULE=on
diff --git a/README.md b/README.md
index 4a67ed9..d607f3c 100644
--- a/README.md
+++ b/README.md
@@ -16,6 +16,37 @@ This plugin will output the coverage details to the CI/CD step's console. A sam
This plugin as well as has the ability to comment on the PR with a summary of the coverage details.

+The PR comment is rendered as markdown and looks like this:
+
+> ## đ Pull Request Code Coverage
+>
+> Coverage below is for **only the lines changed in this PR**.
+>
+> **Modules:** `category-search`
+>
+> | Metric | Coverage | Count |
+> |:---|---:|---:|
+> | â
Covered instructions | **73%** | 8 |
+> | â Missed instructions | 27% | 3 |
+> | đ Lines with coverage data | 22% | 2 |
+> | đ Lines without coverage data | 78% | 7 |
+>
+> âšī¸ What do these metrics mean?
+>
+> - **Covered instructions** â instructions (statements / bytecode) on the changed lines that were executed by your tests.
+> - **Missed instructions** â instructions on the changed lines that were **not** executed by any test.
+> - **Lines with coverage data** â changed lines the coverage tool tracks as executable code.
+> - **Lines without coverage data** â changed lines with no coverage information (comments, blank lines, declarations, etc.).
+>
+>
+> â Lines missing coverage (1)
+>
+> **`category-search/src/main/java/com/tgt/CategorySearchApplication.java:52`**
+> ```java
+> System.out.print("Something");
+> ```
+>
+
Currently, this plugin supports two coverage file format.
* jacoco for jvm based languages like java,kotlin,scala
@@ -86,7 +117,7 @@ Once you have coverage.xml same can be passed as an input to plugin shown below
|coverage_type| true | | **supported values**: jacoco, cobertura
sets the coverage file format |
|coverage_file| true | | path to where the coverage file will be located, relative to the working dir |
|source_dirs| true | | array of source dirs, relative to the working dir |
-|gh_api_base_url| false | | base url of the gh api for posting coverage comments
if not set, coverage details will not be commented on PR |
+|gh_api_base_url| false | https://api.github.com | base url of the gh api for posting coverage comments
defaults to public GitHub; for GitHub Enterprise set this to your host (e.g. `https://git.target.com`) |
|gh_api_key| false | | api key to auth for posting coverage comments
if not set, coverage details will not be commented on PR |
|module | false | \ | sub-module to use if operating inside a multi-module project (e.g. gradle multi-project build) |
diff --git a/go.mod b/go.mod
index de194c4..78138dd 100644
--- a/go.mod
+++ b/go.mod
@@ -1,18 +1,17 @@
module github.com/target/pull-request-code-coverage
-go 1.17
+go 1.26.3
require (
github.com/pkg/errors v0.9.1
- github.com/sirupsen/logrus v1.8.1
- github.com/stretchr/testify v1.7.0
-
+ github.com/sirupsen/logrus v1.9.4
+ github.com/stretchr/testify v1.11.1
)
require (
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
- github.com/stretchr/objx v0.1.0 // indirect
- golang.org/x/sys v0.0.0-20191026070338-33540a1f6037 // indirect
- gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c // indirect
+ github.com/stretchr/objx v0.5.3 // indirect
+ golang.org/x/sys v0.44.0 // indirect
+ gopkg.in/yaml.v3 v3.0.1 // indirect
)
diff --git a/go.sum b/go.sum
index 2fe2451..4c19f8a 100644
--- a/go.sum
+++ b/go.sum
@@ -1,20 +1,18 @@
-github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
-github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE=
-github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
-github.com/stretchr/objx v0.1.0 h1:4G4v2dO3VZwixGIRoQ5Lfboy6nUhCyYzaqnIAPPhYs4=
-github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
-github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
-github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
-github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
-golang.org/x/sys v0.0.0-20191026070338-33540a1f6037 h1:YyJpGZS1sBuBCzLAR1VEpK193GlqGZbnPFnPV/5Rsb4=
-golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+github.com/sirupsen/logrus v1.9.4 h1:TsZE7l11zFCLZnZ+teH4Umoq5BhEIfIzfRDZ1Uzql2w=
+github.com/sirupsen/logrus v1.9.4/go.mod h1:ftWc9WdOfJ0a92nsE2jF5u5ZwH8Bv2zdeOC42RjbV2g=
+github.com/stretchr/objx v0.5.3 h1:jmXUvGomnU1o3W/V5h2VEradbpJDwGrzugQQvL0POH4=
+github.com/stretchr/objx v0.5.3/go.mod h1:rDQraq+vQZU7Fde9LOZLr8Tax6zZvy4kuNKF+QYS+U0=
+github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
+github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
+golang.org/x/sys v0.44.0 h1:ildZl3J4uzeKP07r2F++Op7E9B29JRUy+a27EibtBTQ=
+golang.org/x/sys v0.44.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
-gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
-gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
+gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
diff --git a/internal/plugin/reporter/github_pr.go b/internal/plugin/reporter/github_pr.go
index 6e42a4f..d11b875 100644
--- a/internal/plugin/reporter/github_pr.go
+++ b/internal/plugin/reporter/github_pr.go
@@ -4,6 +4,9 @@ import (
"bytes"
"fmt"
"io"
+ "net/url"
+ "path/filepath"
+ "sort"
"strings"
"github.com/pkg/errors"
@@ -12,6 +15,11 @@ import (
"github.com/target/pull-request-code-coverage/internal/plugin/pluginjson"
)
+// DefaultGithubAPIBaseURL is the REST API root for public GitHub. It is used
+// when no base URL is configured; GitHub Enterprise users override it via
+// gh_api_base_url (e.g. https://git.target.com).
+const DefaultGithubAPIBaseURL = "https://api.github.com"
+
type GithubPullRequest struct {
apiKey string
apiBaseURL string
@@ -50,7 +58,7 @@ func (s *GithubPullRequest) Write(changedLinesWithCoverage domain.SourceLineCove
return errors.Wrap(bodyErr, "Failed creating payload for github")
}
- url := fmt.Sprintf("%v/api/v3/repos/%v/%v/issues/%v/comments", s.apiBaseURL, s.owner, s.repo, s.pr)
+ url := s.commentsURL()
req, newErr := s.httpClient.NewRequest(
"POST",
@@ -63,6 +71,7 @@ func (s *GithubPullRequest) Write(changedLinesWithCoverage domain.SourceLineCove
req.Header.Add("Authorization", "token "+s.apiKey)
req.Header.Add("Content-Type", "application/json")
+ req.Header.Add("User-Agent", "pull-request-code-coverage")
resp, doErr := s.httpClient.Do(req)
@@ -77,64 +86,154 @@ func (s *GithubPullRequest) Write(changedLinesWithCoverage domain.SourceLineCove
return nil
}
+// commentsURL builds the REST endpoint for posting an issue comment.
+//
+// Public GitHub (api.github.com) and Enterprise Cloud (api.*.ghe.com) serve the
+// REST API at the host root, while Enterprise Server (e.g. git.target.com)
+// serves it under /api/v3. We append /api/v3 only for the latter so existing
+// host-only Enterprise configs keep working without change.
+func (s *GithubPullRequest) commentsURL() string {
+ base := strings.TrimRight(s.apiBaseURL, "/")
+
+ if host := hostOf(base); !strings.HasPrefix(host, "api.") && !strings.HasSuffix(base, "/api/v3") {
+ base += "/api/v3"
+ }
+
+ return fmt.Sprintf("%v/repos/%v/%v/issues/%v/comments", base, s.owner, s.repo, s.pr)
+}
+
+// hostOf returns the host component of a URL, or "" if it cannot be parsed.
+func hostOf(rawURL string) string {
+ if u, err := url.Parse(rawURL); err == nil {
+ return u.Host
+ }
+
+ return ""
+}
+
func (s *GithubPullRequest) GetName() string {
return "github pull request reporter"
}
+// metricsLegend explains, in plain language, what each row of the metrics
+// table means so readers of the PR comment don't have to guess.
+const metricsLegend = "âšī¸ What do these metrics mean?
\n\n" +
+ "- **Covered instructions** â instructions (statements / bytecode) on the changed lines that were executed by your tests.\n" +
+ "- **Missed instructions** â instructions on the changed lines that were **not** executed by any test.\n" +
+ "- **Lines with coverage data** â changed lines the coverage tool tracks as executable code.\n" +
+ "- **Lines without coverage data** â changed lines with no coverage information (comments, blank lines, declarations, etc.).\n\n" +
+ " \n"
+
func (s *GithubPullRequest) createCommentBody(changedLinesWithCoverage domain.SourceLineCoverageReport) (io.Reader, error) {
- modules := collectModules(changedLinesWithCoverage)
+ var body strings.Builder
- summaryLines := []string{}
+ body.WriteString("## đ Pull Request Code Coverage\n\n")
+ body.WriteString("Coverage below is for **only the lines changed in this PR**.\n\n")
- if len(modules) > 0 {
- summaryLines = append(summaryLines, fmt.Sprintf("*Modules: %v*\n\n", strings.Join(modules, ", ")))
+ if modules := collectModules(changedLinesWithCoverage); len(modules) > 0 {
+ body.WriteString(fmt.Sprintf("**Modules:** %v\n\n", strings.Join(backtickEach(modules), ", ")))
}
- var missedInstructions string
- for _, r := range changedLinesWithCoverage {
- if r.MissedInstructionCount > 0 {
- missedInstructions += fmt.Sprintf("--- %v\n", lineDescription(r.SourceLine))
- missedInstructions += fmt.Sprintf("%v\n", r.LineValue)
- }
+ body.WriteString(metricsTable(changedLinesWithCoverage))
+ body.WriteString("\n")
+ body.WriteString(metricsLegend)
+
+ if details := missedInstructionsDetails(changedLinesWithCoverage); details != "" {
+ body.WriteString("\n")
+ body.WriteString(details)
+ }
+
+ data := map[string]string{
+ "body": body.String(),
+ }
+
+ dataBytes, marshalErr := s.jsonClient.Marshal(data)
+
+ if marshalErr != nil {
+ return nil, errors.Wrap(marshalErr, "Failed marshalling payload to json")
}
- summaryLines = append(summaryLines, generateSummaryLines(changedLinesWithCoverage, func(linesWithDataCount int, linesWithoutDataCount int, covered int, missed int) []string {
+ return bytes.NewBuffer(dataBytes), nil
+}
+
+// metricsTable renders the coverage numbers as a GitHub-flavoured markdown table.
+func metricsTable(changedLinesWithCoverage domain.SourceLineCoverageReport) string {
+ rows := generateSummaryLines(changedLinesWithCoverage, func(linesWithDataCount int, linesWithoutDataCount int, covered int, missed int) []string {
totalLines := linesWithDataCount + linesWithoutDataCount
totalInstructions := covered + missed
- result := make([]string, 5)
-
- result[0] = fmt.Sprintf("Code Coverage Summary:\n\n")
- result[1] = fmt.Sprintf("Lines Without Coverage Data -> %.f%% (%d)\n", toPercent(safeDiv(float32(linesWithoutDataCount), float32(totalLines), 0)), linesWithoutDataCount)
- result[2] = fmt.Sprintf("Lines With Coverage Data -> %.f%% (%d)\n", toPercent(safeDiv(float32(linesWithDataCount), float32(totalLines), 1)), linesWithDataCount)
- result[3] = fmt.Sprintf("Covered Instructions -> **%.f%%** (%d)\n", toPercent(safeDiv(float32(covered), float32(totalInstructions), 1)), covered)
- result[4] = fmt.Sprintf("Missed Instructions -> %.f%% (%d)\n", toPercent(safeDiv(float32(missed), float32(totalInstructions), 0)), missed)
+ return []string{
+ "| Metric | Coverage | Count |\n",
+ "|:---|---:|---:|\n",
+ fmt.Sprintf("| â
Covered instructions | **%.f%%** | %d |\n", toPercent(safeDiv(float32(covered), float32(totalInstructions), 1)), covered),
+ fmt.Sprintf("| â Missed instructions | %.f%% | %d |\n", toPercent(safeDiv(float32(missed), float32(totalInstructions), 0)), missed),
+ fmt.Sprintf("| đ Lines with coverage data | %.f%% | %d |\n", toPercent(safeDiv(float32(linesWithDataCount), float32(totalLines), 1)), linesWithDataCount),
+ fmt.Sprintf("| đ Lines without coverage data | %.f%% | %d |\n", toPercent(safeDiv(float32(linesWithoutDataCount), float32(totalLines), 0)), linesWithoutDataCount),
+ }
+ })
- return result
- })...)
+ return strings.Join(rows, "")
+}
- var summary string
- if missedInstructions == "" {
- summary = strings.Join(summaryLines, "")
- } else {
+// missedInstructionsDetails renders a collapsible section listing each changed
+// line that has missed instructions, with its source in a syntax-highlighted
+// block. Returns "" when nothing was missed.
+func missedInstructionsDetails(changedLinesWithCoverage domain.SourceLineCoverageReport) string {
+ var missed []domain.SourceLineCoverage
+ for _, r := range changedLinesWithCoverage {
+ if r.MissedInstructionCount > 0 {
+ missed = append(missed, r)
+ }
+ }
- summaryWithoutInstructions := strings.Join(summaryLines, "")
- summary = summaryWithoutInstructions + "\nMissed Instructions summary
\n\n" + "```\n" + missedInstructions + "```" +
- "\n "
+ if len(missed) == 0 {
+ return ""
}
- data := map[string]string{
- "body": summary,
+ var b strings.Builder
+ b.WriteString(fmt.Sprintf("â Lines missing coverage (%d)
\n\n", len(missed)))
+
+ for _, r := range missed {
+ b.WriteString(fmt.Sprintf("**`%v`**\n", lineDescription(r.SourceLine)))
+ b.WriteString(fmt.Sprintf("```%v\n%v\n```\n\n", langForFile(r.FileName), r.LineValue))
}
- dataBytes, marshalErr := s.jsonClient.Marshal(data)
+ b.WriteString(" \n")
- if marshalErr != nil {
- return nil, errors.Wrap(marshalErr, "Failed marshalling payload to json")
+ return b.String()
+}
+
+// langForFile maps a source file's extension to a markdown code-fence language
+// so missed-line snippets get syntax highlighting. Unknown types render plain.
+func langForFile(fileName string) string {
+ switch strings.ToLower(filepath.Ext(fileName)) {
+ case ".go":
+ return "go"
+ case ".java":
+ return "java"
+ case ".kt", ".kts":
+ return "kotlin"
+ case ".scala":
+ return "scala"
+ case ".py":
+ return "python"
+ case ".js":
+ return "javascript"
+ case ".ts":
+ return "typescript"
+ default:
+ return ""
}
+}
- return bytes.NewBuffer(dataBytes), nil
+func backtickEach(items []string) []string {
+ out := make([]string, len(items))
+ for i, item := range items {
+ out[i] = "`" + item + "`"
+ }
+
+ return out
}
func collectModules(changedLinesWithCoverage domain.SourceLineCoverageReport) []string {
@@ -151,5 +250,7 @@ func collectModules(changedLinesWithCoverage domain.SourceLineCoverageReport) []
keys = append(keys, k)
}
+ sort.Strings(keys)
+
return keys
}
diff --git a/internal/plugin/reporter/github_pr_test.go b/internal/plugin/reporter/github_pr_test.go
index f1777c1..5ee72fa 100644
--- a/internal/plugin/reporter/github_pr_test.go
+++ b/internal/plugin/reporter/github_pr_test.go
@@ -13,6 +13,54 @@ import (
"github.com/target/pull-request-code-coverage/internal/plugin/pluginjson"
)
+func TestGithubPullRequest_commentsURL(t *testing.T) {
+
+ tests := []struct {
+ name string
+ apiBaseURL string
+ expected string
+ }{
+ {
+ name: "github enterprise host gets /api/v3 appended",
+ apiBaseURL: "https://git.target.com",
+ expected: "https://git.target.com/api/v3/repos/some_org/some_repo/issues/123/comments",
+ },
+ {
+ name: "trailing slash is trimmed",
+ apiBaseURL: "https://git.target.com/",
+ expected: "https://git.target.com/api/v3/repos/some_org/some_repo/issues/123/comments",
+ },
+ {
+ name: "base url already pointing at /api/v3 is not doubled",
+ apiBaseURL: "https://git.target.com/api/v3",
+ expected: "https://git.target.com/api/v3/repos/some_org/some_repo/issues/123/comments",
+ },
+ {
+ name: "public github uses api.github.com without /api/v3",
+ apiBaseURL: "https://api.github.com",
+ expected: "https://api.github.com/repos/some_org/some_repo/issues/123/comments",
+ },
+ {
+ name: "enterprise cloud data residency api host without /api/v3",
+ apiBaseURL: "https://api.acme.ghe.com",
+ expected: "https://api.acme.ghe.com/repos/some_org/some_repo/issues/123/comments",
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ writer := &GithubPullRequest{
+ apiBaseURL: tt.apiBaseURL,
+ owner: "some_org",
+ repo: "some_repo",
+ pr: "123",
+ }
+
+ assert.Equal(t, tt.expected, writer.commentsURL())
+ })
+ }
+}
+
func TestGithubPullRequest_Write_FailedNewRequest(t *testing.T) {
mockClient := &pluginhttp.MockClient{}
diff --git a/internal/plugin/runner.go b/internal/plugin/runner.go
index 032a99b..9de5fbf 100644
--- a/internal/plugin/runner.go
+++ b/internal/plugin/runner.go
@@ -67,7 +67,8 @@ func (*DefaultRunner) Run(propertyGetter func(string) (string, bool), changedSou
ghAPIBaseURL, ghAPIBaseURLFound := propertyGetter("PARAMETER_GH_API_BASE_URL")
if !ghAPIBaseURLFound {
- logrus.Info("PARAMETER_GH_API_BASE_URL was missing, will not send report to PR comments")
+ ghAPIBaseURL = reporter.DefaultGithubAPIBaseURL
+ logrus.Info(fmt.Sprintf("PARAMETER_GH_API_BASE_URL was missing, defaulting to %v", reporter.DefaultGithubAPIBaseURL))
}
repoPR, repoPRFound := propertyGetter("BUILD_PULL_REQUEST_NUMBER")
@@ -112,7 +113,7 @@ func (*DefaultRunner) Run(propertyGetter func(string) (string, bool), changedSou
reporters := []reporter.Reporter{reporter.NewSimple(reportDefaultOut)}
- if ghAPIKeyFound && ghAPIBaseURLFound && repoPRFound && repoOwnerFound && repoNameFound {
+ if ghAPIKeyFound && repoPRFound && repoOwnerFound && repoNameFound {
reporters = append(reporters, reporter.NewGithubPullRequest(ghAPIKey, ghAPIBaseURL, repoPR, repoOwner, repoName, &pluginhttp.DefaultClient{}, &pluginjson.DefaultClient{}))
}
logrus.Info("enabled reporters are ")
diff --git a/internal/plugin/runner_test.go b/internal/plugin/runner_test.go
index 7529ed6..8ecbfdb 100644
--- a/internal/plugin/runner_test.go
+++ b/internal/plugin/runner_test.go
@@ -107,29 +107,14 @@ Covered Instructions -> 97% (177)
Missed Instructions -> 3% (5)
`, buf.String())
- requestAsserter.AssertRequestWasMade(t, "/api/v3/repos/some_org/some_repo/issues/123/comments", "SOME_API_KEY", map[string]interface{}{
- "body": `Code Coverage Summary:
-
-Lines Without Coverage Data -> 92% (2216)
-Lines With Coverage Data -> 8% (182)
-Covered Instructions -> **97%** (177)
-Missed Instructions -> 3% (5)
-
-Missed Instructions summary
-
-` + "```" + `
---- internal/plugin/runner.go:72
-func GetCoverageReportLoader(coverageType string, sourceDir string) coverage.Loader {
---- main.go:10
- err := plugin.NewRunner().Run(os.LookupEnv, os.Stdin, os.Stdout)
---- main.go:12
- if err != nil {
---- main.go:13
- log.WithFields(log.Fields{
---- main.go:17
- os.Exit(1)
-` +
- "```\n ",
+ requestAsserter.AssertRequestBodyContains(t, "/api/v3/repos/some_org/some_repo/issues/123/comments", "SOME_API_KEY", []string{
+ "## đ Pull Request Code Coverage",
+ "| â
Covered instructions | **97%** | 177 |",
+ "| â Missed instructions | 3% | 5 |",
+ "What do these metrics mean?",
+ "â Lines missing coverage (5)",
+ "`internal/plugin/runner.go:72`",
+ "```go",
})
propGetter.AssertExpectations(t)
@@ -178,29 +163,14 @@ Covered Instructions -> 97% (177)
Missed Instructions -> 3% (5)
`, buf.String())
- requestAsserter.AssertRequestWasMade(t, "/api/v3/repos/some_org/some_repo/issues/123/comments", "SOME_API_KEY", map[string]interface{}{
- "body": `Code Coverage Summary:
-
-Lines Without Coverage Data -> 92% (2216)
-Lines With Coverage Data -> 8% (182)
-Covered Instructions -> **97%** (177)
-Missed Instructions -> 3% (5)
-
-Missed Instructions summary
-
-` + "```" + `
---- internal/plugin/runner.go:72
-func GetCoverageReportLoader(coverageType string, sourceDir string) coverage.Loader {
---- main.go:10
- err := plugin.NewRunner().Run(os.LookupEnv, os.Stdin, os.Stdout)
---- main.go:12
- if err != nil {
---- main.go:13
- log.WithFields(log.Fields{
---- main.go:17
- os.Exit(1)
-` +
- "```\n ",
+ requestAsserter.AssertRequestBodyContains(t, "/api/v3/repos/some_org/some_repo/issues/123/comments", "SOME_API_KEY", []string{
+ "## đ Pull Request Code Coverage",
+ "| â
Covered instructions | **97%** | 177 |",
+ "| â Missed instructions | 3% | 5 |",
+ "What do these metrics mean?",
+ "â Lines missing coverage (5)",
+ "`internal/plugin/runner.go:72`",
+ "```go",
})
propGetter.AssertExpectations(t)
@@ -239,23 +209,13 @@ Covered Instructions -> 73% (8)
Missed Instructions -> 27% (3)
`, buf.String())
- requestAsserter.AssertRequestWasMade(t, "/api/v3/repos/some_org/some_repo/issues/123/comments", "SOME_API_KEY", map[string]interface{}{
- "body": `*Modules: category-search*
-
-Code Coverage Summary:
-
-Lines Without Coverage Data -> 78% (7)
-Lines With Coverage Data -> 22% (2)
-Covered Instructions -> **73%** (8)
-Missed Instructions -> 27% (3)
-
-Missed Instructions summary
-
-` + "```" + `
---- category-search/src/main/java/com/tgt/CategorySearchApplication.java:52
- System.out.print("Something");
-` +
- "```\n ",
+ requestAsserter.AssertRequestBodyContains(t, "/api/v3/repos/some_org/some_repo/issues/123/comments", "SOME_API_KEY", []string{
+ "**Modules:** `category-search`",
+ "| â
Covered instructions | **73%** | 8 |",
+ "| â Missed instructions | 27% | 3 |",
+ "What do these metrics mean?",
+ "category-search/src/main/java/com/tgt/CategorySearchApplication.java:52",
+ "```java",
})
propGetter.AssertExpectations(t)
@@ -295,23 +255,13 @@ Covered Instructions -> 73% (8)
Missed Instructions -> 27% (3)
`, buf.String())
- requestAsserter.AssertRequestWasMade(t, "/api/v3/repos/some_org/some_repo/issues/123/comments", "SOME_API_KEY", map[string]interface{}{
- "body": `*Modules: category-search*
-
-Code Coverage Summary:
-
-Lines Without Coverage Data -> 78% (7)
-Lines With Coverage Data -> 22% (2)
-Covered Instructions -> **73%** (8)
-Missed Instructions -> 27% (3)
-
-Missed Instructions summary
-
-` + "```" + `
---- category-search/src/main/java/com/tgt/CategorySearchApplication.java:52
- System.out.print("Something");
-` +
- "```\n ",
+ requestAsserter.AssertRequestBodyContains(t, "/api/v3/repos/some_org/some_repo/issues/123/comments", "SOME_API_KEY", []string{
+ "**Modules:** `category-search`",
+ "| â
Covered instructions | **73%** | 8 |",
+ "| â Missed instructions | 27% | 3 |",
+ "What do these metrics mean?",
+ "category-search/src/main/java/com/tgt/CategorySearchApplication.java:52",
+ "```java",
})
propGetter.AssertExpectations(t)
@@ -353,24 +303,12 @@ Covered Instructions -> 88% (42)
Missed Instructions -> 12% (6)
`, buf.String())
- requestAsserter.AssertRequestWasMade(t, "/api/v3/repos/some_org/some_repo/issues/123/comments", "SOME_API_KEY", map[string]interface{}{
- "body": `*Modules: category-search*
-
-Code Coverage Summary:
-
-Lines Without Coverage Data -> 47% (7)
-Lines With Coverage Data -> 53% (8)
-Covered Instructions -> **88%** (42)
-Missed Instructions -> 12% (6)
-
-Missed Instructions summary
-
-` + "```" + `
---- category-search/src/main/java/com/tgt/CategorySearchApplication.java:52
- System.out.print("Something");
---- category-search/src/main/kotlin/com/tgt/SomeOtherClass.kt:12
- System.out.print("Something2");
-` + "```\n ",
+ requestAsserter.AssertRequestBodyContains(t, "/api/v3/repos/some_org/some_repo/issues/123/comments", "SOME_API_KEY", []string{
+ "**Modules:** `category-search`",
+ "| â
Covered instructions | **88%** | 42 |",
+ "| â Missed instructions | 12% | 6 |",
+ "category-search/src/main/kotlin/com/tgt/SomeOtherClass.kt:12",
+ "```kotlin",
})
propGetter.AssertExpectations(t)
@@ -413,25 +351,12 @@ Covered Instructions -> 88% (42)
Missed Instructions -> 12% (6)
`, buf.String())
- requestAsserter.AssertRequestWasMade(t, "/api/v3/repos/some_org/some_repo/issues/123/comments", "SOME_API_KEY", map[string]interface{}{
- "body": `*Modules: category-search*
-
-Code Coverage Summary:
-
-Lines Without Coverage Data -> 47% (7)
-Lines With Coverage Data -> 53% (8)
-Covered Instructions -> **88%** (42)
-Missed Instructions -> 12% (6)
-
-Missed Instructions summary
-
-` + "```" + `
---- category-search/src/main/java/com/tgt/CategorySearchApplication.java:52
- System.out.print("Something");
---- category-search/src/main/kotlin/com/tgt/SomeOtherClass.kt:12
- System.out.print("Something2");
-` +
- "```\n ",
+ requestAsserter.AssertRequestBodyContains(t, "/api/v3/repos/some_org/some_repo/issues/123/comments", "SOME_API_KEY", []string{
+ "**Modules:** `category-search`",
+ "| â
Covered instructions | **88%** | 42 |",
+ "| â Missed instructions | 12% | 6 |",
+ "category-search/src/main/kotlin/com/tgt/SomeOtherClass.kt:12",
+ "```kotlin",
})
propGetter.AssertExpectations(t)
diff --git a/internal/test/mocks/mock_gh_api.go b/internal/test/mocks/mock_gh_api.go
index 0b87bc4..8aad00d 100644
--- a/internal/test/mocks/mock_gh_api.go
+++ b/internal/test/mocks/mock_gh_api.go
@@ -39,6 +39,7 @@ func WithMockGithubAPI(doer func(mockServerURL string, requestAsserter GithubAPI
type GithubAPIRequestAsserter interface {
AssertRequestWasMade(t *testing.T, path string, apikey string, body map[string]interface{})
+ AssertRequestBodyContains(t *testing.T, path string, apikey string, substrings []string)
AssertNoRequestsWereMade(t *testing.T)
}
@@ -75,6 +76,38 @@ func (a *DefaultGithubAPIRequestAsserter) AssertRequestWasMade(t *testing.T, pat
assert.Fail(t, fmt.Sprintf("Request was not made for path=%v, apikey=%v, body=%v", path, apikey, body))
}
+// AssertRequestBodyContains finds the request matching path/apikey and asserts
+// its JSON "body" field contains every given substring. Use this (rather than an
+// exact body match) for the PR comment, whose markdown formatting is expected to
+// evolve â assert the meaningful content, not the exact bytes.
+func (a *DefaultGithubAPIRequestAsserter) AssertRequestBodyContains(t *testing.T, path string, apikey string, substrings []string) {
+ for _, r := range a.requests {
+ if r.req.URL.Path != path {
+ continue
+ }
+
+ if r.req.Header.Get("Authorization") != "token "+apikey {
+ continue
+ }
+
+ if r.req.Header.Get("Content-Type") != "application/json" {
+ continue
+ }
+
+ var bodyData map[string]interface{}
+ mustJSONUnmarshall(r.body, &bodyData)
+ body, _ := bodyData["body"].(string)
+
+ for _, sub := range substrings {
+ assert.Contains(t, body, sub)
+ }
+
+ return
+ }
+
+ assert.Fail(t, fmt.Sprintf("Request was not made for path=%v, apikey=%v", path, apikey))
+}
+
func mustJSONUnmarshall(bytes []byte, result interface{}) {
err := json.Unmarshal(bytes, result)