blob: 7eda277976058aa1fa13581643aed183793c1f3d [file] [log] [blame]
# Copyright 2020 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 argparse
import collections
import json
import os
import subprocess
import sys
NO_WORK_TEMPLATE = "ninja: Entering directory `%s'\nninja: no work to do."
def main():
parser = argparse.ArgumentParser(
description="Finds and runs tests affected by current change"
)
parser.add_argument(
"--tests-json",
required=True,
type=argparse.FileType("rb"),
help="Path to tests.json",
)
parser.add_argument(
"--changed-srcs", required=True, nargs="+", help="Changed source paths"
)
parser.add_argument("--ninja", required=True, help="Path to the Ninja executable")
parser.add_argument(
"--fuchsia_targets", required=False, nargs="*", help="Ninja targets for Fuchsia"
)
parser.add_argument(
"--fuchsia-out-dir", required=True, help="Path to the Fuchsia out directory"
)
parser.add_argument(
"--ninja-out", required=False, help="Path to write Ninja output to"
)
parser.add_argument(
"--no-work-status",
required=False,
help=(
"Path to write a JSON boolean indicating whether we concluded there "
"was no work to do"
),
)
args = parser.parse_args()
# Index tests.json
tests = json.load(args.tests_json)
stamp_to_test_names = collections.defaultdict(set)
path_to_test_name = {}
for entry in tests:
# For host tests, we search for the executable path.
path = entry["test"].get("path")
if path:
path_to_test_name[path] = entry["test"]["name"]
continue
# For Fuchsia tests we derive the stamp path from the GN label.
label = entry["test"]["label"]
# Remove leading "//" and toolchain part
# "//my/gn:path(//build/toolchain:host_x64)" -> "my/gn:path"
no_toolchain = label[2:].partition("(")[0]
# Convert label to stamp path
# "my/gn:path" -> "obj/my/gn/path.stamp"
# "foo/bar:bar" -> "obj/foo/bar.stamp"
directory, _, name = no_toolchain.partition(":")
if not name:
# "foo/bar" -> "foo/bar"
stamp = directory
elif directory.rpartition("/")[2] == name:
# "foo/bar:bar" -> "foo/bar"
stamp = directory
else:
# "foo/bar:qux" -> "foo/bar/qux"
stamp = directory + "/" + name
stamp = os.path.join("obj", stamp + ".stamp")
stamp_to_test_names[stamp].add(entry["test"]["name"])
def touch_and_dry_run(srcs, ignore_extensions):
ignored = []
# Touch all changed files so they're newer than any stamps
for src in srcs:
if os.path.exists(src):
if os.path.splitext(src)[1] in ignore_extensions:
ignored.append(src)
else:
os.utime(src, None)
# Build Fuchsia
ninja_output = subprocess.check_output(
[args.ninja, "-C", args.fuchsia_out_dir, "-d", "explain", "-n", "-v"]
+ (args.fuchsia_targets or []),
stderr=subprocess.STDOUT,
).decode("UTF-8")
return ninja_output, ignored
# Our Ninja graph is set up in such a way that touching any GN files
# triggers an action to regenerate the entire graph. So if GN files
# were modified and we touched them then the following dry run results
# are not useful for determining affected tests.
ignore_extensions = (".gn", ".gni")
ninja_output, ignored = touch_and_dry_run(args.changed_srcs, ignore_extensions)
if args.ninja_out:
with open(args.ninja_out, "wt") as outfile:
outfile.write(ninja_output)
# Example line:
# [4/5] some_executable arg1 arg2
# becomes action:
# some_executable arg1 arg2
actions = [line.partition("] ")[2] for line in ninja_output.splitlines()]
# Check stale actions against tests.
affected_tests = set()
for action in actions:
# looking for actions like "touch baz/obj/foo/bar.stamp"
if action.startswith("touch ") and "obj/" in action:
test_names = stamp_to_test_names[action[action.index("obj/") :]]
affected_tests |= test_names
# Looking for any action that includes the test path.
# Different types of host tests have different actions, but they
# all mention the final executable path.
else:
action_parts = [part.strip('"') for part in action.split()]
for action_part in action_parts:
test_name = path_to_test_name.get(action_part)
if test_name:
affected_tests.add(test_name)
# The following tests should never be considered affected
never_affected = (
# These tests use a system image as data, so they appear affected by a
# broad range of changes, but they're almost never actually sensitive
# to said changes. fxbug.dev/67305 tracks generating this list automatically.
"overnet_serial_tests",
"recovery_simulator_boot_test",
"recovery_simulator_serial_test",
)
for name in never_affected:
affected_tests.discard(name)
for test in sorted(affected_tests):
print(test)
if args.no_work_status:
# For determination of "no work to do", we want to consider all files.
if ignored:
ninja_output, _ = touch_and_dry_run(ignored, ())
no_work = (NO_WORK_TEMPLATE % args.fuchsia_out_dir) in ninja_output
with open(args.no_work_status, "w") as f:
json.dump(no_work, f)
if __name__ == "__main__":
main()