Skip to content

Commit

Permalink
Coverage CI report improvement (#2529)
Browse files Browse the repository at this point in the history
* Fix coverage report in coverage CI

* Fix coverage CI typo

* Add master coverage for this PR (must be rollback)

* Add upload to s3 to push correct coverage files for master

* Rollback to fixed CI

* Test running python3 instead of python on bare-metal

* Fix coverage report generation

* Add missing coverage report job if conditional

* Add ignore .cargo libraries in coverage

* Add missing ignore .cargo for coverage

* Add coverage for master (must be rollback)

* Restore previous commit

* Add link to html in files table

* Fix base html url for coverage files
  • Loading branch information
aon authored Oct 19, 2023
1 parent 15599d0 commit c26c59d
Show file tree
Hide file tree
Showing 2 changed files with 300 additions and 16 deletions.
261 changes: 261 additions & 0 deletions .github/scripts/coverage-report.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,261 @@
import argparse
import json


def generate_summary_markdown(
base_covdir, incoming_covdir, base_branch, incoming_branch
):
"""Generate a markdown summary with the coverage differences."""

root_base_coverage = extract_root_coverage(base_covdir)
root_incoming_coverage = extract_root_coverage(incoming_covdir)

coverage_diff = (
root_incoming_coverage["coverage_percent"]
- root_base_coverage["coverage_percent"]
)
total_files_diff = (
root_incoming_coverage["total_files"] - root_base_coverage["total_files"]
)
total_lines_diff = (
root_incoming_coverage["total_lines"] - root_base_coverage["total_lines"]
)
covered_lines_diff = (
root_incoming_coverage["covered_lines"] - root_base_coverage["covered_lines"]
)
missed_lines_diff = (
root_incoming_coverage["missed_lines"] - root_base_coverage["missed_lines"]
)

get_comparison_symbol = lambda x: "+" if x > 0 else "-" if x < 0 else " "

# Generate table data
subtitle = ["##", base_branch, incoming_branch, "+/-", "##"]
coverage = [
f"{get_comparison_symbol(coverage_diff)} Coverage",
f"{root_base_coverage['coverage_percent']:.2f}%",
f"{root_incoming_coverage['coverage_percent']:.2f}%",
f"{'+' if coverage_diff > 0 else ''}{coverage_diff:.2f}%",
"",
]
files = [
f"{get_comparison_symbol(total_files_diff)} Files",
root_base_coverage["total_files"],
root_incoming_coverage["total_files"],
f"{'+' if total_files_diff > 0 else ''}{total_files_diff or ''}",
"",
]
lines = [
f"{get_comparison_symbol(total_lines_diff)} Lines",
root_base_coverage["total_lines"],
root_incoming_coverage["total_lines"],
f"{'+' if total_lines_diff > 0 else ''}{total_lines_diff or ''}",
"",
]
hits = [
f"{get_comparison_symbol(covered_lines_diff)} Hits",
root_base_coverage["covered_lines"],
root_incoming_coverage["covered_lines"],
f"{'+' if covered_lines_diff > 0 else ''}{covered_lines_diff or ''}",
"",
]
misses = [
f"{get_comparison_symbol(missed_lines_diff)} Misses",
root_base_coverage["missed_lines"],
root_incoming_coverage["missed_lines"],
f"{'+' if missed_lines_diff > 0 else ''}{missed_lines_diff or ''}",
"",
]
rows = [subtitle, coverage, files, lines, hits, misses]

# Format table
get_widest_column = lambda x: max([len(str(row[x])) for row in rows])
padding = 3
for i in range(len(rows[0])):
widest_column = get_widest_column(i)
for row in rows:
if i == 0:
row[i] = str(row[i]).ljust(widest_column)
else:
row[i] = str(row[i]).rjust(widest_column + padding)

# Generate table string
table_rows = ["".join(row) for row in rows]
table_width = len(table_rows[0])
separator = "=" * table_width
table_rows.insert(0, f"@@{'Coverage Diff'.center(table_width - 4)}@@")
table_rows.insert(2, separator)
table_rows.insert(6, separator)
table = "\n".join(table_rows)

return table


def generate_comparison_markdown(base_covdir, incoming_covdir, base_html_url):
"""Generate a markdown table with the coverage differences."""
differences = compare_covdir_files(base_covdir, incoming_covdir)

markdown_table_data = []

# Table headers
markdown_table_data.append(["Files Changed", "Coverage", ""])
markdown_table_data.append(["---", "---", "---"])

for item in differences:
# Determine emoji based on coverage difference
if item["diff"] > 0:
emoji = "🔼"
elif item["diff"] < 0:
emoji = "🔽"
else:
emoji = ""

# Append row data
row = [
f"[{item['path']}]({base_html_url}/html{item['path']}.html)",
f"{item['incoming_coverage']:.2f}% ({'+' if item['diff'] > 0 else ''}{item['diff']:.2f}%)",
emoji,
]
markdown_table_data.append(row)

# Convert table data to markdown format
markdown_table = "\n".join(
["| " + " | ".join(row) + " |" for row in markdown_table_data]
)

return markdown_table


def compare_covdir_files(base_covdir, incoming_covdir):
"""Compare two covdir data sets for files only and return differences."""
base_covdir_data = extract_files_coverage(base_covdir)
incoming_covdir_data = extract_files_coverage(incoming_covdir)

# Convert to dictionary for easier lookup
base_covdir_dict = {item["path"]: item["coverage"] for item in base_covdir_data}
incoming_covdir_dict = {
item["path"]: item["coverage"] for item in incoming_covdir_data
}

differences = []
for path, incoming_coverage in incoming_covdir_dict.items():
if path in base_covdir_dict:
base_coverage = base_covdir_dict[path]
diff = incoming_coverage - base_coverage
if diff != 0:
differences.append(
{
"path": path,
"base_coverage": base_coverage,
"incoming_coverage": incoming_coverage,
"diff": diff,
}
)

return differences


def extract_root_coverage(data):
"""Get the coverage summary of the root node."""

total_files = len(extract_files_coverage(data))

return {
"coverage_percent": data["coveragePercent"],
"total_lines": data["linesTotal"],
"covered_lines": data["linesCovered"],
"missed_lines": data["linesMissed"],
"total_files": total_files,
}


def extract_files_coverage(data, path=""):
"""Recursively extract coverage data from the nested structure, considering only leaf nodes."""
results = []

# If the node has children, explore the children nodes
if "children" in data:
for key in data["children"]:
results.extend(
extract_files_coverage(data["children"][key], path + data["name"] + "/")
)
# If the node doesn't have children, consider it a leaf node (file) and extract its coverage
elif "name" in data and "coveragePercent" in data:
results.append(
{"path": path + data["name"], "coverage": data["coveragePercent"]}
)
return results


if __name__ == "__main__":
parser = argparse.ArgumentParser(
prog="coverage-report",
description="This script compares coverage between two covdirs and generates a report markdown summary and table with the differences.",
)
parser.add_argument(
"--base-covdir",
metavar="path",
required=True,
help="covdir path for the base branch",
)
parser.add_argument(
"--incoming-covdir",
metavar="path",
required=True,
help="covdir path for the incoming branch",
)
parser.add_argument(
"--base-branch", metavar="branch", required=True, help="name of the base branch"
)
parser.add_argument(
"--incoming-branch",
metavar="branch",
required=True,
help="name of the incoming branch",
)
parser.add_argument(
"--base-html-url",
metavar="url",
required=True,
help="URL to the base HTML coverage report",
)
parser.add_argument(
"--output",
metavar="path",
required=False,
help="path to the output file, if not specified, the output will be printed to stdout",
)
args = parser.parse_args()

# Open covdir files
with open(args.base_covdir, "r") as f:
base_covdir = json.load(f)
with open(args.incoming_covdir, "r") as f:
incoming_covdir = json.load(f)

# Generate markdown summary
markdown_summary = generate_summary_markdown(
base_covdir,
incoming_covdir,
args.base_branch,
args.incoming_branch,
)

# Generate markdown table
markdown_table = generate_comparison_markdown(
base_covdir, incoming_covdir, args.base_html_url
)

# Generate output
output = f"""```diff
{markdown_summary}
```
{markdown_table}
"""

if args.output:
with open(args.output, "w") as f:
f.write(output)
else:
print(output)
55 changes: 39 additions & 16 deletions .github/workflows/coverage.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ jobs:
runs-on: ubuntu-latest
outputs:
git_branch: ${{ steps.check-git-ref.outputs.git_branch }}
git_target_branch: ${{ steps.check-git-ref.outputs.git_target_branch }}
git_ref: ${{ steps.check-git-ref.outputs.git_ref }}
sha: ${{ steps.get-sha.outputs.sha }}
sha8: ${{ steps.get-sha.outputs.sha8 }}
Expand All @@ -40,11 +41,13 @@ jobs:
run: |
if [[ -n "${{ github.event.pull_request.head.sha }}" ]]; then
echo "git_branch=$(echo ${GITHUB_HEAD_REF})" >> $GITHUB_OUTPUT
echo "git_target_branch=$(echo ${GITHUB_BASE_REF})" >> $GITHUB_OUTPUT
echo "git_ref=${{ github.event.pull_request.head.sha }}" >> $GITHUB_OUTPUT
echo "coverage_dir=pulls/${{ github.event.pull_request.number }}" >> $GITHUB_OUTPUT
echo "coverage_report=true" >> $GITHUB_OUTPUT
else
echo "git_branch=$(echo ${GITHUB_REF#refs/heads/})" >> $GITHUB_OUTPUT
echo "git_target_branch=$(echo ${GITHUB_REF#refs/heads/})" >> $GITHUB_OUTPUT
echo "git_ref=$GITHUB_REF" >> $GITHUB_OUTPUT
echo "coverage_dir=branches/master" >> $GITHUB_OUTPUT
echo "coverage_report=false" >> $GITHUB_OUTPUT
Expand Down Expand Up @@ -151,9 +154,10 @@ jobs:
du -sh proffiles
echo "Executing grcov"
mkdir -p coverage
./grcov proffiles/ -s ./ --binary-path ./target/release/ \
-t html --branch --ignore-not-existing --ignore "target/release/build/*" \
-o coverage/ --llvm 2>&1 \
-t html,covdir --branch --ignore-not-existing --ignore "target/release/build/*" \
--ignore "$HOME/.cargo/**" -o coverage/ --llvm 2>&1 \
| tee grcov.log
INVALID="$(grep invalid grcov.log | \
Expand All @@ -165,18 +169,32 @@ jobs:
cd proffiles/
rm $INVALID
cd ..
echo "Executing grcov again"
rm -rf coverage
mkdir -p coverage
./grcov proffiles/ -s ./ --binary-path ./target/release/ \
-t html --branch --ignore-not-existing --ignore "target/release/build/*" \
-o coverage/ --llvm
-t html,covdir --branch --ignore-not-existing --ignore "target/release/build/*" \
--ignore "$HOME/.cargo/**" -o coverage/ --llvm
fi
if [ "${{ needs.set-tags.outputs.coverage_report }}" == "true" ]; then
echo "Generating coverage report"
wget ${{ vars.S3_COVERAGE_URL }}/branches/master/covdir \
-O base_covdir || true
python3 .github/scripts/coverage-report.py \
--base-covdir ./base_covdir \
--incoming-covdir ./coverage/covdir \
--base-branch ${{ needs.set-tags.outputs.git_target_branch }} \
--incoming-branch ${{ needs.set-tags.outputs.git_branch }} \
--base-html-url ${{ vars.S3_COVERAGE_URL }}/${{ needs.set-tags.outputs.coverage_dir }} \
> coverage_report.md
echo "coverage_date=\"$(date)\"" >> $GITHUB_OUTPUT
fi
echo "coverage_date=\"$(date)\"" >> $GITHUB_OUTPUT
echo "total_percent=$(grep -o '[0-9\.]*%' coverage/html/coverage.json)" >> $GITHUB_OUTPUT
wget ${{ vars.S3_COVERAGE_URL }}/branches/master/html/coverage.json \
-O coverage-master.json || true
echo "master_percent=$(grep -o '[0-9\.]*%' coverage-master.json || echo 'N/A')" >> $GITHUB_OUTPUT
rm -rf proffiles/
- name: Upload coverate to gha
- name: Upload coverage to gha
uses: actions/[email protected]
with:
name: coverage
Expand All @@ -193,6 +211,16 @@ jobs:
acl: "none"
- name: Link To Report
run: echo "${{ vars.S3_COVERAGE_URL }}/${{steps.S3.outputs.object_key}}/html/index.html"
- name: Create coverage report comment
if: ${{ needs.set-tags.outputs.coverage_report == 'true' }}
run: |
mv coverage_report.md temp_coverage_report.md
echo "## [Coverage Report](${{ vars.S3_COVERAGE_URL }}/${{steps.S3.outputs.object_key}}/html/index.html)" > coverage_report.md
cat temp_coverage_report.md >> coverage_report.md
rm temp_coverage_report.md
echo "> Coverage generated ${{ steps.coverage.outputs.coverage_date }}" >> coverage_report.md
echo "Generated coverage report comment"
cat coverage_report.md
- name: Find Comment
if: ${{ needs.set-tags.outputs.coverage_report == 'true' }}
uses: peter-evans/find-comment@v2
Expand All @@ -207,10 +235,5 @@ jobs:
with:
comment-id: ${{ steps.fc.outputs.comment-id }}
issue-number: ${{ github.event.pull_request.number }}
body: |
Coverage generated ${{ steps.coverage.outputs.coverage_date }}:
${{ vars.S3_COVERAGE_URL }}/${{steps.S3.outputs.object_key}}/html/index.html
Master coverage: ${{ steps.coverage.outputs.master_percent }}
Pull coverage: ${{ steps.coverage.outputs.total_percent }}
body-path: coverage_report.md
edit-mode: replace

0 comments on commit c26c59d

Please sign in to comment.