Skip to content

Commit 41f9b91

Browse files
authored
Add code coverage check (#13)
* add code coverage check * mdformat
1 parent 5ae1fd0 commit 41f9b91

File tree

4 files changed

+154
-2
lines changed

4 files changed

+154
-2
lines changed

.github/workflows/rust.yml

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,3 +41,42 @@ jobs:
4141
- uses: actions/checkout@v4
4242
- name: Run Clippy
4343
run: cargo clippy --all-targets --features default
44+
45+
coverage:
46+
name: Code Coverage
47+
runs-on: ubuntu-latest
48+
steps:
49+
- uses: actions/checkout@v4
50+
- uses: actions-rust-lang/setup-rust-toolchain@v1
51+
- name: Install cargo-llvm-cov
52+
uses: taiki-e/install-action@cargo-llvm-cov
53+
- name: Generate code coverage
54+
run: cargo llvm-cov --features default --workspace --json > coverage.json
55+
- name: Coverage (60% by line)
56+
run: python3 utils/code_coverage.py -p 60.0 coverage.json >> $GITHUB_STEP_SUMMARY
57+
- name: Coverage (80% by line)
58+
run: python3 utils/code_coverage.py -p 80.0 coverage.json >> $GITHUB_STEP_SUMMARY
59+
- name: Coverage (90% by line)
60+
run: python3 utils/code_coverage.py -p 90.0 coverage.json >> $GITHUB_STEP_SUMMARY
61+
62+
mdformat:
63+
name: Markdown format
64+
runs-on: ubuntu-latest
65+
steps:
66+
- uses: actions/checkout@v4
67+
- name: Markdown check format
68+
uses: ydah/mdformat-action@main
69+
with:
70+
number: true
71+
72+
73+
pyformat:
74+
name: Python format
75+
runs-on: ubuntu-latest
76+
steps:
77+
- uses: actions/checkout@v4
78+
- name: Format python code
79+
uses: psf/black@stable
80+
with:
81+
options: "--check --verbose"
82+
src: "./utils"

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
name = "circuit"
33
version = "0.1.0"
44
edition = "2024"
5-
default-run = "main"
5+
license = "MIT OR Apache-2.0"
66

77
[dependencies]
88
bitvec = { version = "1.0.1" }

README.md

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,4 +58,3 @@ However, you may want to use another library that leverages a denser representat
5858
Then, open it up and take a look:
5959

6060
![Ripple-carry adder](doc/adder.svg)
61-

utils/code_coverage.py

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
# Copyright 2025 The Circuit Authors
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
import argparse
16+
import sys
17+
import os
18+
import json
19+
20+
# cargo llvm-cov --all-features --workspace --json
21+
if __name__ == "__main__":
22+
parser = argparse.ArgumentParser()
23+
parser.add_argument(
24+
"-p",
25+
"--percent",
26+
dest="percent",
27+
required=False,
28+
help="Minimum code coverage per line per source file",
29+
type=float,
30+
default=80.0,
31+
)
32+
parser.add_argument(
33+
"-w",
34+
"--whitelist",
35+
dest="whitelist",
36+
nargs="+",
37+
help="Files to whitelist from coverage checks",
38+
type=str,
39+
default=["main.rs"],
40+
)
41+
parser.add_argument(
42+
"input", nargs="?", type=argparse.FileType("r"), default=sys.stdin
43+
)
44+
parser.add_argument(
45+
"output", nargs="?", type=argparse.FileType("w"), default=sys.stdout
46+
)
47+
args = parser.parse_args()
48+
49+
whitelisted = set(args.whitelist)
50+
data = json.load(args.input)
51+
data = data["data"]
52+
percent = args.percent
53+
passed = True
54+
55+
print(f"### Code Coverage Summary ({percent:.2f}%)", file=args.output)
56+
57+
for datum in data:
58+
files = datum["files"]
59+
for record in files:
60+
filePassed = True
61+
name = record["filename"]
62+
stem = (
63+
os.path.basename(os.path.dirname(name)) + "/" + os.path.basename(name)
64+
)
65+
if stem in whitelisted:
66+
continue
67+
lineCoverage = record["summary"]["lines"]["percent"]
68+
if lineCoverage < percent:
69+
print(
70+
f"#### {name}: Only {lineCoverage:.2f}% by line", file=args.output
71+
)
72+
print(f"```rust", file=args.output)
73+
passed = False
74+
with open(name, "r") as f:
75+
lines = f.readlines()
76+
covered = set()
77+
startSeg = True
78+
lastLine = None
79+
for segment in record["segments"]:
80+
line = max(segment[0] - 1, 0)
81+
82+
if (
83+
lastLine is not None
84+
and line not in covered
85+
and line != lastLine + 1
86+
):
87+
startSeg = True
88+
89+
if startSeg:
90+
print(
91+
f"// {stem}:{line}",
92+
file=args.output,
93+
)
94+
startSeg = False
95+
96+
executed = segment[2] != 0
97+
if not executed:
98+
txt = lines[line].strip("\n").removeprefix(" }")
99+
if (
100+
line not in covered
101+
and len(lines[line].strip(" \n").removeprefix("}")) > 0
102+
):
103+
print(
104+
f"{txt}",
105+
file=args.output,
106+
)
107+
108+
covered.add(line)
109+
lastLine = line
110+
print(f"```", file=args.output)
111+
112+
if passed:
113+
print("### All files passed", file=args.output)
114+
sys.exit(0 if passed else 1)

0 commit comments

Comments
 (0)