|
13 | 13 | pytest_configure, |
14 | 14 | pytest_sessionfinish, |
15 | 15 | pytest_sessionstart, |
| 16 | + extract_app_from_client, |
| 17 | + wrap_client_with_coverage, |
| 18 | + create_coverage_fixture, |
16 | 19 | ) |
17 | 20 |
|
18 | 21 |
|
@@ -350,3 +353,139 @@ def test_pytest_testnodedown_with_existing_worker_data(self): |
350 | 353 | assert "/new" in worker_data |
351 | 354 | assert "existing_test" in worker_data["/existing"] |
352 | 355 | assert "new_test" in worker_data["/new"] |
| 356 | + |
| 357 | + |
| 358 | +def test_extract_app_from_client_variants(): |
| 359 | + """Extract app from different client shapes.""" |
| 360 | + app = object() |
| 361 | + |
| 362 | + class A: |
| 363 | + def __init__(self): |
| 364 | + self.app = app |
| 365 | + |
| 366 | + class B: |
| 367 | + def __init__(self): |
| 368 | + self.application = app |
| 369 | + |
| 370 | + class Transport: |
| 371 | + def __init__(self): |
| 372 | + self.app = app |
| 373 | + |
| 374 | + class C: |
| 375 | + def __init__(self): |
| 376 | + self._transport = Transport() |
| 377 | + |
| 378 | + class D: |
| 379 | + def __init__(self): |
| 380 | + self._app = app |
| 381 | + |
| 382 | + assert extract_app_from_client(A()) is app |
| 383 | + assert extract_app_from_client(B()) is app |
| 384 | + assert extract_app_from_client(C()) is app |
| 385 | + assert extract_app_from_client(D()) is app |
| 386 | + assert extract_app_from_client(None) is None |
| 387 | + |
| 388 | + |
| 389 | +def test_wrap_client_with_coverage_records_various_call_patterns(): |
| 390 | + """Tracked client records calls for string path, request-like, and kwargs.""" |
| 391 | + recorder = Mock() |
| 392 | + |
| 393 | + class DummyReq: |
| 394 | + def __init__(self, path, method="GET"): |
| 395 | + class URL: |
| 396 | + def __init__(self, p): |
| 397 | + self.path = p |
| 398 | + |
| 399 | + self.url = URL(path) |
| 400 | + self.method = method |
| 401 | + |
| 402 | + class Client: |
| 403 | + def get(self, *args, **kwargs): |
| 404 | + return "GET-OK" |
| 405 | + |
| 406 | + def open(self, *args, **kwargs): |
| 407 | + return "OPEN-OK" |
| 408 | + |
| 409 | + client = Client() |
| 410 | + wrapped = wrap_client_with_coverage(client, recorder, "test_fn") |
| 411 | + |
| 412 | + assert wrapped.get("/foo") == "GET-OK" |
| 413 | + recorder.record_call.assert_called_with("/foo", "test_fn", "GET") |
| 414 | + |
| 415 | + recorder.reset_mock() |
| 416 | + |
| 417 | + req = DummyReq("/bar", method="POST") |
| 418 | + assert wrapped.get(req) == "GET-OK" |
| 419 | + recorder.record_call.assert_called_with("/bar", "test_fn", "POST") |
| 420 | + |
| 421 | + recorder.reset_mock() |
| 422 | + |
| 423 | + assert wrapped.open(path="/baz", method="PUT") == "OPEN-OK" |
| 424 | + recorder.record_call.assert_called_with("/baz", "test_fn", "PUT") |
| 425 | + |
| 426 | + |
| 427 | +def test_create_coverage_fixture_returns_existing_client_when_coverage_disabled(): |
| 428 | + """create_coverage_fixture yields existing fixture when coverage disabled.""" |
| 429 | + fixture = create_coverage_fixture("my_client", existing_fixture_name="existing") |
| 430 | + |
| 431 | + class SimpleSession: |
| 432 | + def __init__(self): |
| 433 | + self.config = Mock() |
| 434 | + self.config.getoption.return_value = False |
| 435 | + |
| 436 | + session = SimpleSession() |
| 437 | + |
| 438 | + class Req: |
| 439 | + def __init__(self): |
| 440 | + self.node = Mock() |
| 441 | + self.node.session = session |
| 442 | + |
| 443 | + def getfixturevalue(self, name): |
| 444 | + if name == "existing": |
| 445 | + return "I-AM-EXISTING-CLIENT" |
| 446 | + raise pytest.FixtureLookupError(name) |
| 447 | + |
| 448 | + req = Req() |
| 449 | + raw_fixture = getattr(fixture, "__wrapped__", fixture) |
| 450 | + gen = raw_fixture(req) |
| 451 | + got = next(gen) |
| 452 | + assert got == "I-AM-EXISTING-CLIENT" |
| 453 | + with pytest.raises(StopIteration): |
| 454 | + next(gen) |
| 455 | + |
| 456 | + |
| 457 | +@patch("pytest_api_cov.frameworks.get_framework_adapter") |
| 458 | +def test_create_coverage_fixture_falls_back_to_app_when_no_existing_and_coverage_disabled(mock_get_adapter): |
| 459 | + """When no existing client but an app fixture exists and coverage disabled, create tracked client.""" |
| 460 | + fixture = create_coverage_fixture("my_client", existing_fixture_name=None) |
| 461 | + |
| 462 | + class SimpleSession: |
| 463 | + def __init__(self): |
| 464 | + self.config = Mock() |
| 465 | + self.config.getoption.return_value = False |
| 466 | + |
| 467 | + session = SimpleSession() |
| 468 | + |
| 469 | + class Req: |
| 470 | + def __init__(self): |
| 471 | + self.node = Mock() |
| 472 | + self.node.session = session |
| 473 | + |
| 474 | + def getfixturevalue(self, name): |
| 475 | + if name == "app": |
| 476 | + return "APP-OBJ" |
| 477 | + raise pytest.FixtureLookupError(name) |
| 478 | + |
| 479 | + adapter = Mock() |
| 480 | + adapter.get_tracked_client.return_value = "CLIENT-FROM-APP" |
| 481 | + mock_get_adapter.return_value = adapter |
| 482 | + |
| 483 | + req = Req() |
| 484 | + # Unwrap pytest.fixture wrapper to call the inner generator directly |
| 485 | + raw_fixture = getattr(fixture, "__wrapped__", fixture) |
| 486 | + gen = raw_fixture(req) |
| 487 | + got = next(gen) |
| 488 | + assert got == "CLIENT-FROM-APP" |
| 489 | + mock_get_adapter.assert_called_once_with("APP-OBJ") |
| 490 | + with pytest.raises(StopIteration): |
| 491 | + next(gen) |
0 commit comments