Skip to content

Commit 4239d7e

Browse files
ci: add code coverage
1 parent c3f0216 commit 4239d7e

6 files changed

Lines changed: 117 additions & 35 deletions

File tree

.github/workflows/pr.yml

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,9 @@ jobs:
4343

4444
steps:
4545
- uses: actions/checkout@v3
46+
with:
47+
# diff-cover needs more history to compute coverage diffs
48+
fetch-depth: 10
4649

4750
- name: Set up Python ${{ env.PYTHON_VERSION }}
4851
uses: actions/setup-python@v5
@@ -63,13 +66,21 @@ jobs:
6366
key: "test-datasets"
6467

6568
- name: PyTest
66-
run: invoke test.pytest
69+
run: invoke test.pytest --coverage
6770

6871
- name: Doctest
69-
run: invoke test.doctest
72+
run: invoke test.doctest --coverage
7073

7174
- name: Check Notebooks
72-
run: invoke test.nb
75+
run: invoke test.nb --coverage
76+
77+
- name: Combine Test Coverages
78+
run: invoke test.cov-combine
79+
80+
- name: Coveralls
81+
uses: coverallsapp/github-action@v2
82+
with:
83+
github-token: ${{ secrets.GITHUB_TOKEN }}
7384

7485
lint:
7586
name: "Code Style"

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,8 @@ htmlcov/
4545
.nox/
4646
.coverage
4747
.coverage.*
48+
.coverage_*
49+
diff-cover.md
4850
.cache
4951
nosetests.xml
5052
coverage.xml

docs/contributing/tests.md

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,3 +84,27 @@ NB_FAST=true pytest --nbmake notebooks/my_notebook.ipynb
8484
```
8585

8686
For more about `NB_FAST` read the [notebooks documentation](../docs.rst#notebooks).
87+
88+
## Code Coverage
89+
90+
Code coverage measures how many statements of code is executed while running
91+
tests. It identifies unused and untested code. We encourage contributors to
92+
use it to write more robust programs, but don't have a target percantage.
93+
94+
To generate code coverage reports add `--cov=capymoa` and `--cov-report=html` to
95+
the pytest command:
96+
97+
```bash
98+
pytest --cov=capymoa --cov-report=html
99+
```
100+
101+
Alternatively, CapyMOA's invoke testing tasks can generate coverage reports with:
102+
103+
```bash
104+
invoke test --coverage
105+
```
106+
107+
See also:
108+
* [coverage.py](https://github.com/coveragepy/coveragepy): Tool for measuring python code coverage.
109+
* [pytest-cov](https://pypi.org/project/pytest-cov/): PyTest plugin to automatically collect code coverage information with coverage.py.
110+
* [diff-cover](https://github.com/Bachmann1234/diff-cover): Program to generate a coverage report for only lines changed between branches.

pyproject.toml

Lines changed: 7 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ dev=[
6767
"invoke~=2.2",
6868
"jupyter~=1.1",
6969
"nbmake~=1.5",
70+
"pytest-cov~=7.0",
7071
"pytest-subtests~=0.15",
7172
"pytest-xdist~=3.8",
7273
"pytest~=9.0",
@@ -76,19 +77,12 @@ dev=[
7677
]
7778

7879
doc=[
79-
# Documentation generator
80-
"sphinx==8.1.3",
81-
# Theme for the documentation
82-
"pydata-sphinx-theme",
83-
# Allows to include Jupyter notebooks in the documentation
84-
"sphinx-autobuild",
85-
# Allows to include Jupyter notebooks in the documentation
86-
"nbsphinx",
87-
# Parses markdown files
88-
"myst-parser",
89-
# Adds design elements to the documentation
90-
"sphinx_design",
91-
"sphinxcontrib-programoutput"
80+
"myst-parser", # Parses markdown files
81+
"nbsphinx", # Sphinx extension to include Jupyter notebooks
82+
"pydata-sphinx-theme", # Theme for the documentation
83+
"sphinx_design", # Sphinx extension for design elements
84+
"sphinx==8.1.3", # Documentation generator
85+
"sphinxcontrib-programoutput", # Output of commands in the documentation
9286
]
9387

9488
[project.urls]

src/capymoa/misc.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,3 +116,7 @@ def save_stream_arff(file: TextIO | Path | str, stream: Stream) -> None:
116116
file.write(str(moa_header))
117117
for instance in tqdm.tqdm(stream, desc="Saving to ARFF"):
118118
file.write(str(instance.java_instance.toString()) + "\n")
119+
120+
121+
def _TODO_REMOVE_ME_I_AM_A_TEST():
122+
print("This is a test function that should be removed.")

tasks.py

Lines changed: 66 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,12 @@
1717
from os import environ
1818

1919
IS_CI = environ.get("CI", "false").lower() == "true"
20+
COVERAGE_DEFAULT = False
21+
22+
23+
def divider(text: str):
24+
"""Print a divider with text centered."""
25+
print(text.center(88, "-"))
2026

2127

2228
def all_exist(files: List[str] = None, directories: List[str] = None) -> bool:
@@ -189,6 +195,7 @@ def notebooks(
189195
k_pattern: Optional[str] = None,
190196
slow: bool = False,
191197
no_skip: bool = False,
198+
coverage: bool = COVERAGE_DEFAULT,
192199
):
193200
"""Run the notebooks and check for errors.
194201
@@ -199,6 +206,7 @@ def notebooks(
199206
executed output.
200207
"""
201208
assert not (not slow and overwrite), "You cannot use `--overwrite` with `--fast`."
209+
env = {"COVERAGE_FILE": ".coverage_notebooks"}
202210

203211
# Set the environment variable to run the notebooks in fast mode.
204212
if not slow:
@@ -220,59 +228,95 @@ def notebooks(
220228
"--durations=5", # Show the duration of each notebook
221229
]
222230
cmd += ["-n=auto"] if parallel else [] # Should we run in parallel?
223-
cmd += (
224-
["--overwrite"] if overwrite else []
225-
) # Overwrite the notebooks with the executed output
231+
# Overwrite the notebooks with the executed output
232+
cmd += ["--overwrite"] if overwrite else []
233+
cmd += ["--cov=capymoa", ""] if coverage else []
234+
226235
if len(skip_notebooks) > 0:
227236
cmd += ["--deselect " + nb for nb in skip_notebooks] # Skip some notebooks
228237

229238
if k_pattern:
230239
cmd += [f"-k {k_pattern}"]
231240

232-
ctx.run(" ".join(cmd), echo=True)
241+
ctx.run(" ".join(cmd), echo=True, env=env)
233242

234243

235244
@task
236-
def pytest(ctx: Context, parallel: bool = True):
245+
def pytest(ctx: Context, parallel: bool = True, coverage: bool = COVERAGE_DEFAULT):
237246
"""Run the tests using pytest."""
247+
env = {"COVERAGE_FILE": ".coverage_pytest"}
248+
238249
cmd = [
239250
"python -m pytest",
240251
"--durations=5", # Show the duration of each test
241252
"--exitfirst", # Exit instantly on first error or failed test
242-
# jpype can raise irrelevant warnings:
243-
# https://github.com/jpype-project/jpype/issues/561
244-
"-p no:faulthandler",
245253
]
254+
if coverage:
255+
cmd.extend(
256+
[
257+
"--cov=capymoa",
258+
]
259+
)
246260
cmd += ["-n=auto"] if parallel else []
247-
ctx.run(" ".join(cmd), echo=True)
261+
ctx.run(" ".join(cmd), echo=True, env=env)
248262

249263

250264
@task
251-
def doctest(ctx: Context, parallel: bool = True):
265+
def doctest(ctx: Context, parallel: bool = True, coverage: bool = COVERAGE_DEFAULT):
252266
"""Run tests defined in docstrings using pytest."""
267+
env = {"COVERAGE_FILE": ".coverage_doctest"}
253268
cmd = [
254269
"python -m pytest",
270+
"--cov=capymoa" if coverage else "",
255271
"--doctest-modules", # Enable doctest tests
256272
"--durations=5", # Show the duration of each test
257273
"--exitfirst", # Exit instantly on first error or failed test
258-
# jpype can raise irrelevant warnings:
259-
# https://github.com/jpype-project/jpype/issues/561
260-
"-p no:faulthandler",
261274
"src/capymoa", # Don't run tests in the `tests` directory
262275
]
263276
cmd += ["-n=auto"] if parallel else []
277+
ctx.run(" ".join(cmd), echo=True, env=env)
278+
279+
280+
@task(aliases=["cov-combine"])
281+
def coverage_combine(ctx: Context):
282+
"""Combine coverage data from different sources."""
283+
cmd = ["python -m coverage combine --keep"]
284+
covfiles = [
285+
".coverage_pytest",
286+
".coverage_doctest",
287+
".coverage_notebooks",
288+
]
289+
for covfile in covfiles:
290+
if Path(covfile).exists():
291+
cmd += [covfile]
264292
ctx.run(" ".join(cmd), echo=True)
265293

266294

295+
@task(aliases=["cov-report"], pre=[coverage_combine])
296+
def coverage_report(ctx: Context):
297+
"""Generate coverage report."""
298+
ctx.run("python -m coverage html", echo=True)
299+
300+
301+
@task(aliases=["cov-clean"])
302+
def coverage_clean(ctx: Context):
303+
"""Clean coverage data."""
304+
ctx.run("python -m coverage erase", echo=True)
305+
ctx.run("rm -rf htmlcov", echo=True)
306+
307+
267308
@task
268-
def all_tests(ctx: Context, parallel: bool = True):
309+
def all_tests(ctx: Context, parallel: bool = True, coverage: bool = COVERAGE_DEFAULT):
269310
"""Run all the tests."""
270-
print("Running all pytest tests ...")
271-
pytest(ctx, parallel)
272-
print("Running all doctests ...")
273-
doctest(ctx, parallel)
274-
print("Running all notebooks ...")
311+
divider("test.pytest")
312+
pytest(ctx, parallel, coverage)
313+
divider("test.doctest")
314+
doctest(ctx, parallel, coverage)
315+
divider("test.notebooks")
275316
notebooks(ctx, parallel)
317+
if coverage:
318+
divider("test.cov-report")
319+
coverage_combine(ctx)
276320

277321

278322
@task
@@ -317,6 +361,9 @@ def format(ctx: Context):
317361
test.add_task(notebooks, "nb")
318362
test.add_task(pytest, "pytest")
319363
test.add_task(doctest, "doctest")
364+
test.add_task(coverage_combine)
365+
test.add_task(coverage_clean)
366+
test.add_task(coverage_report)
320367

321368
ns = Collection()
322369
ns.add_collection(docs)

0 commit comments

Comments
 (0)