chore: import upstream snapshot with attribution

This commit is contained in:
wehub-resource-sync
2026-07-13 13:17:40 +08:00
commit f1825c8ceb
10096 changed files with 2364182 additions and 0 deletions
+53
View File
@@ -0,0 +1,53 @@
load("@rules_python//python:defs.bzl", "py_library")
load("//bazel:python.bzl", "py_test_module_list")
py_library(
name = "conftest",
srcs = glob(["**/conftest.py"]),
visibility = [
"//python/ray/_common/tests:__subpackages__",
],
deps = ["//python/ray/tests:conftest"],
)
# Small tests.
py_test_module_list(
size = "small",
files = [
"test_deprecation.py",
"test_filters.py",
"test_formatters.py",
"test_logging_constants.py",
"test_network_utils.py",
"test_ray_option_utils.py",
"test_retry.py",
"test_signal_semaphore_utils.py",
"test_signature.py",
"test_tls_utils.py",
"test_utils.py",
"test_wait_for_condition.py",
],
tags = [
"exclusive",
"team:core",
],
deps = [
":conftest",
"//:ray_lib",
],
)
py_test_module_list(
size = "large",
files = [
"test_usage_stats.py",
],
tags = [
"exclusive",
"team:core",
],
deps = [
":conftest",
"//:ray_lib",
],
)
+2
View File
@@ -0,0 +1,2 @@
# Imports for filters and formatters tests
pytest_plugins = ["ray.tests.conftest"]
@@ -0,0 +1,97 @@
import sys
from unittest.mock import patch
import pytest
from ray._common.deprecation import (
DEPRECATED_VALUE,
Deprecated,
deprecation_warning,
)
def test_deprecation_warning_warn():
with patch("ray._common.deprecation.logger.warning") as mock_warning:
deprecation_warning("old_feature", "new_feature")
mock_warning.assert_called_once()
args, _ = mock_warning.call_args
assert (
"DeprecationWarning: `old_feature` has been deprecated. Use `new_feature` instead."
in args[0]
)
def test_deprecation_warning_error():
with pytest.raises(ValueError) as excinfo:
deprecation_warning("old_feature", error=True)
assert "`old_feature` has been deprecated." in str(excinfo.value)
def test_deprecated_decorator_function():
with patch("ray._common.deprecation.logger.warning") as mock_warning, patch(
"ray._common.deprecation.log_once"
) as mock_log_once:
mock_log_once.return_value = True
@Deprecated(old="old_func", new="new_func", error=False)
def old_func():
return "result"
result = old_func()
assert result == "result"
mock_warning.assert_called_once()
def test_deprecated_decorator_class():
with patch("ray._common.deprecation.logger.warning") as mock_warning, patch(
"ray._common.deprecation.log_once"
) as mock_log_once:
mock_log_once.return_value = True
@Deprecated(old="OldClass", new="NewClass", error=False)
class OldClass:
pass
instance = OldClass()
assert isinstance(instance, OldClass)
mock_warning.assert_called_once()
def test_deprecated_decorator_method():
with patch("ray._common.deprecation.logger.warning") as mock_warning, patch(
"ray._common.deprecation.log_once"
) as mock_log_once:
mock_log_once.return_value = True
class MyClass:
@Deprecated(old="old_method", new="new_method", error=False)
def old_method(self):
return "method_result"
instance = MyClass()
result = instance.old_method()
assert result == "method_result"
mock_warning.assert_called_once()
def test_deprecated_decorator_error():
with patch("ray._common.deprecation.log_once") as mock_log_once:
mock_log_once.return_value = True
@Deprecated(old="old_func", error=True)
def old_func():
pass
with pytest.raises(ValueError):
old_func()
def test_deprecated_value_constant():
assert (
DEPRECATED_VALUE == -1
), f"DEPRECATED_VALUE should be -1, but got {DEPRECATED_VALUE}"
if __name__ == "__main__":
sys.exit(pytest.main(["-sv", __file__]))
+160
View File
@@ -0,0 +1,160 @@
import logging
import logging.config
import sys
from concurrent.futures import ThreadPoolExecutor
import pytest
import ray
from ray._common.filters import CoreContextFilter
class TestCoreContextFilter:
def test_driver_process(self, shutdown_only):
log_context = ["job_id", "worker_id", "node_id"]
filter = CoreContextFilter()
record = logging.makeLogRecord({})
assert filter.filter(record)
# Ray is not initialized so no context except PID which should be available
for attr in log_context:
assert not hasattr(record, attr)
# PID should be available even when Ray is not initialized
assert hasattr(record, "process")
assert hasattr(record, "_ray_timestamp_ns")
ray.init()
record = logging.makeLogRecord({})
assert filter.filter(record)
runtime_context = ray.get_runtime_context()
expected_values = {
"job_id": runtime_context.get_job_id(),
"worker_id": runtime_context.get_worker_id(),
"node_id": runtime_context.get_node_id(),
"process": record.process,
}
for attr in log_context:
assert hasattr(record, attr)
assert getattr(record, attr) == expected_values[attr]
# This is not a worker process, so actor_id and task_id should not exist.
for attr in ["actor_id", "task_id"]:
assert not hasattr(record, attr)
assert hasattr(record, "_ray_timestamp_ns")
def test_task_process(self, shutdown_only):
@ray.remote
def f():
filter = CoreContextFilter()
record = logging.makeLogRecord({})
assert filter.filter(record)
should_exist = ["job_id", "worker_id", "node_id", "task_id", "process"]
runtime_context = ray.get_runtime_context()
expected_values = {
"job_id": runtime_context.get_job_id(),
"worker_id": runtime_context.get_worker_id(),
"node_id": runtime_context.get_node_id(),
"task_id": runtime_context.get_task_id(),
"task_name": runtime_context.get_task_name(),
"task_func_name": runtime_context.get_task_function_name(),
"process": record.process,
}
for attr in should_exist:
assert hasattr(record, attr)
assert getattr(record, attr) == expected_values[attr]
assert not hasattr(record, "actor_id")
assert not hasattr(record, "actor_name")
assert hasattr(record, "_ray_timestamp_ns")
obj_ref = f.remote()
ray.get(obj_ref)
def test_actor_process(self, shutdown_only):
@ray.remote
class A:
def f(self):
filter = CoreContextFilter()
record = logging.makeLogRecord({})
assert filter.filter(record)
should_exist = [
"job_id",
"worker_id",
"node_id",
"actor_id",
"task_id",
"process",
]
runtime_context = ray.get_runtime_context()
expected_values = {
"job_id": runtime_context.get_job_id(),
"worker_id": runtime_context.get_worker_id(),
"node_id": runtime_context.get_node_id(),
"actor_id": runtime_context.get_actor_id(),
"actor_name": runtime_context.get_actor_name(),
"task_id": runtime_context.get_task_id(),
"task_name": runtime_context.get_task_name(),
"task_func_name": runtime_context.get_task_function_name(),
"process": record.process,
}
for attr in should_exist:
assert hasattr(record, attr)
assert getattr(record, attr) == expected_values[attr]
assert hasattr(record, "_ray_timestamp_ns")
# Record should not have the attribute with a value of an empty string.
assert runtime_context.get_actor_name() == ""
assert not hasattr(record, "actor_name")
actor = A.remote()
ray.get(actor.f.remote())
def test_actor_process_with_thread(self, shutdown_only):
@ray.remote
class MockedRayDataWorker:
def _check_log_record_in_thread(self):
filter = CoreContextFilter()
record = logging.makeLogRecord({})
assert filter.filter(record)
should_exist = [
"job_id",
"worker_id",
"node_id",
"actor_id",
"task_id",
"process",
]
runtime_context = ray.get_runtime_context()
expected_values = {
"job_id": runtime_context.get_job_id(),
"worker_id": runtime_context.get_worker_id(),
"node_id": runtime_context.get_node_id(),
"actor_id": runtime_context.get_actor_id(),
"task_id": runtime_context.get_task_id(),
"process": record.process,
}
for attr in should_exist:
assert hasattr(record, attr)
assert getattr(record, attr) == expected_values[attr]
assert hasattr(record, "_ray_timestamp_ns")
# Record should not have the attribute with a value of an empty string.
assert runtime_context.get_actor_name() == ""
assert not hasattr(record, "actor_name")
assert runtime_context.get_task_name() == ""
assert not hasattr(record, "task_name")
assert runtime_context.get_task_function_name() == ""
assert not hasattr(record, "task_function_name")
return record
def map(self):
with ThreadPoolExecutor(max_workers=1) as executor:
executor.submit(self._check_log_record_in_thread).result()
actor = MockedRayDataWorker.remote()
ray.get(actor.map.remote())
if __name__ == "__main__":
sys.exit(pytest.main(["-sv", __file__]))
+162
View File
@@ -0,0 +1,162 @@
import json
import logging
import logging.config
import sys
import pytest
from ray._common.formatters import JSONFormatter, TextFormatter
class TestJSONFormatter:
def test_empty_record(self, shutdown_only):
formatter = JSONFormatter()
record = logging.makeLogRecord({})
formatted = formatter.format(record)
record_dict = json.loads(formatted)
should_exist = [
"process",
"asctime",
"levelname",
"message",
"filename",
"lineno",
"timestamp_ns",
]
for key in should_exist:
assert key in record_dict
assert len(record_dict) == len(should_exist)
assert "exc_text" not in record_dict
def test_record_with_exception(self, shutdown_only):
formatter = JSONFormatter()
record = logging.makeLogRecord({})
try:
raise ValueError("test")
except ValueError:
record.exc_info = sys.exc_info()
formatted = formatter.format(record)
record_dict = json.loads(formatted)
should_exist = [
"process",
"asctime",
"levelname",
"message",
"filename",
"lineno",
"exc_text",
"timestamp_ns",
]
for key in should_exist:
assert key in record_dict
assert "Traceback (most recent call last):" in record_dict["exc_text"]
assert len(record_dict) == len(should_exist)
def test_record_with_user_provided_context(self, shutdown_only):
formatter = JSONFormatter()
record = logging.makeLogRecord({"user": "ray"})
formatted = formatter.format(record)
record_dict = json.loads(formatted)
should_exist = [
"process",
"asctime",
"levelname",
"message",
"filename",
"lineno",
"user",
"timestamp_ns",
]
for key in should_exist:
assert key in record_dict
assert record_dict["user"] == "ray"
assert len(record_dict) == len(should_exist)
assert "exc_text" not in record_dict
def test_record_with_flatten_keys_invalid_value(self, shutdown_only):
formatter = JSONFormatter()
record = logging.makeLogRecord({"ray_serve_extra_fields": "not_a_dict"})
with pytest.raises(ValueError):
formatter.format(record)
def test_record_with_flatten_keys_valid_dict(self, shutdown_only):
formatter = JSONFormatter()
record = logging.makeLogRecord(
{"ray_serve_extra_fields": {"key1": "value1", "key2": 2}}
)
formatted = formatter.format(record)
record_dict = json.loads(formatted)
should_exist = [
"process",
"asctime",
"levelname",
"message",
"filename",
"lineno",
"key1",
"key2",
"timestamp_ns",
]
for key in should_exist:
assert key in record_dict
assert record_dict["key1"] == "value1", record_dict
assert record_dict["key2"] == 2
assert "ray_serve_extra_fields" not in record_dict
assert len(record_dict) == len(should_exist)
assert "exc_text" not in record_dict
def test_record_with_valid_additional_log_standard_attrs(self, shutdown_only):
formatter = JSONFormatter()
formatter.set_additional_log_standard_attrs(["name"])
record = logging.makeLogRecord({})
formatted = formatter.format(record)
record_dict = json.loads(formatted)
should_exist = [
"process",
"asctime",
"levelname",
"message",
"filename",
"lineno",
"timestamp_ns",
"name",
]
for key in should_exist:
assert key in record_dict
assert len(record_dict) == len(should_exist)
class TestTextFormatter:
def test_record_with_user_provided_context(self):
formatter = TextFormatter()
record = logging.makeLogRecord({"user": "ray"})
formatted = formatter.format(record)
assert "user=ray" in formatted
def test_record_with_exception(self):
formatter = TextFormatter()
record = logging.LogRecord(
name="test_logger",
level=logging.INFO,
pathname="test.py",
lineno=1000,
msg="Test message",
args=None,
exc_info=None,
)
formatted = formatter.format(record)
for s in ["INFO", "Test message", "test.py:1000", "--"]:
assert s in formatted
def test_record_with_valid_additional_log_standard_attrs(self, shutdown_only):
formatter = TextFormatter()
formatter.set_additional_log_standard_attrs(["name"])
record = logging.makeLogRecord({})
formatted = formatter.format(record)
assert "name=" in formatted
if __name__ == "__main__":
sys.exit(pytest.main(["-sv", __file__]))
@@ -0,0 +1,83 @@
import sys
import pytest
from ray._common.logging_constants import (
LOGGER_FLATTEN_KEYS,
LOGRECORD_STANDARD_ATTRS,
LogKey,
)
def test_logrecord_standard_attrs_is_frozenset():
assert isinstance(LOGRECORD_STANDARD_ATTRS, frozenset)
def test_logrecord_standard_attrs_contains_standard_names():
expected = frozenset(
{
"args",
"asctime",
"created",
"exc_info",
"exc_text",
"filename",
"funcName",
"levelname",
"levelno",
"lineno",
"message",
"module",
"msecs",
"msg",
"name",
"pathname",
"process",
"processName",
"relativeCreated",
"stack_info",
"thread",
"threadName",
"taskName",
}
)
assert LOGRECORD_STANDARD_ATTRS == expected
def test_logrecord_standard_attrs_has_expected_size():
assert len(LOGRECORD_STANDARD_ATTRS) == 23
def test_logger_flatten_keys_is_set():
assert isinstance(LOGGER_FLATTEN_KEYS, set)
assert "ray_serve_extra_fields" in LOGGER_FLATTEN_KEYS
def test_logkey_is_enum():
from enum import Enum
assert issubclass(LogKey, Enum)
expected = {
"JOB_ID": "job_id",
"WORKER_ID": "worker_id",
"NODE_ID": "node_id",
"ACTOR_ID": "actor_id",
"TASK_ID": "task_id",
"ACTOR_NAME": "actor_name",
"TASK_NAME": "task_name",
"TASK_FUNCTION_NAME": "task_func_name",
"ASCTIME": "asctime",
"LEVELNAME": "levelname",
"MESSAGE": "message",
"FILENAME": "filename",
"LINENO": "lineno",
"EXC_TEXT": "exc_text",
"PROCESS": "process",
"TIMESTAMP_NS": "timestamp_ns",
}
actual = {member.name: member.value for member in LogKey}
assert actual == expected
if __name__ == "__main__":
sys.exit(pytest.main(["-v", __file__]))
@@ -0,0 +1,17 @@
import sys
import pytest
from ray._common.network_utils import is_localhost
def test_is_localhost():
assert is_localhost("localhost")
assert is_localhost("127.0.0.1")
assert is_localhost("::1")
assert not is_localhost("8.8.8.8")
assert not is_localhost("2001:db8::1")
if __name__ == "__main__":
sys.exit(pytest.main(["-v", __file__]))
@@ -0,0 +1,197 @@
import re
import sys
from unittest.mock import patch
import pytest
from ray._common.ray_option_utils import (
Option,
_check_deprecate_placement_group,
_counting_option,
_resource_option,
_validate_resource_quantity,
_validate_resources,
update_options,
validate_actor_options,
validate_task_options,
)
from ray.util.placement_group import PlacementGroup
class TestOptionValidation:
def test_option_validate(self):
opt = Option(
type_constraint=int, value_constraint=lambda v: "error" if v < 0 else None
)
opt.validate("test", 1)
with pytest.raises(TypeError):
opt.validate("test", "a")
with pytest.raises(ValueError, match="error"):
opt.validate("test", -1)
def test_counting_option(self):
# Test infinite counting option
opt_inf = _counting_option("test_inf", infinite=True)
opt_inf.validate("test_inf", 5)
opt_inf.validate("test_inf", 0)
opt_inf.validate("test_inf", -1) # Represents infinity
opt_inf.validate("test_inf", None)
with pytest.raises(ValueError):
opt_inf.validate("test_inf", -2)
with pytest.raises(TypeError):
opt_inf.validate("test_inf", 1.5)
# Test non-infinite counting option
opt_non_inf = _counting_option("test_non_inf", infinite=False)
opt_non_inf.validate("test_non_inf", 5)
opt_non_inf.validate("test_non_inf", 0)
opt_non_inf.validate("test_non_inf", None)
with pytest.raises(ValueError):
opt_non_inf.validate("test_non_inf", -1)
@patch("ray._raylet.RESOURCE_UNIT_SCALING", 10000)
@patch(
"ray._private.accelerators.get_all_accelerator_resource_names",
return_value={"GPU", "TPU"},
)
@patch("ray._private.accelerators.get_accelerator_manager_for_resource")
def test_validate_resource_quantity(self, mock_get_manager, mock_get_all_names):
# Valid cases
assert _validate_resource_quantity("CPU", 1) is None
assert _validate_resource_quantity("memory", 0) is None
assert _validate_resource_quantity("custom", 0.5) is None
# Invalid cases
err = _validate_resource_quantity("CPU", -1)
assert isinstance(err, str)
assert "cannot be negative" in err
err = _validate_resource_quantity("CPU", 0.00001)
assert isinstance(err, str)
assert "cannot go beyond 0.0001" in err
# Accelerator validation
mock_manager_instance = mock_get_manager.return_value
mock_manager_instance.validate_resource_request_quantity.return_value = (
False,
"mock error",
)
err = _validate_resource_quantity("GPU", 1.5)
assert isinstance(err, str)
assert "mock error" in err
mock_get_manager.assert_called_with("GPU")
mock_manager_instance.validate_resource_request_quantity.assert_called_with(1.5)
mock_manager_instance.validate_resource_request_quantity.return_value = (
True,
"",
)
assert _validate_resource_quantity("TPU", 1) is None
def test_resource_option(self):
opt = _resource_option("CPU")
opt.validate("CPU", 1)
opt.validate("CPU", 0.5)
opt.validate("CPU", None)
with pytest.raises(TypeError):
opt.validate("CPU", "1")
with pytest.raises(ValueError):
opt.validate("CPU", -1.0)
def test_validate_resources(self):
assert _validate_resources(None) is None
assert _validate_resources({"custom": 1}) is None
err = _validate_resources({"CPU": 1, "GPU": 1})
assert isinstance(err, str)
assert "Use the 'num_cpus' and 'num_gpus' keyword" in err
err = _validate_resources({"custom": -1})
assert isinstance(err, str)
assert "cannot be negative" in err
class TestTaskActorOptionValidation:
def test_validate_task_options_valid(self):
validate_task_options({"num_cpus": 2, "max_retries": 3}, in_options=False)
def test_validate_task_options_invalid_keyword(self):
with pytest.raises(ValueError, match="Invalid option keyword"):
validate_task_options({"invalid_option": 1}, in_options=False)
def test_validate_task_options_in_options_invalid(self):
with pytest.raises(
ValueError,
match=re.escape("Setting 'max_calls' is not supported in '.options()'."),
):
validate_task_options({"max_calls": 5}, in_options=True)
def test_validate_actor_options_valid(self):
validate_actor_options({"max_concurrency": 2, "name": "abc"}, in_options=False)
def test_validate_actor_options_invalid_keyword(self):
with pytest.raises(ValueError, match="Invalid option keyword"):
validate_actor_options({"invalid_option": 1}, in_options=False)
def test_validate_actor_options_in_options_invalid(self):
with pytest.raises(
ValueError,
match=re.escape(
"Setting 'concurrency_groups' is not supported in '.options()'."
),
):
validate_actor_options({"concurrency_groups": {}}, in_options=True)
def test_validate_actor_get_if_exists_no_name(self):
with pytest.raises(
ValueError, match="must be specified to use `get_if_exists`"
):
validate_actor_options({"get_if_exists": True}, in_options=False)
def test_validate_actor_object_store_memory_warning(self):
with pytest.warns(
DeprecationWarning,
match="Setting 'object_store_memory' for actors is deprecated",
):
validate_actor_options({"object_store_memory": 100}, in_options=False)
def test_check_deprecate_placement_group(self):
pg = PlacementGroup.empty()
# No error if only one is specified
_check_deprecate_placement_group({"placement_group": pg})
_check_deprecate_placement_group({"scheduling_strategy": "SPREAD"})
# Error if both are specified
with pytest.raises(
ValueError, match="Placement groups should be specified via"
):
_check_deprecate_placement_group(
{"placement_group": pg, "scheduling_strategy": "SPREAD"}
)
# Check no error with default or None placement_group
_check_deprecate_placement_group(
{"placement_group": "default", "scheduling_strategy": "SPREAD"}
)
_check_deprecate_placement_group(
{"placement_group": None, "scheduling_strategy": "SPREAD"}
)
class TestUpdateOptions:
def test_simple_update(self):
original = {"num_cpus": 1, "name": "a"}
new = {"num_cpus": 2, "num_gpus": 1}
updated = update_options(original, new)
assert updated == {"num_cpus": 2, "name": "a", "num_gpus": 1}
def test_update_with_empty_new(self):
original = {"num_cpus": 1}
updated = update_options(original, {})
assert updated == original
def test_update_empty_original(self):
new = {"num_cpus": 1}
updated = update_options({}, new)
assert updated == new
if __name__ == "__main__":
sys.exit(pytest.main(["-sv", __file__]))
+136
View File
@@ -0,0 +1,136 @@
import sys
import pytest
from ray._common.retry import (
call_with_retry,
retry,
)
def test_call_with_retry_immediate_success_with_args():
def func(a, b):
return [a, b]
assert call_with_retry(func, "func", [], 1, 0, "a", "b") == ["a", "b"]
def test_retry_immediate_success_with_object_args():
class MyClass:
@retry("func", [], 1, 0)
def func(self, a, b):
return [a, b]
assert MyClass().func("a", "b") == ["a", "b"]
@pytest.mark.parametrize("use_decorator", [True, False])
def test_retry_last_attempt_successful_with_appropriate_wait_time(
monkeypatch, use_decorator
):
sleep_total = 0
def sleep(x):
nonlocal sleep_total
sleep_total += x
monkeypatch.setattr("time.sleep", sleep)
monkeypatch.setattr("random.uniform", lambda a, b: 1)
pattern = "have not reached 4th attempt"
call_count = 0
def func():
nonlocal call_count
call_count += 1
if call_count == 4:
return "success"
raise ValueError(pattern)
args = ["func", [pattern], 4, 3]
if use_decorator:
assert retry(*args)(func)() == "success"
else:
assert call_with_retry(func, *args) == "success"
assert sleep_total == 6 # 1 + 2 + 3
@pytest.mark.parametrize("use_decorator", [True, False])
def test_retry_unretryable_error(use_decorator):
call_count = 0
def func():
nonlocal call_count
call_count += 1
raise ValueError("unretryable error")
args = ["func", ["only retryable error"], 10, 0]
with pytest.raises(ValueError, match="unretryable error"):
if use_decorator:
retry(*args)(func)()
else:
call_with_retry(func, *args)
assert call_count == 1
@pytest.mark.parametrize("use_decorator", [True, False])
def test_retry_fail_all_attempts_retry_all_errors(use_decorator):
call_count = 0
def func():
nonlocal call_count
call_count += 1
raise ValueError(str(call_count))
args = ["func", None, 3, 0]
with pytest.raises(ValueError):
if use_decorator:
retry(*args)(func)()
else:
call_with_retry(func, *args)
assert call_count == 3
def test_call_with_retry_matches_class_name():
"""Patterns can match the exception class name (e.g., 'RateLimit')."""
class RateLimitError(Exception):
pass
call_count = 0
def func():
nonlocal call_count
call_count += 1
raise RateLimitError("Error code: 429")
with pytest.raises(RateLimitError):
call_with_retry(func, "func", ["RateLimit"], 3, 0)
assert call_count == 3
@pytest.mark.parametrize(
"pattern,should_retry",
[
# Valid regex that is not a literal substring, that matches via regex search
(r"\d{3}", True),
# Invalid regex, re.error is handled by returning False and the error is not retried.
(r"[unclosed", False),
],
)
def test_call_with_retry_regex_matching(pattern, should_retry):
call_count = 0
def func():
nonlocal call_count
call_count += 1
raise ValueError("Error code: 429")
with pytest.raises(ValueError):
call_with_retry(func, "func", [pattern], 3, 0)
assert call_count == (3 if should_retry else 1)
if __name__ == "__main__":
sys.exit(pytest.main(["-sv", __file__]))
@@ -0,0 +1,103 @@
"""Tests for Ray test utility classes.
This module contains pytest-based tests for SignalActor and Semaphore classes
from ray._common.test_utils. These test utility classes are used for coordination
and synchronization in Ray tests.
"""
import sys
import time
import pytest
import ray
from ray._common.test_utils import Semaphore, SignalActor, wait_for_condition
@pytest.fixture(scope="module")
def ray_init():
"""Initialize Ray for the test module."""
ray.init(num_cpus=4)
yield
ray.shutdown()
def test_signal_actor_basic(ray_init):
"""Test basic SignalActor functionality - send and wait operations."""
signal = SignalActor.remote()
# Test initial state
assert ray.get(signal.cur_num_waiters.remote()) == 0
# Test send and wait
ray.get(signal.send.remote())
signal.wait.remote()
assert ray.get(signal.cur_num_waiters.remote()) == 0
def test_signal_actor_multiple_waiters(ray_init):
"""Test SignalActor with multiple waiters and signal clearing."""
signal = SignalActor.remote()
# Create multiple waiters
for _ in range(3):
signal.wait.remote()
# Check number of waiters
wait_for_condition(lambda: ray.get(signal.cur_num_waiters.remote()) == 3)
# Send signal and wait for all waiters
ray.get(signal.send.remote())
# Verify all waiters are done
wait_for_condition(lambda: ray.get(signal.cur_num_waiters.remote()) == 0)
# check that .wait() doesn't block if the signal is already sent
ray.get(signal.wait.remote())
assert ray.get(signal.cur_num_waiters.remote()) == 0
# clear the signal
ray.get(signal.send.remote(clear=True))
signal.wait.remote()
# Verify all waiters are done
wait_for_condition(lambda: ray.get(signal.cur_num_waiters.remote()) == 1)
ray.get(signal.send.remote())
def test_semaphore_basic(ray_init):
"""Test basic Semaphore functionality - acquire, release, and lock status."""
sema = Semaphore.remote(value=2)
# Test initial state
wait_for_condition(lambda: ray.get(sema.locked.remote()) is False)
# Test acquire and release
ray.get(sema.acquire.remote())
ray.get(sema.acquire.remote())
wait_for_condition(lambda: ray.get(sema.locked.remote()) is True)
ray.get(sema.release.remote())
ray.get(sema.release.remote())
wait_for_condition(lambda: ray.get(sema.locked.remote()) is False)
def test_semaphore_concurrent(ray_init):
"""Test Semaphore with concurrent workers to verify resource limiting."""
sema = Semaphore.remote(value=2)
def worker():
ray.get(sema.acquire.remote())
time.sleep(0.1)
ray.get(sema.release.remote())
# Create multiple workers
_ = [worker() for _ in range(4)]
# Verify semaphore is not locked
wait_for_condition(lambda: ray.get(sema.locked.remote()) is False)
if __name__ == "__main__":
sys.exit(pytest.main(["-sv", __file__]))
+515
View File
@@ -0,0 +1,515 @@
"""Tests for Ray signature utility functions.
This module contains pytest-based tests for signature-related functions in
ray._common.signature. These functions are used for extracting, validating,
and flattening function signatures for serialization.
"""
import inspect
import sys
from typing import Any, Optional
from unittest.mock import Mock, patch
import pytest
from ray._common.signature import (
DUMMY_TYPE,
extract_signature,
flatten_args,
get_signature,
recover_args,
validate_args,
)
class TestGetSignature:
"""Tests for the get_signature utility function."""
def test_regular_function(self):
"""Test getting signature from a regular Python function."""
def test_func(a, b=10, *args, **kwargs):
return a + b
sig = get_signature(test_func)
assert sig is not None
assert len(sig.parameters) == 4
assert "a" in sig.parameters
assert "b" in sig.parameters
assert sig.parameters["b"].default == 10
def test_function_with_annotations(self):
"""Test getting signature from a function with type annotations."""
def test_func(a: int, b: str = "default") -> str:
return f"{a}{b}"
sig = get_signature(test_func)
assert sig is not None
assert len(sig.parameters) == 2
assert sig.parameters["a"].annotation is int
assert sig.parameters["b"].annotation is str
assert sig.parameters["b"].default == "default"
def test_function_no_parameters(self):
"""Test getting signature from a function with no parameters."""
def test_func():
return "hello"
sig = get_signature(test_func)
assert sig is not None
assert len(sig.parameters) == 0
def test_lambda_function(self):
"""Test getting signature from a lambda function."""
sig = get_signature(lambda x, y=5: x + y)
assert sig is not None
assert len(sig.parameters) == 2 # x, y
assert sig.parameters["y"].default == 5
@patch("ray._common.signature.is_cython")
def test_cython_function_with_attributes(self, mock_is_cython):
"""Test getting signature from a Cython function with required attributes."""
mock_is_cython.return_value = True
def original_func(x=10):
return x
mock_func = Mock()
mock_func.__code__ = original_func.__code__
mock_func.__annotations__ = original_func.__annotations__
mock_func.__defaults__ = original_func.__defaults__
mock_func.__kwdefaults__ = original_func.__kwdefaults__
sig = get_signature(mock_func)
assert sig is not None
assert len(sig.parameters) == 1
assert "x" in sig.parameters
@patch("ray._common.signature.is_cython")
def test_cython_function_missing_attributes(self, mock_is_cython):
"""Test error handling for Cython function missing required attributes."""
mock_is_cython.return_value = True
# Create a mock Cython function missing required attributes
mock_func = Mock()
del mock_func.__code__ # Remove required attribute
with pytest.raises(TypeError, match="is not a Python function we can process"):
get_signature(mock_func)
def test_method_signature(self):
"""Test getting signature from a class method."""
class TestClass:
def test_method(self, a, b=20):
return a + b
sig = get_signature(TestClass.test_method)
assert sig is not None
assert len(sig.parameters) == 3 # self, a, b
assert "self" in sig.parameters
assert "a" in sig.parameters
assert "b" in sig.parameters
assert sig.parameters["b"].default == 20
class TestExtractSignature:
"""Tests for the extract_signature utility function."""
def test_function_without_ignore_first(self):
"""Test extracting signature from function without ignoring first parameter."""
def test_func(a, b=10, c=None):
return a + b
params = extract_signature(test_func, ignore_first=False)
assert len(params) == 3
assert params[0].name == "a"
assert params[1].name == "b"
assert params[1].default == 10
assert params[2].name == "c"
assert params[2].default is None
def test_method_with_ignore_first(self):
"""Test extracting signature from method ignoring 'self' parameter."""
class TestClass:
def test_method(self, a, b=20):
return a + b
params = extract_signature(TestClass.test_method, ignore_first=True)
assert len(params) == 2
assert params[0].name == "a"
assert params[1].name == "b"
assert params[1].default == 20
def test_function_with_ignore_first(self):
"""Test extracting signature from regular function with ignore_first=True."""
def test_func(x, y, z=30):
return x + y + z
params = extract_signature(test_func, ignore_first=True)
assert len(params) == 2
assert params[0].name == "y"
assert params[1].name == "z"
assert params[1].default == 30
def test_empty_parameters_with_ignore_first(self):
"""Test error handling when method has no parameters but ignore_first=True."""
def test_func():
return "hello"
with pytest.raises(ValueError, match="Methods must take a 'self' argument"):
extract_signature(test_func, ignore_first=True)
def test_single_parameter_with_ignore_first(self):
"""Test extracting signature from method with only 'self' parameter."""
class TestClass:
def test_method(self):
return "hello"
params = extract_signature(TestClass.test_method, ignore_first=True)
assert len(params) == 0
def test_varargs_and_kwargs(self):
"""Test extracting signature with *args and **kwargs."""
def test_func(a, b=10, *args, **kwargs):
return a + b
params = extract_signature(test_func, ignore_first=False)
assert len(params) == 4
assert params[0].name == "a"
assert params[1].name == "b"
assert params[2].name == "args"
assert params[2].kind == inspect.Parameter.VAR_POSITIONAL
assert params[3].name == "kwargs"
assert params[3].kind == inspect.Parameter.VAR_KEYWORD
class TestValidateArgs:
"""Tests for the validate_args utility function."""
def test_valid_positional_args(self):
"""Test validation with valid positional arguments."""
def test_func(a, b, c=30):
return a + b + c
params = extract_signature(test_func)
# Should not raise an exception
validate_args(params, (1, 2), {})
validate_args(params, (1, 2, 3), {})
def test_valid_keyword_args(self):
"""Test validation with valid keyword arguments."""
def test_func(a, b=20, c=30):
return a + b + c
params = extract_signature(test_func)
# Should not raise an exception
validate_args(params, (1,), {"b": 2})
validate_args(params, (1,), {"b": 2, "c": 3})
validate_args(params, (), {"a": 1, "b": 2, "c": 3})
def test_valid_mixed_args(self):
"""Test validation with mixed positional and keyword arguments."""
def test_func(a, b, c=30):
return a + b + c
params = extract_signature(test_func)
# Should not raise an exception
validate_args(params, (1,), {"b": 2})
validate_args(params, (1, 2), {"c": 3})
def test_too_many_positional_args(self):
"""Test error handling for too many positional arguments."""
def test_func(a, b):
return a + b
params = extract_signature(test_func)
with pytest.raises(TypeError):
validate_args(params, (1, 2, 3), {})
def test_missing_required_args(self):
"""Test error handling for missing required arguments."""
def test_func(a, b, c=30):
return a + b + c
params = extract_signature(test_func)
with pytest.raises(TypeError):
validate_args(params, (1,), {}) # Missing 'b'
def test_unexpected_keyword_args(self):
"""Test error handling for unexpected keyword arguments."""
def test_func(a, b):
return a + b
params = extract_signature(test_func)
with pytest.raises(TypeError):
validate_args(params, (1, 2), {"c": 3})
def test_duplicate_args(self):
"""Test error handling for duplicate arguments (positional and keyword)."""
def test_func(a, b, c=30):
return a + b + c
params = extract_signature(test_func)
with pytest.raises(TypeError):
validate_args(params, (1, 2), {"b": 3}) # 'b' specified twice
def test_varargs_validation(self):
"""Test validation with *args and **kwargs."""
def test_func(a, b=20, *args, **kwargs):
return a + b
params = extract_signature(test_func)
# Should not raise an exception
validate_args(params, (1, 2, 3, 4), {"extra": 5})
validate_args(params, (1,), {"b": 2, "extra": 3})
class TestFlattenArgs:
"""Tests for the flatten_args utility function."""
def test_only_positional_args(self):
"""Test flattening with only positional arguments."""
def test_func(a, b, c):
return a + b + c
params = extract_signature(test_func)
flattened = flatten_args(params, (1, 2, 3), {})
expected = [DUMMY_TYPE, 1, DUMMY_TYPE, 2, DUMMY_TYPE, 3]
assert flattened == expected
def test_only_keyword_args(self):
"""Test flattening with only keyword arguments."""
def test_func(a=1, b=2, c=3):
return a + b + c
params = extract_signature(test_func)
flattened = flatten_args(params, (), {"a": 10, "b": 20, "c": 30})
expected = ["a", 10, "b", 20, "c", 30]
assert flattened == expected
def test_mixed_args(self):
"""Test flattening with mixed positional and keyword arguments."""
def test_func(a, b, c=30):
return a + b + c
params = extract_signature(test_func)
flattened = flatten_args(params, (1, 2), {"c": 3})
expected = [DUMMY_TYPE, 1, DUMMY_TYPE, 2, "c", 3]
assert flattened == expected
def test_empty_args(self):
"""Test flattening with no arguments."""
def test_func():
return "hello"
params = extract_signature(test_func)
flattened = flatten_args(params, (), {})
assert flattened == []
def test_complex_types(self):
"""Test flattening with complex argument types."""
def test_func(a, b, c=None):
return a + b
params = extract_signature(test_func)
complex_args = ([1, 2, 3], {"key": "value"})
complex_kwargs = {"c": {"nested": "dict"}}
flattened = flatten_args(params, complex_args, complex_kwargs)
expected = [
DUMMY_TYPE,
[1, 2, 3],
DUMMY_TYPE,
{"key": "value"},
"c",
{"nested": "dict"},
]
assert flattened == expected
def test_invalid_args_raises_error(self):
"""Test that invalid arguments raise TypeError during flattening."""
def test_func(a, b):
return a + b
params = extract_signature(test_func)
with pytest.raises(TypeError):
flatten_args(params, (1, 2, 3), {}) # Too many args
class TestRecoverArgs:
"""Tests for the recover_args utility function."""
def test_only_positional_args(self):
"""Test recovering only positional arguments."""
flattened = [DUMMY_TYPE, 1, DUMMY_TYPE, 2, DUMMY_TYPE, 3]
args, kwargs = recover_args(flattened)
assert args == [1, 2, 3]
assert kwargs == {}
def test_only_keyword_args(self):
"""Test recovering only keyword arguments."""
flattened = ["a", 10, "b", 20, "c", 30]
args, kwargs = recover_args(flattened)
assert args == []
assert kwargs == {"a": 10, "b": 20, "c": 30}
def test_mixed_args(self):
"""Test recovering mixed positional and keyword arguments."""
flattened = [DUMMY_TYPE, 1, DUMMY_TYPE, 2, "c", 3]
args, kwargs = recover_args(flattened)
assert args == [1, 2]
assert kwargs == {"c": 3}
def test_empty_flattened(self):
"""Test recovering from empty flattened list."""
flattened = []
args, kwargs = recover_args(flattened)
assert args == []
assert kwargs == {}
def test_complex_types(self):
"""Test recovering complex argument types."""
flattened = [
DUMMY_TYPE,
[1, 2, 3],
DUMMY_TYPE,
{"key": "value"},
"c",
{"nested": "dict"},
]
args, kwargs = recover_args(flattened)
assert args == [[1, 2, 3], {"key": "value"}]
assert kwargs == {"c": {"nested": "dict"}}
def test_invalid_odd_length(self):
"""Test error handling for odd-length flattened list."""
flattened = [DUMMY_TYPE, 1, "key"] # Odd length
with pytest.raises(
AssertionError, match="Flattened arguments need to be even-numbered"
):
recover_args(flattened)
def test_preserve_order(self):
"""Test that argument order is preserved during flatten/recover."""
def test_func(a, b, c, d, e):
return a + b + c + d + e
params = extract_signature(test_func)
original_args = (1, 2, 3)
original_kwargs = {"d": 4, "e": 5}
flattened = flatten_args(params, original_args, original_kwargs)
recovered_args, recovered_kwargs = recover_args(flattened)
assert recovered_args == [1, 2, 3]
assert recovered_kwargs == {"d": 4, "e": 5}
class TestIntegration:
"""Integration tests for signature utilities working together."""
def test_complete_workflow(self):
"""Test complete workflow from function to flatten/recover."""
def test_func(x: int, y: str = "default", z: Optional[Any] = None):
return f"{x}_{y}_{z}"
# Extract signature
params = extract_signature(test_func)
assert len(params) == 3
# Validate arguments
args = (42, "hello")
kwargs = {"z": [1, 2, 3]}
validate_args(params, args, kwargs)
# Flatten arguments
flattened = flatten_args(params, args, kwargs)
expected = [DUMMY_TYPE, 42, DUMMY_TYPE, "hello", "z", [1, 2, 3]]
assert flattened == expected
# Recover arguments
recovered_args, recovered_kwargs = recover_args(flattened)
assert recovered_args == list(args)
assert recovered_kwargs == kwargs
def test_method_workflow_with_ignore_first(self):
"""Test complete workflow for class methods with ignore_first=True."""
class TestClass:
def test_method(self, a: int, b: str = "test"):
return f"{a}_{b}"
# Extract signature ignoring 'self'
params = extract_signature(TestClass.test_method, ignore_first=True)
assert len(params) == 2
assert params[0].name == "a"
assert params[1].name == "b"
# Validate and flatten
args = (100,)
kwargs = {"b": "custom"}
validate_args(params, args, kwargs)
flattened = flatten_args(params, args, kwargs)
# Recover and verify
recovered_args, recovered_kwargs = recover_args(flattened)
assert recovered_args == list(args)
assert recovered_kwargs == kwargs
def test_varargs_kwargs_workflow(self):
"""Test workflow with functions that have *args and **kwargs."""
def test_func(a, b=10, *args, **kwargs):
return a + b + sum(args) + sum(kwargs.values())
params = extract_signature(test_func)
# Test with extra positional and keyword arguments
args = (1, 2, 3, 4, 5)
kwargs = {"extra1": 10, "extra2": 20}
validate_args(params, args, kwargs)
flattened = flatten_args(params, args, kwargs)
recovered_args, recovered_kwargs = recover_args(flattened)
assert recovered_args == list(args)
assert recovered_kwargs == kwargs
if __name__ == "__main__":
sys.exit(pytest.main(["-v", "-s", __file__]))
@@ -0,0 +1,41 @@
import sys
import pytest
from ray._common.tls_utils import generate_self_signed_tls_certs
def test_generate_self_signed_tls_certs_returns_tuple():
cert_contents, key_contents = generate_self_signed_tls_certs()
assert isinstance(cert_contents, str)
assert isinstance(key_contents, str)
def test_generate_self_signed_tls_certs_pem_format():
cert_contents, key_contents = generate_self_signed_tls_certs()
assert cert_contents.strip().startswith("-----BEGIN CERTIFICATE-----")
assert cert_contents.strip().endswith("-----END CERTIFICATE-----")
assert key_contents.strip().startswith("-----BEGIN")
assert "PRIVATE KEY" in key_contents
def test_generate_self_signed_tls_certs_usable_for_ssl():
import ssl
import tempfile
cert_contents, key_contents = generate_self_signed_tls_certs()
with (
tempfile.NamedTemporaryFile(mode="w", suffix=".crt") as cf,
tempfile.NamedTemporaryFile(mode="w", suffix=".key") as kf,
):
cf.write(cert_contents)
cf.flush()
kf.write(key_contents)
kf.flush()
ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
ctx.load_cert_chain(cf.name, kf.name)
if __name__ == "__main__":
sys.exit(pytest.main(["-v", __file__]))
File diff suppressed because it is too large Load Diff
+291
View File
@@ -0,0 +1,291 @@
"""Tests for Ray utility functions.
This module contains pytest-based tests for utility functions in ray._common.utils.
Test utility classes (SignalActor, Semaphore) are in ray._common.test_utils to
ensure they're included in the Ray package distribution.
"""
import asyncio
import os
import sys
import tempfile
import warnings
import pytest
from ray._common.utils import (
_BACKGROUND_TASKS,
env_bool,
get_or_create_event_loop,
get_system_memory,
load_class,
run_background_task,
try_to_create_directory,
)
# Optional imports for testing
try:
import psutil
PSUTIL_AVAILABLE = True
except ImportError:
PSUTIL_AVAILABLE = False
class TestGetOrCreateEventLoop:
"""Tests for the get_or_create_event_loop utility function."""
def test_existing_event_loop(self):
# With running event loop
expect_loop = asyncio.new_event_loop()
expect_loop.set_debug(True)
asyncio.set_event_loop(expect_loop)
with warnings.catch_warnings():
# Assert no deprecating warnings raised for python>=3.10.
warnings.simplefilter("error")
actual_loop = get_or_create_event_loop()
assert actual_loop == expect_loop, "Loop should not be recreated."
def test_new_event_loop(self):
with warnings.catch_warnings():
# Assert no deprecating warnings raised for python>=3.10.
warnings.simplefilter("error")
loop = get_or_create_event_loop()
assert loop is not None, "new event loop should be created."
class TestEnvBool:
"""Tests for the env_bool utility function."""
_KEY = "_RAY_TEST_ENV_BOOL"
@pytest.mark.parametrize(
"env_value, expected",
[
("1", True),
("true", True),
("True", True),
("TRUE", True),
("0", False),
("false", False),
("False", False),
("FALSE", False),
("yes", False),
("no", False),
("", False),
],
)
def test_env_bool_values(self, env_value, expected, monkeypatch):
monkeypatch.setenv(self._KEY, env_value)
assert env_bool(self._KEY, False) is expected
def test_env_bool_default_when_unset(self, monkeypatch):
monkeypatch.delenv(self._KEY, raising=False)
assert env_bool(self._KEY, False) is False
assert env_bool(self._KEY, True) is True
@pytest.mark.asyncio
async def test_run_background_task():
"""Test the run_background_task utility function."""
result = {}
async def co():
result["start"] = 1
await asyncio.sleep(0)
result["end"] = 1
run_background_task(co())
# Background task is running.
assert len(_BACKGROUND_TASKS) == 1
# co executed.
await asyncio.sleep(0)
# await asyncio.sleep(0) from co is reached.
await asyncio.sleep(0)
# co finished and callback called.
await asyncio.sleep(0)
# The task should be removed from the set once it finishes.
assert len(_BACKGROUND_TASKS) == 0
assert result.get("start") == 1
assert result.get("end") == 1
class TestTryToCreateDirectory:
"""Tests for the try_to_create_directory utility function."""
def test_create_new_directory(self):
"""Test creating a new directory."""
with tempfile.TemporaryDirectory() as temp_dir:
test_dir = os.path.join(temp_dir, "test_dir")
try_to_create_directory(test_dir)
assert os.path.exists(test_dir), "Directory should be created"
assert os.path.isdir(test_dir), "Path should be a directory"
def test_existing_directory(self):
"""Test creating a directory that already exists."""
with tempfile.TemporaryDirectory() as temp_dir:
test_dir = os.path.join(temp_dir, "existing_dir")
# Create directory first
os.makedirs(test_dir)
# Should work without error
try_to_create_directory(test_dir)
assert os.path.exists(test_dir), "Directory should still exist"
def test_nested_directory_creation(self):
"""Test creating nested directory structure."""
with tempfile.TemporaryDirectory() as temp_dir:
nested_dir = os.path.join(temp_dir, "nested", "deep", "structure")
try_to_create_directory(nested_dir)
assert os.path.exists(nested_dir), "Nested directory should be created"
def test_tilde_expansion(self):
"""Test directory creation with tilde expansion."""
with tempfile.TemporaryDirectory() as temp_dir:
fake_home = os.path.join(temp_dir, "fake_home")
os.makedirs(fake_home, exist_ok=True)
# Mock the expanduser for this test
original_expanduser = os.path.expanduser
os.path.expanduser = (
lambda path: path.replace("~", fake_home)
if path.startswith("~")
else path
)
try:
tilde_dir = "~/test_tilde_dir"
try_to_create_directory(tilde_dir)
expected_path = os.path.join(fake_home, "test_tilde_dir")
assert os.path.exists(
expected_path
), "Tilde-expanded directory should be created"
finally:
# Restore original expanduser
os.path.expanduser = original_expanduser
class TestLoadClass:
"""Tests for the load_class utility function."""
def test_load_builtin_class(self):
"""Test loading a builtin class."""
list_class = load_class("builtins.list")
assert list_class is list, "Should load the builtin list class"
def test_load_module(self):
"""Test loading a module."""
path_module = load_class("os.path")
import os.path
assert path_module is os.path, "Should load os.path module"
def test_load_function(self):
"""Test loading a function from a module."""
makedirs_func = load_class("os.makedirs")
assert makedirs_func is os.makedirs, "Should load os.makedirs function"
def test_load_standard_library_class(self):
"""Test loading a standard library class."""
temp_dir_class = load_class("tempfile.TemporaryDirectory")
assert (
temp_dir_class is tempfile.TemporaryDirectory
), "Should load TemporaryDirectory class"
def test_load_nested_module_class(self):
"""Test loading a class from a nested module."""
datetime_class = load_class("datetime.datetime")
import datetime
assert (
datetime_class is datetime.datetime
), "Should load datetime.datetime class"
def test_invalid_path_error(self):
"""Test error handling for invalid paths."""
with pytest.raises(ValueError, match="valid path like mymodule.provider_class"):
load_class("invalid")
def test_nonexistent_module_error(self):
"""Test error handling for nonexistent modules."""
with pytest.raises((ImportError, ModuleNotFoundError)):
load_class("nonexistent_module.SomeClass")
def test_nonexistent_attribute_error(self):
"""Test error handling for nonexistent attributes."""
with pytest.raises(AttributeError):
load_class("os.NonexistentClass")
class TestGetSystemMemory:
"""Tests for the get_system_memory utility function."""
@pytest.mark.skipif(not PSUTIL_AVAILABLE, reason="psutil not available")
def test_cgroups_v1_with_low_limit(self):
"""Test cgroups v1 with low memory limit."""
with tempfile.NamedTemporaryFile("w") as memory_limit_file:
memory_limit_file.write("1073741824") # 1GB
memory_limit_file.flush()
memory = get_system_memory(
memory_limit_filename=memory_limit_file.name,
memory_limit_filename_v2="__does_not_exist__",
)
assert memory == 1073741824, "Should return cgroup limit when low"
@pytest.mark.skipif(not PSUTIL_AVAILABLE, reason="psutil not available")
def test_cgroups_v1_with_high_limit(self):
"""Test cgroups v1 with high memory limit (should fallback to psutil)."""
with tempfile.NamedTemporaryFile("w") as memory_limit_file:
memory_limit_file.write(str(2**63)) # Very high limit
memory_limit_file.flush()
psutil_memory = psutil.virtual_memory().total
memory = get_system_memory(
memory_limit_filename=memory_limit_file.name,
memory_limit_filename_v2="__does_not_exist__",
)
assert (
memory == psutil_memory
), "Should fallback to psutil when cgroup limit is very high"
@pytest.mark.skipif(not PSUTIL_AVAILABLE, reason="psutil not available")
def test_cgroups_v2_with_limit(self):
"""Test cgroups v2 with memory limit set."""
with tempfile.NamedTemporaryFile("w") as memory_max_file:
memory_max_file.write("2147483648\n") # 2GB with newline
memory_max_file.flush()
memory = get_system_memory(
memory_limit_filename="__does_not_exist__",
memory_limit_filename_v2=memory_max_file.name,
)
assert memory == 2147483648, "Should return cgroups v2 limit"
@pytest.mark.skipif(not PSUTIL_AVAILABLE, reason="psutil not available")
def test_cgroups_v2_unlimited(self):
"""Test cgroups v2 with unlimited memory (max)."""
with tempfile.NamedTemporaryFile("w") as memory_max_file:
memory_max_file.write("max")
memory_max_file.flush()
psutil_memory = psutil.virtual_memory().total
memory = get_system_memory(
memory_limit_filename="__does_not_exist__",
memory_limit_filename_v2=memory_max_file.name,
)
assert (
memory == psutil_memory
), "Should fallback to psutil when cgroups v2 is unlimited"
@pytest.mark.skipif(not PSUTIL_AVAILABLE, reason="psutil not available")
def test_no_cgroup_files(self):
"""Test fallback to psutil when no cgroup files exist."""
psutil_memory = psutil.virtual_memory().total
memory = get_system_memory(
memory_limit_filename="__does_not_exist__",
memory_limit_filename_v2="__also_does_not_exist__",
)
assert memory == psutil_memory, "Should use psutil when no cgroup files exist"
if __name__ == "__main__":
sys.exit(pytest.main(["-sv", __file__]))
@@ -0,0 +1,320 @@
import asyncio
import sys
import time
import pytest
from ray._common.test_utils import async_wait_for_condition, wait_for_condition
class TestWaitForCondition:
"""Tests for the synchronous wait_for_condition function."""
def test_immediate_true_condition(self):
"""Test that function returns immediately when condition is already true."""
def always_true():
return True
wait_for_condition(always_true, timeout=5)
def test_condition_becomes_true(self):
"""Test waiting for a condition that becomes true after some time."""
counter = {"value": 0}
def condition():
counter["value"] += 1
return counter["value"] >= 3
wait_for_condition(condition, timeout=5, retry_interval_ms=50)
assert counter["value"] >= 3
def test_timeout_raises_runtime_error(self):
"""Test that timeout raises RuntimeError with appropriate message."""
def always_false():
return False
with pytest.raises(RuntimeError) as exc_info:
wait_for_condition(always_false, timeout=0.2, retry_interval_ms=50)
assert "condition wasn't met before the timeout expired" in str(exc_info.value)
def test_condition_with_kwargs(self):
"""Test passing kwargs to the condition predictor."""
def condition_with_args(target, current=0):
return current >= target
wait_for_condition(condition_with_args, timeout=1, target=5, current=10)
# Should not raise an exception since current >= target
def test_exception_handling_default(self):
"""Test that exceptions are caught by default and timeout occurs."""
def failing_condition():
raise ValueError("Test exception")
with pytest.raises(RuntimeError) as exc_info:
wait_for_condition(failing_condition, timeout=0.2, retry_interval_ms=50)
error_msg = str(exc_info.value)
assert "condition wasn't met before the timeout expired" in error_msg
assert "Last exception:" in error_msg
assert "ValueError: Test exception" in error_msg
def test_exception_handling_raise_true(self):
"""Test that exceptions are raised when raise_exceptions=True."""
def failing_condition():
raise ValueError("Test exception")
with pytest.raises(ValueError) as exc_info:
wait_for_condition(failing_condition, timeout=1, raise_exceptions=True)
assert "Test exception" in str(exc_info.value)
def test_custom_retry_interval(self):
"""Test that custom retry intervals are respected."""
call_times = []
def condition():
call_times.append(time.time())
return len(call_times) >= 3
wait_for_condition(condition, timeout=5, retry_interval_ms=200)
# Verify that calls were spaced approximately 200ms apart
if len(call_times) >= 2:
interval = call_times[1] - call_times[0]
assert 0.15 <= interval <= 0.25 # Allow some tolerance
def test_condition_with_mixed_results(self):
"""Test condition that fails initially then succeeds."""
attempts = {"count": 0}
def intermittent_condition():
attempts["count"] += 1
# Succeed on the 4th attempt
return attempts["count"] >= 4
wait_for_condition(intermittent_condition, timeout=2, retry_interval_ms=100)
assert attempts["count"] >= 4
class TestAsyncWaitForCondition:
"""Tests for the asynchronous async_wait_for_condition function."""
@pytest.mark.asyncio
async def test_immediate_true_condition(self):
"""Test that function returns immediately when condition is already true."""
def always_true():
return True
await async_wait_for_condition(always_true, timeout=5)
@pytest.mark.asyncio
async def test_async_condition_becomes_true(self):
"""Test waiting for an async condition that becomes true after some time."""
counter = {"value": 0}
async def async_condition():
counter["value"] += 1
await asyncio.sleep(0.01) # Small async operation
return counter["value"] >= 3
await async_wait_for_condition(async_condition, timeout=5, retry_interval_ms=50)
assert counter["value"] >= 3
@pytest.mark.asyncio
async def test_sync_condition_becomes_true(self):
"""Test waiting for a sync condition in async context."""
counter = {"value": 0}
def sync_condition():
counter["value"] += 1
return counter["value"] >= 3
await async_wait_for_condition(sync_condition, timeout=5, retry_interval_ms=50)
assert counter["value"] >= 3
@pytest.mark.asyncio
async def test_timeout_raises_runtime_error(self):
"""Test that timeout raises RuntimeError with appropriate message."""
def always_false():
return False
with pytest.raises(RuntimeError) as exc_info:
await async_wait_for_condition(
always_false, timeout=0.2, retry_interval_ms=50
)
assert "condition wasn't met before the timeout expired" in str(exc_info.value)
@pytest.mark.asyncio
async def test_condition_with_kwargs(self):
"""Test passing kwargs to the condition predictor."""
def condition_with_args(target, current=0):
return current >= target
await async_wait_for_condition(
condition_with_args, timeout=1, target=5, current=10
)
# Should not raise an exception since current >= target
@pytest.mark.asyncio
async def test_async_condition_with_kwargs(self):
"""Test passing kwargs to an async condition predictor."""
async def async_condition_with_args(target, current=0):
await asyncio.sleep(0.01)
return current >= target
await async_wait_for_condition(
async_condition_with_args, timeout=1, target=5, current=10
)
# Should not raise an exception since current >= target
@pytest.mark.asyncio
async def test_exception_handling(self):
"""Test that exceptions are caught and timeout occurs."""
def failing_condition():
raise ValueError("Test exception")
with pytest.raises(RuntimeError) as exc_info:
await async_wait_for_condition(
failing_condition, timeout=0.2, retry_interval_ms=50
)
error_msg = str(exc_info.value)
assert "condition wasn't met before the timeout expired" in error_msg
assert "Last exception:" in error_msg
@pytest.mark.asyncio
async def test_async_exception_handling(self):
"""Test that exceptions from async conditions are caught."""
async def async_failing_condition():
await asyncio.sleep(0.01)
raise ValueError("Async test exception")
with pytest.raises(RuntimeError) as exc_info:
await async_wait_for_condition(
async_failing_condition, timeout=0.2, retry_interval_ms=50
)
error_msg = str(exc_info.value)
assert "condition wasn't met before the timeout expired" in error_msg
assert "Last exception:" in error_msg
@pytest.mark.asyncio
async def test_custom_retry_interval(self):
"""Test that custom retry intervals are respected."""
call_times = []
def condition():
call_times.append(time.time())
return len(call_times) >= 3
await async_wait_for_condition(condition, timeout=5, retry_interval_ms=200)
# Verify that calls were spaced approximately 200ms apart
if len(call_times) >= 2:
interval = call_times[1] - call_times[0]
assert 0.15 <= interval <= 0.25 # Allow some tolerance
@pytest.mark.asyncio
async def test_mixed_sync_async_conditions(self):
"""Test that both sync and async conditions work in the same test."""
sync_counter = {"value": 0}
async_counter = {"value": 0}
def sync_condition():
sync_counter["value"] += 1
return sync_counter["value"] >= 2
async def async_condition():
async_counter["value"] += 1
await asyncio.sleep(0.01)
return async_counter["value"] >= 2
# Test sync condition
await async_wait_for_condition(sync_condition, timeout=2, retry_interval_ms=50)
assert sync_counter["value"] >= 2
# Test async condition
await async_wait_for_condition(async_condition, timeout=2, retry_interval_ms=50)
assert async_counter["value"] >= 2
class TestEdgeCases:
"""Tests for edge cases and boundary conditions."""
def test_zero_timeout(self):
"""Test behavior with zero timeout."""
def slow_condition():
time.sleep(0.1)
return True
with pytest.raises(RuntimeError):
wait_for_condition(slow_condition, timeout=0, retry_interval_ms=50)
@pytest.mark.asyncio
async def test_async_zero_timeout(self):
"""Test async behavior with zero timeout."""
async def slow_condition():
await asyncio.sleep(0.1)
return True
with pytest.raises(RuntimeError):
await async_wait_for_condition(
slow_condition, timeout=0, retry_interval_ms=50
)
def test_very_small_retry_interval(self):
"""Test with very small retry interval."""
counter = {"value": 0}
def condition():
counter["value"] += 1
return counter["value"] >= 5
start_time = time.time()
wait_for_condition(condition, timeout=1, retry_interval_ms=1)
elapsed = time.time() - start_time
# Should complete quickly due to small retry interval
assert elapsed < 0.5
assert counter["value"] >= 5
@pytest.mark.asyncio
async def test_async_very_small_retry_interval(self):
"""Test async version with very small retry interval."""
counter = {"value": 0}
def condition():
counter["value"] += 1
return counter["value"] >= 5
start_time = time.time()
await async_wait_for_condition(condition, timeout=1, retry_interval_ms=1)
elapsed = time.time() - start_time
# Should complete quickly due to small retry interval
assert elapsed < 0.5
assert counter["value"] >= 5
if __name__ == "__main__":
sys.exit(pytest.main(["-sv", __file__]))