mirror of
https://github.com/kevinveenbirkenbach/homepage.veen.world.git
synced 2026-04-07 05:12:19 +00:00
- Replace requirements.txt with pyproject.toml for modern Python packaging - Add unit, integration, lint and security test suites under tests/ - Add utils/export_runtime_requirements.py and utils/check_hadolint_sarif.py - Split monolithic CI into reusable lint.yml, security.yml and tests.yml - Refactor ci.yml to orchestrate reusable workflows; publish on semver tag only - Modernize Dockerfile: pin python:3.12-slim, install via pyproject.toml - Expand Makefile with lint, security, test and CI targets - Add test-e2e via act with portfolio container stop/start around run - Fix navbar_logo_visibility.spec.js: win.fullscreen() → win.enterFullscreen() - Set use_reloader=False in app.run() to prevent double-start in CI - Add app/core.* and build artifacts to .gitignore - Fix apt-get → sudo apt-get in tests.yml e2e job - Fix pip install --ignore-installed to handle stale act cache Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
91 lines
3.1 KiB
Python
91 lines
3.1 KiB
Python
#!/usr/bin/env python3
|
|
import ast
|
|
import unittest
|
|
from pathlib import Path
|
|
|
|
|
|
class TestTestFilesContainUnittestTests(unittest.TestCase):
|
|
def setUp(self) -> None:
|
|
self.repo_root = Path(__file__).resolve().parents[2]
|
|
self.tests_dir = self.repo_root / "tests"
|
|
self.assertTrue(
|
|
self.tests_dir.is_dir(),
|
|
f"'tests' directory not found at: {self.tests_dir}",
|
|
)
|
|
|
|
def _iter_test_files(self) -> list[Path]:
|
|
return sorted(self.tests_dir.rglob("test_*.py"))
|
|
|
|
def _file_contains_runnable_unittest_test(self, path: Path) -> bool:
|
|
source = path.read_text(encoding="utf-8")
|
|
|
|
try:
|
|
tree = ast.parse(source, filename=str(path))
|
|
except SyntaxError as error:
|
|
raise AssertionError(f"SyntaxError in {path}: {error}") from error
|
|
|
|
testcase_aliases = {"TestCase"}
|
|
unittest_aliases = {"unittest"}
|
|
|
|
for node in tree.body:
|
|
if isinstance(node, ast.Import):
|
|
for import_name in node.names:
|
|
if import_name.name == "unittest":
|
|
unittest_aliases.add(import_name.asname or "unittest")
|
|
elif isinstance(node, ast.ImportFrom) and node.module == "unittest":
|
|
for import_name in node.names:
|
|
if import_name.name == "TestCase":
|
|
testcase_aliases.add(import_name.asname or "TestCase")
|
|
|
|
def is_testcase_base(base: ast.expr) -> bool:
|
|
if isinstance(base, ast.Name) and base.id in testcase_aliases:
|
|
return True
|
|
|
|
if isinstance(base, ast.Attribute) and base.attr == "TestCase":
|
|
return (
|
|
isinstance(base.value, ast.Name)
|
|
and base.value.id in unittest_aliases
|
|
)
|
|
|
|
return False
|
|
|
|
for node in tree.body:
|
|
if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)) and (
|
|
node.name.startswith("test_")
|
|
):
|
|
return True
|
|
|
|
for node in tree.body:
|
|
if not isinstance(node, ast.ClassDef):
|
|
continue
|
|
|
|
if not any(is_testcase_base(base) for base in node.bases):
|
|
continue
|
|
|
|
for item in node.body:
|
|
if isinstance(item, (ast.FunctionDef, ast.AsyncFunctionDef)) and (
|
|
item.name.startswith("test_")
|
|
):
|
|
return True
|
|
|
|
return False
|
|
|
|
def test_all_test_py_files_contain_runnable_tests(self) -> None:
|
|
test_files = self._iter_test_files()
|
|
self.assertTrue(test_files, "No test_*.py files found under tests/")
|
|
|
|
offenders = []
|
|
for path in test_files:
|
|
if not self._file_contains_runnable_unittest_test(path):
|
|
offenders.append(path.relative_to(self.repo_root).as_posix())
|
|
|
|
self.assertFalse(
|
|
offenders,
|
|
"These test_*.py files do not define any unittest-runnable tests:\n"
|
|
+ "\n".join(f"- {path}" for path in offenders),
|
|
)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|