Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 8 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -145,16 +145,22 @@ jobs:
echo
cat coverage-pr-build.md
} > coverage-pr-comment.md
- name: Publish run summary
run: cat coverage-pr-comment.md >> "$GITHUB_STEP_SUMMARY"
- name: Upload PR-build coverage
id: coverage-artifact
uses: actions/upload-artifact@v7
with:
name: coverage-pr-build
if-no-files-found: error
path: |
coverage-pr-head.lcov
coverage-pr-comment.md
# The archived report has the run link; the upload URL exists only after upload.
- name: Link PR-build artifact and publish run summary
env:
ARTIFACT_URL: ${{ steps.coverage-artifact.outputs.artifact-url }}
run: |
printf '\n<sub>📦 [PR-build coverage report (LCOV and Markdown)](%s)</sub>\n' "$ARTIFACT_URL" >> coverage-pr-comment.md
cat coverage-pr-comment.md >> "$GITHUB_STEP_SUMMARY"
- name: Comment PR-build coverage
if: github.event.pull_request.head.repo.fork != true
uses: marocchino/sticky-pull-request-comment@v3
Expand Down
3 changes: 3 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,9 @@ PR head, builds its debug `patchcov` binary with the lockfile, then measures tha
checkout with cargo-llvm-cov. The local binary renders a separate sticky comment and
enforces 90% patch coverage and 95% overall line coverage. Its report is also in the
run summary and the `coverage-pr-build` artifact, including when a coverage gate fails.
The posted comment and run summary link directly to that artifact, labeled as LCOV and
Markdown. The archived comment includes the run link; the direct artifact link is added
after upload, when its URL is available.
Fork PRs skip the comment and still get the summary, artifact and gates. This job is not
in the ruleset or required-check list and is skipped on pushes and merge groups.

Expand Down
3 changes: 1 addition & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,10 +55,9 @@ report so the per-file table appears:
>
> </details>
>
> <sub>Full per-file summary is attached as the **coverage-summary** build artifact.</sub>

Without `--baseline-report` there is no delta on the `Total` line, no per-file table and no
indirect changes; a "No baseline available yet" notice stands in for them and the patch
indirect changes; a "No baseline report supplied" notice stands in for them and the patch
section is unchanged. With `-o json` or `-o yaml` the same result is structured
data, in the shape described in the [output schema](docs/reference.md#output-schema):

Expand Down
8 changes: 7 additions & 1 deletion docs/reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,10 +60,16 @@ use the repository working directory.
| `--ignore-filename-regex <REGEX>` | Exclude matching files from both reports (repeatable or comma-separated); unioned with `diff.ignore-filename-regex` |
| `--config-dir <PATH>` | Directory searched for `config.yaml` (default: the discovered `.patchcov/`, honouring `PATCHCOV_CONFIG_DIR`) |
| `--artifact-url <URL>` | Link to the full coverage-summary artifact, in the markdown footer |
| `--run-url <URL>` | Link to the CI run, in the markdown footer |
| `--run-url <URL>` | Link to the CI run, in the markdown footer, independently of `--artifact-url` |
| `--commit-url <URL>` | Commit-URL prefix for linking SHAs |
| `--base-sha <SHA>` / `--head-sha <SHA>` | SHAs shown in the markdown `Comparing` line (both are needed for the line to appear) |

Markdown reports omit artifact guidance when no nonempty artifact URL is supplied. An explicit
`--artifact-url` (or `COVERAGE_ARTIFACT_URL`) is labeled as a full per-file coverage summary;
only use it for an artifact containing that summary. A run URL renders even without an
artifact URL. Without `--baseline-report`, per-file deltas and indirect coverage changes
are unavailable; the report makes no assumptions about CI baseline publication.

For both `--fail-under-*` flags, space-separated negative float values such as `-inf`,
`-nan` and `-.5` receive the same explanatory usage error as the `--flag=value` spelling.
A following option instead of a value reports `a value is required`.
Expand Down
68 changes: 48 additions & 20 deletions src/render.rs
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ pub struct RenderOptions {
pub no_explanation: bool,
/// Link to the full coverage-summary artifact.
pub artifact_url: Option<String>,
/// Link to the CI run.
/// Link to the CI run, rendered independently of the artifact URL.
pub run_url: Option<String>,
/// Base (merge-base) commit SHA.
pub base_sha: Option<String>,
Expand Down Expand Up @@ -249,9 +249,8 @@ fn render_markdown(diff: &CoverageDiff, opts: &RenderOptions) -> String {
render_notable_unchanged(diff, &mut out);
} else {
out.push_str(
"_No baseline available yet (first run, or the `main` baseline artifact was \
missing). Per-file deltas will appear on PRs once a baseline has been published \
from `main`._\n\n",
"_No baseline report supplied. Per-file deltas and indirect coverage changes \
require a baseline report._\n\n",
);
}
// Also without a baseline: `ignore` still shapes the total and the patch.
Expand Down Expand Up @@ -542,23 +541,22 @@ fn render_indirect_section(diff: &CoverageDiff, out: &mut String) {
}

fn render_footer(opts: &RenderOptions, out: &mut String) {
match opts.artifact_url.as_deref().filter(|u| !u.is_empty()) {
Some(artifact) => {
out.push_str(&format!(
"<sub>📦 [Full per-file coverage summary]({artifact})"
));
if let Some(run) = opts.run_url.as_deref().filter(|u| !u.is_empty()) {
out.push_str(&format!(" · [run summary]({run})"));
}
out.push_str("</sub>\n");
}
None => {
out.push_str(
"<sub>Full per-file summary is attached as the **coverage-summary** build \
artifact.</sub>\n",
);
let artifact = opts.artifact_url.as_deref().filter(|u| !u.is_empty());
let run = opts.run_url.as_deref().filter(|u| !u.is_empty());
if artifact.is_none() && run.is_none() {
return;
}
out.push_str("<sub>");
if let Some(artifact) = artifact {
out.push_str(&format!("📦 [Full per-file coverage summary]({artifact})"));
}
if let Some(run) = run {
if artifact.is_some() {
out.push_str(" · ");
}
out.push_str(&format!("[run summary]({run})"));
}
out.push_str("</sub>\n");
}

// ---------------------------------------------------------------------------
Expand Down Expand Up @@ -937,7 +935,11 @@ mod tests {
assert!(md.contains("### Patch coverage"));
assert!(md.contains("Patch: **80%** (4/5 new lines covered)"));
assert!(md.contains("`src/a.rs:9`"));
assert!(md.contains("No baseline available yet"));
assert!(md.contains("No baseline report supplied"));
assert!(md.contains("require a baseline report"));
assert!(!md.contains("will appear"));
assert!(!md.contains("baseline artifact"));
assert!(!md.contains("`main`"));
}

#[test]
Expand Down Expand Up @@ -1390,6 +1392,32 @@ mod tests {
assert!(md.contains("[run summary](https://run)"));
}

#[test]
fn markdown_footer_links_are_independent_and_optional() {
for artifact in [None, Some(""), Some("https://artifact")] {
for run in [None, Some(""), Some("https://run")] {
let opts = RenderOptions {
artifact_url: artifact.map(str::to_string),
run_url: run.map(str::to_string),
..Default::default()
};
let md = render(&sample_diff(), &opts, OutputFormat::Markdown).unwrap();
let has_artifact = artifact == Some("https://artifact");
let has_run = run == Some("https://run");
assert_eq!(
md.contains("[Full per-file coverage summary](https://artifact)"),
has_artifact,
"{md}"
);
assert_eq!(md.contains("[run summary](https://run)"), has_run, "{md}");
assert_eq!(md.contains("<sub>"), has_artifact || has_run, "{md}");
assert_eq!(md.contains(" · "), has_artifact && has_run, "{md}");
assert!(!md.contains("coverage-summary"), "{md}");
assert!(!md.contains("attached"), "{md}");
}
}
}

#[test]
fn markdown_comparing_line_and_covered_indirect() {
let mut diff = sample_diff();
Expand Down
Loading