blob: fa6e540faf0fc60525e52dcf0095aa731d23d37d [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
from contextlib import contextmanager
import json
import os
import pathlib
import re
import shutil
import subprocess
import sys
import tempfile
SRC_ROOT = pathlib.Path(__file__).resolve().parents[2]
ISOLATE_MAP_PATH = (pathlib.Path('infra') / 'config' / 'generated' / 'testing' /
'gn_isolate_map.pyl')
UPDATE_RUST_PATH = pathlib.Path('tools') / 'rust' / 'update_rust.py'
RUST_TOOLCHAIN_PATH = pathlib.Path('third_party') / 'rust-toolchain'
def run_git(src_root, args):
"""Runs a git command and returns the CompletedProcess object.
Args:
src_root: Path to the git repository root.
args: List of command line arguments for git.
Returns:
A subprocess.CompletedProcess object.
"""
cmd = ['git'] + args
return subprocess.run(
cmd,
capture_output=True,
text=True,
cwd=src_root,
check=False,
encoding='utf-8',
)
def parse_isolate_map(content_str, source_name):
"""Parses isolate map content string.
Args:
content_str: The string content of the isolate map file (AST literal).
source_name: Name or path of the source file (used for error logging).
Returns:
A dictionary mapping GN labels (strings) to test suite names (strings),
or None if parsing fails.
"""
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.
Args:
src_root: Path to the source root.
Returns:
A dictionary mapping GN labels to test suite names, or None if it fails.
"""
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.
Args:
src_root: Path to the source root.
build_dir: Path to the build directory (e.g., out/Default).
target_file: pathlib.Path to the target file to scan.
Returns:
A tuple (refs, error_message):
refs: A list of strings (GN targets) if successful, or None.
error_message: A string containing error output if failed, or None.
"""
# 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.
Args:
path1: First path string.
path2: Second path string.
Returns:
Integer representing the number of matching path components from the
root.
"""
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.
Args:
gn_refs_output: List of strings representing GN targets.
file_path: pathlib.Path of the target file.
isolate_map: Dictionary mapping GN labels to test suite names.
Returns:
A list of strings representing the names of the test suites that cover
the file, sorted by proximity.
"""
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 get_build_gn_paths(gn_refs_output):
"""Extracts unique BUILD.gn file paths from GN targets.
Args:
gn_refs_output: List of strings representing GN targets.
Returns:
A sorted list of strings representing relative paths to BUILD.gn files.
"""
build_gns = set()
for target in gn_refs_output:
target = target.strip()
if not target.startswith('//'):
continue
path_part = target[2:]
parts = path_part.split(':')
dir_path = parts[0]
if dir_path:
build_gns.add(dir_path + '/BUILD.gn')
else:
build_gns.add('BUILD.gn')
return sorted(list(build_gns))
def check_graph_stability(src_root, revision, target_file, gn_refs_output):
"""Checks if BUILD.gn files or the target file have changed since revision.
Args:
src_root: Path to the source root.
revision: The git revision to compare against.
target_file: pathlib.Path to the target file.
gn_refs_output: List of GN targets that depend on the file.
Returns:
A tuple (stable, changed_files):
stable: Boolean indicating if the graph is stable.
changed_files: List of strings (paths to changed files) if unstable.
"""
build_gns = get_build_gn_paths(gn_refs_output)
rel_target_file = target_file.lstrip('/')
paths_to_check = [rel_target_file] + build_gns
cmd = ['diff', '--name-only', f'{revision}..HEAD', '--'] + paths_to_check
res = run_git(src_root, cmd)
if res.returncode != 0:
print(
f'Warning: Stability check failed to run: {res.stderr.strip()}',
file=sys.stderr,
)
return False, []
changed_files = res.stdout.splitlines()
return len(changed_files) == 0, changed_files
def get_ignored_paths(src_root):
"""Finds ignored file and directory paths in the main checkout.
Args:
src_root: Path to the source root.
Returns:
A list of strings representing relative paths to ignored directories/files.
"""
res = run_git(src_root, ['status', '--ignored', '--porcelain'])
ignored_paths = []
if res.returncode == 0:
for line in res.stdout.splitlines():
if line.startswith('!! '):
path = line[3:]
if path.endswith('/'):
path = path[:-1]
ignored_paths.append(path)
return ignored_paths
def find_nested_repos_and_symlinks(src_root):
"""Finds nested git repos and symlinks in the checkout up to depth 5.
Args:
src_root: Path to the source root.
Returns:
A list of strings representing relative paths to nested repos/symlinks.
"""
src_root = pathlib.Path(src_root)
targets = []
max_depth = 5
for root, dirs, files in os.walk(src_root):
root_path = pathlib.Path(root)
rel_path = root_path.relative_to(src_root)
parts = rel_path.parts
# Skip ignored directories and out/
if parts:
if parts[0] in ('out', '.git'):
dirs.clear()
continue
if len(parts) >= max_depth:
dirs.clear()
continue
# Check for git repo
if '.git' in files or '.git' in dirs:
if parts:
targets.append(rel_path.as_posix())
dirs.clear()
continue
else:
if '.git' in dirs:
dirs.remove('.git')
# Check for symlinks
for d in list(dirs):
p = root_path / d
if p.is_symlink():
targets.append(p.relative_to(src_root).as_posix())
dirs.remove(d)
for f in files:
p = root_path / f
if p.is_symlink():
targets.append(p.relative_to(src_root).as_posix())
return targets
def _stub_missing_target(wt_path, target_ref):
"""Stubs a missing GN target in the worktree.
Args:
wt_path: pathlib.Path to the git worktree root.
target_ref: GN target reference string (e.g., //foo/bar:baz).
Returns:
Boolean indicating if stubbing was successful.
"""
if not target_ref.startswith('//'):
return False
parts = target_ref[2:].split(':')
if len(parts) != 2:
return False
dir_path, target_name = parts
dest_dir = wt_path / dir_path
build_gn_path = dest_dir / 'BUILD.gn'
stub_content = (f'\n# Stub added by test_suite_mapper.py\n'
f'group("{target_name}") {{}}\n')
try:
dest_dir.mkdir(parents=True, exist_ok=True)
with open(build_gn_path, 'a', encoding='utf-8', newline='') as f:
f.write(stub_content)
print(f'Stubbed {target_ref} in {build_gn_path}')
return True
except Exception as e:
print(f'Failed to stub {target_ref}: {e}', file=sys.stderr)
return False
def _spoof_rust_version(wt_path, src_rust_toolchain_dir,
dest_rust_toolchain_dir):
"""Spoofs the Rust VERSION file in the worktree to match update_rust.py.
Args:
wt_path: pathlib.Path to the worktree root.
src_rust_toolchain_dir: pathlib.Path to the source rust toolchain dir.
dest_rust_toolchain_dir: pathlib.Path to the destination rust toolchain
dir in worktree.
Returns:
Boolean indicating if spoofing was successful.
"""
update_rust_path = wt_path / UPDATE_RUST_PATH
if not update_rust_path.exists():
return False
# Parse expected version from update_rust.py
content = update_rust_path.read_text(encoding='utf-8')
rev_match = re.search(r"RUST_REVISION\s*=\s*'([0-9a-f]+)'", content)
sub_match = re.search(r'RUST_SUB_REVISION\s*=\s*([0-9]+)', content)
if not (rev_match and sub_match):
return False
expected_rev = rev_match.group(1)
expected_sub = sub_match.group(1)
# Read live VERSION
src_version_path = src_rust_toolchain_dir / 'VERSION'
if not src_version_path.exists():
return False
version_content = src_version_path.read_text(encoding='utf-8').strip()
# Example VERSION format:
# rustc 1.79.0-dev 49b139rt3 (12487ec81-1-1256 clbr987s chromium)
pattern = r'rustc (\S+) (\S+) \(([0-9a-f]+)-([0-9]+)-(\S+) chromium\)'
match = re.match(pattern, version_content)
if not match:
return False
rust_version = match.group(1)
clang_part = match.group(5)
new_version_content = (
f'rustc {rust_version} {expected_rev} '
f'({expected_rev}-{expected_sub}-{clang_part} chromium)\n')
dest_version_path = dest_rust_toolchain_dir / 'VERSION'
try:
dest_rust_toolchain_dir.mkdir(parents=True, exist_ok=True)
dest_version_path.write_text(new_version_content, encoding='utf-8')
return True
except Exception as e:
print(f'Failed to write spoofed Rust VERSION: {e}', file=sys.stderr)
return False
def _setup_worktree_symlinks(src_root, wt_path, build_dir):
"""Creates symlinks in the worktree for ignored paths and nested repos.
Args:
src_root: Path to the source root.
wt_path: pathlib.Path to the worktree root.
build_dir: Path to the build directory.
"""
ignored_paths = get_ignored_paths(src_root)
extra_targets = find_nested_repos_and_symlinks(src_root)
all_paths = set(ignored_paths + extra_targets)
exclude_prefixes = ['out']
build_dir_parts = pathlib.Path(build_dir).parts
if build_dir_parts:
exclude_prefixes.append(build_dir_parts[0])
sorted_paths = sorted(all_paths, key=lambda p: len(pathlib.Path(p).parts))
for rel_path in sorted_paths:
if rel_path in ('', '.'):
continue
parts = pathlib.Path(rel_path).parts
if parts and parts[0] in exclude_prefixes:
continue
target = src_root / rel_path
link = wt_path / rel_path
if rel_path == RUST_TOOLCHAIN_PATH.as_posix():
if (wt_path / UPDATE_RUST_PATH).exists():
link.mkdir(parents=True, exist_ok=True)
for child in target.iterdir():
if child.name == 'VERSION':
continue
child_link = link / child.name
if child_link.exists() or child_link.is_symlink():
if child_link.is_dir() and not child_link.is_symlink():
shutil.rmtree(child_link)
else:
child_link.unlink()
child_link.symlink_to(child, target_is_directory=child.is_dir())
if _spoof_rust_version(wt_path, target, link):
continue
else:
shutil.rmtree(link)
# Fallthrough to standard symlink creation
# Skip if any parent directory is already a symlink
parent_is_symlink = False
for parent in link.parents:
if parent == wt_path:
break
if parent.is_symlink():
parent_is_symlink = True
break
if parent_is_symlink:
continue
link.parent.mkdir(parents=True, exist_ok=True)
if link.exists() or link.is_symlink():
if link.is_dir() and not link.is_symlink():
shutil.rmtree(link)
else:
link.unlink()
if target.exists():
link.symlink_to(target, target_is_directory=target.is_dir())
def _run_gn_gen_with_stubbing(wt_path, build_dir):
"""Runs gn gen in the worktree, attempting to stub missing targets.
Args:
wt_path: pathlib.Path to the worktree root.
build_dir: Path to the build directory.
Returns:
Boolean indicating if gn gen succeeded (possibly after stubbing).
"""
stubbed_targets = set()
max_retries = 10
for attempt in range(max_retries):
res = subprocess.run(
['gn', 'gen', build_dir],
capture_output=True,
text=True,
cwd=wt_path,
check=False,
encoding='utf-8',
)
if res.returncode == 0:
return True
# Check for unresolved dependencies
if 'ERROR Unresolved dependencies.' in res.stdout:
missing_targets = []
# Regex to find missing targets, e.g., "//foo:bar" or
# "//foo:bar(//toolchain)"
pattern = r'needs\s+([\w./:-]+)(?:\([\w./:-]+\))?'
for line in res.stdout.splitlines():
match = re.search(pattern, line)
if match:
missing_targets.append(match.group(1))
if missing_targets:
stubbed_any = False
for t in set(missing_targets):
if t in stubbed_targets:
print(
f'Error: Target {t} was already stubbed but '
f'still unresolved. Cannot recover.',
file=sys.stderr,
)
return False
if _stub_missing_target(wt_path, t):
stubbed_targets.add(t)
stubbed_any = True
if stubbed_any:
print(f'Retrying gn gen after stubbing... (Attempt {attempt + 1})')
continue
print(
f'Error running gn gen in worktree (Attempt {attempt + 1}): '
f'{res.stdout.strip()}',
file=sys.stderr,
)
if res.stderr:
print(res.stderr.strip(), file=sys.stderr)
break
else:
print(
'Error: Failed to resolve GN graph after maximum retries.',
file=sys.stderr,
)
return False
@contextmanager
def git_worktree(src_root, revision, prefix='wt_cov_map_', parent_dir=None):
"""Context manager for creating and cleaning up a git worktree.
Args:
src_root: Path to the source root.
revision: Git revision to checkout in the worktree.
prefix: Prefix for the temporary directory name.
parent_dir: Parent directory for the temporary directory (defaults to
system temp).
Yields:
pathlib.Path to the created worktree.
"""
with tempfile.TemporaryDirectory(prefix=prefix, dir=parent_dir) as wt_dir:
wt_path = pathlib.Path(wt_dir)
res = run_git(
src_root,
['worktree', 'add', '--detach', '--force',
str(wt_path), revision],
)
if res.returncode != 0:
raise RuntimeError(f'Error creating git worktree: {res.stderr.strip()}')
try:
yield wt_path
finally:
run_git(src_root, ['worktree', 'remove', '--force', str(wt_path)])
def run_heavyweight_resolution(src_root, revision, build_dir, target_file):
"""Runs GN graph resolution at a revision by checking out a worktree.
Args:
src_root: Path to the source root.
revision: Git revision to perform the mapping at.
build_dir: Path to the build directory.
target_file: pathlib.Path to the target file.
Returns:
A list of strings (test suites) if successful, or None.
"""
try:
with git_worktree(src_root, revision,
parent_dir=src_root.parent) as wt_path:
# 2. Create symlinks for non-tracked/nested repo paths
_setup_worktree_symlinks(src_root, wt_path, build_dir)
# 3. Copy args.gn
wt_build_dir = wt_path / build_dir
wt_build_dir.mkdir(parents=True, exist_ok=True)
shutil.copy2(src_root / build_dir / 'args.gn', wt_build_dir / 'args.gn')
# 4. Run gn gen (with auto-recovery for missing targets)
if not _run_gn_gen_with_stubbing(wt_path, build_dir):
return None
# 5. Run gn refs
refs, err = run_gn_refs(wt_path, build_dir, target_file)
if err:
print(f'Error running gn refs in worktree: {err}', file=sys.stderr)
return None
if refs is None:
return None
# 6. Load isolate map at revision (from worktree)
pyl_path = wt_path / ISOLATE_MAP_PATH
if not pyl_path.exists():
print(f'Error: Isolate map not found in worktree.', file=sys.stderr)
return None
try:
with open(pyl_path, 'r', encoding='utf-8') as f:
isolate_map = parse_isolate_map(f.read(), str(pyl_path))
except Exception as e:
print(f'Error reading isolate map from worktree: {e}', file=sys.stderr)
return None
if isolate_map is None:
return None
# 7. Extract test suites
return extract_test_suites(refs, target_file, isolate_map)
except Exception as e:
print(e, file=sys.stderr)
return None
def verify_build_dir(src_root, build_dir):
"""Verifies that build_dir is configured to compile for Linux.
Args:
src_root: Path to the source root.
build_dir: Path to the build directory.
Returns:
A tuple (success, error_message):
success: Boolean indicating if configuration is valid.
error_message: String containing error detail if invalid, or None.
"""
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 _load_isolate_map_for_args(src_root, isolate_map_file, revision,
force_heavyweight):
"""Loads the isolate map based on arguments.
Args:
src_root: Path to the source root.
isolate_map_file: Path to a local isolate map file, or None.
revision: Git revision, or None.
force_heavyweight: Boolean indicating if heavyweight resolution is
forced.
Returns:
A dictionary mapping GN labels to test suite names, or None if it should
be skipped (for heavyweight) or failed.
"""
if isolate_map_file:
if not os.path.exists(isolate_map_file):
print(
f'Error: Isolate map file not found: {isolate_map_file}',
file=sys.stderr,
)
sys.exit(1)
try:
with open(isolate_map_file, 'r', encoding='utf-8') as f:
return parse_isolate_map(f.read(), os.fspath(isolate_map_file))
except Exception as e:
print(f'Error reading isolate map file: {e}', file=sys.stderr)
sys.exit(1)
elif revision:
if not force_heavyweight:
print(
'Error: For lightweight mapping at a revision, you must provide '
'the historical isolate map using --isolate-map-file.',
file=sys.stderr,
)
sys.exit(1)
return None
else:
return load_isolate_map(src_root)
def _check_and_warn_stability(src_root, revision, file_path, refs):
"""Checks graph stability and prints warnings if unstable.
Args:
src_root: Path to the source root.
revision: Git revision.
file_path: pathlib.Path of the target file.
refs: List of GN targets that depend on the file.
Returns:
Boolean indicating if the graph is stable.
"""
stable, changed = check_graph_stability(src_root, revision, file_path, refs)
if not stable:
print(
f'Warning: The GN graph or the file may have changed '
f'since revision {revision}.',
file=sys.stderr,
)
changed_files_display = ', '.join(changed[:5])
if len(changed) > 5:
changed_files_display += f" and {len(changed) - 5} more"
print(
f'The following relevant files were modified: '
f'{changed_files_display}',
file=sys.stderr,
)
print(
'The mapping results might be inaccurate.',
file=sys.stderr,
)
return stable
def _should_run_heavyweight(src_root, revision, file_path, refs,
force_heavyweight, stable, lightweight_suites):
"""Determines if heavyweight resolution should be run.
Args:
src_root: Path to the source root.
revision: Git revision, or None.
file_path: pathlib.Path of the target file.
refs: List of GN targets that depend on the file.
force_heavyweight: Boolean indicating if heavyweight resolution is
forced.
stable: Boolean indicating if the graph is stable.
lightweight_suites: List of test suites from lightweight mapping.
Returns:
Boolean indicating if heavyweight resolution should be run.
"""
if force_heavyweight:
return True
if revision and not stable:
head_isolate_map = load_isolate_map(src_root)
if head_isolate_map and lightweight_suites is not None:
current_suites = extract_test_suites(refs, file_path, head_isolate_map)
if lightweight_suites != current_suites:
print(
'Difference detected between lightweight revision mapping and '
'HEAD mapping, and the build graph is unstable. Triggering '
'heavyweight resolution.',
file=sys.stderr,
)
return True
return False
def _execute_heavyweight_resolution(src_root, revision, build_dir, file_path,
lightweight_suites):
"""Executes heavyweight resolution and handles fallback/logging.
Args:
src_root: Path to the source root.
revision: Git revision.
build_dir: Path to the build directory.
file_path: pathlib.Path of the target file.
lightweight_suites: List of test suites from lightweight mapping.
Returns:
A list of strings (test suites).
"""
if not revision:
print('Error: Heavyweight resolution requires a --revision.',
file=sys.stderr)
sys.exit(1)
print(
f'Running heavyweight GN graph resolution at revision {revision}...',
file=sys.stderr,
)
print(
'This requires checking out a worktree and running gn gen '
'(takes ~1 minute).',
file=sys.stderr,
)
heavyweight_suites = run_heavyweight_resolution(src_root, revision, build_dir,
file_path)
if heavyweight_suites is not None:
if (lightweight_suites is not None
and heavyweight_suites != lightweight_suites):
print(
f'Heavyweight resolution completed. Results corrected: '
f'{lightweight_suites} -> {heavyweight_suites}',
file=sys.stderr,
)
else:
print(
f'Heavyweight resolution completed. '
f'Confirmed results: {heavyweight_suites}',
file=sys.stderr,
)
return heavyweight_suites
else:
print(
'Error: Heavyweight resolution failed. '
'Falling back to lightweight results.',
file=sys.stderr,
)
return lightweight_suites if lightweight_suites is not None else []
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".'),
)
parser.add_argument(
'--revision',
'-r',
help='Git revision (hash, tag, branch) to perform the mapping at.',
)
parser.add_argument(
'--force-heavyweight',
action='store_true',
help=('Force rebuilding the GN build graph at the revision '
'(slower but 100% accurate).'),
)
parser.add_argument(
'--isolate-map-file',
type=pathlib.Path,
help='Path to a local gn_isolate_map.pyl file to use for mapping.',
)
args = parser.parse_args()
src_root = SRC_ROOT
build_dir = args.build_dir
revision = args.revision
if revision:
res = run_git(src_root, ['rev-parse', '--verify', revision])
if res.returncode != 0:
print(f'Error: Invalid revision: {revision}', file=sys.stderr)
sys.exit(1)
revision = res.stdout.strip()
# 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_for_args(src_root, args.isolate_map_file,
revision, args.force_heavyweight)
if isolate_map is None and not args.force_heavyweight:
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)
stable = True
if revision:
stable = _check_and_warn_stability(src_root, revision, args.file_path, refs)
lightweight_suites = None
if isolate_map:
lightweight_suites = extract_test_suites(refs, args.file_path, isolate_map)
run_heavyweight = _should_run_heavyweight(
src_root,
revision,
args.file_path,
refs,
args.force_heavyweight,
stable,
lightweight_suites,
)
if run_heavyweight:
test_suites = _execute_heavyweight_resolution(src_root, revision, build_dir,
args.file_path,
lightweight_suites)
else:
test_suites = lightweight_suites if lightweight_suites is not None else []
print(json.dumps(test_suites, indent=2))
if __name__ == '__main__':
main()