Skip to content

Commit 2680f7b

Browse files
authored
fix: Refactor testing and sort out unit and integration tests (#2975)
* Refactor go feature server Signed-off-by: Kevin Zhang <kzhang@tecton.ai> * Fix lint Signed-off-by: Kevin Zhang <kzhang@tecton.ai> * Fix e2e tests Signed-off-by: Kevin Zhang <kzhang@tecton.ai> * Fix Signed-off-by: Kevin Zhang <kzhang@tecton.ai> * Verify tests Signed-off-by: Kevin Zhang <kzhang@tecton.ai> * Fix lint Signed-off-by: Kevin Zhang <kzhang@tecton.ai> * Address review Signed-off-by: Kevin Zhang <kzhang@tecton.ai> * Fix Signed-off-by: Kevin Zhang <kzhang@tecton.ai> * Address review Signed-off-by: Kevin Zhang <kzhang@tecton.ai> * Fix Signed-off-by: Kevin Zhang <kzhang@tecton.ai> * Fix lint Signed-off-by: Kevin Zhang <kzhang@tecton.ai> * address review Signed-off-by: Kevin Zhang <kzhang@tecton.ai> * Fix Signed-off-by: Kevin Zhang <kzhang@tecton.ai> * Fix Signed-off-by: Kevin Zhang <kzhang@tecton.ai> * Fix lint Signed-off-by: Kevin Zhang <kzhang@tecton.ai> * Fix lint Signed-off-by: Kevin Zhang <kzhang@tecton.ai> * Fix Signed-off-by: Kevin Zhang <kzhang@tecton.ai> * Fix Signed-off-by: Kevin Zhang <kzhang@tecton.ai> * Fix lint Signed-off-by: Kevin Zhang <kzhang@tecton.ai> * Fix Signed-off-by: Kevin Zhang <kzhang@tecton.ai> * Refactor Signed-off-by: Kevin Zhang <kzhang@tecton.ai> * Fix Signed-off-by: Kevin Zhang <kzhang@tecton.ai> * Fx lit Signed-off-by: Kevin Zhang <kzhang@tecton.ai> * Fix lint Signed-off-by: Kevin Zhang <kzhang@tecton.ai> * update fix Signed-off-by: Kevin Zhang <kzhang@tecton.ai> * Fix Signed-off-by: Kevin Zhang <kzhang@tecton.ai> * Revert Signed-off-by: Kevin Zhang <kzhang@tecton.ai> * Fix Signed-off-by: Kevin Zhang <kzhang@tecton.ai> * fix Signed-off-by: Kevin Zhang <kzhang@tecton.ai> * Fix lint Signed-off-by: Kevin Zhang <kzhang@tecton.ai>
1 parent f2696e0 commit 2680f7b

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

47 files changed

+3027
-3021
lines changed

sdk/python/tests/conftest.py

+4-16
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,6 @@
1414
import logging
1515
import multiprocessing
1616
import os
17-
import socket
18-
from contextlib import closing
1917
from datetime import datetime, timedelta
2018
from multiprocessing import Process
2119
from sys import platform
@@ -45,6 +43,7 @@
4543
from tests.integration.feature_repos.universal.data_sources.file import ( # noqa: E402
4644
FileDataSourceCreator,
4745
)
46+
from tests.utils.http_server import check_port_open, free_port # noqa: E402
4847

4948
logger = logging.getLogger(__name__)
5049

@@ -327,7 +326,7 @@ def feature_server_endpoint(environment):
327326
yield environment.feature_store.get_feature_server_endpoint()
328327
return
329328

330-
port = _free_port()
329+
port = free_port()
331330

332331
proc = Process(
333332
target=start_test_local_server,
@@ -340,7 +339,7 @@ def feature_server_endpoint(environment):
340339
proc.start()
341340
# Wait for server to start
342341
wait_retry_backoff(
343-
lambda: (None, _check_port_open("localhost", port)),
342+
lambda: (None, check_port_open("localhost", port)),
344343
timeout_secs=10,
345344
)
346345

@@ -353,23 +352,12 @@ def feature_server_endpoint(environment):
353352
wait_retry_backoff(
354353
lambda: (
355354
None,
356-
not _check_port_open("localhost", environment.get_local_server_port()),
355+
not check_port_open("localhost", environment.get_local_server_port()),
357356
),
358357
timeout_secs=30,
359358
)
360359

361360

362-
def _check_port_open(host, port) -> bool:
363-
with closing(socket.socket(socket.AF_INET, socket.SOCK_STREAM)) as sock:
364-
return sock.connect_ex((host, port)) == 0
365-
366-
367-
def _free_port():
368-
sock = socket.socket()
369-
sock.bind(("", 0))
370-
return sock.getsockname()[1]
371-
372-
373361
@pytest.fixture
374362
def universal_data_sources(environment) -> TestData:
375363
return construct_universal_test_data(environment)

sdk/python/tests/integration/e2e/test_go_feature_server.py

+91-124
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
1-
import socket
21
import threading
32
import time
4-
from contextlib import closing
53
from datetime import datetime
64
from typing import List
75

@@ -11,10 +9,10 @@
119
import pytz
1210
import requests
1311

14-
from feast import FeatureService, FeatureView, ValueType
1512
from feast.embedded_go.online_features_service import EmbeddedOnlineFeatureServer
1613
from feast.feast_object import FeastObject
1714
from feast.feature_logging import LoggingConfig
15+
from feast.feature_service import FeatureService
1816
from feast.infra.feature_servers.base_config import FeatureLoggingConfig
1917
from feast.protos.feast.serving.ServingService_pb2 import (
2018
FieldStatus,
@@ -24,6 +22,7 @@
2422
from feast.protos.feast.serving.ServingService_pb2_grpc import ServingServiceStub
2523
from feast.protos.feast.types.Value_pb2 import RepeatedValue
2624
from feast.type_map import python_values_to_proto_values
25+
from feast.value_type import ValueType
2726
from feast.wait import wait_retry_backoff
2827
from tests.integration.feature_repos.repo_configuration import (
2928
construct_universal_feature_views,
@@ -33,94 +32,8 @@
3332
driver,
3433
location,
3534
)
36-
37-
38-
@pytest.fixture
39-
def initialized_registry(environment, universal_data_sources):
40-
fs = environment.feature_store
41-
42-
_, _, data_sources = universal_data_sources
43-
feature_views = construct_universal_feature_views(data_sources)
44-
45-
feature_service = FeatureService(
46-
name="driver_features",
47-
features=[feature_views.driver],
48-
logging_config=LoggingConfig(
49-
destination=environment.data_source_creator.create_logged_features_destination(),
50-
sample_rate=1.0,
51-
),
52-
)
53-
feast_objects: List[FeastObject] = [feature_service]
54-
feast_objects.extend(feature_views.values())
55-
feast_objects.extend([driver(), customer(), location()])
56-
57-
fs.apply(feast_objects)
58-
fs.materialize(environment.start_date, environment.end_date)
59-
60-
61-
def server_port(environment, server_type: str):
62-
if not environment.test_repo_config.go_feature_serving:
63-
pytest.skip("Only for Go path")
64-
65-
fs = environment.feature_store
66-
67-
embedded = EmbeddedOnlineFeatureServer(
68-
repo_path=str(fs.repo_path.absolute()),
69-
repo_config=fs.config,
70-
feature_store=fs,
71-
)
72-
port = free_port()
73-
if server_type == "grpc":
74-
target = embedded.start_grpc_server
75-
elif server_type == "http":
76-
target = embedded.start_http_server
77-
else:
78-
raise ValueError("Server Type must be either 'http' or 'grpc'")
79-
80-
t = threading.Thread(
81-
target=target,
82-
args=("127.0.0.1", port),
83-
kwargs=dict(
84-
enable_logging=True,
85-
logging_options=FeatureLoggingConfig(
86-
enabled=True,
87-
queue_capacity=100,
88-
write_to_disk_interval_secs=1,
89-
flush_interval_secs=1,
90-
emit_timeout_micro_secs=10000,
91-
),
92-
),
93-
)
94-
t.start()
95-
96-
wait_retry_backoff(
97-
lambda: (None, check_port_open("127.0.0.1", port)), timeout_secs=15
98-
)
99-
100-
yield port
101-
if server_type == "grpc":
102-
embedded.stop_grpc_server()
103-
else:
104-
embedded.stop_http_server()
105-
106-
# wait for graceful stop
107-
time.sleep(5)
108-
109-
110-
@pytest.fixture
111-
def grpc_server_port(environment, initialized_registry):
112-
yield from server_port(environment, "grpc")
113-
114-
115-
@pytest.fixture
116-
def http_server_port(environment, initialized_registry):
117-
yield from server_port(environment, "http")
118-
119-
120-
@pytest.fixture
121-
def grpc_client(grpc_server_port):
122-
ch = grpc.insecure_channel(f"localhost:{grpc_server_port}")
123-
yield ServingServiceStub(ch)
35+
from tests.utils.http_server import check_port_open, free_port
36+
from tests.utils.test_log_creator import generate_expected_logs, get_latest_rows
12437

12538

12639
@pytest.mark.integration
@@ -254,43 +167,97 @@ def retrieve():
254167
pd.testing.assert_frame_equal(expected_logs, persisted_logs, check_dtype=False)
255168

256169

257-
def free_port():
258-
sock = socket.socket()
259-
sock.bind(("", 0))
260-
return sock.getsockname()[1]
170+
"""
171+
Start go feature server either on http or grpc based on the repo configuration for testing.
172+
"""
261173

262174

263-
def check_port_open(host, port) -> bool:
264-
with closing(socket.socket(socket.AF_INET, socket.SOCK_STREAM)) as sock:
265-
return sock.connect_ex((host, port)) == 0
175+
def _server_port(environment, server_type: str):
176+
if not environment.test_repo_config.go_feature_serving:
177+
pytest.skip("Only for Go path")
266178

179+
fs = environment.feature_store
267180

268-
def get_latest_rows(df, join_key, entity_values):
269-
rows = df[df[join_key].isin(entity_values)]
270-
return rows.loc[rows.groupby(join_key)["event_timestamp"].idxmax()]
181+
embedded = EmbeddedOnlineFeatureServer(
182+
repo_path=str(fs.repo_path.absolute()),
183+
repo_config=fs.config,
184+
feature_store=fs,
185+
)
186+
port = free_port()
187+
if server_type == "grpc":
188+
target = embedded.start_grpc_server
189+
elif server_type == "http":
190+
target = embedded.start_http_server
191+
else:
192+
raise ValueError("Server Type must be either 'http' or 'grpc'")
193+
194+
t = threading.Thread(
195+
target=target,
196+
args=("127.0.0.1", port),
197+
kwargs=dict(
198+
enable_logging=True,
199+
logging_options=FeatureLoggingConfig(
200+
enabled=True,
201+
queue_capacity=100,
202+
write_to_disk_interval_secs=1,
203+
flush_interval_secs=1,
204+
emit_timeout_micro_secs=10000,
205+
),
206+
),
207+
)
208+
t.start()
271209

210+
wait_retry_backoff(
211+
lambda: (None, check_port_open("127.0.0.1", port)), timeout_secs=15
212+
)
272213

273-
def generate_expected_logs(
274-
df: pd.DataFrame,
275-
feature_view: FeatureView,
276-
features: List[str],
277-
join_keys: List[str],
278-
timestamp_column: str,
279-
):
280-
logs = pd.DataFrame()
281-
for join_key in join_keys:
282-
logs[join_key] = df[join_key]
283-
284-
for feature in features:
285-
col = f"{feature_view.name}__{feature}"
286-
logs[col] = df[feature]
287-
logs[f"{col}__timestamp"] = df[timestamp_column]
288-
logs[f"{col}__status"] = FieldStatus.PRESENT
289-
if feature_view.ttl:
290-
logs[f"{col}__status"] = logs[f"{col}__status"].mask(
291-
df[timestamp_column]
292-
< datetime.utcnow().replace(tzinfo=pytz.UTC) - feature_view.ttl,
293-
FieldStatus.OUTSIDE_MAX_AGE,
294-
)
214+
yield port
215+
if server_type == "grpc":
216+
embedded.stop_grpc_server()
217+
else:
218+
embedded.stop_http_server()
295219

296-
return logs.sort_values(by=join_keys).reset_index(drop=True)
220+
# wait for graceful stop
221+
time.sleep(5)
222+
223+
224+
# Go test fixtures
225+
226+
227+
@pytest.fixture
228+
def initialized_registry(environment, universal_data_sources):
229+
fs = environment.feature_store
230+
231+
_, _, data_sources = universal_data_sources
232+
feature_views = construct_universal_feature_views(data_sources)
233+
234+
feature_service = FeatureService(
235+
name="driver_features",
236+
features=[feature_views.driver],
237+
logging_config=LoggingConfig(
238+
destination=environment.data_source_creator.create_logged_features_destination(),
239+
sample_rate=1.0,
240+
),
241+
)
242+
feast_objects: List[FeastObject] = [feature_service]
243+
feast_objects.extend(feature_views.values())
244+
feast_objects.extend([driver(), customer(), location()])
245+
246+
fs.apply(feast_objects)
247+
fs.materialize(environment.start_date, environment.end_date)
248+
249+
250+
@pytest.fixture
251+
def grpc_server_port(environment, initialized_registry):
252+
yield from _server_port(environment, "grpc")
253+
254+
255+
@pytest.fixture
256+
def http_server_port(environment, initialized_registry):
257+
yield from _server_port(environment, "http")
258+
259+
260+
@pytest.fixture
261+
def grpc_client(grpc_server_port):
262+
ch = grpc.insecure_channel(f"localhost:{grpc_server_port}")
263+
yield ServingServiceStub(ch)

sdk/python/tests/integration/e2e/test_python_feature_server.py

+7-3
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,9 @@ def test_get_online_features(python_fs_client):
5858
@pytest.mark.integration
5959
@pytest.mark.universal_online_stores
6060
def test_push(python_fs_client):
61-
initial_temp = get_temperatures(python_fs_client, location_ids=[1])[0]
61+
initial_temp = _get_temperatures_from_feature_server(
62+
python_fs_client, location_ids=[1]
63+
)[0]
6264
json_data = json.dumps(
6365
{
6466
"push_source_name": "location_stats_push_source",
@@ -77,10 +79,12 @@ def test_push(python_fs_client):
7779

7880
# Check new pushed temperature is fetched
7981
assert response.status_code == 200
80-
assert get_temperatures(python_fs_client, location_ids=[1]) == [initial_temp * 100]
82+
assert _get_temperatures_from_feature_server(
83+
python_fs_client, location_ids=[1]
84+
) == [initial_temp * 100]
8185

8286

83-
def get_temperatures(client, location_ids: List[int]):
87+
def _get_temperatures_from_feature_server(client, location_ids: List[int]):
8488
get_request_data = {
8589
"features": ["pushable_location_stats:temperature"],
8690
"entities": {"location_id": location_ids},

0 commit comments

Comments
 (0)