212 lines
7.3 KiB
Python
212 lines
7.3 KiB
Python
"""Integration tests for Ray Client working_dir validation with _client_job flag.
|
|
|
|
When connecting via Ray Client with a working_dir in RuntimeEnv, the server-side
|
|
job_config may temporarily contain local extracted paths during the proxy flow.
|
|
The _client_job flag signals that validation should be skipped since the
|
|
working_dir was already validated and uploaded on the client side.
|
|
"""
|
|
import pickle
|
|
import tempfile
|
|
|
|
import pytest
|
|
|
|
from ray.job_config import JobConfig
|
|
|
|
# Parametrized validation tests
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"runtime_env_key,use_list,client_job,should_raise",
|
|
[
|
|
("working_dir", False, False, True),
|
|
("working_dir", False, True, False),
|
|
("py_modules", True, False, True),
|
|
("py_modules", True, True, False),
|
|
],
|
|
ids=[
|
|
"working_dir_without_client_job_fails",
|
|
"working_dir_with_client_job_succeeds",
|
|
"py_modules_without_client_job_fails",
|
|
"py_modules_with_client_job_succeeds",
|
|
],
|
|
)
|
|
def test_local_path_validation(runtime_env_key, use_list, client_job, should_raise):
|
|
"""Test that _client_job flag controls local path validation."""
|
|
with tempfile.TemporaryDirectory(prefix="_ray_pkg_") as tmpdir:
|
|
value = [tmpdir] if use_list else tmpdir
|
|
jc = JobConfig(runtime_env={runtime_env_key: value}, _client_job=client_job)
|
|
|
|
if should_raise:
|
|
with pytest.raises(ValueError, match="not a valid URI"):
|
|
jc._serialize()
|
|
else:
|
|
jc._serialize() # Should succeed
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"runtime_env_key,use_list,client_job",
|
|
[
|
|
("working_dir", False, False),
|
|
("working_dir", False, True),
|
|
("py_modules", True, False),
|
|
("py_modules", True, True),
|
|
],
|
|
ids=[
|
|
"working_dir_without_client_job",
|
|
"working_dir_with_client_job",
|
|
"py_modules_without_client_job",
|
|
"py_modules_with_client_job",
|
|
],
|
|
)
|
|
def test_gcs_uri_always_valid(runtime_env_key, use_list, client_job):
|
|
"""Test that GCS URIs work with or without _client_job flag."""
|
|
uri = "gcs://_ray_pkg_abc.zip"
|
|
value = [uri] if use_list else uri
|
|
jc = JobConfig(runtime_env={runtime_env_key: value}, _client_job=client_job)
|
|
jc._serialize() # Should always succeed
|
|
|
|
|
|
def test_multiple_local_py_modules_with_client_job():
|
|
"""Verify multiple local py_modules paths pass with _client_job=True."""
|
|
with tempfile.TemporaryDirectory(
|
|
prefix="_ray_pkg_a"
|
|
) as tmpdir_a, tempfile.TemporaryDirectory(prefix="_ray_pkg_b") as tmpdir_b:
|
|
jc = JobConfig(
|
|
runtime_env={"py_modules": [tmpdir_a, tmpdir_b]},
|
|
_client_job=True,
|
|
)
|
|
jc._serialize()
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"client_job,should_raise",
|
|
[(True, False), (False, True)],
|
|
ids=["with_client_job_succeeds", "without_client_job_fails"],
|
|
)
|
|
def test_mixed_working_dir_and_py_modules(client_job, should_raise):
|
|
"""Test both working_dir and py_modules together with _client_job flag."""
|
|
with tempfile.TemporaryDirectory(
|
|
prefix="_ray_pkg_wd"
|
|
) as wd_dir, tempfile.TemporaryDirectory(prefix="_ray_pkg_pm") as pm_dir:
|
|
jc = JobConfig(
|
|
runtime_env={"working_dir": wd_dir, "py_modules": [pm_dir]},
|
|
_client_job=client_job,
|
|
)
|
|
|
|
if should_raise:
|
|
with pytest.raises(ValueError, match="not a valid URI"):
|
|
jc._serialize()
|
|
else:
|
|
jc._serialize()
|
|
|
|
|
|
# Pickle roundtrip tests
|
|
|
|
|
|
def test_pickle_roundtrip_preserves_working_dir():
|
|
"""Verify working_dir survives pickle/unpickle."""
|
|
with tempfile.TemporaryDirectory(prefix="_ray_pkg_") as tmpdir:
|
|
jc = JobConfig(runtime_env={"working_dir": tmpdir})
|
|
pickled = pickle.dumps(jc)
|
|
unpickled = pickle.loads(pickled)
|
|
assert unpickled.runtime_env["working_dir"] == tmpdir
|
|
|
|
|
|
def test_pickle_roundtrip_preserves_client_job_flag():
|
|
"""Verify _client_job flag survives pickle/unpickle."""
|
|
jc = JobConfig(_client_job=True)
|
|
pickled = pickle.dumps(jc)
|
|
unpickled = pickle.loads(pickled)
|
|
assert unpickled._client_job is True
|
|
|
|
|
|
def test_double_pickle_roundtrip_simulates_proxy_flow():
|
|
"""Simulate the full proxy flow: client -> proxy -> server."""
|
|
with tempfile.TemporaryDirectory(prefix="_ray_pkg_") as tmpdir:
|
|
# Client creates job_config with working_dir
|
|
client_jc = JobConfig(runtime_env={"working_dir": tmpdir})
|
|
assert client_jc._client_job is False
|
|
|
|
# Serialize for transmission to proxy
|
|
client_serialized = pickle.dumps(client_jc)
|
|
|
|
# Proxy unpickles
|
|
proxy_jc = pickle.loads(client_serialized)
|
|
assert proxy_jc._client_job is False
|
|
|
|
# Proxy re-pickles for forwarding to specific server
|
|
proxy_serialized = pickle.dumps(proxy_jc)
|
|
|
|
# Specific server unpickles and sets _client_job=True
|
|
server_jc = pickle.loads(proxy_serialized)
|
|
assert server_jc._client_job is False
|
|
server_jc._client_job = True
|
|
|
|
# Server calls _serialize() which triggers validation
|
|
server_jc._serialize() # Should succeed
|
|
|
|
|
|
def test_py_modules_pickle_roundtrip_with_client_job():
|
|
"""Verify py_modules + _client_job survives the proxy pickle roundtrip."""
|
|
with tempfile.TemporaryDirectory(prefix="_ray_pkg_") as tmpdir:
|
|
# Client creates job_config with local py_modules
|
|
client_jc = JobConfig(runtime_env={"py_modules": [tmpdir]})
|
|
client_serialized = pickle.dumps(client_jc)
|
|
|
|
# Proxy unpickles and re-pickles
|
|
proxy_jc = pickle.loads(client_serialized)
|
|
proxy_serialized = pickle.dumps(proxy_jc)
|
|
|
|
# Server unpickles and sets _client_job=True
|
|
server_jc = pickle.loads(proxy_serialized)
|
|
assert server_jc._client_job is False
|
|
server_jc._client_job = True
|
|
server_jc._serialize() # Should succeed
|
|
|
|
|
|
# Server Init() code path tests
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"runtime_env,client_job,should_raise",
|
|
[
|
|
({"working_dir": "local_path"}, True, False),
|
|
({"working_dir": "local_path"}, False, True),
|
|
({"working_dir": "local_path", "env_vars": {"TEST": "value"}}, True, False),
|
|
],
|
|
ids=[
|
|
"working_dir_with_client_job",
|
|
"working_dir_without_client_job_fails",
|
|
"working_dir_and_env_vars_with_client_job",
|
|
],
|
|
)
|
|
def test_server_init_code_path(runtime_env, client_job, should_raise):
|
|
"""Simulate server.py Init() with various runtime_env configurations."""
|
|
# Replace placeholder with actual temp directory
|
|
if "working_dir" in runtime_env and runtime_env["working_dir"] == "local_path":
|
|
with tempfile.TemporaryDirectory(prefix="_ray_pkg_") as tmpdir:
|
|
runtime_env = runtime_env.copy()
|
|
runtime_env["working_dir"] = tmpdir
|
|
|
|
# Client creates job_config
|
|
client_jc = JobConfig(runtime_env=runtime_env)
|
|
serialized = pickle.dumps(client_jc)
|
|
|
|
# Server receives and unpickles
|
|
server_jc = pickle.loads(serialized)
|
|
|
|
if client_job:
|
|
server_jc._client_job = True
|
|
|
|
if should_raise:
|
|
with pytest.raises(ValueError, match="not a valid URI"):
|
|
server_jc._serialize()
|
|
else:
|
|
server_jc._serialize()
|
|
|
|
|
|
if __name__ == "__main__":
|
|
import sys
|
|
|
|
sys.exit(pytest.main(["-v", __file__]))
|