from __future__ import annotations
import typing as typ
from coverage_shape_rules import UPLOADER_ACTION, steps_using
from workflow_reader import Workflow, pull_request_closure
ENVIRONMENT: typ.Final = "codescene"
MISSING: typ.Final = f"the uploading job must declare `environment: {ENVIRONMENT}`"
STRAY: typ.Final = f"declares `{ENVIRONMENT}` but uploads nothing"
REACHABLE: typ.Final = f"is reachable from a pull request and declares `{ENVIRONMENT}`"
NO_UPLOADER: typ.Final = "no workflow job calls the CodeScene uploader"
def environment_name(job: dict[str, typ.Any]) -> str | None:
match job.get("environment"):
case str() as name:
return name
case {"name": str() as name}:
return name
case _:
return None
def environment_faults(workflows: list[Workflow]) -> list[str]:
holders = {
(flow.path, job) for flow, job, _ in steps_using(workflows, UPLOADER_ACTION)
}
if not holders:
return [NO_UPLOADER]
return (
_missing(workflows, holders)
+ _stray(workflows, holders)
+ _reachable(pull_request_closure(workflows))
)
def _declaring(workflows: list[Workflow]) -> list[tuple[str, str]]:
return [
(flow.path, name)
for flow in workflows
for name, job in flow.jobs()
if environment_name(job) == ENVIRONMENT
]
def _missing(workflows: list[Workflow], holders: set[tuple[str, str]]) -> list[str]:
declared = set(_declaring(workflows))
return [f"{path}:{job} {MISSING}" for path, job in sorted(holders - declared)]
def _stray(workflows: list[Workflow], holders: set[tuple[str, str]]) -> list[str]:
return [
f"{path}:{job} {STRAY}"
for path, job in _declaring(workflows)
if (path, job) not in holders
]
def _reachable(closure: list[Workflow]) -> list[str]:
return [f"{path}:{job} {REACHABLE}" for path, job in _declaring(closure)]