22import copy
33import json
44import os
5+ import re
56import shutil
67import sys
8+ from enum import Enum
9+ from pathlib import Path
710
811import click
912
1013from .util import get_commands , get_config
1114from .util import run as _run
1215
1316install_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 )
150198def 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 ()
0 commit comments