blob: 866ee9441168b398c1db25532dd4dfb37ab9c5a9 [file]
# 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.
import dataclasses
import os
import pathlib
import re
import tempfile
from typing import Dict, Iterable, List, Set
import setup_modules # pylint: disable=unused-import
import chromium_src.tools.metrics.common.path_util as path_util
import chromium_src.tools.metrics.python_support.dependency_solver as dependency_solver
_THIS_DIR = path_util.METRICS_TOOLS_PATH / 'python_support'
# Paths relative to src/ that we want to run the test from
_TEST_DIRECTORIES_TO_SCAN = [
'tools/metrics',
'tools/variations',
'tools/json_comment_eater',
'tools/json_to_struct',
]
# Lists a directories that contain test files relevant for Chrome Metrics
# tooling as path relative to src/.
TEST_DIRECTORIES_RELATIVE_TO_SRC = [
path_util.CHROMIUM_SRC_PATH / d for d in _TEST_DIRECTORIES_TO_SCAN
]
_EXPECTED_TEST_FILES_SUFFIXES = ['test.py', 'tests.py']
def _get_temp_path(prefix=''):
fd, path = tempfile.mkstemp(prefix=prefix)
os.close(fd)
return path
@dataclasses.dataclass
class TestableScript:
"""Describe a script that can be safely run for testing"""
# The name to identify the script by
identifiable_name: str
# pathlib.Path relative to src/
file_path: pathlib.Path
# Full cmd to run with args included
cmd: List[str]
@classmethod
def CreatePythonScript(
cls, file_path: pathlib.Path, flags: List[str] = []
) -> 'TestableScript':
"""Creates TestableScript object for python script
Args:
file_path - a path to python script relative to src/
flags - a list of flags to pass to the script
Returns:
TestableScript object set up to run using vpython3
"""
return TestableScript(
identifiable_name=str(file_path),
file_path=file_path,
cmd=['vpython3', str(file_path), *flags],
)
# As one of the check we just run some of our existing scripts that don't have
# side effects and check if they finish successfully this is done as a last
# line of defense against changing in dependencies causing failures of scripts.
_TESTABLE_SCRIPTS: List[TestableScript] = [
TestableScript.CreatePythonScript(
file_path=pathlib.Path('tools/metrics/actions/extract_actions.py'),
flags=['--presubmit'],
),
TestableScript.CreatePythonScript(
file_path=pathlib.Path('tools/metrics/actions/pretty_print.py'),
flags=['--presubmit'],
),
TestableScript.CreatePythonScript(
file_path=pathlib.Path('tools/metrics/actions/print_action_names.py')
),
TestableScript.CreatePythonScript(
file_path=pathlib.Path('tools/metrics/histograms/merge_xml.py'),
flags=['--output', _get_temp_path('merge_xml_test')],
),
TestableScript.CreatePythonScript(
file_path=pathlib.Path('tools/metrics/histograms/pretty_print.py'),
flags=[
'--presubmit',
'tools/metrics/histograms/metadata/uma/histograms.xml',
],
),
TestableScript.CreatePythonScript(
file_path=pathlib.Path(
'tools/metrics/histograms/print_expanded_histograms.py'
)
),
TestableScript.CreatePythonScript(
file_path=pathlib.Path('tools/metrics/histograms/print_histogram_names.py')
),
TestableScript.CreatePythonScript(
file_path=pathlib.Path('tools/metrics/histograms/validate_format.py')
),
TestableScript.CreatePythonScript(
file_path=pathlib.Path(
'tools/metrics/histograms/validate_histograms_index.py'
)
),
TestableScript.CreatePythonScript(
file_path=pathlib.Path('tools/metrics/histograms/validate_token.py'),
flags=['tools/metrics/histograms/metadata/uma/histograms.xml'],
),
TestableScript.CreatePythonScript(
file_path=pathlib.Path('tools/metrics/private_metrics/pretty_print.py'),
flags=['--presubmit', 'tools/metrics/private_metrics/dwa.xml'],
),
TestableScript.CreatePythonScript(
file_path=pathlib.Path('tools/metrics/private_metrics/validate_format.py'),
flags=['tools/metrics/private_metrics/dwa.xml'],
),
TestableScript.CreatePythonScript(
file_path=pathlib.Path('tools/metrics/ukm/pretty_print.py'),
flags=['--presubmit'],
),
TestableScript.CreatePythonScript(
file_path=pathlib.Path('tools/metrics/ukm/validate_format.py')
),
# TODO(crbug.com/488367077): Fix this script.
# TestableScript.CreatePythonScript(
# file_path='tools/metrics/histograms/histogram_ownership.py'
# ),
# TODO(crbug.com/488362727): Fix the unmapped histograms.
# TestableScript.CreatePythonScript(file_path=pathlib.Path(
# 'tools/metrics/histograms/find_unmapped_histograms.py')),
]
def _find_all_tests_in(directory_path: pathlib.Path) -> Iterable[pathlib.Path]:
for suffix in _EXPECTED_TEST_FILES_SUFFIXES:
yield from directory_path.rglob(f'*{suffix}')
def find_all_tests() -> Iterable[pathlib.Path]:
"""Finds all python tests in all directories listed in DIRECTORIES_TO_SCAN"""
all_test_files: List[pathlib.Path] = []
for dir in TEST_DIRECTORIES_RELATIVE_TO_SRC:
all_test_files.extend(_find_all_tests_in(dir))
return all_test_files
def validate_gn_sources(target_group: str) -> set[str]:
"""Validates that all test files are listed in target_group of BUILD.gn
Returns: A set of paths missing in BUILD.gn, relative to src/.
"""
dir_path = _THIS_DIR.parent
gn_path = dir_path.joinpath('BUILD.gn')
if not dir_path.is_dir():
raise ValueError(f'Error: Directory {dir_path} not found.')
if not gn_path.is_file():
raise ValueError(f'Error: BUILD file {gn_path} not found.')
with open(gn_path, 'r') as f:
content = f.read()
group_pattern = r"group\(['\"](.+?)['\"]\) \{(.*?)\n\}"
matched_groups = re.findall(group_pattern, content, re.DOTALL)
if not matched_groups:
raise ValueError(
f"Error: Could not find group '{target_group}' in {gn_path}"
)
relevant_groups = [g for g in matched_groups if g[0] == target_group]
if len(relevant_groups) != 1:
raise ValueError(
f"Error: Could not find group '{target_group}' in {gn_path}"
)
group_content = relevant_groups[0][1]
data_pattern = r'data = \[(.*?)\]'
match_data = re.search(data_pattern, group_content, re.DOTALL)
if not match_data:
raise ValueError(
f"Error: Could not find data in group '{target_group}' in {gn_path}"
)
data_list = match_data.group(1)
listed_files = set(re.findall(r'["\'](.*?)["\']', data_list))
listed_files = {pathlib.Path(f.replace('//', '')) for f in listed_files}
actual_files = {
f.relative_to(path_util.CHROMIUM_SRC_PATH) for f in find_all_tests()
}
missing_in_gn = actual_files - listed_files
return {str(f) for f in missing_in_gn}
def _is_script_affected_by(
testable_script: TestableScript,
modified_files: Set[pathlib.Path],
deps_graph: Dict[pathlib.Path, List[pathlib.Path]],
) -> bool:
modified_files = set(
[
p.resolve().relative_to(path_util.CHROMIUM_SRC_PATH)
for p in modified_files
]
)
if testable_script.file_path in modified_files:
return True
all_dependencies = dependency_solver.get_all_dependencies(
deps_graph, testable_script.file_path
)
return any(file in all_dependencies for file in modified_files)
def get_affected_testable_scripts(
modified_files: Set[pathlib.Path],
deps_graph: Dict[pathlib.Path, List[pathlib.Path]],
) -> List[TestableScript]:
"""Returns testable scripts that are affected by changes in modified_files.
Args:
modified_files: list of modified files as absolute paths that can
affects _TESTABLE_SCRIPTS.
"""
return [
testable_script
for testable_script in _TESTABLE_SCRIPTS
if _is_script_affected_by(testable_script, set(modified_files), deps_graph)
]
def get_affected_tests(
modified_files: Set[pathlib.Path],
deps_graph: Dict[pathlib.Path, List[pathlib.Path]],
) -> Iterable[TestableScript]:
"""Finds all python tests that could be affected by the changes."""
all_tests = [
TestableScript.CreatePythonScript(
t.relative_to(path_util.CHROMIUM_SRC_PATH)
)
for t in find_all_tests()
# We cannot detect dependencies for tools outside of tools/metrics
# at the moment.
if t.resolve().is_relative_to(path_util.METRICS_TOOLS_PATH)
]
affected_tests = [
test_script
for test_script in all_tests
if _is_script_affected_by(test_script, set(modified_files), deps_graph)
]
return affected_tests