104 lines
3.9 KiB
Python
104 lines
3.9 KiB
Python
"""Unit tests for get_haproxy_binary() resolution logic.
|
|
|
|
get_haproxy_binary() resolves an HAProxy binary path with this priority:
|
|
1. Explicit RAY_SERVE_HAPROXY_BINARY_PATH override, validated and returned.
|
|
2. Bundled binary from the ``ray-haproxy`` PyPI package.
|
|
3. System ``haproxy`` on PATH.
|
|
4. FileNotFoundError with an actionable message.
|
|
"""
|
|
|
|
import stat
|
|
from unittest.mock import MagicMock, patch
|
|
|
|
import pytest
|
|
|
|
from ray.serve._private.haproxy import get_haproxy_binary
|
|
|
|
# Patch targets: module-level constants imported at the top of haproxy.py.
|
|
HAPROXY_MODULE = "ray.serve._private.haproxy"
|
|
BINARY_PATH_PATCH = f"{HAPROXY_MODULE}.RAY_SERVE_HAPROXY_BINARY_PATH"
|
|
WHICH_PATCH = f"{HAPROXY_MODULE}.shutil.which"
|
|
|
|
|
|
def _make_executable_binary(tmp_path):
|
|
"""Create an empty executable file under tmp_path and return its path."""
|
|
binary = tmp_path / "haproxy"
|
|
binary.write_bytes(b"")
|
|
binary.chmod(binary.stat().st_mode | stat.S_IXUSR)
|
|
return binary
|
|
|
|
|
|
def test_explicit_path_takes_precedence_over_bundled(tmp_path):
|
|
"""An explicit RAY_SERVE_HAPROXY_BINARY_PATH wins over the bundled
|
|
ray-haproxy package, so an operator's custom binary is not silently
|
|
overridden."""
|
|
binary = _make_executable_binary(tmp_path)
|
|
|
|
mock_module = MagicMock()
|
|
mock_module.get_haproxy_binary.return_value = "/bundled/haproxy"
|
|
with patch.dict("sys.modules", {"ray_haproxy": mock_module}), patch(
|
|
BINARY_PATH_PATCH, str(binary)
|
|
):
|
|
assert get_haproxy_binary() == str(binary)
|
|
mock_module.get_haproxy_binary.assert_not_called()
|
|
|
|
|
|
def test_explicit_path_validates_executable(tmp_path):
|
|
"""An explicit RAY_SERVE_HAPROXY_BINARY_PATH is validated before use: it must
|
|
exist and be executable, otherwise resolution raises."""
|
|
binary = _make_executable_binary(tmp_path)
|
|
|
|
with patch(BINARY_PATH_PATCH, str(binary)):
|
|
assert get_haproxy_binary() == str(binary)
|
|
|
|
# Same file without the execute bit is rejected.
|
|
binary.chmod(0o644)
|
|
with patch(BINARY_PATH_PATCH, str(binary)):
|
|
with pytest.raises(FileNotFoundError, match="HAPROXY_BINARY_PATH"):
|
|
get_haproxy_binary()
|
|
|
|
# A nonexistent path is rejected.
|
|
with patch(BINARY_PATH_PATCH, str(tmp_path / "no_such_file")):
|
|
with pytest.raises(FileNotFoundError, match="HAPROXY_BINARY_PATH"):
|
|
get_haproxy_binary()
|
|
|
|
|
|
@patch(BINARY_PATH_PATCH, "")
|
|
def test_bundled_package_used_when_no_explicit_path():
|
|
"""With no explicit path set, the bundled ray-haproxy binary is used."""
|
|
mock_module = MagicMock()
|
|
mock_module.get_haproxy_binary.return_value = "/bundled/haproxy"
|
|
with patch.dict("sys.modules", {"ray_haproxy": mock_module}):
|
|
assert get_haproxy_binary() == "/bundled/haproxy"
|
|
|
|
|
|
@patch(WHICH_PATCH, return_value="/usr/sbin/haproxy")
|
|
@patch(BINARY_PATH_PATCH, "")
|
|
def test_pip_package_oserror_falls_through(_mock_which):
|
|
"""If ray-haproxy is installed but its binary is broken (e.g. missing file,
|
|
bad permissions), the function should fall through to the system haproxy
|
|
rather than crashing the proxy actor. We catch OSError (not just
|
|
FileNotFoundError) so PermissionError is also handled."""
|
|
mock_module = MagicMock()
|
|
mock_module.get_haproxy_binary.side_effect = OSError("bad permissions")
|
|
|
|
with patch.dict("sys.modules", {"ray_haproxy": mock_module}):
|
|
assert get_haproxy_binary() == "/usr/sbin/haproxy"
|
|
|
|
|
|
@patch(WHICH_PATCH, return_value=None)
|
|
@patch(BINARY_PATH_PATCH, "")
|
|
def test_nothing_available_raises_with_instructions(_mock_which):
|
|
"""When no binary is available from any source, the error message must
|
|
tell the user how to fix it (install ray[serve], set the env var, or
|
|
put haproxy on PATH)."""
|
|
with patch.dict("sys.modules", {"ray_haproxy": None}):
|
|
with pytest.raises(FileNotFoundError, match=r"ray\[serve\]"):
|
|
get_haproxy_binary()
|
|
|
|
|
|
if __name__ == "__main__":
|
|
import sys
|
|
|
|
sys.exit(pytest.main(["-v", "-s", __file__]))
|