import os import sys import tempfile from pathlib import Path import jsonschema import pytest import yaml from ray import job_config from ray._private.runtime_env import validation from ray._private.runtime_env.pip import _get_pip_hash from ray._private.runtime_env.plugin_schema_manager import RuntimeEnvPluginSchemaManager from ray._private.runtime_env.validation import ( parse_and_validate_conda, parse_and_validate_excludes, parse_and_validate_py_modules, parse_and_validate_working_dir, ) from ray.runtime_env import RuntimeEnv from ray.runtime_env.runtime_env import ( _validate_no_local_paths, ) _CONDA_DICT = {"dependencies": ["pip", {"pip": ["pip-install-test==0.5"]}]} _PIP_LIST = ["requests==1.0.0", "pip-install-test"] @pytest.fixture def test_directory(): with tempfile.TemporaryDirectory() as tmp_dir: path = Path(tmp_dir) subdir = path / "subdir" subdir.mkdir(parents=True) requirements_file = subdir / "requirements.txt" with requirements_file.open(mode="w") as f: print("\n".join(_PIP_LIST), file=f) good_conda_file = subdir / "good_conda_env.yaml" with good_conda_file.open(mode="w") as f: yaml.dump(_CONDA_DICT, f) bad_conda_file = subdir / "bad_conda_env.yaml" with bad_conda_file.open(mode="w") as f: print("% this is not a YAML file %", file=f) old_dir = os.getcwd() os.chdir(tmp_dir) yield subdir, requirements_file, good_conda_file, bad_conda_file os.chdir(old_dir) @pytest.fixture def set_runtime_env_plugin_schemas(request): runtime_env_plugin_schemas = getattr(request, "param", "0") try: os.environ["RAY_RUNTIME_ENV_PLUGIN_SCHEMAS"] = runtime_env_plugin_schemas # Clear and reload schemas. RuntimeEnvPluginSchemaManager.clear() yield runtime_env_plugin_schemas finally: del os.environ["RAY_RUNTIME_ENV_PLUGIN_SCHEMAS"] def test_key_with_value_none(): parsed_runtime_env = RuntimeEnv(pip=None) assert parsed_runtime_env == {} class TestValidateWorkingDir: def test_validate_bad_path(self): with pytest.raises(ValueError, match="a valid path"): parse_and_validate_working_dir("/does/not/exist") def test_validate_bad_uri(self): with pytest.raises(ValueError, match="a valid URI"): parse_and_validate_working_dir("unknown://abc") def test_validate_invalid_type(self): with pytest.raises(TypeError): parse_and_validate_working_dir(1) def test_validate_remote_invalid_extensions(self): for uri in [ "https://some_domain.com/path/file", "s3://bucket/file", "gs://bucket/file", ]: with pytest.raises( ValueError, match=r"Only \.zip, \.whl, \.tar\.gz, and \.tgz files supported for remote URIs\.", ): parse_and_validate_working_dir(uri) def test_validate_remote_valid_input(self): for uri in [ "https://some_domain.com/path/file.zip", "s3://bucket/file.zip", "gs://bucket/file.zip", "https://some_domain.com/path/file.tar.gz", "s3://bucket/file.tar.gz", "gs://bucket/file.tgz", ]: working_dir = parse_and_validate_working_dir(uri) assert working_dir == uri def test_validate_path_valid_input(self, test_directory): test_dir, _, _, _ = test_directory valid_working_dir_path = str(test_dir) working_dir = parse_and_validate_working_dir(str(valid_working_dir_path)) assert working_dir == valid_working_dir_path class TestValidatePyModules: def test_validate_not_a_list(self): with pytest.raises(TypeError, match="must be a list of strings"): parse_and_validate_py_modules(".") def test_validate_bad_path(self): with pytest.raises(ValueError, match="a valid path"): parse_and_validate_py_modules(["/does/not/exist"]) def test_validate_bad_uri(self): with pytest.raises(ValueError, match="a valid URI"): parse_and_validate_py_modules(["unknown://abc"]) def test_validate_invalid_type(self): with pytest.raises(TypeError): parse_and_validate_py_modules([1]) def test_validate_remote_invalid_extension(self): uris = [ "https://some_domain.com/path/file", "s3://bucket/file", "gs://bucket/file", ] with pytest.raises( ValueError, match="Only .zip, .whl, .tar.gz, and .tgz files supported for remote URIs.", ): parse_and_validate_py_modules(uris) def test_validate_remote_valid_input(self): uris = [ "https://some_domain.com/path/file.zip", "s3://bucket/file.zip", "gs://bucket/file.zip", "https://some_domain.com/path/file.whl", "s3://bucket/file.whl", "gs://bucket/file.whl", "https://some_domain.com/path/file.tar.gz", "s3://bucket/file.tar.gz", "gs://bucket/file.tgz", ] py_modules = parse_and_validate_py_modules(uris) assert py_modules == uris def test_validate_path_valid_input(self, test_directory): test_dir, _, _, _ = test_directory paths = [str(test_dir)] py_modules = parse_and_validate_py_modules(paths) assert py_modules == paths def test_validate_path_and_uri_valid_input(self, test_directory): test_dir, _, _, _ = test_directory uris_and_paths = [ str(test_dir), "https://some_domain.com/path/file.zip", "s3://bucket/file.zip", "gs://bucket/file.zip", "https://some_domain.com/path/file.whl", "s3://bucket/file.whl", "gs://bucket/file.whl", ] py_modules = parse_and_validate_py_modules(uris_and_paths) assert py_modules == uris_and_paths class TestValidateExcludes: def test_validate_excludes_invalid_types(self): with pytest.raises(TypeError): parse_and_validate_excludes(1) with pytest.raises(TypeError): parse_and_validate_excludes(True) with pytest.raises(TypeError): parse_and_validate_excludes("string") with pytest.raises(TypeError): parse_and_validate_excludes(["string", 1]) def test_validate_excludes_empty_list(self): assert RuntimeEnv(excludes=[]) == {} class TestValidateConda: def test_validate_conda_invalid_types(self): with pytest.raises(TypeError): parse_and_validate_conda(1) with pytest.raises(TypeError): parse_and_validate_conda(True) def test_validate_conda_str(self): assert parse_and_validate_conda("my_env_name") == "my_env_name" def test_validate_conda_invalid_path(self): with pytest.raises(ValueError): parse_and_validate_conda("../bad_path.yaml") @pytest.mark.parametrize("absolute_path", [True, False]) def test_validate_conda_valid_file(self, test_directory, absolute_path): _, _, good_conda_file, _ = test_directory if absolute_path: good_conda_file = good_conda_file.resolve() assert parse_and_validate_conda(str(good_conda_file)) == _CONDA_DICT @pytest.mark.parametrize("absolute_path", [True, False]) def test_validate_conda_invalid_file(self, test_directory, absolute_path): _, _, _, bad_conda_file = test_directory if absolute_path: bad_conda_file = bad_conda_file.resolve() with pytest.raises(ValueError): parse_and_validate_conda(str(bad_conda_file)) def test_validate_conda_valid_dict(self): assert parse_and_validate_conda(_CONDA_DICT) == _CONDA_DICT class TestParsedRuntimeEnv: def test_empty(self): assert RuntimeEnv() == {} def test_serialization(self): env1 = RuntimeEnv(pip=["requests"], env_vars={"hi1": "hi1", "hi2": "hi2"}) env2 = RuntimeEnv(env_vars={"hi2": "hi2", "hi1": "hi1"}, pip=["requests"]) assert env1 == env2 serialized_env1 = env1.serialize() serialized_env2 = env2.serialize() # Key ordering shouldn't matter. assert serialized_env1 == serialized_env2 deserialized_env1 = RuntimeEnv.deserialize(serialized_env1) deserialized_env2 = RuntimeEnv.deserialize(serialized_env2) assert env1 == deserialized_env1 == env2 == deserialized_env2 def test_reject_pip_and_conda(self): with pytest.raises(ValueError): RuntimeEnv(pip=["requests"], conda="env_name") def test_ray_commit_injection(self): # Should not be injected if no pip and conda. result = RuntimeEnv(env_vars={"hi": "hi"}) assert "_ray_commit" not in result # Should be injected if pip or conda present. result = RuntimeEnv(pip=["requests"]) assert "_ray_commit" in result result = RuntimeEnv(conda="env_name") assert "_ray_commit" in result # Should not override if passed. result = RuntimeEnv(conda="env_name", _ray_commit="Blah") assert result["_ray_commit"] == "Blah" def test_inject_current_ray(self): # Should not be injected if not provided by env var. result = RuntimeEnv(env_vars={"hi": "hi"}) assert "_inject_current_ray" not in result os.environ["RAY_RUNTIME_ENV_LOCAL_DEV_MODE"] = "1" # Should be injected if provided by env var. result = RuntimeEnv() assert result["_inject_current_ray"] # Should be preserved if passed. result = RuntimeEnv(_inject_current_ray=False) assert not result["_inject_current_ray"] del os.environ["RAY_RUNTIME_ENV_LOCAL_DEV_MODE"] class TestParseJobConfig: def test_parse_runtime_env_from_json_env_variable(self): job_config_json = {"runtime_env": {"working_dir": "uri://abc"}} config = job_config.JobConfig.from_json(job_config_json) assert config.runtime_env == job_config_json.get("runtime_env") assert config.metadata == {} schemas_dir = os.path.dirname(__file__) test_env_1 = os.path.join( os.path.dirname(__file__), "test_runtime_env_validation_1_schema.json" ) test_env_2 = os.path.join( os.path.dirname(__file__), "test_runtime_env_validation_2_schema.json" ) test_env_invalid_path = os.path.join( os.path.dirname(__file__), "test_runtime_env_validation_non_existent.json" ) test_env_bad_json = os.path.join( os.path.dirname(__file__), "test_runtime_env_validation_bad_schema.json" ) @pytest.mark.parametrize( "set_runtime_env_plugin_schemas", [ schemas_dir, f"{test_env_1},{test_env_2}", # Test with an invalid JSON file first in the list f"{test_env_bad_json},{test_env_1},{test_env_2}", # Test with a non-existent JSON file f"{test_env_invalid_path},{test_env_1},{test_env_2}", ], indirect=True, ) @pytest.mark.skipif(sys.platform == "win32", reason="Failing on Windows.") class TestValidateByJsonSchema: def test_validate_pip(self, set_runtime_env_plugin_schemas): runtime_env = RuntimeEnv() runtime_env.set("pip", {"packages": ["requests"], "pip_check": True}) with pytest.raises(jsonschema.exceptions.ValidationError, match="pip_check"): runtime_env.set("pip", {"packages": ["requests"], "pip_check": "1"}) runtime_env["pip"] = {"packages": ["requests"], "pip_check": True} with pytest.raises(jsonschema.exceptions.ValidationError, match="pip_check"): runtime_env["pip"] = {"packages": ["requests"], "pip_check": "1"} def test_validate_working_dir(self, set_runtime_env_plugin_schemas): runtime_env = RuntimeEnv() runtime_env.set("working_dir", "https://abc/file.zip") with pytest.raises(jsonschema.exceptions.ValidationError, match="working_dir"): runtime_env.set("working_dir", ["https://abc/file.zip"]) runtime_env["working_dir"] = "https://abc/file.zip" with pytest.raises(jsonschema.exceptions.ValidationError, match="working_dir"): runtime_env["working_dir"] = ["https://abc/file.zip"] def test_validate_test_env_1(self, set_runtime_env_plugin_schemas): runtime_env = RuntimeEnv() runtime_env.set("test_env_1", {"array": ["123"], "bool": True}) with pytest.raises(jsonschema.exceptions.ValidationError, match="bool"): runtime_env.set("test_env_1", {"array": ["123"], "bool": "1"}) def test_validate_test_env_2(self, set_runtime_env_plugin_schemas): runtime_env = RuntimeEnv() runtime_env.set("test_env_2", "123") with pytest.raises(jsonschema.exceptions.ValidationError, match="test_env_2"): runtime_env.set("test_env_2", ["123"]) @pytest.mark.skipif(sys.platform == "win32", reason="Failing on Windows.") class TestRuntimeEnvPluginSchemaManager: def test(self): RuntimeEnvPluginSchemaManager.clear() # No schemas when starts. assert len(RuntimeEnvPluginSchemaManager.schemas) == 0 # When the `validate` is used first time, the schemas will be loaded lazily. # The validation of pip is enabled. with pytest.raises(jsonschema.exceptions.ValidationError, match="pip_check"): RuntimeEnvPluginSchemaManager.validate( "pip", {"packages": ["requests"], "pip_check": "123"} ) # The validation of test_env_1 is disabled because we haven't set the env var. RuntimeEnvPluginSchemaManager.validate( "test_env_1", {"array": ["123"], "bool": "123"} ) assert len(RuntimeEnvPluginSchemaManager.schemas) != 0 # Set the thirdparty schemas. os.environ["RAY_RUNTIME_ENV_PLUGIN_SCHEMAS"] = schemas_dir # clear the loaded schemas to make sure the schemas chould be reloaded next # time. RuntimeEnvPluginSchemaManager.clear() assert len(RuntimeEnvPluginSchemaManager.schemas) == 0 # The validation of test_env_1 is enabled. with pytest.raises(jsonschema.exceptions.ValidationError, match="bool"): RuntimeEnvPluginSchemaManager.validate( "test_env_1", {"array": ["123"], "bool": "123"} ) class TestValidateUV: def test_parse_and_validate_uv(self, test_directory): # Valid case w/o duplication. result = validation.parse_and_validate_uv({"packages": ["tensorflow"]}) assert result == { "packages": ["tensorflow"], "uv_check": False, "uv_pip_install_options": ["--no-cache"], } # Valid case w/ duplication. result = validation.parse_and_validate_uv( {"packages": ["tensorflow", "tensorflow"]} ) assert result == { "packages": ["tensorflow"], "uv_check": False, "uv_pip_install_options": ["--no-cache"], } # Valid case, use `list` to represent necessary packages. result = validation.parse_and_validate_uv( ["requests==1.0.0", "aiohttp", "ray[serve]"] ) assert result == { "packages": ["requests==1.0.0", "aiohttp", "ray[serve]"], "uv_check": False, } # Invalid case, unsupport keys. with pytest.raises(ValueError): result = validation.parse_and_validate_uv({"random_key": "random_value"}) # Valid case w/ uv version. result = validation.parse_and_validate_uv( {"packages": ["tensorflow"], "uv_version": "==0.4.30"} ) assert result == { "packages": ["tensorflow"], "uv_version": "==0.4.30", "uv_check": False, "uv_pip_install_options": ["--no-cache"], } # Valid requirement files. _, requirements_file, _, _ = test_directory requirements_file = requirements_file.resolve() result = validation.parse_and_validate_uv(str(requirements_file)) assert result == { "packages": ["requests==1.0.0", "pip-install-test"], "uv_check": False, } # Invalid requiremnt files. with pytest.raises(ValueError): result = validation.parse_and_validate_uv("some random non-existent file") # Invalid uv install options. with pytest.raises(TypeError): result = validation.parse_and_validate_uv( { "packages": ["tensorflow"], "uv_version": "==0.4.30", "uv_pip_install_options": [1], } ) # Valid uv install options. result = validation.parse_and_validate_uv( { "packages": ["tensorflow"], "uv_version": "==0.4.30", "uv_pip_install_options": ["--no-cache"], } ) assert result == { "packages": ["tensorflow"], "uv_check": False, "uv_pip_install_options": ["--no-cache"], "uv_version": "==0.4.30", } class TestValidatePip: def test_validate_pip_invalid_types(self): with pytest.raises(TypeError): validation.parse_and_validate_pip(1) with pytest.raises(TypeError): validation.parse_and_validate_pip(True) def test_validate_pip_invalid_path(self): with pytest.raises(ValueError): validation.parse_and_validate_pip("../bad_path.txt") @pytest.mark.parametrize("absolute_path", [True, False]) def test_validate_pip_valid_file(self, test_directory, absolute_path): _, requirements_file, _, _ = test_directory if absolute_path: requirements_file = requirements_file.resolve() result = validation.parse_and_validate_pip(str(requirements_file)) assert result["packages"] == _PIP_LIST assert not result["pip_check"] assert "pip_version" not in result def test_validate_pip_valid_list(self): result = validation.parse_and_validate_pip(_PIP_LIST) assert result["packages"] == _PIP_LIST assert not result["pip_check"] assert "pip_version" not in result def test_validate_ray(self): result = validation.parse_and_validate_pip(["pkg1", "ray", "pkg2"]) assert result["packages"] == ["pkg1", "ray", "pkg2"] assert not result["pip_check"] assert "pip_version" not in result def test_validate_pip_install_options(self): # Happy path for non-empty pip_install_options opts = ["--no-cache-dir", "--no-build-isolation", "--disable-pip-version-check"] result = validation.parse_and_validate_pip( { "packages": ["pkg1", "ray", "pkg2"], "pip_install_options": list(opts), } ) assert result["packages"] == ["pkg1", "ray", "pkg2"] assert not result["pip_check"] assert "pip_version" not in result assert result["pip_install_options"] == opts # Happy path for missing pip_install_options. No default value for field # to maintain backwards compatibility with ray==2.0.1 result = validation.parse_and_validate_pip( { "packages": ["pkg1", "ray", "pkg2"], } ) assert "pip_install_options" not in result with pytest.raises(TypeError) as e: validation.parse_and_validate_pip( { "packages": ["pkg1", "ray", "pkg2"], "pip_install_options": [False], } ) assert "pip_install_options" in str(e) and "must be of type list[str]" in str(e) with pytest.raises(TypeError) as e: validation.parse_and_validate_pip( { "packages": ["pkg1", "ray", "pkg2"], "pip_install_options": None, } ) assert "pip_install_options" in str(e) and "must be of type list[str]" in str(e) class TestGetPipHash: def test_pip_hash_with_requirements_file(self, test_directory): _, requirements_file, _, _ = test_directory req_path = str(requirements_file) pip_dict1 = {"packages": [f"-r {req_path}"]} hash1 = _get_pip_hash(pip_dict1) pip_dict2 = {"packages": ["requests==1.0.0", "pip-install-test"]} hash2 = _get_pip_hash(pip_dict2) assert hash1 == hash2 def test_pip_hash_changes_with_file_content(self, test_directory): _, requirements_file, _, _ = test_directory req_path = str(requirements_file) pip_dict = {"packages": [f"-r {req_path}"]} with open(req_path, "w") as f: f.write("numpy==1.21.0\n") hash1 = _get_pip_hash(pip_dict) with open(req_path, "w") as f: f.write("numpy==1.22.0\n") hash2 = _get_pip_hash(pip_dict) assert hash1 != hash2 def test_pip_hash_with_comments_and_empty_lines(self, test_directory): _, requirements_file, _, _ = test_directory req_path = str(requirements_file) with open(req_path, "w") as f: f.write( "# This is a comment\nnumpy==1.21.0\n\n# Another comment\npandas==1.3.0\n" ) pip_dict = {"packages": [f"-r {req_path}"]} hash1 = _get_pip_hash(pip_dict) pip_dict2 = {"packages": ["numpy==1.21.0", "pandas==1.3.0"]} hash2 = _get_pip_hash(pip_dict2) assert hash1 == hash2 def test_pip_hash_without_r(self): pip_dict = {"packages": ["numpy==1.21.0", "pandas==1.3.0"]} hash1 = _get_pip_hash(pip_dict) pip_dict2 = {"packages": ["numpy==1.21.0", "pandas==1.3.0"]} hash2 = _get_pip_hash(pip_dict2) assert hash1 == hash2 def test_pip_hash_different_packages(self): pip_dict = {"packages": ["numpy==1.21.0"]} hash1 = _get_pip_hash(pip_dict) pip_dict2 = {"packages": ["pandas==1.3.0"]} hash2 = _get_pip_hash(pip_dict2) assert hash1 != hash2 def test_pip_hash_with_pip_install_options(self, test_directory): _, requirements_file, _, _ = test_directory req_path = str(requirements_file) pip_dict = { "packages": [f"-r {req_path}"], "pip_install_options": ["--no-cache-dir"], } hash1 = _get_pip_hash(pip_dict) pip_dict2 = { "packages": [f"-r {req_path}"], "pip_install_options": ["--disable-pip-version-check"], } hash2 = _get_pip_hash(pip_dict2) assert hash1 != hash2 def test_pip_hash_with_circular_reference(self): with tempfile.TemporaryDirectory() as tmpdir: # Create file A file_a = os.path.join(tmpdir, "a.txt") with open(file_a, "w") as f: f.write("-r b.txt\nnumpy==1.21.0\n") # Create file B file_b = os.path.join(tmpdir, "b.txt") with open(file_b, "w") as f: f.write("-r a.txt\npandas==1.3.0\n") pip_dict = {"packages": [f"-r {file_a}"]} hash_val = _get_pip_hash(pip_dict) assert isinstance(hash_val, str) assert len(hash_val) == 40 def test_pip_hash_with_self_reference(self): with tempfile.TemporaryDirectory() as tmpdir: # Create self-referencing file self_file = os.path.join(tmpdir, "self.txt") with open(self_file, "w") as f: f.write("-r self.txt\nnumpy==1.21.0\n") pip_dict = {"packages": [f"-r {self_file}"]} hash_val = _get_pip_hash(pip_dict) assert isinstance(hash_val, str) assert len(hash_val) == 40 def test_pip_hash_with_nested_relative_paths(self): with tempfile.TemporaryDirectory() as tmpdir: # Create directory structure reqs_dir = os.path.join(tmpdir, "reqs") os.makedirs(reqs_dir) # Create base.txt in reqs directory base_file = os.path.join(reqs_dir, "base.txt") with open(base_file, "w") as f: f.write("-r extras.txt\nnumpy==1.21.0\n") # Create extras.txt in the same directory (relative path) extras_file = os.path.join(reqs_dir, "extras.txt") with open(extras_file, "w") as f: f.write("pandas==1.3.0\n") # Test with absolute path to base.txt pip_dict = {"packages": [f"-r {base_file}"]} hash1 = _get_pip_hash(pip_dict) # Test with relative path from tmpdir original_cwd = os.getcwd() try: os.chdir(tmpdir) relative_base = os.path.relpath(base_file, tmpdir) pip_dict2 = {"packages": [f"-r {relative_base}"]} hash2 = _get_pip_hash(pip_dict2) # Hashes should be the same regardless of how we reference the file assert hash1 == hash2 finally: os.chdir(original_cwd) assert isinstance(hash1, str) assert len(hash1) == 40 def test_pip_hash_with_nested_circular_relative_paths(self): with tempfile.TemporaryDirectory() as tmpdir: # Create directory structure reqs_dir = os.path.join(tmpdir, "reqs") os.makedirs(reqs_dir) # Create circular references with relative paths a_file = os.path.join(reqs_dir, "a.txt") with open(a_file, "w") as f: f.write("-r b.txt\nnumpy==1.21.0\n") b_file = os.path.join(reqs_dir, "b.txt") with open(b_file, "w") as f: f.write("-r a.txt\npandas==1.3.0\n") # This should not cause an infinite loop pip_dict = {"packages": [f"-r {a_file}"]} hash_val = _get_pip_hash(pip_dict) assert isinstance(hash_val, str) assert len(hash_val) == 40 def test_pip_hash_with_long_form_requirement(self): with tempfile.NamedTemporaryFile(mode="w", suffix=".txt", delete=False) as f: f.write("numpy==1.21.0\n") temp_file = f.name try: # Test with long-form --requirement flag pip_dict = {"packages": [f"--requirement {temp_file}"]} hash_val = _get_pip_hash(pip_dict) assert isinstance(hash_val, str) assert len(hash_val) == 40 finally: os.unlink(temp_file) def test_pip_hash_with_long_form_requirement_equals(self): with tempfile.NamedTemporaryFile(mode="w", suffix=".txt", delete=False) as f: f.write("numpy==1.21.0\n") temp_file = f.name try: # Test with long-form --requirement=file.txt format pip_dict = {"packages": [f"--requirement={temp_file}"]} hash_val = _get_pip_hash(pip_dict) assert isinstance(hash_val, str) assert len(hash_val) == 40 finally: os.unlink(temp_file) def test_pip_hash_with_invalid_file(self): with tempfile.TemporaryDirectory() as tmpdir: # Test with non-existent file - should raise FileNotFoundError non_existent_file = os.path.join(tmpdir, "non_existent.txt") pip_dict = {"packages": [f"-r {non_existent_file}"]} with pytest.raises(FileNotFoundError): _get_pip_hash(pip_dict) class TestValidateEnvVars: def test_type_validation(self): # Only strings allowed. with pytest.raises(TypeError, match=".*Dict[str, str]*"): validation.parse_and_validate_env_vars({"INT_ENV": 1}) with pytest.raises(TypeError, match=".*Dict[str, str]*"): validation.parse_and_validate_env_vars({1: "hi"}) with pytest.raises(TypeError, match=".*value 123 is of type *"): validation.parse_and_validate_env_vars({"hi": 123}) with pytest.raises(TypeError, match=".*value True is of type *"): validation.parse_and_validate_env_vars({"hi": True}) with pytest.raises(TypeError, match=".*key 1.23 is of type *"): validation.parse_and_validate_env_vars({1.23: "hi"}) def test_validate_no_local_paths_raises_exceptions_on_type_mismatch(): with pytest.raises(TypeError): _validate_no_local_paths(1) with pytest.raises(TypeError): _validate_no_local_paths({}) def test_validate_no_local_paths_fails_if_local_working_dir(): with tempfile.TemporaryDirectory() as tmp_dir: path = Path(tmp_dir) working_dir = path / "working_dir" working_dir.mkdir(parents=True) working_dir_str = str(working_dir) runtime_env = RuntimeEnv(working_dir=working_dir_str) with pytest.raises(ValueError, match="not a valid URI"): _validate_no_local_paths(runtime_env) def test_validate_no_local_paths_fails_if_local_py_module(): with tempfile.NamedTemporaryFile(suffix=".whl") as tmp_file: runtime_env = RuntimeEnv(py_modules=[tmp_file.name, "gcs://some_other_file"]) with pytest.raises(ValueError, match="not a valid URI"): _validate_no_local_paths(runtime_env) if __name__ == "__main__": sys.exit(pytest.main(["-vv", __file__]))