Skip to content

Commit 2c790f1

Browse files
committed
add unit and integration tests
Signed-off-by: Abdullah Yildirim <a3hadi@protonmail.com>
1 parent ada254d commit 2c790f1

16 files changed

Lines changed: 596 additions & 16 deletions

packages/pynumaflow/tests/batchmap/test_async_batch_map.py

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,9 @@
1313
BatchResponses,
1414
BatchResponse,
1515
BatchMapAsyncServer,
16+
NackOptions,
1617
)
18+
from pynumaflow._constants import NACK
1719
from pynumaflow.proto.mapper import map_pb2_grpc
1820
from tests.batchmap.utils import request_generator
1921
from tests.conftest import create_async_loop, start_async_server, teardown_async_server
@@ -23,6 +25,9 @@
2325
LOGGER = setup_logging(__name__)
2426

2527
listen_addr = "unix:///tmp/batch_map.sock"
28+
nack_listen_addr = "unix:///tmp/batch_map_nack.sock"
29+
30+
NACK_TEST_OPTIONS = NackOptions(delay=1000, max_deliveries=3, reason="retry")
2631

2732

2833
class ExampleClass(BatchMapper):
@@ -137,6 +142,53 @@ def test_batch_map(batch_map_stub) -> None:
137142
assert 11 == len(data_resp)
138143

139144

145+
async def nack_handler(datums: AsyncIterable[Datum]) -> BatchResponses:
146+
batch_responses = BatchResponses()
147+
async for datum in datums:
148+
batch_response = BatchResponse.from_id(datum.id)
149+
batch_response.append(Message.to_nack(NACK_TEST_OPTIONS))
150+
batch_responses.append(batch_response)
151+
return batch_responses
152+
153+
154+
async def start_nack_server(udfs):
155+
server = grpc.aio.server()
156+
map_pb2_grpc.add_MapServicer_to_server(udfs, server)
157+
server.add_insecure_port(nack_listen_addr)
158+
logging.info("Starting nack server on %s", nack_listen_addr)
159+
await server.start()
160+
return server, nack_listen_addr
161+
162+
163+
@pytest.fixture(scope="module")
164+
def async_nack_batch_map_server():
165+
loop = create_async_loop()
166+
udfs = BatchMapAsyncServer(nack_handler).servicer
167+
server = start_async_server(loop, start_nack_server(udfs))
168+
yield loop
169+
teardown_async_server(loop, server)
170+
171+
172+
def test_batch_map_nack(async_nack_batch_map_server) -> None:
173+
with grpc.insecure_channel(nack_listen_addr) as channel:
174+
stub = map_pb2_grpc.MapStub(channel)
175+
generator_response = stub.MapFn(request_iterator=request_generator(count=5, session=1))
176+
177+
handshake = next(generator_response)
178+
assert handshake.handshake.sot
179+
180+
data_resp = [r for r in generator_response]
181+
182+
# exclude the trailing EOT response
183+
for resp in data_resp[:-1]:
184+
result = resp.results[0]
185+
assert NACK in result.tags
186+
assert result.nack_options.delay == NACK_TEST_OPTIONS.delay
187+
assert result.nack_options.max_deliveries == NACK_TEST_OPTIONS.max_deliveries
188+
assert result.nack_options.reason == NACK_TEST_OPTIONS.reason
189+
assert data_resp[-1].status.eot is True
190+
191+
140192
def test_is_ready(async_batch_map_server) -> None:
141193
with grpc.insecure_channel(listen_addr) as channel:
142194
stub = map_pb2_grpc.MapStub(channel)

packages/pynumaflow/tests/batchmap/test_messages.py

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import pytest
22

3-
from pynumaflow.batchmapper import Message, DROP, BatchResponse, BatchResponses
3+
from pynumaflow.batchmapper import Message, DROP, BatchResponse, BatchResponses, NackOptions
4+
from pynumaflow._constants import NACK
45
from tests.batchmap.test_datatypes import TEST_ID
56
from tests.testing_utils import mock_message
67

@@ -10,6 +11,27 @@ def _mock_message_object():
1011
return Message(value=value)
1112

1213

14+
def test_message_to_nack():
15+
opts = NackOptions(max_deliveries=2)
16+
msg = Message.to_nack(opts)
17+
assert type(msg) is Message
18+
assert msg.keys == []
19+
assert msg.value == b""
20+
assert msg.tags == [NACK]
21+
assert msg.nack_options == opts
22+
23+
24+
def test_message_to_nack_default_opts():
25+
msg = Message.to_nack()
26+
assert msg.tags == [NACK]
27+
assert msg.nack_options is None
28+
29+
30+
def test_message_default_nack_options():
31+
msg = Message(mock_message())
32+
assert msg.nack_options is None
33+
34+
1335
def test_batch_responses_init():
1436
batch_responses = BatchResponses()
1537
batch_response1 = BatchResponse.from_id(TEST_ID)

packages/pynumaflow/tests/map/test_async_mapper.py

Lines changed: 51 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,8 @@
1616
from pynumaflow.proto.common import metadata_pb2
1717
from pynumaflow.proto.mapper import map_pb2, map_pb2_grpc
1818
from tests.conftest import create_async_loop, start_async_server, teardown_async_server
19-
from tests.map.utils import get_test_datums
19+
from tests.map.utils import get_test_datums, async_nack_map_handler, NACK_TEST_OPTIONS
20+
from pynumaflow._constants import NACK
2021

2122
pytestmark = pytest.mark.integration
2223

@@ -26,6 +27,7 @@
2627
raise_error_from_map = False
2728

2829
SOCK_PATH = "unix:///tmp/async_map.sock"
30+
NACK_SOCK_PATH = "unix:///tmp/async_map_nack.sock"
2931

3032

3133
def request_generator(req):
@@ -106,6 +108,54 @@ def test_run_server(async_map_server):
106108
LOGGER.info("Successfully validated the server")
107109

108110

111+
async def _start_nack_server(udfs):
112+
_server_options = [
113+
("grpc.max_send_message_length", MAX_MESSAGE_SIZE),
114+
("grpc.max_receive_message_length", MAX_MESSAGE_SIZE),
115+
]
116+
server = grpc.aio.server(options=_server_options)
117+
map_pb2_grpc.add_MapServicer_to_server(udfs, server)
118+
server.add_insecure_port(NACK_SOCK_PATH)
119+
logging.info("Starting nack server on %s", NACK_SOCK_PATH)
120+
await server.start()
121+
return server, NACK_SOCK_PATH
122+
123+
124+
@pytest.fixture(scope="module")
125+
def async_nack_map_server():
126+
"""Module-scoped fixture: async gRPC map server whose handler nacks every message."""
127+
loop = create_async_loop()
128+
129+
server_obj = MapAsyncServer(mapper_instance=async_nack_map_handler)
130+
udfs = server_obj.servicer
131+
server = start_async_server(loop, _start_nack_server(udfs))
132+
133+
yield loop
134+
135+
teardown_async_server(loop, server)
136+
137+
138+
def test_map_nack(async_nack_map_server):
139+
with grpc.insecure_channel(NACK_SOCK_PATH) as channel:
140+
stub = map_pb2_grpc.MapStub(channel)
141+
request = get_test_datums()
142+
generator_response = stub.MapFn(request_iterator=request_generator(request))
143+
144+
responses = list(generator_response)
145+
146+
# 1 handshake + 3 data responses
147+
assert len(responses) == 4
148+
assert responses[0].handshake.sot
149+
150+
for resp in responses[1:]:
151+
assert len(resp.results) == 1
152+
result = resp.results[0]
153+
assert NACK in result.tags
154+
assert result.nack_options.delay == NACK_TEST_OPTIONS.delay
155+
assert result.nack_options.max_deliveries == NACK_TEST_OPTIONS.max_deliveries
156+
assert result.nack_options.reason == NACK_TEST_OPTIONS.reason
157+
158+
109159
def test_map(map_stub):
110160
request = get_test_datums()
111161
generator_response: Iterator[map_pb2.MapResponse] = map_stub.MapFn(

packages/pynumaflow/tests/map/test_messages.py

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import pytest
22

3-
from pynumaflow.mapper import Messages, Message, DROP, Mapper, Datum
3+
from pynumaflow.mapper import Messages, Message, DROP, Mapper, Datum, NackOptions
4+
from pynumaflow._constants import NACK
45
from tests.testing_utils import mock_message
56

67

@@ -35,6 +36,27 @@ def test_message_to_drop():
3536
assert mock_obj["Tags"] == msg.tags
3637

3738

39+
def test_message_to_nack():
40+
opts = NackOptions(max_deliveries=2)
41+
msg = Message.to_nack(opts)
42+
assert type(msg) is Message
43+
assert msg.keys == []
44+
assert msg.value == b""
45+
assert msg.tags == [NACK]
46+
assert msg.nack_options == opts
47+
48+
49+
def test_message_to_nack_default_opts():
50+
msg = Message.to_nack()
51+
assert msg.tags == [NACK]
52+
assert msg.nack_options is None
53+
54+
55+
def test_message_default_nack_options():
56+
msg = Message(mock_message())
57+
assert msg.nack_options is None
58+
59+
3860
def test_message_to():
3961
mock_obj = {"Keys": ["__KEY__"], "Value": mock_message(), "Tags": ["__TAG__"]}
4062
msg = Message(value=mock_obj["Value"], keys=mock_obj["Keys"], tags=mock_obj["Tags"])

packages/pynumaflow/tests/map/test_sync_mapper.py

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,15 @@
55

66
from pynumaflow.mapper import MapServer
77
from pynumaflow.proto.mapper import map_pb2
8-
from tests.map.utils import map_handler, err_map_handler, ExampleMap, get_test_datums
8+
from tests.map.utils import (
9+
map_handler,
10+
err_map_handler,
11+
ExampleMap,
12+
get_test_datums,
13+
nack_map_handler,
14+
NACK_TEST_OPTIONS,
15+
)
16+
from pynumaflow._constants import NACK
917
from tests.conftest import collect_responses, drain_responses, send_test_requests
1018

1119

@@ -97,6 +105,32 @@ def test_map_forward_message(map_test_server):
97105
assert code == StatusCode.OK
98106

99107

108+
def test_map_nack():
109+
my_server = MapServer(mapper_instance=nack_map_handler)
110+
services = {map_pb2.DESCRIPTOR.services_by_name["Map"]: my_server.servicer}
111+
test_server = server_from_dictionary(services, strict_real_time())
112+
113+
test_datums = get_test_datums(handshake=True)
114+
method = _invoke_map_fn(test_server)
115+
send_test_requests(method, test_datums)
116+
responses = collect_responses(method)
117+
118+
metadata, code, details = method.termination()
119+
# 1 handshake + 3 data responses
120+
assert len(responses) == 4
121+
assert responses[0].handshake.sot
122+
123+
for resp in responses[1:]:
124+
assert len(resp.results) == 1
125+
result = resp.results[0]
126+
# nack messages carry the NACK tag and the serialized nack options
127+
assert NACK in result.tags
128+
assert result.nack_options.delay == NACK_TEST_OPTIONS.delay
129+
assert result.nack_options.max_deliveries == NACK_TEST_OPTIONS.max_deliveries
130+
assert result.nack_options.reason == NACK_TEST_OPTIONS.reason
131+
assert code == StatusCode.OK
132+
133+
100134
def test_invalid_input():
101135
with pytest.raises(TypeError):
102136
MapServer()

packages/pynumaflow/tests/map/utils.py

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,22 @@
11
from pynumaflow.proto.common import metadata_pb2
22
from pynumaflow.proto.mapper import map_pb2
33

4-
from pynumaflow.mapper import Datum, Messages, Message, Mapper
4+
from pynumaflow.mapper import Datum, Messages, Message, Mapper, NackOptions
55
from tests.testing_utils import mock_message, mock_headers, get_time_args
66

77

8+
# NackOptions used by the nack handlers below; also asserted by the round-trip tests.
9+
NACK_TEST_OPTIONS = NackOptions(delay=1000, max_deliveries=3, reason="retry")
10+
11+
12+
def nack_map_handler(keys: list[str], datum: Datum) -> Messages:
13+
return Messages(Message.to_nack(NACK_TEST_OPTIONS))
14+
15+
16+
async def async_nack_map_handler(keys: list[str], datum: Datum) -> Messages:
17+
return Messages(Message.to_nack(NACK_TEST_OPTIONS))
18+
19+
820
async def async_map_error_fn(keys: list[str], datum: Datum) -> Messages:
921
raise ValueError("error invoking map")
1022

packages/pynumaflow/tests/mapstream/test_async_map_stream.py

Lines changed: 59 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,9 @@
1010
Message,
1111
Datum,
1212
MapStreamAsyncServer,
13+
NackOptions,
1314
)
15+
from pynumaflow._constants import NACK
1416
from pynumaflow.proto.mapper import map_pb2_grpc
1517
from tests.mapstream.utils import request_generator
1618
from tests.conftest import create_async_loop, start_async_server, teardown_async_server
@@ -25,6 +27,9 @@
2527
raise_error_from_map = False
2628

2729
SOCK_PATH = "unix:///tmp/async_map_stream.sock"
30+
NACK_SOCK_PATH = "unix:///tmp/async_map_stream_nack.sock"
31+
32+
NACK_TEST_OPTIONS = NackOptions(delay=1000, max_deliveries=3, reason="retry")
2833

2934

3035
async def async_map_stream_handler(keys: list[str], datum: Datum) -> AsyncIterable[Message]:
@@ -85,7 +90,7 @@ def test_map_stream(map_stream_stub):
8590

8691
# Prepare expected payload
8792
expected_payload = bytes(
88-
"payload:test_mock_message " "event_time:2022-09-12 16:00:00 watermark:2022-09-12 16:01:00",
93+
"payload:test_mock_message event_time:2022-09-12 16:00:00 watermark:2022-09-12 16:01:00",
8994
encoding="utf-8",
9095
)
9196

@@ -106,16 +111,63 @@ def test_map_stream(map_stream_stub):
106111
result_msg_count += 1
107112

108113
# Validate totals
109-
assert (
110-
result_msg_count == expected_result_msgs
111-
), f"Expected {expected_result_msgs} result messages, got {result_msg_count}"
114+
assert result_msg_count == expected_result_msgs, (
115+
f"Expected {expected_result_msgs} result messages, got {result_msg_count}"
116+
)
112117
assert eot_count == expected_eots, f"Expected {expected_eots} EOT messages, got {eot_count}"
113118

114119
# Validate 10 messages per request id: test-id-0..test-id-(req_count-1)
115120
for i in range(req_count):
116-
assert (
117-
id_counter[f"test-id-{i}"] == 10
118-
), f"Expected 10 results for test-id-{i}, got {id_counter[f'test-id-{i}']}"
121+
assert id_counter[f"test-id-{i}"] == 10, (
122+
f"Expected 10 results for test-id-{i}, got {id_counter[f'test-id-{i}']}"
123+
)
124+
125+
126+
async def async_nack_map_stream_handler(keys: list[str], datum: Datum) -> AsyncIterable[Message]:
127+
for i in range(3):
128+
yield Message.to_nack(NACK_TEST_OPTIONS)
129+
130+
131+
async def _start_nack_server(udfs):
132+
server = grpc.aio.server()
133+
map_pb2_grpc.add_MapServicer_to_server(udfs, server)
134+
server.add_insecure_port(NACK_SOCK_PATH)
135+
logging.info("Starting nack server on %s", NACK_SOCK_PATH)
136+
await server.start()
137+
return server, NACK_SOCK_PATH
138+
139+
140+
@pytest.fixture(scope="module")
141+
def async_nack_map_stream_server():
142+
loop = create_async_loop()
143+
server_obj = MapStreamAsyncServer(map_stream_instance=async_nack_map_stream_handler)
144+
udfs = server_obj.servicer
145+
server = start_async_server(loop, _start_nack_server(udfs))
146+
yield loop
147+
teardown_async_server(loop, server)
148+
149+
150+
def test_map_stream_nack(async_nack_map_stream_server):
151+
with grpc.insecure_channel(NACK_SOCK_PATH) as channel:
152+
stub = map_pb2_grpc.MapStub(channel)
153+
generator_response = stub.MapFn(request_iterator=request_generator(count=2, session=1))
154+
155+
handshake = next(generator_response)
156+
assert handshake.handshake.sot
157+
158+
result_count = 0
159+
for msg in generator_response:
160+
if hasattr(msg, "status") and msg.status.eot:
161+
continue
162+
result = msg.results[0]
163+
assert NACK in result.tags
164+
assert result.nack_options.delay == NACK_TEST_OPTIONS.delay
165+
assert result.nack_options.max_deliveries == NACK_TEST_OPTIONS.max_deliveries
166+
assert result.nack_options.reason == NACK_TEST_OPTIONS.reason
167+
result_count += 1
168+
169+
# 2 requests x 3 nack messages each
170+
assert result_count == 6
119171

120172

121173
def test_is_ready(async_map_stream_server):

0 commit comments

Comments
 (0)