Skip to content

Commit 00b9c31

Browse files
ganesh-k13jarrodmillmanstefanv
authored
Add --gcov-report flag to spin test (#159)
* ENH: Added `--generate-gcov-report` flag to `spin test` * TST: Added testcases for html coverage reports * MAINT: Rename flag to `gcov-report` * MAINT: Fix doc string for `gcov-report` Co-authored-by: Jarrod Millman <[email protected]> * TST: Added UT for `gcov-report` * ENH, TST: Added checks for missing debug files * Simplify coverage logic, and build on demand - Like the existing coverage option, `spin test --gcov` is now enough to generate gcov reports. - The format of the report is set with `--gcov-format`. - Coverage builds are triggered when `--gcov` is added (necessary to generate the coverage report), but no rebuild is done when the flag is removed (too expensive). * Fix test command in README * Fix tests, now that coverage build is triggered automatically --------- Co-authored-by: Jarrod Millman <[email protected]> Co-authored-by: Stefan van der Walt <[email protected]>
1 parent 49ccdd0 commit 00b9c31

File tree

5 files changed

+151
-27
lines changed

5 files changed

+151
-27
lines changed

.github/workflows/test.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ jobs:
2929
if: matrix.os == 'ubuntu-latest'
3030
run: |
3131
sudo apt-get update
32-
sudo apt-get install -y gdb
32+
sudo apt-get install -y gdb lcov
3333
- name: Tests PyTest
3434
run: |
3535
pipx run nox --forcecolor -s test

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -247,7 +247,7 @@ fc-cache -f -v
247247
`spin` tests are invoked using:
248248

249249
```
250-
nox -s tests
250+
nox -s test
251251
```
252252

253253
## History

noxfile.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,5 +3,5 @@
33

44
@nox.session
55
def test(session: nox.Session) -> None:
6-
session.install(".", "pytest", "build", "meson-python", "ninja")
6+
session.install(".", "pytest", "build", "meson-python", "ninja", "gcovr")
77
session.run("pytest", "spin", *session.posargs)

spin/cmds/meson.py

Lines changed: 120 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,26 @@
22
import copy
33
import json
44
import os
5+
import re
56
import shutil
67
import sys
8+
from enum import Enum
9+
from pathlib import Path
710

811
import click
912

1013
from .util import get_commands, get_config
1114
from .util import run as _run
1215

1316
install_dir = "build-install"
17+
build_dir = "build"
18+
19+
20+
class GcovReportFormat(str, Enum):
21+
html = "html"
22+
xml = "xml"
23+
text = "text"
24+
sonarqube = "sonarqube"
1425

1526

1627
# Allow specification of meson binary in configuration
@@ -124,6 +135,54 @@ def _meson_version_configured():
124135
pass
125136

126137

138+
def _meson_coverage_configured() -> bool:
139+
try:
140+
build_options_fn = os.path.join(
141+
"build", "meson-info", "intro-buildoptions.json"
142+
)
143+
with open(build_options_fn) as f:
144+
build_options = json.load(f)
145+
for b in build_options:
146+
if (b["name"] == "b_coverage") and (b["value"] is True):
147+
return True
148+
except:
149+
pass
150+
151+
return False
152+
153+
154+
def _check_coverage_tool_installation(coverage_type: GcovReportFormat):
155+
requirements = { # https://github.com/mesonbuild/meson/blob/6e381714c7cb15877e2bcaa304b93c212252ada3/docs/markdown/Unit-tests.md?plain=1#L49-L62
156+
GcovReportFormat.html: ["Gcovr/GenHTML", "lcov"],
157+
GcovReportFormat.xml: ["Gcovr (version 3.3 or higher)"],
158+
GcovReportFormat.text: ["Gcovr (version 3.3 or higher)"],
159+
GcovReportFormat.sonarqube: ["Gcovr (version 4.2 or higher)"],
160+
}
161+
162+
# First check the presence of a valid build
163+
if not (os.path.exists(build_dir)):
164+
raise click.ClickException(
165+
"`build` folder not found, cannot generate coverage reports. "
166+
"Generate coverage artefacts by running `spin test --gcov`"
167+
)
168+
169+
debug_files = Path(build_dir).rglob("*.gcno")
170+
if len(list(debug_files)) == 0:
171+
raise click.ClickException(
172+
"Debug build not found, cannot generate coverage reports.\n\n"
173+
"Please rebuild using `spin build --clean --gcov` first."
174+
)
175+
176+
# Verify the tools are installed prior to the build
177+
p = _run(["ninja", "-C", build_dir, "-t", "targets", "all"], output=False)
178+
if f"coverage-{coverage_type.value}" not in p.stdout.decode("ascii"):
179+
raise click.ClickException(
180+
f"coverage-{coverage_type.value} is not supported... "
181+
f"Ensure the following are installed: {', '.join(requirements[coverage_type])} "
182+
"and rerun `spin test --gcov`"
183+
)
184+
185+
127186
@click.command()
128187
@click.option("-j", "--jobs", help="Number of parallel tasks to launch", type=int)
129188
@click.option("--clean", is_flag=True, help="Clean build directory before build")
@@ -133,18 +192,7 @@ def _meson_version_configured():
133192
@click.option(
134193
"--gcov",
135194
is_flag=True,
136-
help="""Enable C code coverage via `gcov`.
137-
138-
The meson-generated `build/build.ninja` has targets for compiling
139-
coverage reports.
140-
141-
E.g., to build an HTML report, in the `build` directory run
142-
`ninja coverage-html`.
143-
144-
To see a list all supported formats, run
145-
`ninja -t targets | grep coverage-`.
146-
147-
Also see https://mesonbuild.com/howtox.html#producing-a-coverage-report.""",
195+
help="Enable C code coverage using `gcov`. Use `spin test --gcov` to generate reports.",
148196
)
149197
@click.argument("meson_args", nargs=-1)
150198
def build(meson_args, jobs=None, clean=False, verbose=False, gcov=False, quiet=False):
@@ -165,7 +213,6 @@ def build(meson_args, jobs=None, clean=False, verbose=False, gcov=False, quiet=F
165213
166214
CFLAGS="-O0 -g" spin build
167215
"""
168-
build_dir = "build"
169216
meson_args = list(meson_args)
170217

171218
if gcov:
@@ -191,7 +238,9 @@ def build(meson_args, jobs=None, clean=False, verbose=False, gcov=False, quiet=F
191238
# Build dir has been configured; check if it was configured by
192239
# current version of Meson
193240

194-
if _meson_version() != _meson_version_configured():
241+
if (_meson_version() != _meson_version_configured()) or (
242+
gcov and not _meson_coverage_configured()
243+
):
195244
_run(setup_cmd + ["--reconfigure"], output=not quiet)
196245

197246
# Any other conditions that warrant a reconfigure?
@@ -255,17 +304,30 @@ def _get_configured_command(command_name):
255304
"-c",
256305
"--coverage",
257306
is_flag=True,
258-
help="Generate a coverage report of executed tests. An HTML copy of the report is written to `build/coverage`.",
307+
help="Generate a Python coverage report of executed tests. An HTML copy of the report is written to `build/coverage`.",
259308
)
260309
@click.option(
261310
"--gcov",
262311
is_flag=True,
263-
help="Enable C code coverage via `gcov`. `gcov` output goes to `build/**/*.gc*`. "
264-
"Reports can be generated using `ninja coverage*` commands. "
265-
"See https://mesonbuild.com/howtox.html#producing-a-coverage-report",
312+
help="Generate a C coverage report in `build/meson-logs/coveragereport`.",
313+
)
314+
@click.option(
315+
"--gcov-format",
316+
type=click.Choice(GcovReportFormat),
317+
default="html",
318+
help=f"Format of the gcov report. Can be one of {', '.join(e.value for e in GcovReportFormat)}.",
266319
)
267320
@click.pass_context
268-
def test(ctx, pytest_args, n_jobs, tests, verbose, coverage=False, gcov=False):
321+
def test(
322+
ctx,
323+
pytest_args,
324+
n_jobs,
325+
tests,
326+
verbose,
327+
coverage=False,
328+
gcov=None,
329+
gcov_format=None,
330+
):
269331
"""🔧 Run tests
270332
271333
PYTEST_ARGS are passed through directly to pytest, e.g.:
@@ -315,7 +377,7 @@ def test(ctx, pytest_args, n_jobs, tests, verbose, coverage=False, gcov=False):
315377
click.secho(
316378
"Invoking `build` prior to running tests:", bold=True, fg="bright_green"
317379
)
318-
ctx.invoke(build_cmd, gcov=gcov)
380+
ctx.invoke(build_cmd, gcov=bool(gcov))
319381

320382
package = cfg.get("tool.spin.package", None)
321383
if (not pytest_args) and (not tests):
@@ -372,10 +434,44 @@ def test(ctx, pytest_args, n_jobs, tests, verbose, coverage=False, gcov=False):
372434
cmd = [sys.executable, "-P", "-m", "pytest"]
373435
else:
374436
cmd = ["pytest"]
375-
_run(
376-
cmd + list(pytest_args),
377-
replace=True,
378-
)
437+
p = _run(cmd + list(pytest_args))
438+
439+
if gcov:
440+
# Verify the tools are present
441+
click.secho(
442+
"Verifying gcov dependencies...",
443+
bold=True,
444+
fg="bright_yellow",
445+
)
446+
_check_coverage_tool_installation(gcov_format)
447+
448+
# Generate report
449+
click.secho(
450+
f"Generating {gcov_format.value} coverage report...",
451+
bold=True,
452+
fg="bright_yellow",
453+
)
454+
p = _run(
455+
[
456+
"ninja",
457+
"-C",
458+
build_dir,
459+
f"coverage-{gcov_format.value.lower()}",
460+
],
461+
output=False,
462+
)
463+
coverage_folder = click.style(
464+
re.search(r"file://(.*)", p.stdout.decode("utf-8")).group(1),
465+
bold=True,
466+
fg="bright_yellow",
467+
)
468+
click.secho(
469+
f"Coverage report generated successfully and written to {coverage_folder}",
470+
bold=True,
471+
fg="bright_green",
472+
)
473+
474+
raise SystemExit(p.returncode)
379475

380476

381477
@click.command()

spin/tests/test_build_cmds.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import tempfile
55
from pathlib import Path
66

7+
import pytest
78
from testutil import skip_on_windows, skip_unless_linux, spin, stdout
89

910
from spin.cmds.util import run
@@ -27,6 +28,33 @@ def test_debug_builds():
2728
assert len(list(debug_files)) != 0, "debug files not generated for gcov build"
2829

2930

31+
def test_coverage_builds():
32+
"""Does gcov test generate coverage files?"""
33+
spin("test", "--gcov")
34+
35+
coverage_files = Path(".").rglob("*.gcda")
36+
assert len(list(coverage_files)) != 0, "coverage files not generated for gcov build"
37+
38+
39+
@pytest.mark.parametrize(
40+
"report_type,output_file",
41+
[
42+
("html", Path("coveragereport/index.html")),
43+
("xml", Path("coverage.xml")),
44+
("text", Path("coverage.txt")),
45+
("sonarqube", Path("sonarqube.xml")),
46+
],
47+
)
48+
def test_coverage_reports(report_type, output_file):
49+
"""Does gcov test generate coverage reports?"""
50+
spin("test", "--gcov", f"--gcov-format={report_type}")
51+
52+
coverage_report = Path("./build/meson-logs", output_file)
53+
assert (
54+
coverage_report.exists()
55+
), f"coverage report not generated for gcov build ({report_type})"
56+
57+
3058
def test_expand_pythonpath():
3159
"""Does an $ENV_VAR get expanded in `spin run`?"""
3260
output = spin("run", "echo $PYTHONPATH")

0 commit comments

Comments
 (0)