Files
2026-07-13 13:17:40 +08:00

449 lines
16 KiB
Python

#!/usr/bin/env python3
"""Tests for build_image.py"""
import tempfile
import unittest
from pathlib import Path
from unittest import mock
from build_image import (
DEFAULT_ARCHITECTURE,
IMAGE_TYPE_CONFIG,
REGISTRY_PREFIX,
SUPPORTED_IMAGE_TYPES,
WANDA_SPEC_PATHS,
BuildError,
ImageBuildConfig,
RayImage,
RayImageError,
main,
)
class TestRayImageName(unittest.TestCase):
"""Test RayImage.wanda_image_name property."""
def test_ray_cpu(self):
ri = RayImage("ray", "3.10", "cpu")
self.assertEqual(ri.wanda_image_name, "ray-py3.10-cpu")
def test_ray_cuda(self):
ri = RayImage("ray", "3.10", "cu12.8.1-cudnn")
self.assertEqual(ri.wanda_image_name, "ray-py3.10-cu12.8.1-cudnn")
def test_ray_extra_cpu(self):
ri = RayImage("ray-extra", "3.10", "cpu")
self.assertEqual(ri.wanda_image_name, "ray-extra-py3.10-cpu")
def test_ray_llm_cuda(self):
ri = RayImage("ray-llm", "3.12", "cu13.0.0-cudnn")
self.assertEqual(ri.wanda_image_name, "ray-llm-py3.12-cu13.0.0-cudnn")
def test_aarch64_suffix(self):
ri = RayImage("ray", "3.10", "cpu", "aarch64")
self.assertEqual(ri.wanda_image_name, "ray-py3.10-cpu-aarch64")
class TestRayImageValidation(unittest.TestCase):
"""Test RayImage.validate()."""
def test_valid_ray(self):
RayImage("ray", "3.10", "cpu").validate()
def test_unknown_type(self):
with self.assertRaises(RayImageError):
RayImage("bad-type", "3.10", "cpu").validate()
def test_invalid_python(self):
with self.assertRaises(RayImageError):
RayImage("ray-llm", "3.10", "cu13.0.0-cudnn").validate()
def test_invalid_platform(self):
with self.assertRaises(RayImageError):
RayImage("ray", "3.10", "nonexistent").validate()
def test_invalid_architecture(self):
with self.assertRaises(RayImageError):
RayImage("ray-llm", "3.12", "cu13.0.0-cudnn", "aarch64").validate()
class TestGetWandaSpecPath(unittest.TestCase):
"""Test RayImage.get_wanda_spec_path()."""
def _spec(self, image_type, platform):
return RayImage(
image_type=image_type, python_version="3.10", platform=platform
).get_wanda_spec_path()
def test_ray_cpu(self):
self.assertEqual(self._spec("ray", "cpu"), "ci/docker/ray-image-cpu.wanda.yaml")
def test_ray_cuda(self):
self.assertEqual(
self._spec("ray", "cu12.8.1-cudnn"),
"ci/docker/ray-image-cuda.wanda.yaml",
)
def test_ray_extra_cpu(self):
self.assertEqual(
self._spec("ray-extra", "cpu"),
"ci/docker/ray-extra-image-cpu.wanda.yaml",
)
def test_ray_extra_cuda(self):
self.assertEqual(
self._spec("ray-extra", "cu12.1.1-cudnn8"),
"ci/docker/ray-extra-image-cuda.wanda.yaml",
)
def test_ray_llm_cuda(self):
self.assertEqual(
self._spec("ray-llm", "cu13.0.0-cudnn"),
"ci/docker/ray-llm-image-cuda.wanda.yaml",
)
def test_ray_llm_extra_cuda(self):
self.assertEqual(
self._spec("ray-llm-extra", "cu13.0.0-cudnn"),
"ci/docker/ray-llm-extra-image-cuda.wanda.yaml",
)
def test_ray_llm_cpu_raises(self):
with self.assertRaises(RayImageError):
self._spec("ray-llm", "cpu")
def test_ray_tpu(self):
self.assertEqual(
self._spec("ray", "tpu"),
"ci/docker/ray-image-tpu.wanda.yaml",
)
class TestValidation(unittest.TestCase):
"""Test from_args() rejects invalid combinations."""
def test_unknown_build_image_type(self):
with (
mock.patch("ci.build.build_common._platform.system", return_value="Linux"),
mock.patch(
"ci.build.build_common._platform.machine", return_value="x86_64"
),
):
with self.assertRaises(BuildError) as ctx:
ImageBuildConfig.from_args("ray-ml", "3.10", "cpu")
self.assertIn("Unknown image type", str(ctx.exception))
def test_invalid_python_raises_build_error(self):
with (
mock.patch("ci.build.build_common._platform.system", return_value="Linux"),
mock.patch(
"ci.build.build_common._platform.machine", return_value="x86_64"
),
):
with self.assertRaises(BuildError):
ImageBuildConfig.from_args("ray-llm", "3.10", "cu13.0.0-cudnn")
def _config(**kwargs):
image_type = kwargs.pop("image_type", "ray")
python_version = kwargs.pop("python_version", "3.10")
image_platform = kwargs.pop("platform", "cpu")
architecture = kwargs.pop("architecture", "x86_64")
ray_version = kwargs.pop("ray_version", "3.0.0.dev0")
commit = kwargs.pop("commit", "fake1234")
ray_image = RayImage(
image_type=image_type,
python_version=python_version,
platform=image_platform,
architecture=architecture,
)
cfg = IMAGE_TYPE_CONFIG[image_type]
nightly_alias = (
f"{REGISTRY_PREFIX}{ray_image.repo}:nightly{ray_image.variation_suffix}"
if (
ray_image.python_version == cfg["default_python"]
and ray_image.platform == cfg["default_platform"]
and ray_image.architecture == DEFAULT_ARCHITECTURE
)
else None
)
build_env: dict[str, str] = {
"PYTHON_VERSION": ray_image.python_version,
"MANYLINUX_VERSION": "",
"HOSTTYPE": ray_image.architecture,
"ARCH_SUFFIX": ray_image.arch_suffix,
"BUILDKITE_COMMIT": commit,
"RAY_VERSION": ray_version,
"IS_LOCAL_BUILD": "true",
"IMAGE_TYPE": ray_image.repo,
}
if ray_image.platform.startswith("cu"):
build_env["CUDA_VERSION"] = ray_image.platform.removeprefix("cu")
defaults = dict(
ray_image=ray_image,
ray_root=Path("/fake/ray"),
raymake_version="0.0.0",
manylinux_version="",
ray_version=ray_version,
commit=commit,
wanda_spec_path=ray_image.get_wanda_spec_path(),
wanda_image_tag=f"{REGISTRY_PREFIX}{ray_image.wanda_image_name}",
nightly_alias=nightly_alias,
build_env=build_env,
)
defaults.update(kwargs)
return ImageBuildConfig(**defaults)
class TestConfigWandaImageTag(unittest.TestCase):
"""Test wanda_image_name and wanda_image_tag on ImageBuildConfig."""
def test_ray_cpu(self):
c = _config()
self.assertEqual(c.ray_image.wanda_image_name, "ray-py3.10-cpu")
self.assertEqual(c.wanda_image_tag, f"{REGISTRY_PREFIX}ray-py3.10-cpu")
def test_ray_cuda(self):
c = _config(platform="cu12.8.1-cudnn")
self.assertEqual(c.ray_image.wanda_image_name, "ray-py3.10-cu12.8.1-cudnn")
def test_ray_extra_cpu(self):
c = _config(image_type="ray-extra")
self.assertEqual(c.ray_image.wanda_image_name, "ray-extra-py3.10-cpu")
def test_ray_llm_cuda(self):
c = _config(
image_type="ray-llm", python_version="3.12", platform="cu13.0.0-cudnn"
)
self.assertEqual(c.ray_image.wanda_image_name, "ray-llm-py3.12-cu13.0.0-cudnn")
def test_aarch64_suffix(self):
c = _config(architecture="aarch64")
self.assertEqual(c.ray_image.wanda_image_name, "ray-py3.10-cpu-aarch64")
class TestConfigNightlyAlias(unittest.TestCase):
"""Test nightly_alias on ImageBuildConfig."""
def test_ray_default_has_nightly(self):
c = _config()
self.assertEqual(c.nightly_alias, f"{REGISTRY_PREFIX}ray:nightly")
def test_ray_extra_default_has_nightly_extra(self):
c = _config(image_type="ray-extra")
self.assertEqual(c.nightly_alias, f"{REGISTRY_PREFIX}ray:nightly-extra")
def test_ray_llm_default_has_nightly(self):
c = _config(
image_type="ray-llm", python_version="3.12", platform="cu13.0.0-cudnn"
)
self.assertEqual(c.nightly_alias, f"{REGISTRY_PREFIX}ray-llm:nightly")
def test_ray_llm_extra_default_has_nightly_extra(self):
c = _config(
image_type="ray-llm-extra",
python_version="3.12",
platform="cu13.0.0-cudnn",
)
self.assertEqual(c.nightly_alias, f"{REGISTRY_PREFIX}ray-llm:nightly-extra")
def test_non_default_python_no_alias(self):
c = _config(python_version="3.12")
self.assertIsNone(c.nightly_alias)
def test_non_default_platform_no_alias(self):
c = _config(platform="cu12.8.1-cudnn")
self.assertIsNone(c.nightly_alias)
def _make_ray_root(tmpdir):
"""Create a minimal ray root with config files for property tests."""
root = Path(tmpdir)
(root / ".rayciversion").write_text("0.31.0")
(root / "rayci.env").write_text(
"MANYLINUX_VERSION=260128.221a193\nRAY_VERSION=3.0.0.dev0\n"
)
return root
class TestBuildEnv(unittest.TestCase):
"""Test build_env returns correct environment variables."""
def test_cpu_env(self):
env = _config().build_env
self.assertEqual(env["PYTHON_VERSION"], "3.10")
self.assertEqual(env["HOSTTYPE"], "x86_64")
self.assertEqual(env["ARCH_SUFFIX"], "")
self.assertEqual(env["IS_LOCAL_BUILD"], "true")
self.assertEqual(env["IMAGE_TYPE"], "ray")
self.assertNotIn("CUDA_VERSION", env)
def test_cuda_env(self):
env = _config(platform="cu12.8.1-cudnn").build_env
self.assertEqual(env["CUDA_VERSION"], "12.8.1-cudnn")
def test_tpu_env(self):
env = _config(platform="tpu").build_env
self.assertNotIn("CUDA_VERSION", env)
def test_ray_extra_image_type(self):
env = _config(image_type="ray-extra").build_env
self.assertEqual(env["IMAGE_TYPE"], "ray")
def test_ray_llm_image_type(self):
env = _config(
image_type="ray-llm", python_version="3.12", platform="cu13.0.0-cudnn"
).build_env
self.assertEqual(env["IMAGE_TYPE"], "ray-llm")
def test_ray_llm_extra_image_type(self):
env = _config(
image_type="ray-llm-extra",
python_version="3.12",
platform="cu13.0.0-cudnn",
).build_env
self.assertEqual(env["IMAGE_TYPE"], "ray-llm")
class TestPlatformDetection(unittest.TestCase):
"""Test _detect_host_arch() returns correct values."""
def test_darwin_arm64(self):
with (
mock.patch("ci.build.build_common._platform.system", return_value="Darwin"),
mock.patch("ci.build.build_common._platform.machine", return_value="arm64"),
):
arch = ImageBuildConfig._detect_host_arch()
self.assertEqual(arch, "aarch64")
def test_linux_x86_64(self):
with (
mock.patch("ci.build.build_common._platform.system", return_value="Linux"),
mock.patch(
"ci.build.build_common._platform.machine", return_value="x86_64"
),
):
arch = ImageBuildConfig._detect_host_arch()
self.assertEqual(arch, "x86_64")
def test_linux_aarch64(self):
with (
mock.patch("ci.build.build_common._platform.system", return_value="Linux"),
mock.patch(
"ci.build.build_common._platform.machine", return_value="aarch64"
),
):
arch = ImageBuildConfig._detect_host_arch()
self.assertEqual(arch, "aarch64")
def test_unsupported_platform_raises(self):
with (
mock.patch(
"ci.build.build_common._platform.system", return_value="Windows"
),
mock.patch("ci.build.build_common._platform.machine", return_value="AMD64"),
):
with self.assertRaises(BuildError):
ImageBuildConfig._detect_host_arch()
class TestFromArgs(unittest.TestCase):
"""Test ImageBuildConfig.from_args() factory method."""
def test_from_args_creates_config(self):
with tempfile.TemporaryDirectory() as tmpdir:
ray_root = _make_ray_root(tmpdir)
with (
mock.patch(
"ci.build.build_common._platform.system", return_value="Linux"
),
mock.patch(
"ci.build.build_common._platform.machine", return_value="x86_64"
),
mock.patch("build_image.find_ray_root", return_value=ray_root),
mock.patch("build_image.get_git_commit", return_value="abc1234"),
):
config = ImageBuildConfig.from_args("ray", "3.10", "cpu")
self.assertEqual(config.ray_image.image_type, "ray")
self.assertEqual(config.ray_image.python_version, "3.10")
self.assertEqual(config.ray_image.platform, "cpu")
self.assertEqual(config.ray_image.architecture, "x86_64")
self.assertEqual(config.ray_image.arch_suffix, "")
self.assertEqual(config.raymake_version, "0.31.0")
self.assertEqual(config.manylinux_version, "260128.221a193")
self.assertEqual(config.ray_version, "3.0.0.dev0")
def test_from_args_rejects_invalid(self):
with (
mock.patch("ci.build.build_common._platform.system", return_value="Linux"),
mock.patch(
"ci.build.build_common._platform.machine", return_value="x86_64"
),
):
with self.assertRaises(BuildError):
ImageBuildConfig.from_args("ray-llm", "3.10", "cpu")
def test_from_args_rejects_missing_wanda_spec(self):
"""ray-extra + tpu passes validate() but has no wanda spec."""
with (
mock.patch("ci.build.build_common._platform.system", return_value="Linux"),
mock.patch(
"ci.build.build_common._platform.machine", return_value="x86_64"
),
):
with self.assertRaises(BuildError):
ImageBuildConfig.from_args("ray-extra", "3.10", "tpu")
def test_from_args_rejects_unsupported_arch(self):
"""ray-llm only supports x86_64; aarch64 should be rejected."""
with tempfile.TemporaryDirectory() as tmpdir:
ray_root = _make_ray_root(tmpdir)
with (
mock.patch(
"ci.build.build_common._platform.system", return_value="Linux"
),
mock.patch(
"ci.build.build_common._platform.machine", return_value="aarch64"
),
mock.patch("build_image.find_ray_root", return_value=ray_root),
):
with self.assertRaises(BuildError):
ImageBuildConfig.from_args("ray-llm", "3.12", "cu13.0.0-cudnn")
class TestSupportedImageTypes(unittest.TestCase):
"""Test SUPPORTED_IMAGE_TYPES covers all expected types."""
def test_contains_all_types(self):
for name in ("ray", "ray-extra", "ray-llm", "ray-llm-extra"):
self.assertIn(name, SUPPORTED_IMAGE_TYPES)
def test_all_plain_strings(self):
for name in SUPPORTED_IMAGE_TYPES:
self.assertIsInstance(name, str)
self.assertNotIn(".", name) # not "RayType.RAY"
def test_wanda_spec_keys_use_valid_image_types(self):
for image_type, _ in WANDA_SPEC_PATHS:
self.assertIn(image_type, IMAGE_TYPE_CONFIG)
class TestHelpOutput(unittest.TestCase):
"""Test that --help prints without error."""
def test_no_args_prints_help_and_exits_zero(self):
with mock.patch("build_image.sys.argv", ["build_image"]):
with self.assertRaises(SystemExit) as ctx:
main()
self.assertEqual(ctx.exception.code, 0)
if __name__ == "__main__":
unittest.main()