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

512 lines
14 KiB
Python

import os
import sys
from typing import List, Optional, Set, Tuple
import click
import yaml
from ci.ray_ci.configs import (
PYTHON_VERSIONS,
)
from ci.ray_ci.container import _DOCKER_ECR_REPO
from ci.ray_ci.linux_tester_container import LinuxTesterContainer
from ci.ray_ci.tester_container import TesterContainer
from ci.ray_ci.utils import ci_init, ecr_docker_login
from ci.ray_ci.windows_tester_container import WindowsTesterContainer
from ray_release.test import Test, TestState
CUDA_COPYRIGHT = """
==========
== CUDA ==
==========
CUDA Version 11.8.0
Container image Copyright (c) 2016-2023, NVIDIA CORPORATION & AFFILIATES. All rights reserved.
This container image and its contents are governed by the NVIDIA Deep Learning Container License.
By pulling and using the container, you accept the terms and conditions of this license:
https://developer.nvidia.com/ngc/nvidia-deep-learning-container-license
A copy of this license is made available in this container at /NGC-DL-CONTAINER-LICENSE for your convenience.
""" # noqa: E501
DEFAULT_EXCEPT_TAGS = {"manual"}
# Gets the path of product/tools/docker (i.e. the parent of 'common')
bazel_workspace_dir = os.environ.get("BUILD_WORKSPACE_DIRECTORY", "")
@click.command()
@click.argument("targets", required=True, type=str, nargs=-1)
@click.argument("team", required=True, type=str, nargs=1)
@click.option(
"--workers",
default="1",
type=str,
help=("Number of concurrent test jobs to run."),
)
@click.option(
"--worker-id",
default="0",
type=str,
help=("Index of the concurrent shard to run."),
)
@click.option(
"--parallelism-per-worker",
default=1,
type=int,
help=("Number of concurrent test jobs to run per worker."),
)
@click.option(
"--except-tags",
default="",
type=str,
help=("Except tests with the given tags."),
)
@click.option(
"--only-tags",
default="",
type=str,
help=("Only include tests with the given tags."),
)
@click.option(
"--cache-test-results",
is_flag=True,
show_default=True,
default=False,
help=("If cache and use test results in bazel cache."),
)
@click.option(
"--run-flaky-tests",
is_flag=True,
show_default=True,
default=False,
help=("Run flaky tests."),
)
@click.option(
"--run-high-impact-tests",
is_flag=True,
show_default=True,
default=False,
help=(
"Run only high impact tests. "
"High impact tests are tests that often catch regressions in the past."
),
)
@click.option(
"--skip-ray-installation",
is_flag=True,
show_default=True,
default=False,
help=("Skip ray installation."),
)
@click.option(
"--build-only",
is_flag=True,
show_default=True,
default=False,
help=("Build ray only, skip running tests."),
)
@click.option(
"--gpus",
default=0,
type=int,
help=("Number of GPUs to use for the test."),
)
@click.option(
"--network",
type=str,
help="Network to use for the test.",
)
@click.option(
"--test-env",
multiple=True,
type=str,
help="Environment variables to set for the test.",
)
@click.option(
"--test-arg",
type=str,
help=("Arguments to pass to the test."),
)
@click.option(
"--python-version",
type=click.Choice(list(PYTHON_VERSIONS.keys())),
help=("Python version to build the wheel with"),
)
@click.option(
"--build-name",
type=str,
help="Name of the build used to run tests",
)
@click.option(
"--build-type",
type=click.Choice(
[
# python build types
"optimized",
"debug",
"asan",
"wheel",
"wheel-aarch64",
# cpp build types
"clang",
"asan-clang",
"ubsan",
"tsan-clang",
"cgroup",
# java build types
"java",
# with cpp and java worker support
"multi-lang",
# do not build ray
"skip",
]
),
default="optimized",
)
@click.option(
"--install-mask",
type=str,
help="A install mask string to install ray with",
)
@click.option(
"--bisect-run-test-target",
type=str,
help="Test target to run in bisection mode",
)
@click.option(
"--operating-system",
default="linux",
type=click.Choice(["linux", "windows"]),
help=("Operating system to run tests on"),
)
@click.option(
"--tmp-filesystem",
type=str,
help=("Filesystem to use for /tmp"),
)
@click.option(
"--privileged",
is_flag=True,
show_default=True,
default=False,
help="Run the test in a privileged Docker container",
)
def main(
targets: List[str],
team: str,
workers: str,
worker_id: str,
parallelism_per_worker: int,
operating_system: str,
except_tags: str,
only_tags: str,
cache_test_results: bool,
run_flaky_tests: bool,
run_high_impact_tests: bool,
skip_ray_installation: bool,
build_only: bool,
gpus: int,
network: Optional[str],
test_env: Tuple[str],
test_arg: Optional[str],
python_version: Optional[str],
build_name: Optional[str],
build_type: Optional[str],
install_mask: Optional[str],
bisect_run_test_target: Optional[str],
tmp_filesystem: Optional[str],
privileged: bool,
) -> None:
if not bazel_workspace_dir:
raise Exception("Please use `bazelisk run //ci/ray_ci`")
os.chdir(bazel_workspace_dir)
ci_init()
ecr_docker_login(_DOCKER_ECR_REPO.split("/")[0])
bisect_run_test_target = bisect_run_test_target or os.environ.get(
"RAYCI_BISECT_TEST_TARGET"
)
container = _get_container(
team,
operating_system,
int(workers) if workers else 1,
int(worker_id) if worker_id else 0,
parallelism_per_worker,
gpus,
network=network,
tmp_filesystem=tmp_filesystem,
test_env=list(test_env),
python_version=python_version,
build_name=build_name,
build_type=build_type,
skip_ray_installation=skip_ray_installation,
install_mask=install_mask,
privileged=privileged,
)
if build_only:
sys.exit(0)
print("--- Listing test targets", file=sys.stderr)
if bisect_run_test_target:
test_targets = [bisect_run_test_target]
else:
get_high_impact_tests = (
run_high_impact_tests or os.environ.get("RAYCI_MICROCHECK_RUN") == "1"
)
lookup_test_database = os.environ.get("RAYCI_DISABLE_TEST_DB") != "1"
test_targets = _get_test_targets(
container,
targets,
team,
operating_system,
except_tags=_add_default_except_tags(except_tags),
only_tags=only_tags,
get_flaky_tests=run_flaky_tests,
get_high_impact_tests=get_high_impact_tests,
lookup_test_database=lookup_test_database,
)
if not test_targets:
print("--- No tests to run", file=sys.stderr)
sys.exit(0)
print(f"+++ Running {len(test_targets)} tests", file=sys.stderr)
success = container.run_tests(
team,
test_targets,
test_arg,
is_bisect_run=bisect_run_test_target is not None,
run_flaky_tests=run_flaky_tests,
cache_test_results=cache_test_results,
)
sys.exit(0 if success else 42)
def _add_default_except_tags(except_tags: str) -> str:
final_except_tags = set(DEFAULT_EXCEPT_TAGS)
if except_tags:
final_except_tags.update(except_tags.split(","))
return ",".join(final_except_tags)
def _get_container(
team: str,
operating_system: str,
workers: int,
worker_id: int,
parallelism_per_worker: int,
gpus: int,
network: Optional[str],
tmp_filesystem: Optional[str] = None,
test_env: Optional[List[str]] = None,
python_version: Optional[str] = None,
build_name: Optional[str] = None,
build_type: Optional[str] = None,
install_mask: Optional[str] = None,
skip_ray_installation: bool = False,
privileged: bool = False,
) -> TesterContainer:
shard_count = workers * parallelism_per_worker
shard_start = worker_id * parallelism_per_worker
shard_end = (worker_id + 1) * parallelism_per_worker
if not build_name:
build_name = (
f"{team}build-py{python_version}" if python_version else f"{team}build"
)
if operating_system == "linux":
return LinuxTesterContainer(
build_name,
test_envs=test_env,
shard_count=shard_count,
shard_ids=list(range(shard_start, shard_end)),
gpus=gpus,
network=network,
skip_ray_installation=skip_ray_installation,
build_type=build_type,
python_version=python_version,
tmp_filesystem=tmp_filesystem,
install_mask=install_mask,
privileged=privileged,
)
if operating_system == "windows":
return WindowsTesterContainer(
build_name,
network=network,
test_envs=test_env,
shard_count=shard_count,
shard_ids=list(range(shard_start, shard_end)),
skip_ray_installation=skip_ray_installation,
)
assert False, f"Unsupported operating system: {operating_system}"
def _get_tag_matcher(tag: str) -> str:
"""
Return a regular expression that matches the given bazel tag. This is required for
an exact tag match because bazel query uses regex to match tags.
The word boundary is escaped twice because it is used in a python string and then
used again as a string in bazel query.
"""
return f"\\\\b{tag}\\\\b"
def _get_all_test_query(
targets: List[str],
team: str,
except_tags: Optional[str] = None,
only_tags: Optional[str] = None,
) -> str:
"""
Get all test targets that are owned by a particular team, except those that
have the given tags
"""
test_query = " union ".join([f"tests({target})" for target in targets])
query = f"attr(tags, '{_get_tag_matcher(f'team:{team}')}', {test_query})"
if only_tags:
only_query = " union ".join(
[
f"attr(tags, '{_get_tag_matcher(t)}', {test_query})"
for t in only_tags.split(",")
]
)
query = f"{query} intersect ({only_query})"
if except_tags:
except_query = " union ".join(
[
f"attr(tags, '{_get_tag_matcher(t)}', {test_query})"
for t in except_tags.split(",")
]
)
query = f"{query} except ({except_query})"
return query
def _get_test_targets(
container: TesterContainer,
targets: str,
team: str,
operating_system: str,
except_tags: Optional[str] = "",
only_tags: Optional[str] = "",
yaml_dir: Optional[str] = None,
get_flaky_tests: bool = False,
get_high_impact_tests: bool = False,
lookup_test_database: bool = True,
) -> List[str]:
"""
Get test targets that are owned by a particular team
"""
query = _get_all_test_query(targets, team, except_tags, only_tags)
test_targets = {
target
for target in container.run_script_with_output(
[
f'bazel query "{query}"',
]
)
.strip()
.split(os.linesep)
if target
}
flaky_tests = set(
_get_flaky_test_targets(
team,
operating_system,
yaml_dir,
lookup_test_database=lookup_test_database,
)
)
if get_flaky_tests:
# run flaky test cases, so we include flaky tests in the list of targets
# provided by users
final_targets = test_targets.intersection(flaky_tests)
else:
# normal case, we want to exclude flaky tests from the list of targets provided
# by users
final_targets = test_targets.difference(flaky_tests)
if get_high_impact_tests:
# run high impact test cases, so we include only high impact tests in the list
# of targets provided by users
prefix = f"{operating_system}:"
# TODO(can): we should also move the logic of _get_new_tests into the
# gen_microcheck_tests function; this is currently blocked by the fact that
# we need a container to run _get_new_tests
high_impact_tests = Test.gen_microcheck_tests(
prefix=prefix,
bazel_workspace_dir=bazel_workspace_dir,
team=team,
).union(_get_new_tests(prefix, container))
final_targets = high_impact_tests.intersection(final_targets)
return sorted(final_targets)
def _get_new_tests(prefix: str, container: TesterContainer) -> Set[str]:
"""
Get all local test targets that are not in database
"""
local_test_targets = set(
container.run_script_with_output(['bazel query "tests(//...)"'])
.strip()
.split(os.linesep)
)
db_test_targets = {test.get_target() for test in Test.gen_from_s3(prefix=prefix)}
return local_test_targets.difference(db_test_targets)
def _get_flaky_test_targets(
team: str,
operating_system: str,
yaml_dir: Optional[str],
lookup_test_database: bool,
) -> List[str]:
"""
Get all test targets that are flaky
"""
if not yaml_dir:
yaml_dir = os.path.join(bazel_workspace_dir, "ci/ray_ci")
yaml_flaky_tests = set()
yaml_flaky_file = os.path.join(yaml_dir, f"{team}.tests.yml")
if os.path.exists(yaml_flaky_file):
with open(yaml_flaky_file, "rb") as f:
# load flaky tests from yaml
yaml_flaky_tests = set(yaml.safe_load(f)["flaky_tests"])
# load flaky tests from DB
if lookup_test_database:
s3_flaky_tests = {
# remove "linux:" prefix for linux tests to be consistent with the
# interface supported in the yaml file
test.get_name().lstrip("linux:")
for test in Test.gen_from_s3(prefix=f"{operating_system}:")
if test.get_oncall() == team and test.get_state() == TestState.FLAKY
}
all_flaky_tests = sorted(yaml_flaky_tests.union(s3_flaky_tests))
else:
all_flaky_tests = sorted(yaml_flaky_tests)
# linux tests are prefixed with "//"
if operating_system == "linux":
return [test for test in all_flaky_tests if test.startswith("//")]
# and other os tests are prefixed with "os:"
os_prefix = f"{operating_system}:"
return [
test.lstrip(os_prefix) for test in all_flaky_tests if test.startswith(os_prefix)
]