Coverage for tests/test_detected_features.py: 100%

74 statements  

« prev     ^ index     » next       coverage.py v7.15.4, created at 2026-08-18 22:03 +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_CONSUMER_SOURCE: typing.Final = """from faststream import FastStream 

200from faststream.rabbit import RabbitBroker 

201 

202 

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

204faststream_app = FastStream(rabbit_broker) 

205 

206 

207@rabbit_broker.subscriber("commands") 

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

209""" 

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

211 

212 

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

214 

215 

216@rabbit_broker.publisher("events") 

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

218 return event 

219""" 

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

221 

222 

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

224 

225 

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

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

228""" 

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

230 

231 

232COMMANDS_QUEUE = "commands" 

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

234 

235 

236@rabbit_broker.subscriber(COMMANDS_QUEUE) 

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

238""" 

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

240 

241 

242kafka_broker = KafkaBroker("localhost:9092") 

243 

244 

245@kafka_broker.subscriber("orders") 

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

247 

248 

249@kafka_broker.subscriber("payments") 

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

251 

252 

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

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

255 return order 

256 

257 

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

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

260 return payment 

261""" 

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

263 

264 

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

266 

267 

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

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

270""" 

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

272 

273 

274kafka_broker = KafkaBroker("localhost:9092") 

275""" 

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

277from granian.constants import HTTPModes, Interfaces 

278 

279 

280granian.Granian( 

281 "src.main:app", 

282 port=8000, 

283 workers=4, 

284 interface=Interfaces.ASGI, 

285 http=HTTPModes.http2, 

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

287).serve() 

288""" 

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

290workers = 1 

291worker_class = "uvicorn_worker.UvicornWorker" 

292""" 

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

294import tornado.httpclient 

295from werkzeug.datastructures import Headers 

296 

297import fastapi 

298 

299 

300app = fastapi.FastAPI() 

301""" 

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

303 

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

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

306""" 

307 

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

309''' 

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

311 

312import uvicorn 

313 

314 

315executor = ThreadPoolExecutor(max_workers=10) 

316transport = 8080 

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

318""" 

319_ASYNC_DATABASE_EDGE: typing.Final = ( 

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

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

322) 

323_REPLICA_DATABASE_EDGE: typing.Final = ( 

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

325) 

326_PLAIN_REDIS_NODE_MARK: typing.Final = "redisdb[" 

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

328_ALL_FEATURE_CASES: typing.Final = types.MappingProxyType( 

329 { 

330 "every served method": ( 

331 _EVERY_METHOD_SOURCE, 

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

333 (), 

334 ), 

335 "litestar route": ( 

336 _LITESTAR_ROUTE_SOURCE, 

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

338 (), 

339 ), 

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

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

342 _PREFIXED_ROUTE_SOURCE, 

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

344 (), 

345 ), 

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

347 "async http client": ( 

348 _ASYNC_CLIENT_SOURCE, 

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

350 (), 

351 ), 

352 "sync http clients": ( 

353 _SYNC_CLIENTS_SOURCE, 

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

355 ("async",), 

356 ), 

357 "task queue with a worker": ( 

358 _CELERY_SOURCE, 

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

360 (), 

361 ), 

362 "every task queue": ( 

363 _EVERY_QUEUE_SOURCE, 

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

365 (), 

366 ), 

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

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

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

370 "redis cache": ( 

371 _REDIS_CACHE_SOURCE, 

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

373 (), 

374 ), 

375 "redis cluster": ( 

376 _REDIS_CLUSTER_SOURCE, 

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

378 (_PLAIN_REDIS_NODE_MARK,), 

379 ), 

380 "redis sentinel": ( 

381 _REDIS_SENTINEL_SOURCE, 

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

383 (_PLAIN_REDIS_NODE_MARK,), 

384 ), 

385 "redis cluster over asyncio": ( 

386 _ASYNC_REDIS_CLUSTER_SOURCE, 

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

388 (_PLAIN_REDIS_NODE_MARK,), 

389 ), 

390 "redis sentinel over asyncio": ( 

391 _ASYNC_REDIS_SENTINEL_SOURCE, 

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

393 (_PLAIN_REDIS_NODE_MARK,), 

394 ), 

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

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

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

398 "async database": ( 

399 _ASYNC_DATABASE_SOURCE, 

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

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

402 ), 

403 "session attrs beside the dsn": ( 

404 _SESSION_ATTRS_BESIDE_THE_DSN_SOURCE, 

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

406 (_CREDENTIALS_MARK,), 

407 ), 

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

409 "session attrs in libpq options": ( 

410 _SESSION_ATTRS_IN_LIBPQ_OPTIONS_SOURCE, 

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

412 (), 

413 ), 

414 "session attrs in a setting": ( 

415 _SESSION_ATTRS_IN_A_SETTING_SOURCE, 

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

417 (), 

418 ), 

419 "replicated database": ( 

420 _REPLICA_DATABASE_SOURCE, 

421 ( 

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

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

424 _REPLICA_DATABASE_EDGE, 

425 ), 

426 (_CREDENTIALS_MARK,), 

427 ), 

428 "replicated database in double quotes": ( 

429 _DOUBLE_QUOTED_REPLICA_SOURCE, 

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

431 (_CREDENTIALS_MARK,), 

432 ), 

433 "pool far from the engine call": ( 

434 _POOL_FAR_FROM_THE_ENGINE_SOURCE, 

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

436 ("postgresql_psycopgdb1",), 

437 ), 

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

439 "consumed messages": ( 

440 _CONSUMER_SOURCE, 

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

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

443 ), 

444 "messages published by a decorator": ( 

445 _PUBLISHER_DECORATOR_SOURCE, 

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

447 ("rabbit -->",), 

448 ), 

449 "messages published by a call": ( 

450 _PUBLISH_CALL_SOURCE, 

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

452 ("rabbit -->",), 

453 ), 

454 "every topic of one broker": ( 

455 _TWO_TOPIC_FLOW_SOURCE, 

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

457 (), 

458 ), 

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

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

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

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

463 "application server properties": ( 

464 _GRANIAN_SOURCE, 

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

466 (), 

467 ), 

468 "worker class naming both servers": ( 

469 _GUNICORN_CONFIG_SOURCE, 

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

471 (), 

472 ), 

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

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

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

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

477 _THREAD_POOL_SOURCE, 

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

479 ("workers", "port 8080"), 

480 ), 

481 }, 

482) 

483_ALL_SERVER_SOURCES: typing.Final = types.MappingProxyType( 

484 { 

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

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

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

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

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

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

491 "uwsgi": "import uwsgi\n", 

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

493 "bjoern": "import bjoern\n", 

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

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

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

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

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

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

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

501 }, 

502) 

503_REQUIRED_SHOWCASE_MARKS: typing.Final = ( 

504 "REST", 

505 "httpx", 

506 "aiohttp", 

507 "requests", 

508 "niquests", 

509 "celery", 

510 "taskiq", 

511 "arq", 

512 "rq", 

513 "dramatiq", 

514 "huey", 

515 "rabbit", 

516 "kafka", 

517 "nats", 

518 "redis", 

519 "retry", 

520 "sentinel", 

521 "cluster", 

522 "postgresql", 

523 "sqlite", 

524 "replicas", 

525 "HPA", 

526 "cpu", 

527 "RAM", 

528 "GPU", 

529 "ConfigMap_app_config", 

530 "Secret_app_secrets", 

531 "PersistentVolume", 

532 "granian", 

533 "gunicorn", 

534 "uvicorn", 

535) 

536_ALL_FEATURE_LITERALS: typing.Final = ( 

537 *_ALL_SERVER_SOURCES, 

538 "fastapi", 

539 "litestar", 

540 "faststream", 

541 "redis", 

542 "sqlalchemy", 

543 "create_engine", 

544 "target_session_attrs", 

545 "postgresql", 

546 "mysql", 

547 "sqlite", 

548 "oracle", 

549 "mssql", 

550 "mariadb", 

551 "cockroachdb", 

552 "httpx", 

553 "aiohttp", 

554 "requests", 

555 "niquests", 

556 "celery", 

557 "taskiq", 

558 "arq", 

559 "rq", 

560 "dramatiq", 

561 "huey", 

562 "run_simple", 

563 "worker_class", 

564 "--worker-class", 

565) 

566_HYPOTHESIS_EXAMPLES: typing.Final = 30 

567_FIRST_PRINTABLE_CODE: typing.Final = 32 

568_LAST_PRINTABLE_CODE: typing.Final = 126 

569_LONGEST_RANDOM_SOURCE: typing.Final = 200 

570 

571 

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

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

574 

575 

576_UNRELATED_SOURCE_STRATEGY: typing.Final = st.text( 

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

578 max_size=_LONGEST_RANDOM_SOURCE, 

579).filter(_has_no_feature_literal) 

580 

581 

582@pytest.mark.parametrize( 

583 ("source_code", "expected_parts", "forbidden_parts"), 

584 _ALL_FEATURE_CASES.values(), 

585 ids=_ALL_FEATURE_CASES, 

586) 

587def test_source_reaches_the_diagram( 

588 tmp_path: pathlib.Path, 

589 source_code: str, 

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

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

592) -> None: 

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

594 

595 for one_expected_part in expected_parts: 

596 assert one_expected_part in rendered_diagram, one_expected_part 

597 for one_forbidden_part in forbidden_parts: 

598 assert one_forbidden_part not in rendered_diagram, one_forbidden_part 

599 

600 

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

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

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

604 

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

606 

607 

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

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

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

611 

612 

613@hypothesis.settings( 

614 deadline=None, 

615 max_examples=_HYPOTHESIS_EXAMPLES, 

616 suppress_health_check=[hypothesis.HealthCheck.function_scoped_fixture], 

617) 

618@hypothesis.given(source_code=_UNRELATED_SOURCE_STRATEGY) 

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

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