|
| 1 | +""" |
| 2 | +Parity guard for the hand-maintained synchronous and asynchronous client pairs. |
| 3 | +
|
| 4 | +The sync and async implementations in this package are written and maintained by hand as |
| 5 | +parallel files. This test asserts that each public sync/async pair exposes the same public |
| 6 | +surface, so that a method or property added to one side but forgotten on the other fails CI. |
| 7 | +
|
| 8 | +The public surface includes properties as well as methods, so the comparison is over all |
| 9 | +public attribute names (anything not starting with ``_``) rather than callables only. |
| 10 | +""" |
| 11 | + |
| 12 | +import pytest |
| 13 | + |
| 14 | +from ld_eventsource.async_client import AsyncSSEClient |
| 15 | +from ld_eventsource.config.async_connect_strategy import (AsyncConnectionClient, |
| 16 | + AsyncConnectionResult, |
| 17 | + AsyncConnectStrategy) |
| 18 | +from ld_eventsource.config.connect_strategy import (ConnectionClient, |
| 19 | + ConnectionResult, |
| 20 | + ConnectStrategy) |
| 21 | +from ld_eventsource.sse_client import SSEClient |
| 22 | + |
| 23 | +# Each entry pairs a public synchronous class with its asynchronous counterpart, plus |
| 24 | +# allowlists of names that are intentionally present on only one side. Every allowlisted |
| 25 | +# name must carry a justification; if the test flags a genuinely one-sided member, add it |
| 26 | +# here with a comment rather than weakening the comparison. |
| 27 | +_PAIRS = [ |
| 28 | + { |
| 29 | + "sync": SSEClient, |
| 30 | + "async": AsyncSSEClient, |
| 31 | + # Names allowed to exist only on the sync class. |
| 32 | + "sync_only": set(), |
| 33 | + # Names allowed to exist only on the async class. |
| 34 | + "async_only": set(), |
| 35 | + }, |
| 36 | + { |
| 37 | + "sync": ConnectStrategy, |
| 38 | + "async": AsyncConnectStrategy, |
| 39 | + "sync_only": set(), |
| 40 | + "async_only": set(), |
| 41 | + }, |
| 42 | + { |
| 43 | + "sync": ConnectionClient, |
| 44 | + "async": AsyncConnectionClient, |
| 45 | + "sync_only": set(), |
| 46 | + "async_only": set(), |
| 47 | + }, |
| 48 | + { |
| 49 | + "sync": ConnectionResult, |
| 50 | + "async": AsyncConnectionResult, |
| 51 | + "sync_only": set(), |
| 52 | + "async_only": set(), |
| 53 | + }, |
| 54 | +] |
| 55 | + |
| 56 | + |
| 57 | +def _public_names(cls): |
| 58 | + return {name for name in dir(cls) if not name.startswith("_")} - set(dir(object)) |
| 59 | + |
| 60 | + |
| 61 | +@pytest.mark.parametrize( |
| 62 | + "pair", |
| 63 | + _PAIRS, |
| 64 | + ids=[f"{p['sync'].__name__}/{p['async'].__name__}" for p in _PAIRS], |
| 65 | +) |
| 66 | +def test_sync_async_public_surface_matches(pair): |
| 67 | + sync_names = _public_names(pair["sync"]) |
| 68 | + async_names = _public_names(pair["async"]) |
| 69 | + |
| 70 | + sync_only = sync_names - async_names - pair["sync_only"] |
| 71 | + async_only = async_names - sync_names - pair["async_only"] |
| 72 | + |
| 73 | + assert not sync_only, ( |
| 74 | + f"{pair['sync'].__name__} exposes public names with no counterpart on " |
| 75 | + f"{pair['async'].__name__}: {sorted(sync_only)}. Add the missing member to the " |
| 76 | + f"async class, or allowlist it in 'sync_only' with a justification." |
| 77 | + ) |
| 78 | + assert not async_only, ( |
| 79 | + f"{pair['async'].__name__} exposes public names with no counterpart on " |
| 80 | + f"{pair['sync'].__name__}: {sorted(async_only)}. Add the missing member to the " |
| 81 | + f"sync class, or allowlist it in 'async_only' with a justification." |
| 82 | + ) |
0 commit comments