diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 525c069..362046c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -21,3 +21,25 @@ jobs: - name: Test run: ctest --test-dir build --output-on-failure + + project-validator: + runs-on: ubuntu-24.04 + steps: + - uses: actions/checkout@v4 + + - uses: actions/checkout@v4 + with: + repository: cpp-best-practices/project_validator + ref: main + path: project_validator + + - uses: actions/setup-python@v5 + with: + python-version: "3.14" + + - name: Install validator dependencies + run: python -m pip install "PyYAML>=6.0" + + - name: Validate project + working-directory: project_validator + run: python -m project_validator --build --exclude project_validator ${{ github.workspace }} diff --git a/tests/test_ci_workflow.py b/tests/test_ci_workflow.py new file mode 100644 index 0000000..d45c380 --- /dev/null +++ b/tests/test_ci_workflow.py @@ -0,0 +1,57 @@ +#!/usr/bin/env python3 +"""Unit tests for .github/workflows/ci.yml.""" + +from __future__ import annotations + +import re +import unittest +from pathlib import Path + +WORKFLOW = Path(__file__).resolve().parent.parent / ".github" / "workflows" / "ci.yml" +VALIDATOR_JOB = "project-validator" +JOB_HEADER = re.compile(r"^ (?P[A-Za-z0-9_-]+):\s*$") + + +def workflow_jobs(text: str) -> dict[str, str]: + """Each job under `jobs:`, keyed by name, with its indented body.""" + jobs: dict[str, list[str]] = {} + current: list[str] | None = None + in_jobs = False + for line in text.splitlines(): + if not line.startswith(" ") and line.strip(): + in_jobs = line.rstrip() == "jobs:" + current = None + continue + header = JOB_HEADER.match(line) if in_jobs else None + if header is not None: + current = jobs.setdefault(header.group("name"), []) + elif current is not None: + current.append(line) + return {name: "\n".join(body) for name, body in jobs.items()} + + +class ProjectValidatorJobTest(unittest.TestCase): + def setUp(self) -> None: + self.jobs = workflow_jobs(WORKFLOW.read_text(encoding="utf-8")) + + def validator_job(self) -> str: + self.assertIn(VALIDATOR_JOB, self.jobs, f"{WORKFLOW} has no {VALIDATOR_JOB} job") + return self.jobs[VALIDATOR_JOB] + + def test_checks_out_the_validator_main_branch(self) -> None: + job = self.validator_job() + self.assertRegex(job, re.compile(r"^\s+repository:\s*cpp-best-practices/project_validator\s*$", re.M)) + self.assertRegex(job, re.compile(r"^\s+ref:\s*main\s*$", re.M)) + + def test_runs_the_validator_on_this_project(self) -> None: + self.assertRegex(self.validator_job(), r"-m project_validator\b.*\$\{\{\s*github\.workspace\s*\}\}") + + +class WorkflowJobsTest(unittest.TestCase): + def test_splits_jobs_by_name(self) -> None: + text = "name: ci\njobs:\n first:\n runs-on: a\n second:\n runs-on: b\nother:\n third:\n" + self.assertEqual(workflow_jobs(text), {"first": " runs-on: a", "second": " runs-on: b"}) + + +if __name__ == "__main__": + unittest.main()