blob: 6b5ec68b171bfd5d97faef110030d4b481b59c0b [file]
#!/usr/bin/env python3
# Copyright 2026 The Chromium Authors
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""Tool to check if a test suite is barred by ci_only = True configuration."""
import argparse
import json
import pathlib
import subprocess
import sys
from typing import Any, Callable, NamedTuple
class TrybotEntry(NamedTuple):
"""Holds trybot name and target group name.
Attributes:
trybot: Name of the trybot.
group: Target group name (e.g. chromium.coverage).
"""
trybot: str
group: str
class BuilderConfig(NamedTuple):
"""Holds builder name and its ci_only status.
Attributes:
builder: Name of the builder.
ci_only: Boolean status indicating if the builder is ci_only.
"""
builder: str
ci_only: bool
def discover_trybots_for_test_suite(
src_root: pathlib.Path,
test_suite: str,
revision: str | None = None,
) -> list[TrybotEntry]:
"""Discovers all trybots and target groups configuring the test suite.
Args:
src_root: The root directory of the chromium source checkout.
test_suite: Name of the test suite to search for.
revision: Optional git revision (commit hash/branch) to search at.
Returns:
A list of TrybotEntry NamedTuples.
"""
# Match exact JSON pattern for the test suite name.
pattern = f'\\"name\\": \\"{test_suite}\\"'
cmd = [
'git',
'grep',
'-l',
pattern,
]
if revision:
cmd.append(revision)
cmd.extend(['--', 'infra/config/generated/builders/try/*/targets/*.json'])
result = subprocess.run(cmd,
cwd=src_root,
capture_output=True,
text=True,
check=False)
if result.returncode != 0:
return []
trybots_and_groups = []
for line in result.stdout.splitlines():
line = line.strip()
if not line:
continue
if revision and ':' in line:
line = line.split(':', 1)[1]
path = pathlib.Path(line)
parts = path.parts
trybot = parts[5]
group = path.stem
trybots_and_groups.append(TrybotEntry(trybot=trybot, group=group))
return trybots_and_groups
def read_local(src_root: pathlib.Path, rel_path: str) -> str | None:
"""Reads a configuration file from the local disk.
Args:
src_root: The root directory of the chromium source checkout.
rel_path: Relative path to the file from the source root.
Returns:
The file contents as a string, or None if the file was missing/unread.
"""
file_path = src_root / rel_path
if not file_path.exists():
return None
try:
return file_path.read_text(encoding='utf-8')
except OSError as e:
print(f'Error reading local file {file_path}: {e}', file=sys.stderr)
return None
def read_git(src_root: pathlib.Path, revision: str,
rel_path: str) -> str | None:
"""Reads a configuration file from a git revision.
Args:
src_root: The root directory of the chromium source checkout.
revision: The git revision (commit hash/branch) to query.
rel_path: Relative path to the file from the source root.
Returns:
The file contents as a string, or None if the git command failed.
"""
cmd = ['git', 'show', f'{revision}:{rel_path}']
result = subprocess.run(cmd,
cwd=src_root,
capture_output=True,
text=True,
check=False)
if result.returncode != 0:
return None
return result.stdout
def check_coverage_enabled(read_fn: Callable[[str], str | None],
trybot: str) -> bool:
"""Checks if coverage is enabled in the trybot's GN args.
Args:
read_fn: Callable that takes a relative path and returns file content.
trybot: Name of the trybot to inspect.
Returns:
True if code coverage is enabled for Clang or JaCoCo, False otherwise.
"""
gn_args_path = f'infra/config/generated/builders/try/{trybot}/gn-args.json'
gn_args_content = read_fn(gn_args_path)
if not gn_args_content:
return False
try:
gn_args = json.loads(gn_args_content).get('gn_args', {})
return bool(
gn_args.get('use_clang_coverage', False)
or gn_args.get('use_jacoco_coverage', False))
except json.JSONDecodeError:
return False
def check_trybot_coverage_config(
read_fn: Callable[[str], str | None],
trybot: str,
group: str,
test_suite: str,
) -> list[BuilderConfig]:
"""Checks target specs for a trybot to see if a test suite is ci_only.
Args:
read_fn: Callable that takes a relative path and returns file content.
trybot: Name of the trybot to check.
group: Target group name (e.g. chromium.coverage).
test_suite: Name of the test suite to verify.
Returns:
A list of BuilderConfig NamedTuples.
"""
target_spec_rel_path = (
f'infra/config/generated/builders/try/{trybot}/targets/{group}.json')
target_spec_content = read_fn(target_spec_rel_path)
if not target_spec_content:
return []
try:
target_spec = json.loads(target_spec_content)
except json.JSONDecodeError as e:
print(
f'Error parsing target spec {target_spec_rel_path}: {e}',
file=sys.stderr,
)
return []
results = []
for builder_name, builder_specs in target_spec.items():
suite_found = False
is_ci_only = False
for test_type in [
'gtest_tests',
'isolated_scripts',
'junit_tests',
'scripts',
]:
tests = builder_specs.get(test_type, [])
for test in tests:
if test.get('name') == test_suite:
suite_found = True
is_ci_only = test.get('ci_only', False)
break
if suite_found:
break
if suite_found:
results.append(BuilderConfig(builder=builder_name, ci_only=is_ci_only))
return results
def main() -> None:
parser = argparse.ArgumentParser(
description=('Check if a test suite is barred by ci_only = True on'
' trybots.'))
parser.add_argument(
'test_suite',
help='Name of the test suite (e.g. chrome_junit_tests)',
)
parser.add_argument(
'--revision',
'-r',
help=('Git revision to check the configuration at (e.g., commit hash,'
' branch).'),
)
args = parser.parse_args()
script_dir = pathlib.Path(__file__).resolve().parent
src_root = script_dir.parents[1]
if not src_root.exists():
print(f'Error: Src path {src_root} does not exist', file=sys.stderr)
sys.exit(1)
if args.revision:
# Check if the revision is valid
res = subprocess.run(
['git', 'rev-parse', '--verify', args.revision],
cwd=src_root,
capture_output=True,
text=True,
check=False,
)
if res.returncode != 0:
print(f'Error: Invalid revision: {args.revision}', file=sys.stderr)
sys.exit(1)
trybots_and_groups = discover_trybots_for_test_suite(src_root,
args.test_suite,
args.revision)
if not trybots_and_groups:
print(
f"Warning: Test suite '{args.test_suite}' was not found on any"
' trybots.',
file=sys.stderr,
)
sys.exit(0)
if args.revision:
read_fn = lambda path: read_git(src_root, args.revision, path)
else:
read_fn = lambda path: read_local(src_root, path)
revision_msg = f' at revision {args.revision}' if args.revision else ''
print(f"Checking test suite '{args.test_suite}' across"
f' {len(trybots_and_groups)} configured trybots{revision_msg}...')
for entry in sorted(trybots_and_groups, key=lambda e: e.trybot):
trybot = entry.trybot
group = entry.group
configs = check_trybot_coverage_config(read_fn, trybot, group,
args.test_suite)
coverage_enabled = check_coverage_enabled(read_fn, trybot)
for config in configs:
status = 'ci_only = True' if config.ci_only else 'ci_only = False'
coverage_status = ('Coverage: Enabled'
if coverage_enabled else 'Coverage: Disabled')
print(f' Trybot: {trybot:<30} | Builder: {group}:{config.builder:<30}'
f' | {status:<15} | {coverage_status}')
if __name__ == '__main__':
main()