blob: 14ea85f21aa90d112c1dc2fbbdbf26ab2d1b9fa6 [file]
"""Reads the build and test flags of BazelCI presubmit tasks.
BazelCI's bazelci.py loads `.bazelci/presubmit.yml` with `yaml.safe_load`, so
YAML merge keys (`<<:`) resolve the way PyYAML resolves them. It then flattens
each task's `build_flags` and `test_flags` with `flatten()`, which yields a
mapping's keys. This module mirrors that, except that it doesn't expand the
task matrix: matrix values only fill `${{ }}` placeholders, so a task stands
for all of its matrix jobs.
See https://github.com/bazelbuild/continuous-integration/blob/master/buildkite/bazelci.py
"""
from __future__ import annotations
from collections.abc import Iterable, Mapping
import yaml
PRESUBMIT_PATH = ".bazelci/presubmit.yml"
FLAG_ATTRS = (
# keep-sorted start
"build_flags",
"test_flags",
# keep-sorted end
)
def load_tasks(text: str) -> dict[str, dict[str, object]]:
"""Returns the tasks of a presubmit.yml, with YAML merges resolved."""
return yaml.safe_load(text)["tasks"]
def diff_tasks(
old_tasks: Mapping[str, Mapping[str, object]],
new_tasks: Mapping[str, Mapping[str, object]],
) -> list[str]:
"""Describes how each task's flags changed between two presubmit.yml files.
Args:
old_tasks: The tasks before the change, from `load_tasks`.
new_tasks: The tasks after the change, from `load_tasks`.
Returns:
One line per added task, removed task, or changed flags attribute,
sorted by task. A changed attribute lists removed flags with `-` and
added flags with `+`, then the new order if the kept flags changed
order, since Bazel uses a repeated flag's last value.
"""
lines = []
for name in sorted(old_tasks.keys() | new_tasks.keys()):
if name not in new_tasks:
lines.append(f"{name}: removed")
continue
if name not in old_tasks:
parts = []
for attr in FLAG_ATTRS:
flags = " ".join(_task_flags(new_tasks[name], attr)) or "(none)"
parts.append(f"{attr}: {flags}")
lines.append(f"{name}: added; {'; '.join(parts)}")
continue
for attr in FLAG_ATTRS:
old_flags = _task_flags(old_tasks[name], attr)
new_flags = _task_flags(new_tasks[name], attr)
if old_flags == new_flags:
continue
changes = [f"-{flag}" for flag in old_flags if flag not in new_flags]
changes += [f"+{flag}" for flag in new_flags if flag not in old_flags]
kept_old = [flag for flag in old_flags if flag in new_flags]
kept_new = [flag for flag in new_flags if flag in old_flags]
if kept_old != kept_new:
changes += ["reordered:", *new_flags]
lines.append(f"{name}: {attr}: {' '.join(changes)}")
return lines
def tasks_with_flag(tasks: Mapping[str, Mapping[str, object]], flag: str) -> list[str]:
"""Lists the tasks that pass `flag`, and in which flags attributes."""
lines = []
for name in sorted(tasks):
attrs = [attr for attr in FLAG_ATTRS if flag in _task_flags(tasks[name], attr)]
if attrs:
lines.append(f"{name}: {', '.join(attrs)}")
return lines
def _flatten(value: Iterable[object]) -> list[str]:
# Like bazelci.py's `flatten()`: keeps strings, expands one level of
# nested lists, and drops anything else.
flags = []
for item in value:
if isinstance(item, str):
flags.append(item)
elif isinstance(item, list):
flags.extend(item)
return flags
def _task_flags(task: Mapping[str, object], attr: str) -> list[str]:
value = task.get(attr, {})
assert isinstance(value, (dict, list)) # type assert
return _flatten(value)