forked from HAL9000/cleveragents-core
219 lines
7.8 KiB
Python
219 lines
7.8 KiB
Python
"""Step definitions for coverage threshold enforcement feature."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import ast
|
|
import re
|
|
import tomllib
|
|
from pathlib import Path
|
|
from typing import Any
|
|
|
|
import yaml
|
|
from behave import given, then, when
|
|
|
|
PROJECT_ROOT = Path(__file__).resolve().parent.parent.parent
|
|
|
|
|
|
@given("the noxfile.py exists")
|
|
def step_noxfile_exists(context: Any) -> None:
|
|
noxfile = PROJECT_ROOT / "noxfile.py"
|
|
if not noxfile.is_file():
|
|
raise FileNotFoundError(f"noxfile.py not found at {noxfile}")
|
|
context.noxfile_path = noxfile
|
|
context.noxfile_content = noxfile.read_text(encoding="utf-8")
|
|
|
|
|
|
@given("the pyproject.toml exists")
|
|
def step_pyproject_exists(context: Any) -> None:
|
|
pyproject = PROJECT_ROOT / "pyproject.toml"
|
|
if not pyproject.is_file():
|
|
raise FileNotFoundError(f"pyproject.toml not found at {pyproject}")
|
|
context.pyproject_path = pyproject
|
|
context.pyproject_content = pyproject.read_text(encoding="utf-8")
|
|
|
|
|
|
@given("the CI workflow file exists")
|
|
def step_ci_workflow_exists(context: Any) -> None:
|
|
ci_file = PROJECT_ROOT / ".forgejo" / "workflows" / "ci.yml"
|
|
if not ci_file.is_file():
|
|
raise FileNotFoundError(f"CI workflow not found at {ci_file}")
|
|
context.ci_file_path = ci_file
|
|
context.ci_file_content = ci_file.read_text(encoding="utf-8")
|
|
|
|
|
|
@when("I parse the COVERAGE_THRESHOLD constant from noxfile.py")
|
|
def step_parse_threshold(context: Any) -> None:
|
|
content = context.noxfile_content
|
|
tree = ast.parse(content)
|
|
threshold = None
|
|
for node in ast.walk(tree):
|
|
if isinstance(node, ast.Assign):
|
|
for target in node.targets:
|
|
if (
|
|
isinstance(target, ast.Name)
|
|
and target.id == "COVERAGE_THRESHOLD"
|
|
and isinstance(node.value, ast.Constant)
|
|
):
|
|
threshold = node.value.value
|
|
if threshold is None:
|
|
raise ValueError("COVERAGE_THRESHOLD constant not found in noxfile.py")
|
|
context.coverage_threshold = threshold
|
|
|
|
|
|
@then("the coverage threshold should be {expected:d}")
|
|
def step_check_threshold(context: Any, expected: int) -> None:
|
|
actual = context.coverage_threshold
|
|
if actual != expected:
|
|
raise AssertionError(f"Expected coverage threshold {expected}, got {actual}")
|
|
|
|
|
|
@when("I read the coverage_report session source from noxfile.py")
|
|
def step_read_coverage_session(context: Any) -> None:
|
|
content = context.noxfile_content
|
|
# Extract the coverage_report function body using regex
|
|
# Find the function definition and capture everything until the next
|
|
# top-level definition or end of file
|
|
pattern = r"(def coverage_report\(.*?\n(?:(?: .*\n|[ \t]*\n)*))"
|
|
match = re.search(pattern, content)
|
|
if match is None:
|
|
raise ValueError("coverage_report function not found in noxfile.py")
|
|
context.coverage_session_source = match.group(1)
|
|
|
|
|
|
@then("the session should contain a fail-under argument matching the threshold")
|
|
def step_check_fail_under(context: Any) -> None:
|
|
source = context.coverage_session_source
|
|
# The session should reference COVERAGE_THRESHOLD in a fail-under argument
|
|
if "fail-under" not in source:
|
|
raise AssertionError(
|
|
"coverage_report session does not contain --fail-under argument"
|
|
)
|
|
if "COVERAGE_THRESHOLD" not in source:
|
|
raise AssertionError(
|
|
"coverage_report session does not reference COVERAGE_THRESHOLD constant"
|
|
)
|
|
|
|
|
|
@when("I parse the coverage run configuration from pyproject.toml")
|
|
def step_parse_coverage_run(context: Any) -> None:
|
|
content = context.pyproject_content
|
|
data = tomllib.loads(content)
|
|
coverage_run = data.get("tool", {}).get("coverage", {}).get("run", {})
|
|
context.coverage_run_config = coverage_run
|
|
|
|
|
|
@then("branch coverage should be enabled")
|
|
def step_check_branch_coverage(context: Any) -> None:
|
|
config = context.coverage_run_config
|
|
if not config.get("branch", False):
|
|
raise AssertionError("Branch coverage is not enabled in coverage.run config")
|
|
|
|
|
|
@then('the source list should include "{source}"')
|
|
def step_check_source_list(context: Any, source: str) -> None:
|
|
config = context.coverage_run_config
|
|
sources = config.get("source", [])
|
|
if source not in sources:
|
|
raise AssertionError(f"'{source}' not in coverage.run source list: {sources}")
|
|
|
|
|
|
@then("the data file should be under the build directory")
|
|
def step_check_data_file(context: Any) -> None:
|
|
config = context.coverage_run_config
|
|
data_file = config.get("data_file", "")
|
|
if not data_file.startswith("build/"):
|
|
raise AssertionError(f"Coverage data file '{data_file}' is not under build/")
|
|
|
|
|
|
@when("I parse the coverage html configuration from pyproject.toml")
|
|
def step_parse_coverage_html(context: Any) -> None:
|
|
content = context.pyproject_content
|
|
data = tomllib.loads(content)
|
|
coverage_html = data.get("tool", {}).get("coverage", {}).get("html", {})
|
|
context.coverage_html_config = coverage_html
|
|
|
|
|
|
@then("the html directory should be under the build directory")
|
|
def step_check_html_dir(context: Any) -> None:
|
|
config = context.coverage_html_config
|
|
directory = config.get("directory", "")
|
|
if not directory.startswith("build/"):
|
|
raise AssertionError(
|
|
f"Coverage HTML directory '{directory}' is not under build/"
|
|
)
|
|
|
|
|
|
@when("I parse the coverage xml configuration from pyproject.toml")
|
|
def step_parse_coverage_xml(context: Any) -> None:
|
|
content = context.pyproject_content
|
|
data = tomllib.loads(content)
|
|
coverage_xml = data.get("tool", {}).get("coverage", {}).get("xml", {})
|
|
context.coverage_xml_config = coverage_xml
|
|
|
|
|
|
@then("the xml output should be under the build directory")
|
|
def step_check_xml_output(context: Any) -> None:
|
|
config = context.coverage_xml_config
|
|
output = config.get("output", "")
|
|
if not output.startswith("build/"):
|
|
raise AssertionError(f"Coverage XML output '{output}' is not under build/")
|
|
|
|
|
|
@when("I parse the coverage job from the CI workflow")
|
|
def step_parse_ci_coverage(context: Any) -> None:
|
|
content = context.ci_file_content
|
|
data = yaml.safe_load(content)
|
|
jobs = data.get("jobs", {})
|
|
coverage_job = jobs.get("coverage", {})
|
|
if not coverage_job:
|
|
raise ValueError("No 'coverage' job found in CI workflow")
|
|
context.ci_coverage_job = coverage_job
|
|
|
|
|
|
@then("the coverage job should run nox -s coverage_report")
|
|
def step_check_ci_coverage_nox(context: Any) -> None:
|
|
job = context.ci_coverage_job
|
|
steps = job.get("steps", [])
|
|
found = False
|
|
for step in steps:
|
|
run_cmd = step.get("run", "")
|
|
if "nox -s coverage_report" in run_cmd:
|
|
found = True
|
|
break
|
|
if not found:
|
|
raise AssertionError(
|
|
"CI coverage job does not contain 'nox -s coverage_report'"
|
|
)
|
|
|
|
|
|
@then("the coverage job should depend on lint and typecheck")
|
|
def step_check_ci_coverage_deps(context: Any) -> None:
|
|
job = context.ci_coverage_job
|
|
needs = job.get("needs", [])
|
|
if "lint" not in needs:
|
|
raise AssertionError(
|
|
f"CI coverage job does not depend on 'lint': needs={needs}"
|
|
)
|
|
if "typecheck" not in needs:
|
|
raise AssertionError(
|
|
f"CI coverage job does not depend on 'typecheck': needs={needs}"
|
|
)
|
|
|
|
|
|
@then("the session should emit a COVERAGE OK summary line")
|
|
def step_check_ok_summary(context: Any) -> None:
|
|
source = context.coverage_session_source
|
|
if "COVERAGE OK:" not in source:
|
|
raise AssertionError(
|
|
"coverage_report session does not emit 'COVERAGE OK:' summary"
|
|
)
|
|
|
|
|
|
@then("the session should emit a COVERAGE FAILED summary line")
|
|
def step_check_failed_summary(context: Any) -> None:
|
|
source = context.coverage_session_source
|
|
if "COVERAGE FAILED:" not in source:
|
|
raise AssertionError(
|
|
"coverage_report session does not emit 'COVERAGE FAILED:' summary"
|
|
)
|