Files
ray-project--ray/python/ray/tests/test_client_working_dir.py
2026-07-13 13:17:40 +08:00

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__]))