blob: 1ddacc2ad3fda9edd82453a17f107a77e4e636cb [file] [log] [blame]
# Copyright 2025 The Fuchsia Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
import json
import os
import sys
from pathlib import Path
sys.path.insert(0, os.path.dirname(__file__))
from ninja_artifacts import NinjaRunner
def create_test_artifacts_mapping(build_dir: Path) -> dict[str, set[str]]:
"""Generate a mapping from test labels to Ninja artifacts.
Args:
build_dir: Ninja build directory.
Returns:
A { target_label -> artifact_list } dictionary, mapping GN or Bazel
target labels to a set of Ninja artifact paths, relative to the Ninja
build directory, that each test target should produce or use at
runtime.
"""
result: dict[str, set[str]] = {}
tests_json_path = build_dir / "tests.json"
with tests_json_path.open("rt") as f:
tests_json = json.load(f)
for entry in tests_json:
test = entry["test"]
target_label = test["label"]
artifacts = set()
# test.path is used by host tests to point to the main test executable.
path: str | None = test.get("path")
if path:
assert isinstance(path, str)
artifacts.add(path)
# test.new_path is used by device tests to point to the host test runner executable.
# See https://fxbug.dev(458823250)
new_path: str | None = test.get("new_path")
if new_path:
assert isinstance(new_path, str)
artifacts.add(new_path)
# test.package_manifests is a list of path to package manifest files. Those are generated
# at build time, so cannot be read directly, but they should be part of the affected
# artifacts when the corresponding source file changes.
package_manifests: list[str] | None = test.get("package_manifests")
if package_manifests:
assert isinstance(package_manifests, list)
artifacts.update(package_manifests)
# test.package_manifest_deps path points to a JSON file that contains an array
# of string paths to other package manifests, corresponding to extra packages
# needed at runtime during testing. Since it is generated at regeneration time,
# before the build, it is safe to read here.
package_manifest_deps_path: str | None = test.get(
"package_manifest_deps"
)
if package_manifest_deps_path:
assert isinstance(package_manifest_deps_path, str)
with (build_dir / package_manifest_deps_path).open("rt") as f:
manifests = json.load(f)
assert isinstance(manifests, list)
artifacts.update(manifests)
artifacts.add(package_manifest_deps_path)
# The test.runtime_deps path points to a JSON file that contains an array of
# string paths to Ninja artifacts needed at runtime. Since it is generated
# at regeneration time, before the build, it is safe to read here.
runtime_deps_path: str | None = test.get("runtime_deps")
if runtime_deps_path:
with (build_dir / runtime_deps_path).open("rt") as f:
runtime_deps = json.load(f)
assert isinstance(runtime_deps, list)
artifacts.update(runtime_deps)
artifacts.add(runtime_deps_path)
# It is common to get duplicates because runtime_deps and package_manifest_deps
# are the result of GN metadata collection, which does simple concatenation of
# lists instead of unions of sets.
result[target_label] = artifacts
return result
def find_tests_affected_by_changed_files(
changed_files: list[str],
fuchsia_dir: Path,
ninja_runner: NinjaRunner,
) -> set[str]:
"""Return the set of test labels that are affected by a set of changed files.
Given a set of paths to changed files (for example after applying a
git commit just after the last build), determine which targets need to
be rebuilt (and for tests re-run), return the set of tests labels that
would need to be rebuilt and then re-run after the build.
Args:
changed_files: List of file path strings, relative to Fuchsia source directory,
of files that were changed since the last build.
fuchsia_dir: Path to Fuchsia source directory.
ninja_runner: A NinjaRunner instance.
Returns:
A set of test target labels.
"""
changed_sources: set[str] = set()
for file in changed_files:
if os.path.isabs(file):
changed_sources.add(os.path.relpath(file, fuchsia_dir))
else:
changed_sources.add(str(file))
build_dir = ninja_runner.build_dir
# Read the content of tests.json to determine which important artifacts
# each test requires at runtime.
test_artifacts = create_test_artifacts_mapping(build_dir)
# The list of source files as they must appear in the Ninja build plan.
# All source inputs appear with a prefix like ../../ that corresponds
# to the relative path from the build directory to the Fuchsia source one.
source_prefix = os.path.relpath(fuchsia_dir, build_dir) + "/"
ninja_sources = [
f"{source_prefix}{source_path}"
for source_path in sorted(changed_sources)
]
# Run the 'affected' tool which returns the list of Ninja artifacts affected
# by the changed sources. --ignore-errors is used because the changed file list
# might include things that are not build plan inputs and should be ignored.
# --depfile is used to ensure that implicit dependencies from the last build are
# followed properly. This is critical for Bazel defined targets that are built
# through bazel_action() GN target definitions.
#
# Note that for now, all Bazel targets, tests or not, must be wrapped through
# GN bazel_action() targets.
tool_output = ninja_runner.run_and_extract_output(
[
"-t",
"affected",
"--depfile",
"--ignore-errors",
]
+ ninja_sources
)
affected_artifacts = set(tool_output.splitlines())
return {
test_label
for test_label, artifacts in test_artifacts.items()
if bool(set(artifacts) & affected_artifacts)
}