Skip to content

Commit 236970d

Browse files
committed
wip
1 parent bb1043b commit 236970d

4 files changed

Lines changed: 106 additions & 11 deletions

File tree

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,3 +75,4 @@ e2e-tests = "uv run pytest tests/ -v -s -m 'account or basin or stream'"
7575
e2e-account-tests = "uv run pytest tests/ -v -s -m account"
7676
e2e-basin-tests = "uv run pytest tests/ -v -s -m basin"
7777
e2e-stream-tests = "uv run pytest tests/ -v -s -m stream"
78+
correctness-tests = "uv run pytest tests/ -v -s -m correctness"

pytest.ini

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,3 +18,4 @@ markers =
1818
stream: tests for stream operations
1919
metrics: tests for metrics operations
2020
access_tokens: tests for access token operations
21+
correctness: correctness tests

tests/conftest.py

Lines changed: 30 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
import pytest
66
import pytest_asyncio
77

8-
from s2_sdk import S2, Compression, Endpoints, S2Basin, S2Stream
8+
from s2_sdk import S2, Compression, Endpoints, Retry, S2Basin, S2Stream
99

1010
pytest_plugins = ["pytest_asyncio"]
1111

@@ -50,22 +50,35 @@ def endpoints() -> Endpoints | None:
5050
return None
5151

5252

53+
@pytest.fixture(scope="session")
54+
def retry() -> Retry | None:
55+
return None
56+
57+
5358
@pytest_asyncio.fixture(scope="session")
5459
async def s2(
55-
access_token: str, compression: Compression, endpoints: Endpoints | None
60+
access_token: str,
61+
compression: Compression,
62+
endpoints: Endpoints | None,
63+
retry: Retry | None,
5664
) -> AsyncGenerator[S2, None]:
57-
async with S2(access_token, endpoints=endpoints, compression=compression) as s2:
65+
async with S2(
66+
access_token,
67+
endpoints=endpoints,
68+
compression=compression,
69+
retry=retry,
70+
) as s2:
5871
yield s2
5972

6073

6174
@pytest.fixture
62-
def basin_name() -> str:
63-
return _basin_name()
75+
def basin_name(basin_prefix: str) -> str:
76+
return _basin_name(basin_prefix)
6477

6578

6679
@pytest.fixture
67-
def basin_names() -> list[str]:
68-
return [_basin_name() for _ in range(3)]
80+
def basin_names(basin_prefix: str) -> list[str]:
81+
return [_basin_name(basin_prefix) for _ in range(3)]
6982

7083

7184
@pytest.fixture
@@ -94,8 +107,10 @@ async def basin(s2: S2, basin_name: str) -> AsyncGenerator[S2Basin, None]:
94107

95108

96109
@pytest_asyncio.fixture(scope="class")
97-
async def shared_basin(s2: S2) -> AsyncGenerator[S2Basin, None]:
98-
basin_name = _basin_name()
110+
async def shared_basin(
111+
s2: S2, basin_prefix: str
112+
) -> AsyncGenerator[S2Basin, None]:
113+
basin_name = _basin_name(basin_prefix)
99114
await s2.create_basin(name=basin_name)
100115

101116
try:
@@ -117,8 +132,12 @@ async def stream(
117132
await basin.delete_stream(stream_name)
118133

119134

120-
def _basin_name() -> str:
121-
return f"{BASIN_PREFIX}-{uuid.uuid4().hex[:8]}"
135+
def _basin_name(prefix: str) -> str:
136+
suffix = uuid.uuid4().hex[:8]
137+
prefix = prefix.strip("-")[: 48 - len(suffix) - 1].strip("-")
138+
if not prefix:
139+
return suffix
140+
return f"{prefix}-{suffix}"
122141

123142

124143
def _stream_name() -> str:

tests/test_correctness.py

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
import asyncio
2+
import sys
3+
4+
import pytest
5+
6+
from s2_sdk import Batching, Record, Retry, S2Stream, SeqNum
7+
8+
TOTAL_RECORDS = 64
9+
10+
11+
@pytest.fixture(scope="session")
12+
def retry() -> Retry:
13+
return Retry(max_attempts=sys.maxsize)
14+
15+
16+
@pytest.fixture(scope="session")
17+
def basin_prefix() -> str:
18+
return "python-correctness"
19+
20+
21+
@pytest.mark.correctness
22+
@pytest.mark.asyncio
23+
async def test_concurrent_producer_and_consumer_remain_gapless(stream: S2Stream):
24+
async def read_records() -> tuple[int, int]:
25+
highest_contiguous_index = -1
26+
last_seq_num: int | None = None
27+
observed_records = 0
28+
29+
async for batch in stream.read_session(start=SeqNum(0)):
30+
for record in batch.records:
31+
seq_num = record.seq_num
32+
if last_seq_num is None:
33+
assert seq_num == 0
34+
else:
35+
assert seq_num == last_seq_num + 1
36+
last_seq_num = seq_num
37+
38+
body = record.body.decode()
39+
index = int(body)
40+
assert 0 <= index < TOTAL_RECORDS
41+
assert index <= highest_contiguous_index + 1
42+
43+
if index == highest_contiguous_index + 1:
44+
highest_contiguous_index = index
45+
observed_records += 1
46+
47+
if highest_contiguous_index + 1 >= TOTAL_RECORDS:
48+
assert highest_contiguous_index == TOTAL_RECORDS - 1
49+
assert observed_records >= TOTAL_RECORDS
50+
return highest_contiguous_index, observed_records
51+
52+
raise AssertionError("read session ended before all records were observed")
53+
54+
async def append_records() -> None:
55+
async with stream.producer(batching=Batching(max_records=4)) as producer:
56+
tickets = []
57+
for i in range(TOTAL_RECORDS):
58+
ticket = await producer.submit(Record(body=str(i).encode()))
59+
tickets.append(ticket)
60+
61+
for ticket in tickets:
62+
ack = await ticket
63+
assert ack.seq_num >= 0
64+
65+
read_task = asyncio.create_task(read_records())
66+
append_task = asyncio.create_task(append_records())
67+
try:
68+
read_result, _ = await asyncio.gather(read_task, append_task)
69+
finally:
70+
for task in (read_task, append_task):
71+
if not task.done():
72+
task.cancel()
73+
74+
assert read_result[0] == TOTAL_RECORDS - 1

0 commit comments

Comments
 (0)