chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,261 @@
|
||||
import itertools
|
||||
import random
|
||||
import sys
|
||||
from typing import Dict, List
|
||||
|
||||
import pytest
|
||||
|
||||
from ray import serve
|
||||
from ray.serve._private.config import DeploymentConfig
|
||||
|
||||
|
||||
def get_random_dict_combos(d: Dict, n: int) -> List[Dict]:
|
||||
"""Gets n random combinations of dictionary d.
|
||||
|
||||
Args:
|
||||
d: The source dictionary to draw combinations from.
|
||||
n: The maximum number of combinations to return.
|
||||
|
||||
Returns:
|
||||
List of dictionary combinations of lengths from 0 to len(d). List
|
||||
contains n random combinations of d's elements.
|
||||
"""
|
||||
|
||||
# Shuffle dictionary without modifying original dictionary
|
||||
d = dict(random.sample(list(d.items()), len(d)))
|
||||
|
||||
combos = []
|
||||
|
||||
# Sample random combos of random size
|
||||
subset_sizes = list(range(len(d) + 1))
|
||||
random.shuffle(subset_sizes)
|
||||
|
||||
for subset_size in subset_sizes:
|
||||
subset_combo_iterator = map(
|
||||
dict, itertools.combinations(d.items(), subset_size)
|
||||
)
|
||||
if len(combos) < n:
|
||||
subset_combos = list(
|
||||
itertools.islice(subset_combo_iterator, n - len(combos))
|
||||
)
|
||||
combos.extend(subset_combos)
|
||||
else:
|
||||
break
|
||||
|
||||
return combos
|
||||
|
||||
|
||||
class TestGetDictCombos:
|
||||
def test_empty(self):
|
||||
assert get_random_dict_combos({}, 1) == [{}]
|
||||
|
||||
def test_basic(self):
|
||||
d = {"a": 1, "b": 2, "c": 3}
|
||||
combos = get_random_dict_combos(d, 8)
|
||||
|
||||
# Sort combos for comparison (sort by length, break ties by value sum)
|
||||
combos.sort(key=lambda d: len(d) * 100 + sum(d.values()))
|
||||
|
||||
assert combos == [
|
||||
# Dictionaries of length 0
|
||||
{},
|
||||
# Dictionaries of length 1
|
||||
*({"a": 1}, {"b": 2}, {"c": 3}),
|
||||
# Dictionaries of length 2
|
||||
*({"a": 1, "b": 2}, {"a": 1, "c": 3}, {"b": 2, "c": 3}),
|
||||
# Dictionaries of length 3
|
||||
{"a": 1, "b": 2, "c": 3},
|
||||
]
|
||||
|
||||
def test_len(self):
|
||||
d = {i: i + 1 for i in range(50)}
|
||||
assert len(get_random_dict_combos(d, 1000)) == 1000
|
||||
|
||||
def test_randomness(self):
|
||||
d = {i: i + 1 for i in range(1000)}
|
||||
combo1 = get_random_dict_combos(d, 1000)[0]
|
||||
combo2 = get_random_dict_combos(d, 1000)[0]
|
||||
assert combo1 != combo2
|
||||
|
||||
|
||||
class TestDeploymentOptions:
|
||||
# Deployment options mapped to sample input
|
||||
deployment_options = {
|
||||
"name": "test",
|
||||
"num_replicas": 1,
|
||||
"ray_actor_options": {},
|
||||
"user_config": {},
|
||||
"max_ongoing_requests": 10,
|
||||
"autoscaling_config": None,
|
||||
"graceful_shutdown_wait_loop_s": 10,
|
||||
"graceful_shutdown_timeout_s": 10,
|
||||
"health_check_period_s": 10,
|
||||
"health_check_timeout_s": 10,
|
||||
}
|
||||
|
||||
deployment_option_combos = get_random_dict_combos(deployment_options, 1000)
|
||||
|
||||
@pytest.mark.parametrize("options", deployment_option_combos)
|
||||
def test_user_configured_option_names(self, options: Dict):
|
||||
"""Check that user_configured_option_names tracks the correct options.
|
||||
|
||||
Args:
|
||||
options: Maps deployment option strings (e.g. "name",
|
||||
"num_replicas", etc.) to sample inputs. Pairs come from
|
||||
TestDeploymentOptions.deployment_options.
|
||||
"""
|
||||
|
||||
@serve.deployment(**options)
|
||||
def f():
|
||||
pass
|
||||
|
||||
assert f._deployment_config.user_configured_option_names == set(options.keys())
|
||||
|
||||
@pytest.mark.parametrize("options", deployment_option_combos)
|
||||
def test_user_configured_option_names_serialized(self, options: Dict):
|
||||
"""Check user_configured_option_names after serialization.
|
||||
|
||||
Args:
|
||||
options: Maps deployment option strings (e.g. "name",
|
||||
"num_replicas", etc.) to sample inputs. Pairs come from
|
||||
TestDeploymentOptions.deployment_options.
|
||||
"""
|
||||
|
||||
# init_kwargs requires independent serialization, so we omit it.
|
||||
if "init_kwargs" in options:
|
||||
del options["init_kwargs"]
|
||||
|
||||
@serve.deployment(**options)
|
||||
def f():
|
||||
pass
|
||||
|
||||
serialized_config = f._deployment_config.to_proto_bytes()
|
||||
deserialized_config = DeploymentConfig.from_proto_bytes(serialized_config)
|
||||
|
||||
assert deserialized_config.user_configured_option_names == set(options.keys())
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"option",
|
||||
[
|
||||
"num_replicas",
|
||||
"autoscaling_config",
|
||||
"user_config",
|
||||
],
|
||||
)
|
||||
def test_nullable_options(self, option: str):
|
||||
"""Check that nullable options can be set to None."""
|
||||
|
||||
deployment_options = {option: None}
|
||||
|
||||
# One of "num_replicas" or "autoscaling_config" must be provided.
|
||||
if option == "num_replicas":
|
||||
deployment_options["autoscaling_config"] = {
|
||||
"min_replicas": 1,
|
||||
"max_replicas": 5,
|
||||
"target_ongoing_requests": 5,
|
||||
}
|
||||
elif option == "autoscaling_config":
|
||||
deployment_options["num_replicas"] = 5
|
||||
|
||||
# Deployment should be created without error.
|
||||
@serve.deployment(**deployment_options)
|
||||
def f():
|
||||
pass
|
||||
|
||||
@pytest.mark.parametrize("options", deployment_option_combos)
|
||||
def test_options(self, options):
|
||||
"""Check that updating options also updates user_configured_options_names."""
|
||||
|
||||
@serve.deployment
|
||||
def f():
|
||||
pass
|
||||
|
||||
f = f.options(**options)
|
||||
assert f._deployment_config.user_configured_option_names == set(options.keys())
|
||||
|
||||
def test_deployment_decorator_version_removed(self):
|
||||
with pytest.raises(
|
||||
ValueError,
|
||||
match=r"`version` in `@serve\.deployment` has been removed",
|
||||
):
|
||||
|
||||
@serve.deployment(version="abcd")
|
||||
def f():
|
||||
pass
|
||||
|
||||
def test_deployment_options_version_removed(self):
|
||||
@serve.deployment
|
||||
def f():
|
||||
pass
|
||||
|
||||
with pytest.raises(
|
||||
ValueError,
|
||||
match=r"`version` in `Deployment\.options\(\)` has been removed",
|
||||
):
|
||||
f.options(version="abcd")
|
||||
|
||||
def test_deployment_route_prefix_removed(self):
|
||||
@serve.deployment
|
||||
def f():
|
||||
pass
|
||||
|
||||
assert not hasattr(f, "route_prefix")
|
||||
with pytest.raises(AttributeError):
|
||||
_ = f.route_prefix
|
||||
|
||||
with pytest.raises(TypeError, match="route_prefix"):
|
||||
|
||||
@serve.deployment(route_prefix="/prefix")
|
||||
def g():
|
||||
pass
|
||||
|
||||
with pytest.raises(TypeError, match="route_prefix"):
|
||||
f.options(route_prefix="/prefix")
|
||||
|
||||
def test_eager_placement_group_validation(self):
|
||||
"""Check that placement groups are validated early.
|
||||
|
||||
Placement group bundles should be validated when the deployment is
|
||||
defined, not when it's deployed.
|
||||
"""
|
||||
|
||||
with pytest.raises(ValueError):
|
||||
|
||||
# PG bundle with empty resources is invalid.
|
||||
@serve.deployment(
|
||||
placement_group_bundles=[{"CPU": 0, "GPU": 0}],
|
||||
ray_actor_options={"num_cpus": 0, "num_gpus": 0},
|
||||
)
|
||||
def f():
|
||||
pass
|
||||
|
||||
def test_deployment_url_removed(self):
|
||||
@serve.deployment
|
||||
def f():
|
||||
pass
|
||||
|
||||
assert not hasattr(f, "url")
|
||||
with pytest.raises(AttributeError):
|
||||
_ = f.url
|
||||
|
||||
def test_placement_group_strategy_without_bundles(self):
|
||||
"""Check that specifying strategy requires also specifying bundles."""
|
||||
|
||||
with pytest.raises(ValueError):
|
||||
|
||||
# PG strategy without bundles is invalid.
|
||||
@serve.deployment(placement_group_strategy="PACK")
|
||||
def f():
|
||||
pass
|
||||
|
||||
# PG strategy with bundles is valid.
|
||||
@serve.deployment(
|
||||
placement_group_strategy="PACK",
|
||||
placement_group_bundles=[{"CPU": 10}],
|
||||
)
|
||||
def g():
|
||||
pass
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(pytest.main(["-v", "-s", __file__]))
|
||||
Reference in New Issue
Block a user