Skip to content

fix: add a AsyncTestClientTransport for the AsyncTestClient#4142

Closed
euri10 wants to merge 14 commits intolitestar-org:mainfrom
euri10:1920
Closed

fix: add a AsyncTestClientTransport for the AsyncTestClient#4142
euri10 wants to merge 14 commits intolitestar-org:mainfrom
euri10:1920

Conversation

@euri10
Copy link
Copy Markdown
Contributor

@euri10 euri10 commented Apr 24, 2025

Description

Closes

fixes #1920

@codecov
Copy link
Copy Markdown

codecov Bot commented Apr 24, 2025

Codecov Report

Attention: Patch coverage is 85.71429% with 4 lines in your changes missing coverage. Please review.

Project coverage is 98.35%. Comparing base (6225a82) to head (4dffcb5).
Report is 1 commits behind head on main.

Files with missing lines Patch % Lines
litestar/testing/transport.py 85.18% 4 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #4142      +/-   ##
==========================================
- Coverage   98.37%   98.35%   -0.02%     
==========================================
  Files         348      348              
  Lines       15845    15861      +16     
  Branches     1749     1750       +1     
==========================================
+ Hits        15587    15600      +13     
- Misses        123      127       +4     
+ Partials      135      134       -1     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@euri10 euri10 marked this pull request as draft April 24, 2025 07:38
@github-actions
Copy link
Copy Markdown

Documentation preview will be available shortly at https://litestar-org.github.io/litestar-docs-preview/4142

@euri10
Copy link
Copy Markdown
Contributor Author

euri10 commented Apr 24, 2025

any idea how to cover this ? I dont understand why it pops now though

@euri10 euri10 marked this pull request as ready for review April 24, 2025 14:31

app = Litestar(route_handlers=[return_loop_id])

async with AsyncTestClient(app) as client_1, AsyncTestClient(app) as client_2:
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should even be the same loop with different client instances, right?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i think so, this added test is just the mcve from the issue and it fails before the changes, is there something I should add ?

Copy link
Copy Markdown
Contributor Author

@euri10 euri10 Apr 24, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I realize the diff is overly complicate, the PR is simple: before AsyncTestClient used TestClientTransport hence the issue.
so I created a BaseClientTransport that essentially takes care of request/response and added a ASGITestClientTransport that inherits httpx ASGIBaseTransport and overrides handle_async_request while TestClientTransport overrides handle_request, the BaseClientTransport is what's common between the 2 transports.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i think so, this added test is just the mcve from the issue and it fails before the changes, is there something I should add ?

Yeah, maybe a test for the transport in particular that shows the client always uses the currently running loop?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

actually if this test was using TestClient instead of AsyncTestClient it wouldn't pass as start_blocking_portal creates a new event loop.

This should even be the same loop with different client instances, right?

by different client instances you meant a AsyncTestClient and a TestClient ?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No I mean different instances AsyncClient. But actually, I think the test should probably just show that the client uses the running loop from the context it's created in.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With this refactoring done, I think you actually don't even need 2 transports. The sync version could just do

with self.client.portal() as portal:
    return portal.call(self.handle_async_request, request)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the benefit of having 2 transports is that they respectively inherit their httpx counterparts so I think it's a little bit more mypy friendly but I may be wrong

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay, that makes sense. Maybe could still move the request handling fn into the common base class?

@provinzkraut
Copy link
Copy Markdown
Member

I don't think the behaviour is fully fixed yet, as the LifespanHandler still uses a portal.

async def test_lifespan_loop() -> None:
    mock = MagicMock()

    @contextlib.asynccontextmanager
    async def lifespan(app: Litestar) -> AsyncGenerator[None, None]:
        mock(asyncio.get_running_loop())
        yield

    app = Litestar(lifespan=[lifespan])

    async with AsyncTestClient(app):
        pass

    mock.assert_called_once_with(asyncio.get_running_loop())

@euri10
Copy link
Copy Markdown
Contributor Author

euri10 commented Apr 26, 2025

alright maybe we nee don top of this a version of LifespanHandler that "skips" the portal in case of the AsyncClient ?

@provinzkraut
Copy link
Copy Markdown
Member

alright maybe we nee don top of this a version of LifespanHandler that "skips" the portal in case of the AsyncClient ?

I think so. Or, probably easier to do, we just have LifespanHandler be async by default, and wrap it with a portal call for the sync implementation?

@euri10
Copy link
Copy Markdown
Contributor Author

euri10 commented Apr 26, 2025

im kinda bocked on 2 failing tests during the lifespan shutdown though the new tests work fine, but I dont see a clear reason, any idea ?

@provinzkraut
Copy link
Copy Markdown
Member

im kinda bocked on 2 failing tests during the lifespan shutdown though the new tests work fine, but I dont see a clear reason, any idea ?

Removing the pytest.raises gives us this:

FAILED tests/unit/test_testing/test_test_client.py::test_error_handling_on_shutdown[AsyncTestClient-asyncio] - exceptiongroup.ExceptionGroup: unhandled errors in a TaskGroup (1 sub-exception)
FAILED tests/unit/test_testing/test_test_client.py::test_error_handling_on_shutdown[AsyncTestClient-trio] - exceptiongroup.ExceptionGroup: unhandled errors in a TaskGroup (1 sub-exception)
FAILED tests/unit/test_testing/test_test_client.py::test_error_handling_on_shutdown[TestClient-asyncio] - RuntimeError
FAILED tests/unit/test_testing/test_test_client.py::test_error_handling_on_shutdown[TestClient-trio] - RuntimeError

so it seems that the issue is that the async client raises an exception group, where the sync client just raises a regular exception. The reason for that is that the async lifespan handler handles things in a task group.

I think the cleanest solution here would be to have the implementation be in the async handler only, and just adapt that for the sync version with the portal calls. Then the behaviour should be the same. (I think)

@euri10
Copy link
Copy Markdown
Contributor Author

euri10 commented Sep 25, 2025

closing, this was fixed by #4291

@euri10 euri10 closed this Sep 25, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Bug: the app for test client is running in a different event loop

2 participants