465 lines
15 KiB
Python
465 lines
15 KiB
Python
from unittest.mock import MagicMock, patch
|
|
|
|
import datasets
|
|
import pyarrow
|
|
import pytest
|
|
import requests
|
|
from packaging.version import Version
|
|
|
|
import ray
|
|
from ray.data.dataset import Dataset, MaterializedDataset
|
|
from ray.tests.conftest import * # noqa
|
|
|
|
|
|
@pytest.fixture
|
|
def mock_hf_dataset():
|
|
"""Create a mock HuggingFace dataset for testing."""
|
|
texts = [
|
|
"Climate change is a serious threat to our planet",
|
|
"We need to take action on global warming",
|
|
"Renewable energy is the future",
|
|
"Fossil fuels are destroying the environment",
|
|
"Solar power is becoming more affordable",
|
|
"Wind energy is growing rapidly",
|
|
"Electric vehicles are the way forward",
|
|
"Carbon emissions must be reduced",
|
|
"Green technology is advancing quickly",
|
|
"Sustainability is important for future generations",
|
|
"Climate science is well established",
|
|
"Ocean levels are rising due to warming",
|
|
"Extreme weather events are increasing",
|
|
"Biodiversity loss is accelerating",
|
|
"Deforestation contributes to climate change",
|
|
"Clean energy jobs are growing",
|
|
"Energy efficiency saves money",
|
|
"Public transportation reduces emissions",
|
|
"Plant-based diets help the environment",
|
|
"Recycling is essential for sustainability",
|
|
]
|
|
|
|
# Create labels array with exactly the same length as texts
|
|
labels = [i % 2 for i in range(len(texts))] # Alternating 0s and 1s
|
|
|
|
return datasets.Dataset.from_dict(
|
|
{
|
|
"text": texts,
|
|
"label": labels,
|
|
}
|
|
)
|
|
|
|
|
|
@pytest.fixture
|
|
def mock_hf_dataset_dict(mock_hf_dataset):
|
|
"""Create a mock HuggingFace DatasetDict for testing."""
|
|
return datasets.DatasetDict({"train": mock_hf_dataset})
|
|
|
|
|
|
@pytest.fixture
|
|
def mock_hf_iterable_dataset():
|
|
"""Create a mock HuggingFace IterableDataset for testing."""
|
|
texts = [
|
|
"Streaming climate tweet 1: The planet is warming",
|
|
"Streaming climate tweet 2: Renewable energy is key",
|
|
"Streaming climate tweet 3: We must act now",
|
|
"Streaming climate tweet 4: Solar panels everywhere",
|
|
"Streaming climate tweet 5: Wind turbines are beautiful",
|
|
"Streaming climate tweet 6: Electric cars are the future",
|
|
"Streaming climate tweet 7: Carbon neutral by 2050",
|
|
"Streaming climate tweet 8: Green energy revolution",
|
|
"Streaming climate tweet 9: Climate action needed",
|
|
"Streaming climate tweet 10: Sustainable development",
|
|
"Streaming climate tweet 11: Ocean conservation",
|
|
"Streaming climate tweet 12: Forest protection",
|
|
"Streaming climate tweet 13: Clean air matters",
|
|
"Streaming climate tweet 14: Water conservation",
|
|
"Streaming climate tweet 15: Biodiversity protection",
|
|
]
|
|
|
|
labels = [1, 0, 1, 0, 0, 0, 1, 0, 1, 0, 1, 1, 0, 0, 1]
|
|
|
|
dataset = datasets.Dataset.from_dict(
|
|
{
|
|
"text": texts,
|
|
"label": labels,
|
|
}
|
|
)
|
|
iterable_dataset = dataset.to_iterable_dataset()
|
|
iterable_dataset.expected_count = len(texts)
|
|
return iterable_dataset
|
|
|
|
|
|
@pytest.fixture
|
|
def mock_parquet_urls():
|
|
"""Fixture providing mock parquet URLs for testing."""
|
|
return [
|
|
"https://huggingface.co/datasets/test/parquet/train-00000-of-00001.parquet",
|
|
"https://huggingface.co/datasets/test/parquet/train-00001-of-00001.parquet",
|
|
]
|
|
|
|
|
|
@pytest.fixture
|
|
def mock_resolved_urls():
|
|
"""Fixture providing mock resolved URLs (after HTTP redirects) for testing."""
|
|
return [
|
|
"https://cdn-lfs.huggingface.co/datasets/test/parquet/train-00000-of-00001.parquet",
|
|
"https://cdn-lfs.huggingface.co/datasets/test/parquet/train-00001-of-00001.parquet",
|
|
]
|
|
|
|
|
|
@pytest.fixture
|
|
def mock_ray_dataset(mock_hf_dataset):
|
|
"""Fixture providing a mock Ray dataset that matches the mock HuggingFace dataset."""
|
|
return ray.data.from_items(
|
|
[
|
|
{"text": text, "label": label}
|
|
for text, label in zip(mock_hf_dataset["text"], mock_hf_dataset["label"])
|
|
]
|
|
)
|
|
|
|
|
|
@pytest.fixture
|
|
def mock_successful_http_responses(mock_parquet_urls):
|
|
"""Fixture providing mock successful HTTP responses for URL resolution."""
|
|
mock_responses = []
|
|
for url in mock_parquet_urls:
|
|
mock_response = MagicMock()
|
|
mock_response.status_code = 200
|
|
mock_response.url = url
|
|
mock_responses.append(mock_response)
|
|
return mock_responses
|
|
|
|
|
|
@pytest.fixture
|
|
def mock_redirected_http_responses(mock_parquet_urls, mock_resolved_urls):
|
|
"""Fixture providing mock HTTP responses that simulate redirects."""
|
|
mock_responses = []
|
|
for original_url, resolved_url in zip(mock_parquet_urls, mock_resolved_urls):
|
|
mock_response = MagicMock()
|
|
mock_response.status_code = 200
|
|
mock_response.url = resolved_url
|
|
mock_responses.append(mock_response)
|
|
return mock_responses
|
|
|
|
|
|
@pytest.fixture
|
|
def mock_huggingface_datasource():
|
|
"""Fixture providing the HuggingFaceDatasource class for mocking."""
|
|
from ray.data._internal.datasource.huggingface_datasource import (
|
|
HuggingFaceDatasource,
|
|
)
|
|
|
|
return HuggingFaceDatasource
|
|
|
|
|
|
def verify_http_requests(mock_requests_head, expected_urls):
|
|
"""Verify that HTTP requests were made correctly."""
|
|
assert mock_requests_head.call_count == len(expected_urls)
|
|
|
|
for i, url in enumerate(expected_urls):
|
|
call_args = mock_requests_head.call_args_list[i]
|
|
assert call_args[0][0] == url
|
|
assert call_args[1]["allow_redirects"] is True
|
|
assert call_args[1]["timeout"] == 5
|
|
|
|
|
|
def verify_read_parquet_call(mock_read_parquet, expected_urls):
|
|
"""Verify that read_parquet was called with correct parameters."""
|
|
mock_read_parquet.assert_called_once()
|
|
call_args = mock_read_parquet.call_args
|
|
|
|
# Check that the parquet URLs were passed
|
|
assert call_args[0][0] == expected_urls
|
|
|
|
# Check that the filesystem is HTTPFileSystem
|
|
assert "filesystem" in call_args[1]
|
|
assert "HTTPFileSystem" in str(type(call_args[1]["filesystem"]))
|
|
|
|
# Check that retry_exceptions includes FileNotFoundError and ClientResponseError
|
|
assert "ray_remote_args" in call_args[1]
|
|
assert FileNotFoundError in call_args[1]["ray_remote_args"]["retry_exceptions"]
|
|
|
|
|
|
def verify_dataset_creation(ds, mock_hf_dataset):
|
|
"""Verify that the dataset was created successfully."""
|
|
assert isinstance(ds, MaterializedDataset)
|
|
assert ds.count() == mock_hf_dataset.num_rows
|
|
|
|
|
|
def setup_parquet_mocks(
|
|
mock_huggingface_datasource,
|
|
mock_parquet_urls,
|
|
mock_http_responses,
|
|
mock_ray_dataset,
|
|
):
|
|
"""Setup common mocking pattern for parquet-based tests."""
|
|
patches = []
|
|
|
|
# Mock the list_parquet_urls_from_dataset method
|
|
datasource_patch = patch.object(
|
|
mock_huggingface_datasource,
|
|
"list_parquet_urls_from_dataset",
|
|
return_value=mock_parquet_urls,
|
|
)
|
|
patches.append(datasource_patch)
|
|
|
|
# Mock the requests.head calls
|
|
requests_patch = patch("requests.head")
|
|
patches.append(requests_patch)
|
|
|
|
# Mock the read_parquet function
|
|
read_parquet_patch = patch("ray.data.read_api.read_parquet")
|
|
patches.append(read_parquet_patch)
|
|
|
|
# Start all patches
|
|
datasource_mock = datasource_patch.start()
|
|
requests_mock = requests_patch.start()
|
|
read_parquet_mock = read_parquet_patch.start()
|
|
|
|
# Configure mocks
|
|
requests_mock.side_effect = mock_http_responses
|
|
read_parquet_mock.return_value = mock_ray_dataset
|
|
|
|
return datasource_mock, requests_mock, read_parquet_mock, patches
|
|
|
|
|
|
def hfds_assert_equals(hfds: datasets.Dataset, ds: Dataset):
|
|
hfds_table = hfds.data.table
|
|
ds_table = pyarrow.concat_tables([ray.get(tbl) for tbl in ds.to_arrow_refs()])
|
|
|
|
sorting = [(name, "descending") for name in hfds_table.column_names]
|
|
hfds_table = hfds_table.sort_by(sorting)
|
|
ds_table = ds_table.sort_by(sorting)
|
|
|
|
assert hfds_table.equals(ds_table)
|
|
|
|
|
|
@pytest.mark.parametrize("num_par", [1, 4])
|
|
def test_from_huggingface(mock_hf_dataset_dict, ray_start_regular_shared, num_par):
|
|
# Check that DatasetDict is not directly supported.
|
|
assert isinstance(mock_hf_dataset_dict, datasets.DatasetDict)
|
|
with pytest.raises(
|
|
DeprecationWarning,
|
|
match="You provided a Hugging Face DatasetDict",
|
|
):
|
|
ray.data.from_huggingface(mock_hf_dataset_dict)
|
|
|
|
ray_datasets = {
|
|
"train": ray.data.from_huggingface(
|
|
mock_hf_dataset_dict["train"], override_num_blocks=num_par
|
|
),
|
|
}
|
|
|
|
assert isinstance(ray_datasets["train"], ray.data.Dataset)
|
|
hfds_assert_equals(mock_hf_dataset_dict["train"], ray_datasets["train"])
|
|
|
|
# Test reading in a split Hugging Face dataset yields correct individual datasets
|
|
base_hf_dataset = mock_hf_dataset_dict["train"]
|
|
hf_dataset_split = base_hf_dataset.train_test_split(test_size=0.2)
|
|
ray_dataset_split_train = ray.data.from_huggingface(hf_dataset_split["train"])
|
|
assert ray_dataset_split_train.count() == hf_dataset_split["train"].num_rows
|
|
|
|
|
|
@pytest.mark.skipif(
|
|
datasets.Version(datasets.__version__) < datasets.Version("2.8.0"),
|
|
reason="IterableDataset.iter() added in 2.8.0",
|
|
)
|
|
@pytest.mark.skipif(
|
|
Version(pyarrow.__version__) < Version("8.0.0"),
|
|
reason="pyarrow.Table.to_reader() added in 8.0.0",
|
|
)
|
|
# Note, pandas is excluded here because IterableDatasets do not support pandas format.
|
|
@pytest.mark.parametrize(
|
|
"batch_format",
|
|
[None, "numpy", "arrow", "torch", "tensorflow", "jax"],
|
|
)
|
|
def test_from_huggingface_streaming(
|
|
mock_hf_iterable_dataset, batch_format, ray_start_regular_shared
|
|
):
|
|
hfds = mock_hf_iterable_dataset.with_format(batch_format)
|
|
assert isinstance(hfds, datasets.IterableDataset)
|
|
|
|
ds = ray.data.from_huggingface(hfds)
|
|
expected_count = mock_hf_iterable_dataset.expected_count
|
|
assert ds.count() == expected_count
|
|
|
|
|
|
@pytest.mark.skipif(
|
|
datasets.Version(datasets.__version__) < datasets.Version("2.8.0"),
|
|
reason="IterableDataset.iter() added in 2.8.0",
|
|
)
|
|
def test_from_huggingface_dynamic_generated(ray_start_regular_shared):
|
|
# https://github.com/ray-project/ray/issues/49529
|
|
# Mock the dynamic dataset loading
|
|
mock_dataset = datasets.Dataset.from_dict(
|
|
{
|
|
"text": [
|
|
"dynamic tweet 1",
|
|
"dynamic tweet 2",
|
|
"dynamic tweet 3",
|
|
"dynamic tweet 4",
|
|
"dynamic tweet 5",
|
|
],
|
|
"label": [0, 1, 0, 1, 0],
|
|
}
|
|
)
|
|
mock_iterable = mock_dataset.to_iterable_dataset()
|
|
|
|
with patch("datasets.load_dataset", return_value=mock_iterable):
|
|
hfds = datasets.load_dataset(
|
|
"dataset-org/dream",
|
|
split="test",
|
|
streaming=True,
|
|
trust_remote_code=True,
|
|
)
|
|
ds = ray.data.from_huggingface(hfds)
|
|
ds.take(1)
|
|
|
|
|
|
@pytest.mark.parametrize("override_num_blocks", [1, 2, 4, 8])
|
|
def test_from_huggingface_override_num_blocks(
|
|
mock_hf_dataset, ray_start_regular_shared, override_num_blocks
|
|
):
|
|
"""Test that override_num_blocks works correctly with HuggingFace datasets."""
|
|
hf_train = mock_hf_dataset
|
|
|
|
ds_subset = ray.data.from_huggingface(
|
|
hf_train, override_num_blocks=override_num_blocks
|
|
)
|
|
|
|
assert isinstance(ds_subset, MaterializedDataset)
|
|
|
|
# Verify number of blocks
|
|
assert ds_subset.num_blocks() == override_num_blocks
|
|
|
|
# Verify data integrity
|
|
assert ds_subset.count() == hf_train.num_rows
|
|
hfds_assert_equals(hf_train, ds_subset)
|
|
|
|
# Test with a smaller subset to test edge cases
|
|
small_size = max(override_num_blocks * 3, 10)
|
|
hf_small = hf_train.select(range(min(small_size, hf_train.num_rows)))
|
|
ds_small = ray.data.from_huggingface(
|
|
hf_small, override_num_blocks=override_num_blocks
|
|
)
|
|
|
|
# Verify number of blocks
|
|
assert ds_small.num_blocks() == override_num_blocks
|
|
|
|
# Verify data integrity
|
|
assert ds_small.count() == hf_small.num_rows
|
|
hfds_assert_equals(hf_small, ds_small)
|
|
|
|
|
|
def test_from_huggingface_with_parquet_files(
|
|
mock_hf_dataset,
|
|
ray_start_regular_shared,
|
|
mock_parquet_urls,
|
|
mock_ray_dataset,
|
|
mock_successful_http_responses,
|
|
mock_huggingface_datasource,
|
|
):
|
|
"""Test the distributed read path when parquet file URLs are available."""
|
|
datasource_mock, requests_mock, read_parquet_mock, patches = setup_parquet_mocks(
|
|
mock_huggingface_datasource,
|
|
mock_parquet_urls,
|
|
mock_successful_http_responses,
|
|
mock_ray_dataset,
|
|
)
|
|
|
|
try:
|
|
ds = ray.data.from_huggingface(mock_hf_dataset)
|
|
|
|
# Verify HTTP requests
|
|
verify_http_requests(requests_mock, mock_parquet_urls)
|
|
|
|
# Verify read_parquet call
|
|
verify_read_parquet_call(read_parquet_mock, mock_parquet_urls)
|
|
|
|
# Verify dataset creation
|
|
verify_dataset_creation(ds, mock_hf_dataset)
|
|
|
|
finally:
|
|
# Stop all patches
|
|
for patch_obj in patches:
|
|
patch_obj.stop()
|
|
|
|
|
|
def test_from_huggingface_with_resolved_urls(
|
|
mock_hf_dataset,
|
|
ray_start_regular_shared,
|
|
mock_parquet_urls,
|
|
mock_resolved_urls,
|
|
mock_ray_dataset,
|
|
mock_redirected_http_responses,
|
|
mock_huggingface_datasource,
|
|
):
|
|
"""Test the URL resolution logic when HTTP redirects are encountered."""
|
|
datasource_mock, requests_mock, read_parquet_mock, patches = setup_parquet_mocks(
|
|
mock_huggingface_datasource,
|
|
mock_parquet_urls,
|
|
mock_redirected_http_responses,
|
|
mock_ray_dataset,
|
|
)
|
|
|
|
try:
|
|
ds = ray.data.from_huggingface(mock_hf_dataset)
|
|
|
|
# Verify HTTP requests
|
|
verify_http_requests(requests_mock, mock_parquet_urls)
|
|
|
|
# Verify read_parquet call with resolved URLs
|
|
verify_read_parquet_call(read_parquet_mock, mock_resolved_urls)
|
|
|
|
# Verify dataset creation
|
|
verify_dataset_creation(ds, mock_hf_dataset)
|
|
|
|
finally:
|
|
# Stop all patches
|
|
for patch_obj in patches:
|
|
patch_obj.stop()
|
|
|
|
|
|
def test_from_huggingface_url_resolution_failures(
|
|
mock_hf_dataset,
|
|
ray_start_regular_shared,
|
|
mock_parquet_urls,
|
|
mock_ray_dataset,
|
|
mock_huggingface_datasource,
|
|
):
|
|
"""Test URL resolution failures fall back to single node read."""
|
|
# Convert the mock dataset to an IterableDataset so it uses the read_datasource fallback
|
|
mock_iterable_dataset = mock_hf_dataset.to_iterable_dataset()
|
|
|
|
with patch.object(
|
|
mock_huggingface_datasource,
|
|
"list_parquet_urls_from_dataset",
|
|
return_value=mock_parquet_urls,
|
|
):
|
|
# Mock the requests.head calls to simulate failures
|
|
with patch("requests.head") as mock_requests_head:
|
|
# Configure mock to raise an exception for all URLs
|
|
mock_requests_head.side_effect = requests.RequestException(
|
|
"Connection failed"
|
|
)
|
|
|
|
# Mock the fallback path
|
|
with patch("ray.data.read_api.read_datasource") as mock_read_datasource:
|
|
mock_read_datasource.return_value = mock_ray_dataset
|
|
|
|
ds = ray.data.from_huggingface(mock_iterable_dataset)
|
|
|
|
# Verify that requests.head was called for each URL
|
|
assert mock_requests_head.call_count == len(mock_parquet_urls)
|
|
|
|
# Verify that the fallback read_datasource was called
|
|
mock_read_datasource.assert_called_once()
|
|
|
|
# Verify the dataset was created successfully via fallback
|
|
verify_dataset_creation(ds, mock_hf_dataset)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
import sys
|
|
|
|
sys.exit(pytest.main(["-v", __file__]))
|