chore: import upstream snapshot with attribution
This commit is contained in:
@@ -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",
|
||||
],
|
||||
)
|
||||
@@ -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__]))
|
||||
@@ -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__]))
|
||||
@@ -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__]))
|
||||
@@ -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__]))
|
||||
@@ -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
@@ -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__]))
|
||||
Reference in New Issue
Block a user