|
1 | 1 | from datetime import timedelta |
2 | 2 | from typing import cast |
3 | | -from unittest.mock import MagicMock |
| 3 | +from unittest.mock import MagicMock, patch |
4 | 4 |
|
5 | 5 | import pytest |
6 | 6 | from pyspark.sql import DataFrame |
|
15 | 15 | from feast.infra.common.retrieval_task import HistoricalRetrievalTask |
16 | 16 | from feast.infra.compute_engines.spark.compute import SparkComputeEngine |
17 | 17 | from feast.infra.compute_engines.spark.job import SparkDAGRetrievalJob |
| 18 | +from feast.infra.compute_engines.spark.utils import _ensure_s3a_event_log_dir |
18 | 19 | from feast.infra.offline_stores.contrib.spark_offline_store.spark import ( |
19 | 20 | SparkOfflineStore, |
20 | 21 | ) |
@@ -192,5 +193,76 @@ def tqdm_builder(length): |
192 | 193 | spark_environment.teardown() |
193 | 194 |
|
194 | 195 |
|
| 196 | +# --------------------------------------------------------------------------- |
| 197 | +# Unit tests for _ensure_s3a_event_log_dir — no Spark dependency needed |
| 198 | +# --------------------------------------------------------------------------- |
| 199 | + |
| 200 | + |
| 201 | +def _base_conf(event_log_dir: str) -> dict: |
| 202 | + return { |
| 203 | + "spark.eventLog.enabled": "true", |
| 204 | + "spark.eventLog.dir": event_log_dir, |
| 205 | + "spark.hadoop.fs.s3a.endpoint": "http://minio:9000", |
| 206 | + } |
| 207 | + |
| 208 | + |
| 209 | +@patch("feast.infra.compute_engines.spark.utils.boto3") |
| 210 | +def test_ensure_s3a_event_log_dir_creates_placeholder_when_empty(mock_boto3): |
| 211 | + """S3A prefix doesn't exist → placeholder object is written.""" |
| 212 | + s3 = MagicMock() |
| 213 | + mock_boto3.client.return_value = s3 |
| 214 | + s3.list_objects_v2.return_value = {"KeyCount": 0} |
| 215 | + |
| 216 | + _ensure_s3a_event_log_dir(_base_conf("s3a://my-bucket/spark-events/")) |
| 217 | + |
| 218 | + s3.list_objects_v2.assert_called_once_with( |
| 219 | + Bucket="my-bucket", Prefix="spark-events/", MaxKeys=1 |
| 220 | + ) |
| 221 | + s3.put_object.assert_called_once_with( |
| 222 | + Bucket="my-bucket", Key="spark-events/.keep", Body=b"" |
| 223 | + ) |
| 224 | + |
| 225 | + |
| 226 | +@patch("feast.infra.compute_engines.spark.utils.boto3") |
| 227 | +def test_ensure_s3a_event_log_dir_skips_when_prefix_exists(mock_boto3): |
| 228 | + """S3A prefix already has objects → no placeholder written.""" |
| 229 | + s3 = MagicMock() |
| 230 | + mock_boto3.client.return_value = s3 |
| 231 | + s3.list_objects_v2.return_value = {"KeyCount": 3} |
| 232 | + |
| 233 | + _ensure_s3a_event_log_dir(_base_conf("s3a://my-bucket/spark-events/")) |
| 234 | + |
| 235 | + s3.put_object.assert_not_called() |
| 236 | + |
| 237 | + |
| 238 | +@patch("feast.infra.compute_engines.spark.utils.boto3") |
| 239 | +def test_ensure_s3a_event_log_dir_noop_when_event_log_disabled(mock_boto3): |
| 240 | + """spark.eventLog.enabled != true → boto3 never called.""" |
| 241 | + _ensure_s3a_event_log_dir( |
| 242 | + {"spark.eventLog.enabled": "false", "spark.eventLog.dir": "s3a://b/p/"} |
| 243 | + ) |
| 244 | + mock_boto3.client.assert_not_called() |
| 245 | + |
| 246 | + |
| 247 | +@patch("feast.infra.compute_engines.spark.utils.boto3") |
| 248 | +def test_ensure_s3a_event_log_dir_noop_for_non_s3a_path(mock_boto3): |
| 249 | + """Non-S3A paths (hdfs://, file://, etc.) are left untouched.""" |
| 250 | + _ensure_s3a_event_log_dir( |
| 251 | + {"spark.eventLog.enabled": "true", "spark.eventLog.dir": "hdfs:///spark-logs"} |
| 252 | + ) |
| 253 | + mock_boto3.client.assert_not_called() |
| 254 | + |
| 255 | + |
| 256 | +@patch("feast.infra.compute_engines.spark.utils.boto3") |
| 257 | +def test_ensure_s3a_event_log_dir_non_fatal_on_s3_error(mock_boto3): |
| 258 | + """boto3 errors are swallowed — SparkContext will surface its own error.""" |
| 259 | + s3 = MagicMock() |
| 260 | + mock_boto3.client.return_value = s3 |
| 261 | + s3.list_objects_v2.side_effect = Exception("connection refused") |
| 262 | + |
| 263 | + # Must not raise |
| 264 | + _ensure_s3a_event_log_dir(_base_conf("s3a://my-bucket/spark-events/")) |
| 265 | + |
| 266 | + |
195 | 267 | if __name__ == "__main__": |
196 | 268 | test_spark_compute_engine_get_historical_features() |
0 commit comments