blob: 1ce28e61ecb9a61d6c1fe290af2991a54e325e25 [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.
"""TestSuiteMapper: Maps a file to its target test suite."""
import argparse
import ast
import json
import os
import pathlib
import re
import subprocess
import sys
SRC_ROOT = pathlib.Path(__file__).resolve().parents[2]
ISOLATE_MAP_PATH = (pathlib.Path('infra') / 'config' / 'generated' / 'testing' /
'gn_isolate_map.pyl')
def parse_isolate_map(content_str, source_name):
"""Parses isolate map content string."""
try:
data = ast.literal_eval(content_str)
# Map label -> suite name, excluding additional_compile_target
label_to_suite = {}
if not isinstance(data, dict):
print(f'Error parsing {source_name}: Expected a dictionary',
file=sys.stderr)
return None
for suite, details in data.items():
if isinstance(
details, dict) and details.get('type') == 'additional_compile_target':
continue
if isinstance(details, dict):
label = details.get('label')
if label:
label_to_suite[label] = suite
return label_to_suite
except Exception as e:
print(f'Error parsing gn_isolate_map.pyl from {source_name}: {e}',
file=sys.stderr)
return None
def load_isolate_map(src_root):
"""Loads and parses the gn_isolate_map.pyl file from the workspace."""
src_root = pathlib.Path(src_root)
pyl_path = src_root / ISOLATE_MAP_PATH
if not pyl_path.exists():
print(
f'Error: Isolate map not found at {pyl_path}. '
f'Please run main.star or sync.',
file=sys.stderr,
)
return None
try:
with open(pyl_path, 'r', encoding='utf-8') as f:
return parse_isolate_map(f.read(), str(pyl_path))
except Exception as e:
print(f'Error reading {pyl_path}: {e}', file=sys.stderr)
return None
def run_gn_refs(src_root, build_dir, target_file):
"""Executes `gn refs` to retrieve the dependent targets closure."""
# Normalize target file to gn syntax
gn_target = target_file.as_posix()
if not gn_target.startswith('//'):
gn_target = '//' + gn_target.lstrip('/')
# Execute gn refs command
cmd = ['gn', 'refs', build_dir, '--all', gn_target]
try:
res = subprocess.run(
cmd,
capture_output=True,
text=True,
cwd=src_root,
check=False,
encoding='utf-8',
)
if res.returncode != 0:
return None, res.stderr.strip()
return res.stdout.splitlines(), None
except Exception as e:
return None, str(e)
def common_dir_prefix_len(path1, path2):
"""Calculates the number of common leading directory components."""
p1 = path1.strip('/').split('/')
p2 = path2.strip('/').split('/')
common = 0
for c1, c2 in zip(p1, p2):
if c1 == c2:
common += 1
else:
break
return common
def extract_test_suites(gn_refs_output, file_path, isolate_map):
"""Scans and filters transitive GN targets for executable test suites.
Heuristic: Prefers test suites that are in a closer directory to the
target file.
"""
resolved_suites = {}
# Normalize file_path to relative path without leading // for comparison
file_path_str = file_path.as_posix()
file_dir = str(pathlib.Path(file_path_str.lstrip('/')).parent)
for target in gn_refs_output:
target = target.strip()
if not target:
continue
parts = target.split(':')
target_dir = parts[0].lstrip('/') if len(parts) > 1 else ''
# Look up in isolate map
if target in isolate_map:
suite_name = isolate_map[target]
# Score based on how many directory components are shared.
# The 'score' is the depth of the common directory prefix between the
# file and the target.
score = common_dir_prefix_len(file_dir, target_dir)
resolved_suites[suite_name] = max(resolved_suites.get(suite_name, -1),
score)
if not resolved_suites:
return []
max_score = max(resolved_suites.values())
# Filter: if we have any matches with score > 0, discard matches with
# score == 0 (e.g., targets in the root directory).
if max_score > 0:
filtered_suites = [
name for name, score in resolved_suites.items() if score > 0
]
else:
filtered_suites = list(resolved_suites.keys())
return sorted(filtered_suites)
def verify_build_dir(src_root, build_dir):
"""Verifies that build_dir is configured to compile for Linux."""
src_root = pathlib.Path(src_root)
args_gn_path = src_root / build_dir / 'args.gn'
if not args_gn_path.exists():
return (
False,
f'args.gn not found in {build_dir}. Please run `gn gen`.',
)
target_os = None
try:
with open(args_gn_path, 'r', encoding='utf-8') as f:
for line in f:
line = line.split('#')[0].strip()
if not line:
continue
parts = line.split('=', 1)
if len(parts) == 2:
key = parts[0].strip()
val = parts[1].strip().strip('"\'')
if key == 'target_os':
target_os = val
break
except Exception as e:
return False, f'Error reading {args_gn_path}: {e}'
if target_os and target_os != 'linux':
return (
False,
f'Unsupported target_os: {target_os}. '
f"Only 'linux' is supported initially.",
)
if not target_os and not sys.platform.startswith('linux'):
return (
False,
f'Host platform {sys.platform} is not Linux, '
f"and target_os is not set to 'linux' in args.gn.",
)
return True, None
def main():
parser = argparse.ArgumentParser(
description='Maps a Chromium file path to its covering test suites.')
parser.add_argument(
'file_path',
type=pathlib.Path,
help='Relative path to the target file (e.g., chrome/browser/.../file.cc)'
)
parser.add_argument(
'--build-dir',
type=pathlib.Path,
default=pathlib.Path('out/Default'),
help=('Build directory (defaults to out/Default). Must contain args.gn '
'and be configured for target_os="linux".'),
)
args = parser.parse_args()
src_root = SRC_ROOT
build_dir = args.build_dir
# Verify build directory exists
if not (src_root / build_dir).exists():
print(
f'Error: Build directory {build_dir} does not exist or is not '
f'configured.',
file=sys.stderr,
)
print(
f'Please run `gn gen {build_dir}` or supply a valid --build-dir.',
file=sys.stderr,
)
sys.exit(1)
success, err = verify_build_dir(src_root, build_dir)
if not success:
print(f'Error: {err}', file=sys.stderr)
sys.exit(1)
isolate_map = load_isolate_map(src_root)
if isolate_map is None:
sys.exit(1)
# Run references scan (uses current workspace GN graph)
refs, err = run_gn_refs(src_root, build_dir, args.file_path)
if err:
print(f'Error running GN refs query: {err}', file=sys.stderr)
sys.exit(1)
if not refs:
print(json.dumps([]))
sys.exit(0)
test_suites = extract_test_suites(refs, args.file_path, isolate_map)
print(json.dumps(test_suites, indent=2))
if __name__ == '__main__':
main()