Skip to content

Commit 3acd969

Browse files
committed
test: Add sync/async public API parity guard
Asserts that each hand-maintained sync/async client pair (SSEClient, ConnectStrategy, ConnectionClient, ConnectionResult and their Async counterparts) exposes the same public surface, including properties. A member added to one side but forgotten on the other now fails CI. Intentionally one-sided names can be allowlisted per pair with a justification.
1 parent 56be7ad commit 3acd969

1 file changed

Lines changed: 82 additions & 0 deletions

File tree

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
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

Comments
 (0)