Skip to content
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions example_pkg/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ package = 'example_pkg'

"Build" = [
"spin.cmds.meson.build",
"spin.cmds.meson.coverage",
"spin.cmds.meson.test",
"spin.cmds.build.sdist",
"spin.cmds.build.wheel",
Expand Down
4 changes: 3 additions & 1 deletion noxfile.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,7 @@

@nox.session
def test(session: nox.Session) -> None:
session.install(".", "pytest", "build", "meson-python", "ninja", "gcovr")
session.install(
".", "pytest", "build", "meson-python", "ninja", "gcovr", "pytest-cov"
)
session.run("pytest", "spin", *session.posargs)
120 changes: 120 additions & 0 deletions spin/cmds/meson.py
Original file line number Diff line number Diff line change
Expand Up @@ -680,6 +680,126 @@ def test(
raise SystemExit(pytest_p.returncode)


def _resolve_cov_report(report: str, base: Path) -> str:
"""Resolve a --cov-report value, rebasing relative paths under `base`."""
if ":" not in report:
return report

fmt, dest = report.split(":", 1)
dest_path = Path(dest)
if not dest_path.is_absolute():
dest_path = base / dest_path
if dest_path.exists():
click.secho(f"Removing `{dest_path}`", fg="bright_yellow")
if dest_path.is_dir():
shutil.rmtree(dest_path)
else:
dest_path.unlink()
dest_path.parent.mkdir(parents=True, exist_ok=True)
return f"{fmt}:{dest_path}"


@click.command()
@click.argument("pytest_args", nargs=-1)
@click.option(
"-j",
"n_jobs",
metavar="N_JOBS",
default="1",
help="Number of parallel jobs for testing with pytest-xdist.",
)
@click.option(
"--tests",
"-t",
metavar="TESTS",
help="Which tests to run. Can be a module, function, class, or method.",
)
@click.option("--verbose", "-v", is_flag=True, default=False)
@click.option(
"--cov-report",
"cov_report",
multiple=True,
metavar="TYPE",
help=(
"Coverage report type passed to pytest-cov (e.g. term, term-missing, "
"html:dir, xml:file.xml, json:file.json, lcov:file.lcov, annotate:dir). "
"Can be specified multiple times. Defaults to `term`."
),
)
@build_option
@build_dir_option
@click.pass_context
def coverage(
ctx,
*,
pytest_args,
n_jobs,
tests,
verbose,
cov_report,
build=None,
build_dir=None,
):
"""📊 Run tests with Python code coverage

Generate coverage reports using pytest-cov. By default, a terminal
report is printed. Supports any report type that pytest-cov supports.

For file-based reports, use the `type:path` format. Relative paths
are placed under `build/coverage/`.

To generate an HTML report:

spin coverage --cov-report html:htmlcov

Multiple report types can be specified:

spin coverage --cov-report term-missing --cov-report xml:coverage.xml

Run coverage on specific tests:

\b
spin coverage -t example_pkg.echo
spin coverage example_pkg/tests

Pass additional pytest arguments after `--`:

spin coverage -- --durations=10 -k "test_foo"

Run tests in parallel (requires pytest-xdist):

spin coverage -j auto
"""
cfg = get_config()
package = cfg.get("tool.spin.package", None)
if package is None:
click.secho(
"Please specify `package = packagename` under `tool.spin` section of `pyproject.toml`",
fg="bright_red",
)
raise SystemExit(1)

# Build --cov-report flags, resolving relative paths under build/coverage/
coverage_base = Path.cwd() / "build" / "coverage"
cov_args = [f"--cov={package}"]
cov_reports = cov_report or ("term",)
for report in cov_reports:
cov_args.append(f"--cov-report={_resolve_cov_report(report, coverage_base)}")

# Prepend cov args so user's `--` args come after
pytest_args = tuple(cov_args) + (pytest_args or ())

ctx.invoke(
test,
pytest_args=pytest_args,
n_jobs=n_jobs,
tests=tests,
verbose=verbose,
build=build,
build_dir=build_dir,
)


@click.command()
@click.option(
"--code", "-c", metavar="CODE", help="Python program passed in as a string"
Expand Down
15 changes: 15 additions & 0 deletions spin/tests/test_build_cmds.py
Original file line number Diff line number Diff line change
Expand Up @@ -201,3 +201,18 @@ def test_parallel_builds(example_pkg):
assert "build-install" in example_pkg_path
assert "parallel/build-install" in example_pkg_parallel_path
assert "parallel/build-install" not in example_pkg_path


def test_coverage_default(example_pkg):
"""Does `spin coverage` run and produce terminal coverage output?"""
p = spin("coverage", sys_exit=False)
assert p.returncode == 0
assert "coverage" in stdout(p).lower() or "TOTAL" in stdout(p)


def test_coverage_with_cov_report(example_pkg):
"""Does `spin coverage --cov-report` generate a file-based report?"""
p = spin("coverage", "--cov-report", "json:coverage.json", sys_exit=False)
assert p.returncode == 0
report = Path("build/coverage/coverage.json")
assert report.exists(), f"coverage report not generated at {report}"
Loading