Coverage for tests/test_detected_features.py: 100%

77 statements  

« prev     ^ index     » next       coverage.py v7.17.2, created at 2026-11-09 14:41 +0000

1import pathlib 

2import types 

3import typing 

4 

5import hypothesis 

6import pytest 

7from hypothesis import strategies as st 

8 

9from tests import diagram_rendering 

10from tests.diagram_parts import EDGE_ARROW 

11 

12 

13# What a parser found is only interesting once it reaches an arrow, so every case here is one 

14# source file, the diagram parts it has to draw and the parts it may not. 

15_EVERY_METHOD_SOURCE: typing.Final = '''import fastapi 

16 

17router = fastapi.APIRouter() 

18 

19 

20@router.get("/x") 

21@router.post("/x") 

22@router.put("/x") 

23@router.patch("/x") 

24@router.delete("/x") 

25@router.head("/x") 

26@router.options("/x") 

27@router.trace("/x") 

28async def handle_everything() -> None: 

29 """A route is traffic the service receives whatever verb it answers.""" 

30''' 

31_LITESTAR_ROUTE_SOURCE: typing.Final = """from litestar import post 

32 

33 

34@post("/items") 

35async def create_item(data: dict) -> dict: 

36 return data 

37""" 

38_ROUTE_WITHOUT_FRAMEWORK_SOURCE: typing.Final = """@router.get("/x") 

39async def read_items() -> list[dict]: 

40 return [] 

41""" 

42_PREFIXED_ROUTE_SOURCE: typing.Final = """import fastapi 

43 

44API_PREFIX = "/api/v1" 

45router = fastapi.APIRouter() 

46 

47 

48@router.get(f"{API_PREFIX}/items") 

49async def read_items() -> list[dict]: 

50 return [] 

51""" 

52_MOCKED_TEST_SOURCE: typing.Final = """from unittest import mock 

53 

54from fastapi.testclient import TestClient 

55 

56 

57@mock.patch("src.payments.fetch_payment_status") 

58def test_fetching(fetch_mock: mock.MagicMock) -> None: ... 

59""" 

60_ASYNC_CLIENT_SOURCE: typing.Final = """import httpx 

61 

62 

63payments_client = httpx.AsyncClient(base_url="https://payments.example.com") 

64""" 

65_SYNC_CLIENTS_SOURCE: typing.Final = """import niquests 

66import requests 

67 

68 

69def fetch_payment_status(payment_id: int) -> dict: 

70 requests.get(f"https://payments.example.com/{payment_id}", timeout=10) 

71 return niquests.get(f"https://payments.example.com/{payment_id}", timeout=10).json() 

72""" 

73_CELERY_SOURCE: typing.Final = '''import celery 

74 

75"""In the cluster the tasks are executed by `celery worker`.""" 

76 

77celery_app = celery.Celery(broker="redis://localhost:6379/0") 

78 

79 

80@celery_app.task 

81def send_receipt(order_id: int) -> None: ... 

82''' 

83_EVERY_QUEUE_SOURCE: typing.Final = """import arq 

84import celery 

85import dramatiq 

86import huey 

87import rq 

88import taskiq 

89""" 

90_TWO_BROKER_QUEUE_SOURCE: typing.Final = """import celery 

91 

92 

93celery_app = celery.Celery(broker="amqp://localhost:5672/", backend="redis://localhost:6379/1") 

94""" 

95_RABBITMQ_BROKER_SOURCE: typing.Final = """import dramatiq 

96 

97 

98dramatiq_broker = dramatiq.RabbitMQBroker(url="amqp://localhost:5672/") 

99""" 

100_POSTGRESQL_BROKER_SOURCE: typing.Final = """import huey 

101 

102 

103huey_broker = PostgreSQLBroker("postgres://localhost:5432/tasks") 

104""" 

105_REDIS_CACHE_SOURCE: typing.Final = """import redis 

106import redis.asyncio 

107 

108 

109RETRY_POLICY = redis.Retry(ExponentialBackoff(), 3) 

110cache_client = redis.asyncio.Redis(host="cache.internal", retry=RETRY_POLICY) 

111""" 

112_REDIS_CLUSTER_SOURCE: typing.Final = """from redis.cluster import RedisCluster 

113 

114 

115cluster_client = RedisCluster(startup_nodes=[]) 

116""" 

117_REDIS_SENTINEL_SOURCE: typing.Final = """from redis.sentinel import Sentinel 

118 

119 

120sentinel_client = Sentinel([("sentinel-one", 26379)]) 

121""" 

122_ASYNC_REDIS_CLUSTER_SOURCE: typing.Final = """from redis.asyncio import RedisCluster 

123 

124 

125cluster_client = RedisCluster(startup_nodes=[]) 

126""" 

127_ASYNC_REDIS_SENTINEL_SOURCE: typing.Final = """from redis.asyncio.sentinel import Sentinel 

128 

129 

130sentinel_client = Sentinel([("sentinel-one", 26379)]) 

131""" 

132_REDIS_IMPORT_ONLY_SOURCE: typing.Final = """import redis 

133 

134 

135CACHE_HOST = "cache.internal" 

136""" 

137_ASYNC_DATABASE_SOURCE: typing.Final = """from sqlalchemy.ext.asyncio import create_async_engine 

138 

139 

140async_engine = create_async_engine( 

141 "postgresql+asyncpg://user:password@pg-primary:5432/orders?target_session_attrs=read-write", 

142) 

143""" 

144_REPLICA_DATABASE_SOURCE: typing.Final = """from sqlalchemy import create_engine 

145 

146 

147replica_engine = create_engine( 

148 'postgresql+psycopg://user:password@pg-replica-one:5432,pg-replica-two:5432/orders', 

149 pool_size=10, 

150) 

151""" 

152_DOUBLE_QUOTED_REPLICA_SOURCE: typing.Final = """from sqlalchemy import create_engine 

153 

154 

155replica_engine = create_engine( 

156 "postgresql+psycopg://user:password@pg-replica-one:5432,pg-replica-two:5432/orders", 

157 pool_size=10, 

158) 

159""" 

160_LINES_BETWEEN_ENGINE_AND_POOL: typing.Final = 40 

161_POOL_FAR_FROM_THE_ENGINE_SOURCE: typing.Final = ( 

162 "from sqlalchemy import create_engine\n\n\n" 

163 "replica_engine = create_engine('postgresql+psycopg://pg-replica-one:5432,pg-replica-two:5432/orders')\n" 

164 + "# padding that keeps the unrelated pool far away from the engine call\n" * _LINES_BETWEEN_ENGINE_AND_POOL 

165 + "adapter_settings = {'pool_maxsize': 10}\n" 

166) 

167_SESSION_ATTRS_BESIDE_THE_DSN_SOURCE: typing.Final = """from sqlalchemy import create_engine 

168 

169 

170primary_engine = create_engine( 

171 "postgresql+psycopg://user:password@pg-primary:5432/orders", 

172 connect_args={"target_session_attrs": "read-write"}, 

173) 

174""" 

175_SESSION_ATTRS_IN_A_SETTING_SOURCE: typing.Final = """from sqlalchemy import create_engine 

176 

177 

178TARGET_SESSION_ATTRS = "standby" 

179 

180replica_engine = create_engine("postgresql+psycopg://pg-replica:5432/orders") 

181""" 

182_SESSION_ATTRS_IN_LIBPQ_OPTIONS_SOURCE: typing.Final = """from sqlalchemy import create_engine 

183 

184 

185LIBPQ_OPTIONS = "host=pg-one,pg-two target_session_attrs=read-write" 

186 

187primary_engine = create_engine( 

188 "postgresql+psycopg://pg-one:5432/orders", 

189 connect_args={"options": LIBPQ_OPTIONS}, 

190) 

191""" 

192_DATABASE_URL_BEHIND_A_CONSTANT_SOURCE: typing.Final = """from sqlalchemy import create_engine 

193 

194from src.config import DATABASE_URL 

195 

196 

197engine = create_engine(DATABASE_URL) 

198""" 

199_POOLED_ASYNC_ENGINE_SOURCE: typing.Final = """from sqlalchemy.ext.asyncio import create_async_engine 

200 

201 

202async_engine = create_async_engine( 

203 "postgresql+asyncpg://pg-one:5432,pg-two:5432/orders", 

204 pool_size=10, 

205 target_session_attrs="prefer-standby", 

206) 

207""" 

208_ENGINE_FROM_A_FACTORY_TABLE_SOURCE: typing.Final = """from sqlalchemy import create_engine 

209 

210 

211ENGINE_FACTORIES = {"primary": create_engine} 

212primary_engine = ENGINE_FACTORIES["primary"]("postgresql+psycopg://pg-one:5432,pg-two:5432/orders", pool_size=10) 

213""" 

214_UNPARSEABLE_DATABASE_SOURCE: typing.Final = """from sqlalchemy import create_engine 

215 

216 

217primary_engine = create_engine("postgresql+psycopg://pg-primary:5432/orders" 

218""" 

219_CONSUMER_SOURCE: typing.Final = """from faststream import FastStream 

220from faststream.rabbit import RabbitBroker 

221 

222 

223rabbit_broker = RabbitBroker("amqp://user:password@localhost:5672/") 

224faststream_app = FastStream(rabbit_broker) 

225 

226 

227@rabbit_broker.subscriber("commands") 

228async def handle_command(command: dict) -> None: ... 

229""" 

230_PUBLISHER_DECORATOR_SOURCE: typing.Final = """from faststream.rabbit import RabbitBroker 

231 

232 

233rabbit_broker = RabbitBroker("amqp://localhost:5672/") 

234 

235 

236@rabbit_broker.publisher("events") 

237async def publish_event(event: dict) -> dict: 

238 return event 

239""" 

240_PUBLISH_CALL_SOURCE: typing.Final = """from faststream.rabbit import RabbitBroker 

241 

242 

243rabbit_broker = RabbitBroker("amqp://localhost:5672/") 

244 

245 

246async def publish_event(event: dict) -> None: 

247 await rabbit_broker.publish(event, queue="events") 

248""" 

249_TOPIC_BEHIND_A_CONSTANT_SOURCE: typing.Final = """from faststream.rabbit import RabbitBroker 

250 

251 

252COMMANDS_QUEUE = "commands" 

253rabbit_broker = RabbitBroker("amqp://localhost:5672/") 

254 

255 

256@rabbit_broker.subscriber(COMMANDS_QUEUE) 

257async def handle_command(command: dict) -> None: ... 

258""" 

259_TWO_TOPIC_FLOW_SOURCE: typing.Final = """from faststream.kafka import KafkaBroker 

260 

261 

262kafka_broker = KafkaBroker("localhost:9092") 

263 

264 

265@kafka_broker.subscriber("orders") 

266async def handle_order(order: dict) -> None: ... 

267 

268 

269@kafka_broker.subscriber("payments") 

270async def handle_payment(payment: dict) -> None: ... 

271 

272 

273@kafka_broker.publisher("orders-done") 

274async def publish_order_done(order: dict) -> dict: 

275 return order 

276 

277 

278@kafka_broker.publisher("payments-done") 

279async def publish_payment_done(payment: dict) -> dict: 

280 return payment 

281""" 

282_PIPED_TOPIC_SOURCE: typing.Final = """from faststream.nats import NatsBroker 

283 

284 

285nats_broker = NatsBroker("nats://localhost:4222") 

286 

287 

288@nats_broker.subscriber("orders|paid") 

289async def handle_paid_order(order: dict) -> None: ... 

290""" 

291_BROKER_WITHOUT_A_FLOW_SOURCE: typing.Final = """from faststream.kafka import KafkaBroker 

292 

293 

294kafka_broker = KafkaBroker("localhost:9092") 

295""" 

296_GRANIAN_SOURCE: typing.Final = """import granian 

297from granian.constants import HTTPModes, Interfaces 

298 

299 

300granian.Granian( 

301 "src.main:app", 

302 port=8000, 

303 workers=4, 

304 interface=Interfaces.ASGI, 

305 http=HTTPModes.http2, 

306 ssl_cert="/etc/tls/tls.crt", 

307).serve() 

308""" 

309_GUNICORN_CONFIG_SOURCE: typing.Final = """bind = "0.0.0.0:8000" 

310workers = 1 

311worker_class = "uvicorn_worker.UvicornWorker" 

312""" 

313_CARRIER_LIBRARIES_SOURCE: typing.Final = """import gevent 

314import tornado.httpclient 

315from werkzeug.datastructures import Headers 

316 

317import fastapi 

318 

319 

320app = fastapi.FastAPI() 

321""" 

322_MENTIONS_WITHOUT_IMPORTS_SOURCE: typing.Final = '''"""Runbook of the service. 

323 

324Work is handed over to celery, taskiq, arq, rq, dramatiq and huey workers, and the payments 

325API is called over httpx, aiohttp, requests and niquests. 

326""" 

327 

328RUNBOOK_URL = "https://wiki.example.com/runbook" 

329''' 

330_THREAD_POOL_SOURCE: typing.Final = """from concurrent.futures import ThreadPoolExecutor 

331 

332import uvicorn 

333 

334 

335executor = ThreadPoolExecutor(max_workers=10) 

336transport = 8080 

337uvicorn.run("src.main:app") 

338""" 

339_ASYNC_DATABASE_EDGE: typing.Final = ( 

340 'app_svc --> |"async, postgresql+asyncpg://***@pg-primary:5432/orders' 

341 '?target_session_attrs=read-write"| postgresql_asyncpgdb' 

342) 

343_REPLICA_DATABASE_EDGE: typing.Final = ( 

344 'app_svc --> |"postgresql+psycopg://***@pg-replica-one:5432,pg-replica-two:5432/orders"| postgresql_psycopgdb0' 

345) 

346_PLAIN_REDIS_NODE_MARK: typing.Final = "redisdb[" 

347_CREDENTIALS_MARK: typing.Final = "user:password" 

348_ALL_FEATURE_CASES: typing.Final = types.MappingProxyType( 

349 { 

350 "every served method": ( 

351 _EVERY_METHOD_SOURCE, 

352 ('external_client --> |"REST (delete, get, head, options, patch, post, put, trace)"| app_svc',), 

353 (), 

354 ), 

355 "litestar route": ( 

356 _LITESTAR_ROUTE_SOURCE, 

357 ('external_client --> |"REST (post)"| app_svc',), 

358 (), 

359 ), 

360 "route without a framework": (_ROUTE_WITHOUT_FRAMEWORK_SOURCE, (), ("REST",)), 

361 "route behind an f-string prefix": ( 

362 _PREFIXED_ROUTE_SOURCE, 

363 ('external_client --> |"REST (get)"| app_svc',), 

364 (), 

365 ), 

366 "mocked test file is not an api": (_MOCKED_TEST_SOURCE, (), ("REST",)), 

367 "async http client": ( 

368 _ASYNC_CLIENT_SOURCE, 

369 ('app_svc --> |"HTTP (async, httpx)"| External_API', 'External_API["External API"]'), 

370 (), 

371 ), 

372 "sync http clients": ( 

373 _SYNC_CLIENTS_SOURCE, 

374 ('app_svc --> |"HTTP (niquests, requests)"| External_API',), 

375 ("async",), 

376 ), 

377 "task queue with a worker": ( 

378 _CELERY_SOURCE, 

379 ('app_svc --> |"Tasks (celery, workers, redis)"| TaskQueue_Worker', 'TaskQueue_Worker["Task workers"]'), 

380 (), 

381 ), 

382 "every task queue": ( 

383 _EVERY_QUEUE_SOURCE, 

384 ('app_svc --> |"Tasks (arq, celery, dramatiq, huey, rq, taskiq)"| TaskQueue_Worker',), 

385 (), 

386 ), 

387 "queue behind two brokers": (_TWO_BROKER_QUEUE_SOURCE, ('"Tasks (celery, rabbitmq, redis)"',), ()), 

388 "rabbitmq backed queue": (_RABBITMQ_BROKER_SOURCE, ('"Tasks (dramatiq, rabbitmq)"',), ()), 

389 "postgresql backed queue": (_POSTGRESQL_BROKER_SOURCE, ('"Tasks (huey, postgresql)"',), ('db["',)), 

390 "redis cache": ( 

391 _REDIS_CACHE_SOURCE, 

392 ('app_svc --> |"async, retry"| redisdb', 'redisdb["redis"]'), 

393 (), 

394 ), 

395 "redis cluster": ( 

396 _REDIS_CLUSTER_SOURCE, 

397 ('redisdb_cluster0["redis cluster #0"]', 'redisdb_cluster2["redis cluster #2"]', "app_svc --> redisdb"), 

398 (_PLAIN_REDIS_NODE_MARK,), 

399 ), 

400 "redis sentinel": ( 

401 _REDIS_SENTINEL_SOURCE, 

402 ('redisdb_sentinel0["redis sentinel #0"]', 'redisdb_sentinel2["redis sentinel #2"]'), 

403 (_PLAIN_REDIS_NODE_MARK,), 

404 ), 

405 "redis cluster over asyncio": ( 

406 _ASYNC_REDIS_CLUSTER_SOURCE, 

407 ('redisdb_cluster0["redis cluster #0"]', '|"async"|'), 

408 (_PLAIN_REDIS_NODE_MARK,), 

409 ), 

410 "redis sentinel over asyncio": ( 

411 _ASYNC_REDIS_SENTINEL_SOURCE, 

412 ('redisdb_sentinel0["redis sentinel #0"]', '|"async"|'), 

413 (_PLAIN_REDIS_NODE_MARK,), 

414 ), 

415 "redis import alone": (_REDIS_IMPORT_ONLY_SOURCE, (), ("redisdb",)), 

416 # The dsn is drawn whole, so the attribute inside it is already on the arrow: repeating it 

417 # beside the dsn is what the forbidden part guards against. 

418 "async database": ( 

419 _ASYNC_DATABASE_SOURCE, 

420 ('postgresql_asyncpgdb["postgresql+asyncpg"]', _ASYNC_DATABASE_EDGE), 

421 (_CREDENTIALS_MARK, "read-write, read-write"), 

422 ), 

423 "session attrs beside the dsn": ( 

424 _SESSION_ATTRS_BESIDE_THE_DSN_SOURCE, 

425 ('app_svc --> |"postgresql+psycopg://***@pg-primary:5432/orders, read-write"| postgresql_psycopgdb',), 

426 (_CREDENTIALS_MARK,), 

427 ), 

428 # The libpq keyword/value spelling quotes the whole string, not the value inside it. 

429 "session attrs in libpq options": ( 

430 _SESSION_ATTRS_IN_LIBPQ_OPTIONS_SOURCE, 

431 ('app_svc --> |"postgresql+psycopg://pg-one:5432/orders, read-write"| postgresql_psycopgdb',), 

432 (), 

433 ), 

434 "session attrs in a setting": ( 

435 _SESSION_ATTRS_IN_A_SETTING_SOURCE, 

436 ('app_svc --> |"postgresql+psycopg://pg-replica:5432/orders, standby"| postgresql_psycopgdb',), 

437 (), 

438 ), 

439 "replicated database": ( 

440 _REPLICA_DATABASE_SOURCE, 

441 ( 

442 'postgresql_psycopgdb0["postgresql+psycopg #0"]', 

443 'postgresql_psycopgdb2["postgresql+psycopg #2"]', 

444 _REPLICA_DATABASE_EDGE, 

445 ), 

446 (_CREDENTIALS_MARK,), 

447 ), 

448 "replicated database in double quotes": ( 

449 _DOUBLE_QUOTED_REPLICA_SOURCE, 

450 ('postgresql_psycopgdb0["postgresql+psycopg #0"]', 'postgresql_psycopgdb2["postgresql+psycopg #2"]'), 

451 (_CREDENTIALS_MARK,), 

452 ), 

453 "pool far from the engine call": ( 

454 _POOL_FAR_FROM_THE_ENGINE_SOURCE, 

455 ('postgresql_psycopgdb0["postgresql+psycopg #0"]',), 

456 ("postgresql_psycopgdb1",), 

457 ), 

458 "database url behind a constant": (_DATABASE_URL_BEHIND_A_CONSTANT_SOURCE, (), ('db["',)), 

459 "pooled async engine": ( 

460 _POOLED_ASYNC_ENGINE_SOURCE, 

461 ( 

462 ( 

463 'app_svc --> |"async, postgresql+asyncpg://pg-one:5432,pg-two:5432/orders, prefer-standby"| ' 

464 "postgresql_asyncpgdb2" 

465 ), 

466 ), 

467 (), 

468 ), 

469 # An engine called through anything but its name is not recognised as one: the dsn is still 

470 # drawn, the pool and the replicas behind it are not. 

471 "engine from a factory table": ( 

472 _ENGINE_FROM_A_FACTORY_TABLE_SOURCE, 

473 ('postgresql_psycopgdb["postgresql+psycopg"]',), 

474 ("postgresql_psycopgdb0",), 

475 ), 

476 "unparseable database source": (_UNPARSEABLE_DATABASE_SOURCE, (), ('db["',)), 

477 "consumed messages": ( 

478 _CONSUMER_SOURCE, 

479 ('rabbit --> |"commands"| app_svc', 'rabbit["rabbit"]'), 

480 ("kafka", "nats", "user:password"), 

481 ), 

482 "messages published by a decorator": ( 

483 _PUBLISHER_DECORATOR_SOURCE, 

484 ('app_svc --> |"events"| rabbit',), 

485 ("rabbit -->",), 

486 ), 

487 "messages published by a call": ( 

488 _PUBLISH_CALL_SOURCE, 

489 ('app_svc --> |"events"| rabbit',), 

490 ("rabbit -->",), 

491 ), 

492 "every topic of one broker": ( 

493 _TWO_TOPIC_FLOW_SOURCE, 

494 ('kafka --> |"orders, payments"| app_svc', 'app_svc --> |"orders-done, payments-done"| kafka'), 

495 (), 

496 ), 

497 # The pipe is the edge label's own delimiter: a topic carrying one may not reach the page. 

498 "topic spelled with a pipe": (_PIPED_TOPIC_SOURCE, ('nats --> |"orderspaid"| app_svc',), ("orders|paid",)), 

499 "topic behind a constant": (_TOPIC_BEHIND_A_CONSTANT_SOURCE, ("rabbit --> app_svc",), ('|"',)), 

500 "broker without a flow": (_BROKER_WITHOUT_A_FLOW_SOURCE, (), ("kafka",)), 

501 "application server properties": ( 

502 _GRANIAN_SOURCE, 

503 ('external_client --> |"Served by granian, 4 workers, port 8000, TLS, HTTP/2"| app_svc',), 

504 (), 

505 ), 

506 "worker class naming both servers": ( 

507 _GUNICORN_CONFIG_SOURCE, 

508 ('external_client --> |"Served by gunicorn, uvicorn, single worker, port 8000"| app_svc',), 

509 (), 

510 ), 

511 "carrier library is not a server": (_CARRIER_LIBRARIES_SOURCE, (), ("Served by",)), 

512 # A name in prose is what the prefilter matches on, and only the import decides. 

513 "technology named but never imported": (_MENTIONS_WITHOUT_IMPORTS_SOURCE, (), (EDGE_ARROW,)), 

514 "thread pool is not a server worker": ( 

515 _THREAD_POOL_SOURCE, 

516 ('external_client --> |"Served by uvicorn"| app_svc',), 

517 ("workers", "port 8080"), 

518 ), 

519 }, 

520) 

521_ALL_SERVER_SOURCES: typing.Final = types.MappingProxyType( 

522 { 

523 "granian": "import granian\n\ngranian.Granian('src.main:app').serve()\n", 

524 "uvicorn": "import uvicorn\n\nuvicorn.run('src.main:app')\n", 

525 "gunicorn": "from gunicorn.app.base import BaseApplication\n", 

526 "hypercorn": "from hypercorn.asyncio import serve\n", 

527 "daphne": "from daphne.server import Server\n", 

528 "waitress": "from waitress import serve\n", 

529 "uwsgi": "import uwsgi\n", 

530 "mod_wsgi": "from mod_wsgi import server\n", 

531 "bjoern": "import bjoern\n", 

532 "meinheld": "from meinheld import server\n", 

533 "cheroot": "from cheroot.wsgi import Server\n", 

534 "tornado": "import tornado.httpserver\n", 

535 "gevent": "from gevent.pywsgi import WSGIServer\n", 

536 "eventlet": "import eventlet.wsgi\n", 

537 "werkzeug": "from werkzeug.serving import run_simple\n", 

538 "wsgiref": "from wsgiref.simple_server import make_server\n", 

539 }, 

540) 

541_REQUIRED_SHOWCASE_MARKS: typing.Final = ( 

542 "REST", 

543 "httpx", 

544 "aiohttp", 

545 "requests", 

546 "niquests", 

547 "celery", 

548 "taskiq", 

549 "arq", 

550 "rq", 

551 "dramatiq", 

552 "huey", 

553 "rabbit", 

554 "kafka", 

555 "nats", 

556 "redis", 

557 "retry", 

558 "sentinel", 

559 "cluster", 

560 "postgresql", 

561 "sqlite", 

562 "replicas", 

563 "HPA", 

564 "cpu", 

565 "RAM", 

566 "GPU", 

567 "ConfigMap_app_config", 

568 "Secret_app_secrets", 

569 "PersistentVolume", 

570 "granian", 

571 "gunicorn", 

572 "uvicorn", 

573) 

574_ALL_FEATURE_LITERALS: typing.Final = ( 

575 *_ALL_SERVER_SOURCES, 

576 "fastapi", 

577 "litestar", 

578 "faststream", 

579 "redis", 

580 "sqlalchemy", 

581 "create_engine", 

582 "target_session_attrs", 

583 "postgresql", 

584 "mysql", 

585 "sqlite", 

586 "oracle", 

587 "mssql", 

588 "mariadb", 

589 "cockroachdb", 

590 "httpx", 

591 "aiohttp", 

592 "requests", 

593 "niquests", 

594 "celery", 

595 "taskiq", 

596 "arq", 

597 "rq", 

598 "dramatiq", 

599 "huey", 

600 "run_simple", 

601 "worker_class", 

602 "--worker-class", 

603) 

604_HYPOTHESIS_EXAMPLES: typing.Final = 30 

605_FIRST_PRINTABLE_CODE: typing.Final = 32 

606_LAST_PRINTABLE_CODE: typing.Final = 126 

607_LONGEST_RANDOM_SOURCE: typing.Final = 200 

608 

609 

610def _has_no_feature_literal(source_code: str, /) -> bool: 

611 return not any(one_literal in source_code.lower() for one_literal in _ALL_FEATURE_LITERALS) 

612 

613 

614_UNRELATED_SOURCE_STRATEGY: typing.Final = st.text( 

615 alphabet=st.characters(min_codepoint=_FIRST_PRINTABLE_CODE, max_codepoint=_LAST_PRINTABLE_CODE), 

616 max_size=_LONGEST_RANDOM_SOURCE, 

617).filter(_has_no_feature_literal) 

618 

619 

620@pytest.mark.parametrize( 

621 ("source_code", "expected_parts", "forbidden_parts"), 

622 _ALL_FEATURE_CASES.values(), 

623 ids=_ALL_FEATURE_CASES, 

624) 

625def test_source_reaches_the_diagram( 

626 tmp_path: pathlib.Path, 

627 source_code: str, 

628 expected_parts: tuple[str, ...], 

629 forbidden_parts: tuple[str, ...], 

630) -> None: 

631 rendered_diagram: typing.Final = diagram_rendering.render_source_diagram(tmp_path, source_code) 

632 

633 for one_expected_part in expected_parts: 

634 assert one_expected_part in rendered_diagram, one_expected_part 

635 for one_forbidden_part in forbidden_parts: 

636 assert one_forbidden_part not in rendered_diagram, one_forbidden_part 

637 

638 

639@pytest.mark.parametrize("server_name", _ALL_SERVER_SOURCES) 

640def test_every_server_reaches_the_diagram(tmp_path: pathlib.Path, server_name: str) -> None: 

641 rendered_diagram: typing.Final = diagram_rendering.render_source_diagram(tmp_path, _ALL_SERVER_SOURCES[server_name]) 

642 

643 assert f'external_client --> |"Served by {server_name}"| app_svc' in rendered_diagram 

644 

645 

646@pytest.mark.parametrize("feature_mark", _REQUIRED_SHOWCASE_MARKS) 

647def test_showcase_shows_every_supported_feature(feature_mark: str) -> None: 

648 assert feature_mark in diagram_rendering.render_example_diagram(diagram_rendering.SHOWCASE_SETTINGS) 

649 

650 

651@hypothesis.settings( 

652 deadline=None, 

653 max_examples=_HYPOTHESIS_EXAMPLES, 

654 suppress_health_check=[hypothesis.HealthCheck.function_scoped_fixture], 

655) 

656@hypothesis.given(source_code=_UNRELATED_SOURCE_STRATEGY) 

657def test_unrelated_source_draws_the_service_alone(tmp_path: pathlib.Path, source_code: str) -> None: 

658 assert EDGE_ARROW not in diagram_rendering.render_source_diagram(tmp_path, source_code)