193 lines
6.0 KiB
Python
193 lines
6.0 KiB
Python
"""Activate result task groups in the existing build."""
|
|
|
|
import json
|
|
import os
|
|
import sys
|
|
from typing import Annotated, Optional
|
|
|
|
import structlog
|
|
import typer
|
|
from urllib3.util import Retry
|
|
|
|
from evergreen.api import (
|
|
DEFAULT_HTTP_RETRY_ATTEMPTS,
|
|
DEFAULT_HTTP_RETRY_BACKOFF_FACTOR,
|
|
DEFAULT_HTTP_RETRY_CODES,
|
|
EvergreenApi,
|
|
RetryingEvergreenApi,
|
|
)
|
|
|
|
if __name__ == "__main__" and __package__ is None:
|
|
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
|
|
|
from buildscripts.util.cmdutils import enable_logging
|
|
from buildscripts.util.fileops import read_yaml_file
|
|
|
|
LOGGER = structlog.getLogger(__name__)
|
|
|
|
EVG_CONFIG_FILE = "./.evergreen.yml"
|
|
|
|
app = typer.Typer(pretty_exceptions_show_locals=False)
|
|
|
|
|
|
def get_executed_test_labels(build_events_file: str) -> set[str]:
|
|
"""
|
|
Parse a Bazel build events NDJSON file and return all executed test target labels.
|
|
|
|
:param build_events_file: Path to the build_events.json NDJSON file.
|
|
:return: Set of Bazel target labels that had test results.
|
|
"""
|
|
labels = set()
|
|
with open(build_events_file) as f:
|
|
for line in f:
|
|
line = line.strip()
|
|
if not line:
|
|
continue
|
|
event = json.loads(line)
|
|
if "testResult" in event:
|
|
label = event["id"]["testResult"]["label"]
|
|
labels.add(label)
|
|
return labels
|
|
|
|
|
|
def get_result_tasks(evg_api, build_id):
|
|
tasks = []
|
|
for task in evg_api.tasks_by_build(build_id):
|
|
# Result tasks are bazel targets that start with "//"
|
|
if task.display_name.startswith("//") and "_burn_in_" not in task.display_name:
|
|
tasks.append(task)
|
|
return tasks
|
|
|
|
|
|
def activate_or_restart_tasks(evg_api, tasks, version_id, build_variant):
|
|
activate = []
|
|
for task in tasks:
|
|
if task.activated:
|
|
evg_api.restart_task(task.task_id)
|
|
else:
|
|
activate.append(task.display_name)
|
|
|
|
if activate:
|
|
variants = [{"name": build_variant, "tasks": activate}]
|
|
evg_api.activate_version_tasks(version_id, variants)
|
|
|
|
|
|
def assert_all_tests_have_tasks(tasks, build_events_file):
|
|
executed_labels = get_executed_test_labels(build_events_file)
|
|
task_names = set([task.display_name for task in tasks])
|
|
missing = executed_labels - task_names
|
|
if missing:
|
|
missing_sorted = sorted(missing)
|
|
LOGGER.error(
|
|
"Executed tests have no corresponding Evergreen task — "
|
|
"this indicates a bug in task generation",
|
|
missing_count=len(missing_sorted),
|
|
missing_tasks=missing_sorted,
|
|
)
|
|
raise RuntimeError(
|
|
f"{len(missing_sorted)} executed test(s) have no corresponding Evergreen task: "
|
|
+ ", ".join(missing_sorted)
|
|
)
|
|
|
|
|
|
def activate_result_task_group(
|
|
build_variant: str,
|
|
task_name: str,
|
|
version_id: str,
|
|
evg_api: EvergreenApi,
|
|
build_events_file: Optional[str] = None,
|
|
) -> None:
|
|
"""
|
|
Activate the result task group for the given variant and task.
|
|
|
|
:param build_variant: The build variant name.
|
|
:param task_name: The task name (e.g., "resmoke_tests").
|
|
:param version_id: The Evergreen version ID.
|
|
:param evg_api: Evergreen API client.
|
|
:param build_events_file: Optional path to build_events.json. When provided, asserts that
|
|
every executed test has a corresponding Evergreen task; raises RuntimeError otherwise.
|
|
"""
|
|
|
|
try:
|
|
version = evg_api.version_by_id(version_id)
|
|
build_id = version.build_variants_map.get(build_variant)
|
|
|
|
if not build_id:
|
|
LOGGER.warning(
|
|
"Build variant not found in version",
|
|
build_variant=build_variant,
|
|
version_id=version_id,
|
|
)
|
|
return
|
|
|
|
result_tasks = get_result_tasks(evg_api, build_id)
|
|
|
|
if build_events_file:
|
|
assert_all_tests_have_tasks(result_tasks, build_events_file)
|
|
|
|
activate_or_restart_tasks(evg_api, result_tasks, version_id, build_variant)
|
|
|
|
except Exception:
|
|
result_task_group_name = f"{task_name}_results_{build_variant}"
|
|
LOGGER.error(
|
|
"Failed to activate result task group",
|
|
task_group=result_task_group_name,
|
|
build_variant=build_variant,
|
|
version_id=version_id,
|
|
exc_info=True,
|
|
)
|
|
raise
|
|
|
|
|
|
@app.command()
|
|
def main(
|
|
expansion_file: Annotated[
|
|
str, typer.Option(help="Location of expansions file generated by evergreen.")
|
|
],
|
|
evergreen_config: Annotated[
|
|
str, typer.Option(help="Location of evergreen configuration file.")
|
|
] = EVG_CONFIG_FILE,
|
|
build_events_file: Annotated[
|
|
Optional[str],
|
|
typer.Option(
|
|
help="Path to the Bazel build events NDJSON file (build_events.json). "
|
|
"When provided, asserts that every executed test has a corresponding "
|
|
"Evergreen task, raising an error if any are missing."
|
|
),
|
|
] = None,
|
|
verbose: Annotated[bool, typer.Option(help="Enable verbose logging.")] = False,
|
|
) -> None:
|
|
"""
|
|
Activate the result task group for the current build variant and task.
|
|
"""
|
|
enable_logging(verbose)
|
|
|
|
expansions = read_yaml_file(expansion_file)
|
|
build_variant = expansions.get("build_variant")
|
|
task_name = expansions.get("task_name")
|
|
version_id = expansions.get("version_id")
|
|
|
|
if not all([build_variant, task_name, version_id]):
|
|
LOGGER.error(
|
|
"Missing required expansions",
|
|
build_variant=build_variant,
|
|
task_name=task_name,
|
|
version_id=version_id,
|
|
)
|
|
return
|
|
|
|
evg_api = RetryingEvergreenApi.get_api(config_file=evergreen_config, log_on_error=True)
|
|
evg_api._http_retry = Retry(
|
|
total=DEFAULT_HTTP_RETRY_ATTEMPTS + 10,
|
|
backoff_factor=DEFAULT_HTTP_RETRY_BACKOFF_FACTOR,
|
|
status_forcelist=DEFAULT_HTTP_RETRY_CODES,
|
|
raise_on_status=False,
|
|
raise_on_redirect=False,
|
|
)
|
|
|
|
activate_result_task_group(build_variant, task_name, version_id, evg_api, build_events_file)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
app()
|