From 530dc0334a156cbf44f2d655276647709cb30a9e Mon Sep 17 00:00:00 2001 From: Henry Schreiner Date: Wed, 17 Apr 2024 03:30:31 -0400 Subject: [PATCH] feat: testing function Signed-off-by: Henry Schreiner --- docs/plugins.md | 14 ++++++++++ src/repo_review/checks.py | 23 ++++++++++++++++ src/repo_review/processor.py | 19 ++++++-------- src/repo_review/testing.py | 51 ++++++++++++++++++++++++++++++++++++ tests/test_package.py | 8 ++++++ 5 files changed, 104 insertions(+), 11 deletions(-) create mode 100644 src/repo_review/testing.py diff --git a/docs/plugins.md b/docs/plugins.md index ca5da4f..76ceff6 100644 --- a/docs/plugins.md +++ b/docs/plugins.md @@ -53,6 +53,20 @@ You have `processed.results` and `processed.families` from the return of {func}`~repo_review.processor.collect_all` to get `.fixtures`, `.checks`, and `.families`. +### Unit testing + +You can also run unit tests with the {func}`~repo_review.testing.compute_check` helper. It is used like this: + +```python +def test_has_tool_ruff_unit() -> None: + assert repo_review.testing.compute_check("RF001", ruff={}).result + assert not repo_review.testing.compute_check("RF001", ruff=None).result +``` + +It takes the check name and any fixtures as keyword arguments. It returns a +{class}`~repo_review.checks.Check` instance, so you can see if the `.result` is +`True`/`False`/`None`, or check any of the other properties. + ## An existing package Since writing a plugin does not require depending on repo-review, you can also diff --git a/src/repo_review/checks.py b/src/repo_review/checks.py index e7f0cfe..33f936f 100644 --- a/src/repo_review/checks.py +++ b/src/repo_review/checks.py @@ -120,3 +120,26 @@ def get_check_description(name: str, check: Check) -> str: .. versionadded:: 0.8 """ return (check.__doc__ or "").format(self=check, name=name) + + +def process_result_bool( + result: str | bool | None, check: Check, name: str +) -> str | None: + """ + This converts a bool into a string given a check and name. If the result is a string + or None, it is returned as is. + + :param result: The result to process. + :param check: The check instance. + :param name: The name of the check. + :return: The final string or None. + + .. versionadded:: 0.11 + """ + if isinstance(result, bool): + return ( + "" + if result + else (check.check.__doc__ or "Check failed").format(name=name, self=check) + ) + return result diff --git a/src/repo_review/processor.py b/src/repo_review/processor.py index 9670cfc..7c7de64 100644 --- a/src/repo_review/processor.py +++ b/src/repo_review/processor.py @@ -10,7 +10,13 @@ import markdown_it from ._compat.importlib.resources.abc import Traversable -from .checks import Check, collect_checks, get_check_url, is_allowed +from .checks import ( + Check, + collect_checks, + get_check_url, + is_allowed, + process_result_bool, +) from .families import Family, collect_families from .fixtures import apply_fixtures, collect_fixtures, compute_fixtures, pyproject from .ghpath import EmptyTraversable @@ -211,16 +217,7 @@ def process( for name in ts.static_order(): if all(completed.get(n, "") == "" for n in graph[name]): result = apply_fixtures({"name": name, **fixtures}, tasks[name].check) - if isinstance(result, bool): - completed[name] = ( - "" - if result - else (tasks[name].check.__doc__ or "Check failed").format( - name=name, self=tasks[name] - ) - ) - else: - completed[name] = result + completed[name] = process_result_bool(result, tasks[name], name) else: completed[name] = None diff --git a/src/repo_review/testing.py b/src/repo_review/testing.py new file mode 100644 index 0000000..de558ce --- /dev/null +++ b/src/repo_review/testing.py @@ -0,0 +1,51 @@ +""" +Helpers for testing repo-review plugins. +""" + +from __future__ import annotations + +import importlib.metadata +import textwrap +from typing import Any + +from .checks import Check, get_check_url, process_result_bool +from .fixtures import apply_fixtures +from .processor import Result + + +def compute_check(name: str, /, **fixtures: Any) -> Result: + """ + A helper function to compute a check given fixtures, intended for testing. + Currently, all fixtures are required to be passed in as keyword arguments, + transiative fixtures are not supported. + + :param name: The name of the check to compute. + :param fixtures: The fixtures to use when computing the check. + :return: The computed result. + + .. versionadded:: 0.11 + """ + + check_functions = ( + ep.load() for ep in importlib.metadata.entry_points(group="repo_review.checks") + ) + checks = { + k: v + for func in check_functions + for k, v in apply_fixtures(fixtures, func).items() + } + check: Check = checks[name] + completed_raw = apply_fixtures({"name": name, **fixtures}, check.check) + completed = process_result_bool(completed_raw, check, name) + result = None if completed is None else not completed + doc = check.__doc__ or "" + err_msg = completed or "" + + return Result( + family=check.family, + name=name, + description=doc.format(self=check, name=name).strip(), + result=result, + err_msg=textwrap.dedent(err_msg), + url=get_check_url(name, check), + ) diff --git a/tests/test_package.py b/tests/test_package.py index 487ac32..ba00580 100644 --- a/tests/test_package.py +++ b/tests/test_package.py @@ -6,6 +6,7 @@ import pytest import repo_review as m +import repo_review.testing from repo_review.processor import process DIR = Path(__file__).parent.resolve() @@ -43,3 +44,10 @@ def test_broken_validate_pyproject(tmp_path: Path) -> None: (result,) = (r for r in results.results if r.name == "VPP001") assert "must match pattern" in result.err_msg assert not result.result + + +def test_testing_function(): + pytest.importorskip("sp_repo_review") + + assert repo_review.testing.compute_check("RF001", ruff={}).result + assert not repo_review.testing.compute_check("RF001", ruff=None).result