Files
temp/features/steps/coverage_threshold_enforcement_steps.py
T

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"
)