|
1 | 1 | #!/usr/bin/env python3 |
2 | | -"""Repository runtime smoke test. |
| 2 | +"""Repo-specific runtime contract test. |
3 | 3 |
|
4 | | -This test intentionally avoids external services and validates: |
5 | | -1) repo has at least one runnable/build marker |
6 | | -2) Python source files in repo compile successfully |
| 4 | +This validates that the repository's primary runtime module(s): |
| 5 | +- compile successfully |
| 6 | +- keep expected runtime entrypoints/routes/contracts |
7 | 7 | """ |
8 | 8 |
|
9 | 9 | from __future__ import annotations |
|
12 | 12 | import py_compile |
13 | 13 | import sys |
14 | 14 |
|
15 | | -REPO_ROOT = Path(__file__).resolve().parents[1] |
16 | | - |
17 | | -SKIP_DIRS = { |
18 | | - ".git", |
19 | | - ".venv", |
20 | | - "venv", |
21 | | - "node_modules", |
22 | | - "dist", |
23 | | - "build", |
24 | | - "__pycache__", |
25 | | -} |
26 | | - |
27 | | - |
28 | | -def has_runnable_marker(root: Path) -> bool: |
29 | | - markers = [ |
30 | | - "package.json", |
31 | | - "pyproject.toml", |
32 | | - "requirements.txt", |
33 | | - "setup.py", |
34 | | - "manage.py", |
35 | | - "pom.xml", |
36 | | - "build.gradle", |
37 | | - "build.gradle.kts", |
38 | | - "go.mod", |
39 | | - "Cargo.toml", |
40 | | - "Dockerfile", |
41 | | - "docker-compose.yml", |
42 | | - "docker-compose.yaml", |
43 | | - "Makefile", |
44 | | - ] |
45 | | - for marker in markers: |
46 | | - if list(root.rglob(marker)): |
47 | | - return True |
48 | | - |
49 | | - if list(root.rglob("*.csproj")) or list(root.rglob("*.sln")): |
50 | | - return True |
51 | | - |
52 | | - return False |
53 | | - |
54 | | - |
55 | | -def iter_python_files(root: Path): |
56 | | - for py_file in root.rglob("*.py"): |
57 | | - rel = py_file.relative_to(root) |
58 | | - if any(part in SKIP_DIRS for part in rel.parts): |
59 | | - continue |
60 | | - yield py_file |
| 15 | +ROOT = Path(__file__).resolve().parents[1] |
| 16 | + |
| 17 | + |
| 18 | +def require_file(path: str) -> Path: |
| 19 | + p = ROOT / path |
| 20 | + if not p.exists(): |
| 21 | + raise AssertionError(f"Missing expected file: {path}") |
| 22 | + return p |
| 23 | + |
| 24 | + |
| 25 | +def compile_file(path: str) -> None: |
| 26 | + p = require_file(path) |
| 27 | + py_compile.compile(str(p), doraise=True) |
| 28 | + |
| 29 | + |
| 30 | +def read_text(path: str) -> str: |
| 31 | + return require_file(path).read_text() |
| 32 | + |
| 33 | + |
| 34 | +def require_contains(path: str, needle: str) -> None: |
| 35 | + text = read_text(path) |
| 36 | + if needle not in text: |
| 37 | + raise AssertionError(f"Expected marker not found in {path}: {needle}") |
| 38 | + |
| 39 | + |
| 40 | +def ok(msg: str) -> None: |
| 41 | + print(f"PASS: {msg}") |
| 42 | + |
| 43 | + |
| 44 | +def fail(msg: str) -> int: |
| 45 | + print(f"FAIL: {msg}") |
| 46 | + return 1 |
61 | 47 |
|
62 | 48 |
|
63 | 49 | def main() -> int: |
64 | | - if not has_runnable_marker(REPO_ROOT): |
65 | | - print("FAIL: no runnable/build marker found") |
66 | | - return 1 |
| 50 | + try: |
| 51 | + repo = ROOT.name |
| 52 | + |
| 53 | + if repo == "FARM-Auth": |
| 54 | + compile_file("backend/main.py") |
| 55 | + require_contains("backend/main.py", "app = FastAPI()") |
| 56 | + require_contains("backend/main.py", "@app.on_event(\"startup\")") |
| 57 | + require_contains("backend/main.py", "get_users_router") |
| 58 | + require_contains("backend/main.py", "get_todo_router") |
| 59 | + ok("FARM-Auth runtime contracts") |
| 60 | + |
| 61 | + elif repo == "FARM-Intro": |
| 62 | + compile_file("backend/main.py") |
| 63 | + require_contains("backend/main.py", "app = FastAPI()") |
| 64 | + require_contains("backend/main.py", "app.include_router(todo_router") |
| 65 | + require_contains("backend/main.py", "prefix=\"/task\"") |
| 66 | + ok("FARM-Intro runtime contracts") |
| 67 | + |
| 68 | + elif repo == "a2a-mcp-mongodb-multiagents": |
| 69 | + compile_file("mcp/main.py") |
| 70 | + require_contains("mcp/main.py", "mcp = FastMCP(") |
| 71 | + require_contains("mcp/main.py", "@mcp.tool") |
| 72 | + require_contains("mcp/main.py", "async def connect_to_mongo") |
| 73 | + ok("a2a MCP runtime contracts") |
| 74 | + |
| 75 | + elif repo == "beanie-example": |
| 76 | + compile_file("src/beaniecocktails/__init__.py") |
| 77 | + compile_file("src/beaniecocktails/scripts/init_db.py") |
| 78 | + require_contains("src/beaniecocktails/__init__.py", "app = FastAPI(lifespan=app_lifespan)") |
| 79 | + require_contains("src/beaniecocktails/__init__.py", "init_beanie(") |
| 80 | + require_contains("src/beaniecocktails/__init__.py", "app.include_router(cocktail_router") |
| 81 | + ok("beanie-example runtime contracts") |
| 82 | + |
| 83 | + elif repo == "docbridge": |
| 84 | + compile_file("examples/why/why/__init__.py") |
| 85 | + require_contains("examples/why/why/__init__.py", "app = FastAPI(lifespan=db_lifespan)") |
| 86 | + require_contains("examples/why/why/__init__.py", "@app.get(\"/profiles/{user_id}\")") |
| 87 | + require_contains("examples/why/why/__init__.py", "class Profile(Document)") |
| 88 | + ok("docbridge runtime contracts") |
| 89 | + |
| 90 | + elif repo == "farm-stack-to-do-app": |
| 91 | + compile_file("backend/src/todo/server.py") |
| 92 | + require_contains("backend/src/todo/server.py", "app = FastAPI(lifespan=lifespan") |
| 93 | + require_contains("backend/src/todo/server.py", "@app.get(\"/api/lists\")") |
| 94 | + require_contains("backend/src/todo/server.py", "@app.post(\"/api/lists\"") |
| 95 | + require_contains("backend/src/todo/server.py", "@app.patch(\"/api/lists/{list_id}/checked_state\")") |
| 96 | + ok("farm-stack-to-do-app runtime contracts") |
| 97 | + |
| 98 | + elif repo == "hr_agentic_chatbot": |
| 99 | + compile_file("app.py") |
| 100 | + require_contains("app.py", "@cl.on_chat_start") |
| 101 | + require_contains("app.py", "@cl.on_message") |
| 102 | + require_contains("app.py", "create_workflow(") |
| 103 | + ok("hr_agentic_chatbot runtime contracts") |
| 104 | + |
| 105 | + elif repo == "mongodb-atlas-fastapi": |
| 106 | + compile_file("app/main.py") |
| 107 | + require_contains("app/main.py", "app = FastAPI()") |
| 108 | + require_contains("app/main.py", "@app.get(\"/\", response_description=\"Student API HealthCheck\")") |
| 109 | + require_contains("app/main.py", "@app.get(\"/students\"") |
| 110 | + ok("mongodb-atlas-fastapi runtime contracts") |
| 111 | + |
| 112 | + elif repo == "mongodb-with-starlette": |
| 113 | + compile_file("app.py") |
| 114 | + require_contains("app.py", "app = Starlette(") |
| 115 | + require_contains("app.py", "Route(\"/\", create_student, methods=[\"POST\"])") |
| 116 | + require_contains("app.py", "Route(\"/{id}\", delete_student, methods=[\"DELETE\"])") |
| 117 | + ok("mongodb-with-starlette runtime contracts") |
| 118 | + |
| 119 | + elif repo == "mongodb-with-tornado": |
| 120 | + compile_file("app.py") |
| 121 | + require_contains("app.py", "class MainHandler(tornado.web.RequestHandler)") |
| 122 | + require_contains("app.py", "app = tornado.web.Application(") |
| 123 | + require_contains("app.py", "(r\"/(?P<student_id>\\w+)\", MainHandler)") |
| 124 | + ok("mongodb-with-tornado runtime contracts") |
| 125 | + |
| 126 | + elif repo == "mongodb-with-sanic": |
| 127 | + compile_file("app.py") |
| 128 | + require_contains("app.py", "app = Sanic(__name__)") |
| 129 | + require_contains("app.py", "@app.route(\"/\", methods=[\"POST\"])") |
| 130 | + require_contains("app.py", "@app.route(\"/<id>\", methods=[\"DELETE\"])") |
| 131 | + ok("mongodb-with-sanic runtime contracts") |
| 132 | + |
| 133 | + elif repo == "celeb-matcher-farm": |
| 134 | + compile_file("backend/src/server.py") |
| 135 | + require_contains("backend/src/server.py", "app = FastAPI(lifespan=lifespan") |
| 136 | + require_contains("backend/src/server.py", "@app.post(\"/api/search\")") |
| 137 | + require_contains("backend/src/server.py", "class SearchPayload(BaseModel)") |
| 138 | + ok("celeb-matcher-farm runtime contracts") |
| 139 | + |
| 140 | + else: |
| 141 | + raise AssertionError(f"No repository-specific contract defined for: {repo}") |
67 | 142 |
|
68 | | - py_files = list(iter_python_files(REPO_ROOT)) |
69 | | - if not py_files: |
70 | | - print("PASS: no python files to compile; marker checks passed") |
71 | 143 | return 0 |
72 | | - |
73 | | - failures = [] |
74 | | - for py_file in py_files: |
75 | | - try: |
76 | | - py_compile.compile(str(py_file), doraise=True) |
77 | | - except py_compile.PyCompileError as exc: |
78 | | - failures.append((py_file, exc.msg)) |
79 | | - |
80 | | - if failures: |
81 | | - print(f"FAIL: {len(failures)} python files failed to compile") |
82 | | - for py_file, msg in failures[:20]: |
83 | | - rel = py_file.relative_to(REPO_ROOT) |
84 | | - print(f" - {rel}: {msg}") |
85 | | - return 1 |
86 | | - |
87 | | - print(f"PASS: compiled {len(py_files)} python files") |
88 | | - return 0 |
| 144 | + except Exception as exc: |
| 145 | + return fail(str(exc)) |
89 | 146 |
|
90 | 147 |
|
91 | 148 | if __name__ == "__main__": |
|
0 commit comments