2022-03-09 09:50:51 +00:00
|
|
|
import json
|
2022-02-16 17:35:02 +00:00
|
|
|
import os
|
2022-03-18 12:57:41 +00:00
|
|
|
import re
|
2022-05-17 17:03:12 +01:00
|
|
|
from typing import Dict, List, Optional, Tuple
|
2022-02-16 17:35:02 +00:00
|
|
|
|
2022-03-09 09:50:51 +00:00
|
|
|
import jsonschema
|
2022-02-16 17:35:02 +00:00
|
|
|
import yaml
|
|
|
|
from ray_release.anyscale_util import find_cloud_by_name
|
2022-06-28 18:14:01 +01:00
|
|
|
from ray_release.exception import ReleaseTestCLIError, ReleaseTestConfigError
|
2022-02-16 17:35:02 +00:00
|
|
|
from ray_release.logger import logger
|
2022-06-28 18:14:01 +01:00
|
|
|
from ray_release.util import DeferredEnvVar, deep_update
|
2022-02-16 17:35:02 +00:00
|
|
|
|
|
|
|
|
|
|
|
class Test(dict):
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
|
|
DEFAULT_WHEEL_WAIT_TIMEOUT = 7200 # Two hours
|
|
|
|
DEFAULT_COMMAND_TIMEOUT = 1800
|
|
|
|
DEFAULT_BUILD_TIMEOUT = 1800
|
|
|
|
DEFAULT_CLUSTER_TIMEOUT = 1800
|
2022-03-11 08:03:50 +09:00
|
|
|
DEFAULT_AUTOSUSPEND_MINS = 120
|
2022-05-05 11:42:21 +09:00
|
|
|
DEFAULT_WAIT_FOR_NODES_TIMEOUT = 3000
|
2022-02-16 17:35:02 +00:00
|
|
|
|
2022-06-28 18:14:01 +01:00
|
|
|
DEFAULT_CLOUD_ID = DeferredEnvVar(
|
|
|
|
"RELEASE_DEFAULT_CLOUD_ID",
|
|
|
|
"cld_4F7k8814aZzGG8TNUGPKnc",
|
|
|
|
)
|
|
|
|
DEFAULT_ANYSCALE_PROJECT = DeferredEnvVar(
|
|
|
|
"RELEASE_DEFAULT_PROJECT",
|
|
|
|
"prj_2xR6uT6t7jJuu1aCwWMsle",
|
|
|
|
)
|
2022-05-17 17:03:12 +01:00
|
|
|
DEFAULT_PYTHON_VERSION = (3, 7)
|
2022-02-16 17:35:02 +00:00
|
|
|
|
|
|
|
RELEASE_PACKAGE_DIR = os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))
|
|
|
|
|
2022-03-09 09:50:51 +00:00
|
|
|
RELEASE_TEST_SCHEMA_FILE = os.path.join(
|
|
|
|
RELEASE_PACKAGE_DIR, "ray_release", "schema.json"
|
|
|
|
)
|
|
|
|
|
2022-02-16 17:35:02 +00:00
|
|
|
|
|
|
|
def read_and_validate_release_test_collection(config_file: str) -> List[Test]:
|
|
|
|
"""Read and validate test collection from config file"""
|
|
|
|
with open(config_file, "rt") as fp:
|
|
|
|
test_config = yaml.safe_load(fp)
|
|
|
|
|
|
|
|
validate_release_test_collection(test_config)
|
|
|
|
return test_config
|
|
|
|
|
|
|
|
|
2022-03-09 09:50:51 +00:00
|
|
|
def load_schema_file(path: Optional[str] = None) -> Dict:
|
|
|
|
path = path or RELEASE_TEST_SCHEMA_FILE
|
|
|
|
with open(path, "rt") as fp:
|
|
|
|
return json.load(fp)
|
|
|
|
|
|
|
|
|
2022-02-16 17:35:02 +00:00
|
|
|
def validate_release_test_collection(test_collection: List[Test]):
|
2022-03-09 09:50:51 +00:00
|
|
|
try:
|
|
|
|
schema = load_schema_file()
|
|
|
|
except Exception as e:
|
|
|
|
raise ReleaseTestConfigError(
|
|
|
|
f"Could not load release test validation schema: {e}"
|
|
|
|
) from e
|
2022-02-16 17:35:02 +00:00
|
|
|
|
2022-03-09 09:50:51 +00:00
|
|
|
num_errors = 0
|
|
|
|
for test in test_collection:
|
|
|
|
error = validate_test(test, schema)
|
|
|
|
if error:
|
|
|
|
logger.error(
|
|
|
|
f"Failed to validate test {test.get('name', '(unnamed)')}: {error}"
|
|
|
|
)
|
|
|
|
num_errors += 1
|
|
|
|
|
|
|
|
if num_errors > 0:
|
2022-02-16 17:35:02 +00:00
|
|
|
raise ReleaseTestConfigError(
|
2022-03-09 09:50:51 +00:00
|
|
|
f"Release test configuration error: Found {num_errors} test "
|
|
|
|
f"validation errors."
|
2022-02-16 17:35:02 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
|
2022-03-09 15:22:49 +00:00
|
|
|
def validate_test(test: Test, schema: Optional[Dict] = None) -> Optional[str]:
|
|
|
|
schema = schema or load_schema_file()
|
|
|
|
|
2022-03-09 09:50:51 +00:00
|
|
|
try:
|
|
|
|
jsonschema.validate(test, schema=schema)
|
|
|
|
except (jsonschema.ValidationError, jsonschema.SchemaError) as e:
|
|
|
|
return str(e.message)
|
|
|
|
except Exception as e:
|
|
|
|
return str(e)
|
2022-02-16 17:35:02 +00:00
|
|
|
|
|
|
|
|
|
|
|
def find_test(test_collection: List[Test], test_name: str) -> Optional[Test]:
|
|
|
|
"""Find test with `test_name` in `test_collection`"""
|
|
|
|
for test in test_collection:
|
|
|
|
if test["name"] == test_name:
|
|
|
|
return test
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
|
|
def as_smoke_test(test: Test) -> Test:
|
|
|
|
if "smoke_test" not in test:
|
2022-04-14 06:12:27 +01:00
|
|
|
raise ReleaseTestCLIError(
|
2022-02-16 17:35:02 +00:00
|
|
|
f"Requested smoke test, but test with name {test['name']} does "
|
|
|
|
f"not have any smoke test configuration."
|
|
|
|
)
|
|
|
|
|
|
|
|
smoke_test_config = test.pop("smoke_test")
|
|
|
|
new_test = deep_update(test, smoke_test_config)
|
|
|
|
return new_test
|
|
|
|
|
|
|
|
|
2022-05-17 17:03:12 +01:00
|
|
|
def parse_python_version(version: str) -> Tuple[int, int]:
|
|
|
|
"""From XY and X.Y to (X, Y)"""
|
|
|
|
match = re.match(r"^([0-9])\.?([0-9]+)$", version)
|
|
|
|
if not match:
|
|
|
|
raise ReleaseTestConfigError(f"Invalid Python version string: {version}")
|
2022-02-16 17:35:02 +00:00
|
|
|
|
2022-05-17 17:03:12 +01:00
|
|
|
return int(match.group(1)), int(match.group(2))
|
2022-02-16 17:35:02 +00:00
|
|
|
|
|
|
|
|
|
|
|
def get_test_cloud_id(test: Test) -> str:
|
|
|
|
cloud_id = test["cluster"].get("cloud_id", None)
|
|
|
|
cloud_name = test["cluster"].get("cloud_name", None)
|
|
|
|
if cloud_id and cloud_name:
|
|
|
|
raise RuntimeError(
|
|
|
|
f"You can't supply both a `cloud_name` ({cloud_name}) and a "
|
|
|
|
f"`cloud_id` ({cloud_id}) in the test cluster configuration. "
|
|
|
|
f"Please provide only one."
|
|
|
|
)
|
|
|
|
elif cloud_name and not cloud_id:
|
|
|
|
cloud_id = find_cloud_by_name(cloud_name)
|
|
|
|
if not cloud_id:
|
|
|
|
raise RuntimeError(f"Couldn't find cloud with name `{cloud_name}`.")
|
|
|
|
else:
|
2022-06-28 18:14:01 +01:00
|
|
|
cloud_id = cloud_id or str(DEFAULT_CLOUD_ID)
|
2022-02-16 17:35:02 +00:00
|
|
|
return cloud_id
|