621 lines
21 KiB
Python
621 lines
21 KiB
Python
from typing import Any, Dict, List, Optional, Type
|
|
|
|
import numpy as np
|
|
import pandas as pd
|
|
import pyarrow as pa
|
|
import pytest
|
|
from typing_extensions import Hashable
|
|
|
|
import ray
|
|
from ray._common.retry import matches_error
|
|
from ray.data._internal.datasource.parquet_datasource import ParquetDatasource
|
|
from ray.data._internal.execution.interfaces import ExecutionResources
|
|
from ray.data._internal.execution.util import merge_label_selector
|
|
from ray.data._internal.logical.interfaces import LogicalPlan
|
|
from ray.data._internal.logical.interfaces.logical_operator import LogicalOperator
|
|
from ray.data._internal.logical.operators import Read
|
|
from ray.data._internal.memory_tracing import (
|
|
leak_report,
|
|
trace_allocation,
|
|
trace_deallocation,
|
|
)
|
|
from ray.data._internal.planner.exchange.sort_task_spec import SortKey
|
|
from ray.data._internal.remote_fn import _make_hashable, cached_remote_fn
|
|
from ray.data._internal.usage.util import (
|
|
_recorded_operators,
|
|
_recorded_operators_lock,
|
|
)
|
|
from ray.data._internal.util import (
|
|
NULL_SENTINEL,
|
|
find_partition_index,
|
|
get_max_task_capacity,
|
|
iterate_with_retry,
|
|
merge_resources_to_ray_remote_args,
|
|
rows_same,
|
|
)
|
|
from ray.data.tests.conftest import * # noqa: F401, F403
|
|
|
|
|
|
def _check_usage_record(op_names: List[str], clear_after_check: Optional[bool] = True):
|
|
"""Check if operators with given names in `op_names` have been used.
|
|
If `clear_after_check` is True, we clear the list of recorded operators
|
|
(so that subsequent checks do not use existing records of operator usage)."""
|
|
for op_name in op_names:
|
|
assert op_name not in ("Unknown", "ReadCustom", "WriteCustom"), op_name
|
|
with _recorded_operators_lock:
|
|
assert _recorded_operators.get(op_name, 0) > 0, (
|
|
op_name,
|
|
_recorded_operators,
|
|
)
|
|
if clear_after_check:
|
|
with _recorded_operators_lock:
|
|
_recorded_operators.clear()
|
|
|
|
|
|
# Utilities for structural logical plan inspection
|
|
# These provide type-safe alternatives to string-based plan matching
|
|
|
|
|
|
def plan_has_operator(plan: LogicalPlan, op_type: Type[LogicalOperator]) -> bool:
|
|
"""Check if plan contains at least one operator of given type.
|
|
|
|
Args:
|
|
plan: The logical plan to inspect
|
|
op_type: The operator type to search for
|
|
|
|
Returns:
|
|
True if at least one operator of the given type exists in the plan
|
|
"""
|
|
return any(isinstance(op, op_type) for op in plan.dag.post_order_iter())
|
|
|
|
|
|
def plan_operator_comes_before(
|
|
plan: LogicalPlan,
|
|
first_type: Type[LogicalOperator],
|
|
second_type: Type[LogicalOperator],
|
|
) -> bool:
|
|
"""Check if any operator of first_type comes before any operator of second_type.
|
|
|
|
Args:
|
|
plan: The logical plan to inspect
|
|
first_type: The operator type that should come first
|
|
second_type: The operator type that should come second
|
|
|
|
Returns:
|
|
True if at least one operator of first_type appears before at least one
|
|
operator of second_type in post-order traversal, False otherwise.
|
|
"""
|
|
operators = list(plan.dag.post_order_iter())
|
|
first_indices = [i for i, op in enumerate(operators) if isinstance(op, first_type)]
|
|
second_indices = [
|
|
i for i, op in enumerate(operators) if isinstance(op, second_type)
|
|
]
|
|
|
|
if not first_indices or not second_indices:
|
|
return False
|
|
|
|
# Check if the earliest first_type comes before the earliest second_type
|
|
return min(first_indices) < min(second_indices)
|
|
|
|
|
|
def get_operators_of_type(
|
|
plan: LogicalPlan, op_type: Type[LogicalOperator]
|
|
) -> List[LogicalOperator]:
|
|
"""Get all operators of a specific type from the plan.
|
|
|
|
Args:
|
|
plan: The logical plan to inspect
|
|
op_type: The operator type to search for
|
|
|
|
Returns:
|
|
List of all operators of the given type in post-order traversal
|
|
"""
|
|
return [op for op in plan.dag.post_order_iter() if isinstance(op, op_type)]
|
|
|
|
|
|
def get_operator_types(plan: LogicalPlan) -> List[str]:
|
|
"""Get list of operator type names in post-order traversal.
|
|
|
|
Args:
|
|
plan: The logical plan to inspect
|
|
|
|
Returns:
|
|
List of operator class names in the order they appear in post-order traversal
|
|
"""
|
|
return [type(op).__name__ for op in plan.dag.post_order_iter()]
|
|
|
|
|
|
def test_cached_remote_fn():
|
|
def foo():
|
|
pass
|
|
|
|
cpu_only_foo = cached_remote_fn(foo, num_cpus=1)
|
|
cached_cpu_only_foo = cached_remote_fn(foo, num_cpus=1)
|
|
|
|
assert cpu_only_foo == cached_cpu_only_foo
|
|
|
|
gpu_only_foo = cached_remote_fn(foo, num_gpus=1)
|
|
|
|
assert cpu_only_foo != gpu_only_foo
|
|
|
|
|
|
def test_null_sentinel():
|
|
"""Check that NULL_SENTINEL sorts greater than any other value."""
|
|
|
|
assert NULL_SENTINEL != NULL_SENTINEL
|
|
assert NULL_SENTINEL < NULL_SENTINEL
|
|
assert NULL_SENTINEL <= NULL_SENTINEL
|
|
assert not NULL_SENTINEL > NULL_SENTINEL
|
|
assert not NULL_SENTINEL >= NULL_SENTINEL
|
|
|
|
# With NoneType
|
|
assert None > NULL_SENTINEL
|
|
assert None >= NULL_SENTINEL
|
|
assert NULL_SENTINEL < None
|
|
assert NULL_SENTINEL <= None
|
|
assert NULL_SENTINEL != None # noqa: E711
|
|
|
|
# With np.nan
|
|
assert np.nan > NULL_SENTINEL
|
|
assert np.nan >= NULL_SENTINEL
|
|
assert NULL_SENTINEL < np.nan
|
|
assert NULL_SENTINEL <= np.nan
|
|
assert NULL_SENTINEL != np.nan
|
|
|
|
# Rest
|
|
assert NULL_SENTINEL > 1000
|
|
assert NULL_SENTINEL > "abc"
|
|
assert NULL_SENTINEL != 1000
|
|
assert NULL_SENTINEL != "abc"
|
|
assert not NULL_SENTINEL < 1000
|
|
assert not NULL_SENTINEL < "abc"
|
|
assert not NULL_SENTINEL <= 1000
|
|
assert not NULL_SENTINEL <= "abc"
|
|
assert NULL_SENTINEL >= 1000
|
|
assert NULL_SENTINEL >= "abc"
|
|
|
|
|
|
def test_make_hashable():
|
|
valid_args = {
|
|
"int": 0,
|
|
"float": 1.2,
|
|
"str": "foo",
|
|
"dict": {
|
|
0: 0,
|
|
1.2: 1.2,
|
|
},
|
|
"list": list(range(10)),
|
|
"tuple": tuple(range(3)),
|
|
"type": Hashable,
|
|
}
|
|
|
|
hashable_args = _make_hashable(valid_args)
|
|
|
|
assert hash(hashable_args) == hash(
|
|
(
|
|
("dict", ((0, 0), (1.2, 1.2))),
|
|
("float", 1.2),
|
|
("int", 0),
|
|
("list", (0, 1, 2, 3, 4, 5, 6, 7, 8, 9)),
|
|
("str", "foo"),
|
|
("tuple", (0, 1, 2)),
|
|
("type", Hashable),
|
|
)
|
|
)
|
|
|
|
# Invalid case # 1: can't mix up key types
|
|
invalid_args = {0: 1, "bar": "baz"}
|
|
|
|
with pytest.raises(TypeError) as exc_info:
|
|
_make_hashable(invalid_args)
|
|
|
|
assert (
|
|
str(exc_info.value) == "'<' not supported between instances of 'str' and 'int'"
|
|
)
|
|
|
|
|
|
@pytest.mark.parametrize("enabled", [False, True])
|
|
def test_memory_tracing(enabled):
|
|
ctx = ray.data.context.DataContext.get_current()
|
|
ctx.trace_allocations = enabled
|
|
ref1 = ray.put(np.zeros(1024 * 1024))
|
|
ref2 = ray.put(np.zeros(1024 * 1024))
|
|
ref3 = ray.put(np.zeros(1024 * 1024))
|
|
trace_allocation(ref1, "test1")
|
|
trace_allocation(ref2, "test2")
|
|
trace_allocation(ref3, "test5")
|
|
trace_deallocation(ref1, "test3", free=False)
|
|
trace_deallocation(ref2, "test4", free=True)
|
|
ray.get(ref1)
|
|
with pytest.raises(ray.exceptions.ObjectFreedError):
|
|
ray.get(ref2)
|
|
report = leak_report()
|
|
print(report)
|
|
|
|
if enabled:
|
|
assert "Leaked object, created at test1" in report, report
|
|
assert "Leaked object, created at test5" in report, report
|
|
assert "Freed object from test2 at test4" in report, report
|
|
assert "skipped dealloc at test3" in report, report
|
|
else:
|
|
assert "test1" not in report, report
|
|
assert "test2" not in report, report
|
|
assert "test3" not in report, report
|
|
assert "test4" not in report, report
|
|
assert "test5" not in report, report
|
|
|
|
|
|
def get_parquet_read_logical_op(
|
|
ray_remote_args: Optional[Dict[str, Any]] = None,
|
|
**read_kwargs,
|
|
) -> Read:
|
|
datasource = ParquetDatasource(paths="example://iris.parquet")
|
|
if "parallelism" not in read_kwargs:
|
|
read_kwargs["parallelism"] = 10
|
|
read_op = Read(
|
|
datasource=datasource,
|
|
datasource_or_legacy_reader=datasource,
|
|
ray_remote_args=ray_remote_args,
|
|
**read_kwargs,
|
|
)
|
|
return read_op
|
|
|
|
|
|
@ray.remote(num_cpus=0)
|
|
class ConcurrencyCounter:
|
|
def __init__(self):
|
|
self.concurrency = 0
|
|
self.max_concurrency = 0
|
|
|
|
def inc(self):
|
|
self.concurrency += 1
|
|
if self.concurrency > self.max_concurrency:
|
|
self.max_concurrency = self.concurrency
|
|
return self.concurrency
|
|
|
|
def decr(self):
|
|
self.concurrency -= 1
|
|
return self.concurrency
|
|
|
|
def get_max_concurrency(self):
|
|
return self.max_concurrency
|
|
|
|
|
|
def test_iterate_with_retry():
|
|
has_raised_error = False
|
|
|
|
class MockIterable:
|
|
"""Iterate over the numbers 0, 1, 2, and raise an error on the first iteration
|
|
attempt.
|
|
"""
|
|
|
|
def __init__(self, fail_at_index=3):
|
|
self._index = -1
|
|
self._fail_at_index = fail_at_index
|
|
|
|
def __iter__(self):
|
|
return self
|
|
|
|
def __next__(self):
|
|
self._index += 1
|
|
|
|
if self._index >= 10:
|
|
raise StopIteration
|
|
|
|
nonlocal has_raised_error
|
|
if self._index == self._fail_at_index and not has_raised_error:
|
|
has_raised_error = True
|
|
raise RuntimeError("Transient error")
|
|
|
|
return self._index
|
|
|
|
expected = list(range(10))
|
|
assert list(iterate_with_retry(MockIterable, description="get item")) == expected
|
|
|
|
has_raised_error = False
|
|
assert (
|
|
list(iterate_with_retry(MockIterable, description="get item", max_attempts=2))
|
|
== expected
|
|
)
|
|
|
|
|
|
def test_iterate_with_retry_unwrap_cause():
|
|
"""unwrap_cause=True makes `match` patterns search e.__cause__ as well."""
|
|
attempts = 0
|
|
|
|
class MockIterable:
|
|
def __init__(self):
|
|
nonlocal attempts
|
|
attempts += 1
|
|
|
|
def __iter__(self):
|
|
return self
|
|
|
|
def __next__(self):
|
|
try:
|
|
raise RuntimeError("transient inner")
|
|
except RuntimeError as inner:
|
|
raise ValueError("outer wrapper") from inner
|
|
|
|
# unwrap_cause=True: pattern matches the cause → all attempts consumed.
|
|
attempts = 0
|
|
with pytest.raises(ValueError, match="outer wrapper"):
|
|
list(
|
|
iterate_with_retry(
|
|
MockIterable,
|
|
description="get item",
|
|
match=["transient inner"],
|
|
max_attempts=2,
|
|
unwrap_cause=True,
|
|
)
|
|
)
|
|
assert attempts == 2
|
|
|
|
# unwrap_cause=False: cause invisible → not retryable → only one attempt.
|
|
attempts = 0
|
|
with pytest.raises(ValueError, match="outer wrapper"):
|
|
list(
|
|
iterate_with_retry(
|
|
MockIterable,
|
|
description="get item",
|
|
match=["transient inner"],
|
|
max_attempts=2,
|
|
unwrap_cause=False,
|
|
)
|
|
)
|
|
assert attempts == 1
|
|
|
|
|
|
def test_iterate_with_retry_matches_class_name():
|
|
"""Patterns can match the exception class name (e.g., 'RateLimit')."""
|
|
|
|
class RateLimitError(Exception):
|
|
pass
|
|
|
|
attempts = 0
|
|
|
|
class MockIterable:
|
|
def __init__(self):
|
|
nonlocal attempts
|
|
attempts += 1
|
|
|
|
def __iter__(self):
|
|
return self
|
|
|
|
def __next__(self):
|
|
raise RateLimitError("Error code: 429")
|
|
|
|
attempts = 0
|
|
with pytest.raises(RateLimitError):
|
|
list(
|
|
iterate_with_retry(
|
|
MockIterable,
|
|
description="get item",
|
|
match=["RateLimit"],
|
|
max_attempts=2,
|
|
)
|
|
)
|
|
assert attempts == 2
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"pattern, error_message, expected",
|
|
[
|
|
# Plain substring match.
|
|
("transient", "transient network error", True),
|
|
# Regex match when substring check fails.
|
|
("40[0-9]", "HTTP 404 not found", True),
|
|
# Substring takes priority: literal "(503)" found before regex is tried.
|
|
("(503)", "error (503) returned", True),
|
|
# Invalid regex falls back to False, not re.error.
|
|
("[unclosed", "some error message", False),
|
|
# No match at all.
|
|
("rate limit", "connection refused", False),
|
|
],
|
|
)
|
|
def test_matches_error(pattern, error_message, expected):
|
|
"""Retry helper matches substring first, then regex; invalid patterns do not raise."""
|
|
assert matches_error(pattern, error_message) is expected
|
|
|
|
|
|
def test_find_partition_index_single_column_ascending():
|
|
table = pa.table({"value": [1, 2, 2, 3, 5]})
|
|
sort_key = SortKey(key=["value"], descending=[False])
|
|
assert find_partition_index(table, (0,), sort_key) == 0 # all entries > 0
|
|
assert find_partition_index(table, (2,), sort_key) == 1 # first match index
|
|
assert find_partition_index(table, (4,), sort_key) == 4 # belongs after 3, before 5
|
|
assert find_partition_index(table, (6,), sort_key) == 5 # all entries < 6
|
|
|
|
|
|
def test_find_partition_index_single_column_descending():
|
|
table = pa.table({"value": [5, 3, 2, 2, 1]})
|
|
sort_key = SortKey(key=["value"], descending=[True])
|
|
assert find_partition_index(table, (6,), sort_key) == 0 # belongs before 5
|
|
assert find_partition_index(table, (3,), sort_key) == 2 # after the last 3
|
|
assert find_partition_index(table, (2,), sort_key) == 4 # after the last 2
|
|
assert find_partition_index(table, (0,), sort_key) == 5 # all entries > 0
|
|
|
|
|
|
def test_find_partition_index_multi_column():
|
|
# Table sorted by col1 asc, then col2 desc.
|
|
table = pa.table({"col1": [1, 1, 1, 2, 2], "col2": [3, 2, 1, 2, 1]})
|
|
sort_key = SortKey(key=["col1", "col2"], descending=[False, True])
|
|
# Insert value (1,3) -> belongs before (1,2)
|
|
assert find_partition_index(table, (1, 3), sort_key) == 0
|
|
# Insert value (1,2) -> belongs after the first (1,3) and before (1,2)
|
|
# because col1 ties, col2 descending
|
|
assert find_partition_index(table, (1, 2), sort_key) == 1
|
|
# Insert value (2,2) -> belongs right before (2,2) that starts at index 3
|
|
assert find_partition_index(table, (2, 2), sort_key) == 3
|
|
# Insert value (0, 4) -> belongs at index 0 (all col1 > 0)
|
|
assert find_partition_index(table, (0, 4), sort_key) == 0
|
|
# Insert value (2,0) -> belongs after (2,1)
|
|
assert find_partition_index(table, (2, 0), sort_key) == 5
|
|
|
|
|
|
def test_find_partition_index_with_nulls():
|
|
# _NullSentinel is sorted greater, so they appear after all real values.
|
|
table = pa.table({"value": [1, 2, 3, None, None]})
|
|
sort_key = SortKey(key=["value"], descending=[False])
|
|
# Insert (2,) -> belongs after 1, before 2 => index 1
|
|
# (But the actual find_partition_index uses the table as-is.)
|
|
assert find_partition_index(table, (2,), sort_key) == 1
|
|
# Insert (4,) -> belongs before any null => index 3
|
|
assert find_partition_index(table, (4,), sort_key) == 3
|
|
# Insert (None,) -> always belongs at the end
|
|
assert find_partition_index(table, (None,), sort_key) == 3
|
|
|
|
|
|
def test_find_partition_index_with_nan():
|
|
# NaN sorts after regular values in Arrow (before nulls).
|
|
table = pa.table({"value": [1.0, 2.0, 3.0, float("nan"), float("nan")]})
|
|
sort_key = SortKey(key=["value"], descending=[False])
|
|
assert find_partition_index(table, (2.0,), sort_key) == 1
|
|
assert find_partition_index(table, (4.0,), sort_key) == 3
|
|
|
|
|
|
def test_find_partition_index_with_nan_and_nulls():
|
|
# NaN sorts after regular values, nulls sort after NaN.
|
|
table = pa.table({"value": [1.0, 2.0, 3.0, float("nan"), None]})
|
|
sort_key = SortKey(key=["value"], descending=[False])
|
|
assert find_partition_index(table, (2.0,), sort_key) == 1
|
|
assert find_partition_index(table, (4.0,), sort_key) == 3
|
|
assert find_partition_index(table, (None,), sort_key) == 3
|
|
|
|
|
|
def test_find_partition_index_duplicates():
|
|
table = pa.table({"value": [2, 2, 2, 2, 2]})
|
|
sort_key = SortKey(key=["value"], descending=[False])
|
|
# Insert (2,) in a table of all 2's -> first matching index is 0
|
|
assert find_partition_index(table, (2,), sort_key) == 0
|
|
# Insert (1,) -> belongs at index 0
|
|
assert find_partition_index(table, (1,), sort_key) == 0
|
|
# Insert (3,) -> belongs at index 5
|
|
assert find_partition_index(table, (3,), sort_key) == 5
|
|
|
|
|
|
def test_find_partition_index_duplicates_descending():
|
|
table = pa.table({"value": [2, 2, 2, 2, 2]})
|
|
sort_key = SortKey(key=["value"], descending=[True])
|
|
# Insert (2,) in a table of all 2's -> belongs at index 5
|
|
assert find_partition_index(table, (2,), sort_key) == 5
|
|
# Insert (1,) -> belongs at index 5
|
|
assert find_partition_index(table, (1,), sort_key) == 5
|
|
# Insert (3,) -> belongs at index 0
|
|
assert find_partition_index(table, (3,), sort_key) == 0
|
|
|
|
|
|
def test_merge_resources_to_ray_remote_args():
|
|
ray_remote_args = {}
|
|
ray_remote_args = merge_resources_to_ray_remote_args(1, 1, 1, ray_remote_args)
|
|
assert ray_remote_args == {"num_cpus": 1, "num_gpus": 1, "memory": 1}
|
|
|
|
ray_remote_args = {"other_resource": 1}
|
|
ray_remote_args = merge_resources_to_ray_remote_args(1, 1, 1, ray_remote_args)
|
|
assert ray_remote_args == {
|
|
"num_cpus": 1,
|
|
"num_gpus": 1,
|
|
"memory": 1,
|
|
"other_resource": 1,
|
|
}
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"actual, expected, expected_equal",
|
|
[
|
|
(pd.DataFrame({"a": [1]}), pd.DataFrame({"a": [1]}), True),
|
|
# Different value.
|
|
(pd.DataFrame({"a": [1]}), pd.DataFrame({"a": [2]}), False),
|
|
# Extra column.
|
|
(pd.DataFrame({"a": [1]}), pd.DataFrame({"a": [1], "b": [2]}), False),
|
|
# Different number of rows.
|
|
(pd.DataFrame({"a": [1]}), pd.DataFrame({"a": [1, 1]}), False),
|
|
# Same rows, but different order.
|
|
(pd.DataFrame({"a": [1, 2]}), pd.DataFrame({"a": [2, 1]}), True),
|
|
],
|
|
)
|
|
def test_rows_same(actual: pd.DataFrame, expected: pd.DataFrame, expected_equal: bool):
|
|
if expected_equal:
|
|
assert rows_same(actual, expected)
|
|
else:
|
|
with pytest.raises(AssertionError):
|
|
assert rows_same(actual, expected)
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"allocated,min_scheduling,expected",
|
|
[
|
|
(None, ExecutionResources(cpu=1, gpu=1), 0),
|
|
(ExecutionResources(cpu=1, gpu=1), ExecutionResources(cpu=1, gpu=1), 1),
|
|
(
|
|
ExecutionResources(cpu=1, gpu=1),
|
|
ExecutionResources(cpu=0, gpu=0),
|
|
float("inf"),
|
|
),
|
|
(ExecutionResources(cpu=1, gpu=1), ExecutionResources(cpu=0, gpu=1), 1),
|
|
(ExecutionResources(cpu=1, gpu=1), ExecutionResources(cpu=1, gpu=0), 1),
|
|
],
|
|
)
|
|
def test_get_max_task_capacity(allocated, min_scheduling, expected):
|
|
assert get_max_task_capacity(allocated, min_scheduling) == expected
|
|
|
|
|
|
class TestMergeLabelSelector:
|
|
"""Tests for ``merge_label_selector``.
|
|
|
|
The helper merges a DataContext-level label_selector into a ray_remote_args
|
|
dict. Operator-level entries win on key conflicts.
|
|
"""
|
|
|
|
def test_ctx_none_returns_input_unchanged(self):
|
|
args = {"num_cpus": 1}
|
|
assert merge_label_selector(args, None) is args
|
|
|
|
def test_ctx_empty_returns_input_unchanged(self):
|
|
args = {"num_cpus": 1}
|
|
assert merge_label_selector(args, {}) is args
|
|
|
|
def test_ctx_only(self):
|
|
args = {"num_cpus": 1}
|
|
out = merge_label_selector(args, {"subcluster": "train"})
|
|
assert out == {"num_cpus": 1, "label_selector": {"subcluster": "train"}}
|
|
assert args == {"num_cpus": 1} # input not mutated
|
|
|
|
def test_op_only_no_ctx(self):
|
|
args = {"label_selector": {"node": "X"}}
|
|
assert merge_label_selector(args, None) is args
|
|
|
|
def test_op_and_ctx_no_collision(self):
|
|
args = {"label_selector": {"node": "X"}}
|
|
out = merge_label_selector(args, {"subcluster": "train"})
|
|
assert out["label_selector"] == {"subcluster": "train", "node": "X"}
|
|
|
|
def test_op_wins_on_collision(self):
|
|
args = {"label_selector": {"subcluster": "val"}}
|
|
out = merge_label_selector(args, {"subcluster": "train"})
|
|
assert out["label_selector"] == {"subcluster": "val"}
|
|
|
|
def test_input_not_mutated(self):
|
|
args = {"label_selector": {"node": "X"}}
|
|
ctx = {"subcluster": "train"}
|
|
merge_label_selector(args, ctx)
|
|
assert args == {"label_selector": {"node": "X"}}
|
|
assert ctx == {"subcluster": "train"}
|
|
|
|
|
|
def test_execution_options_label_selector_field():
|
|
"""Smoke test that ExecutionOptions exposes label_selector."""
|
|
from ray.data._internal.execution.interfaces import ExecutionOptions
|
|
|
|
options = ExecutionOptions()
|
|
assert options.label_selector is None
|
|
|
|
options = ExecutionOptions(label_selector={"subcluster": "train"})
|
|
assert options.label_selector == {"subcluster": "train"}
|
|
|
|
|
|
if __name__ == "__main__":
|
|
import sys
|
|
|
|
sys.exit(pytest.main(["-v", __file__]))
|