blob: 7f8a474f1f29453ca94cd59341b757cc6c115b52 [file] [log] [blame]
#!/usr/bin/env python3
# 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 pathlib
import subprocess
import sys
def main():
parser = argparse.ArgumentParser(
description="Finds and runs tests affected by current change")
parser.add_argument(
"--verbose", action="store_true", help="Print verbose messages")
parser.add_argument(
"--dry-run", action="store_true", help="Don't run affected tests")
parser.add_argument(
"test_args", nargs=argparse.REMAINDER, help="Arguments for `fx test`")
parser.add_argument(
"--out-dir", required=True, help="Path to the Fuchsia build directory")
args = parser.parse_args()
# Find all modified files
upstream = (
subprocess.run(
[
"git", "rev-parse", "--abbrev-ref", "--symbolic-full-name",
"@{u}"
],
capture_output=True,
encoding="UTF-8",
).stdout.strip() or "origin/master")
local_commit = subprocess.check_output(
["git", "rev-list", "HEAD", upstream, "--"],
encoding="UTF-8").splitlines()[0]
diff_base = (
local_commit and subprocess.run(
["git", "rev-parse", local_commit + '^'],
capture_output=True,
encoding="UTF-8").stdout.strip() or "HEAD")
modified = subprocess.check_output(
["git", "diff", "--name-only", diff_base], encoding="UTF-8")
if not modified:
print("No modified files")
return 0
if args.verbose:
print("Modified files:")
print(modified)
# Rebuild
subprocess.check_call(["fx", "build", "updates"])
# Note: we know all test labels so we could `fx build` only tests.
# However there is no guarantee that GN labels in tests.json are valid.
# In fact some of them aren't. o_O
# Also, `fx build <target>` is broken when <target> begins with "zircon/"
# and is actually in the Fuchsia GN build, not in the legacy ZN build.
# Index tests.json
with open(os.path.join(args.out_dir, "tests.json")) as tests_json:
tests = json.load(tests_json)
phony_rule_to_test_name = collections.defaultdict(set)
for entry in tests:
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 rule path
# "my/gn:path" -> "my/gn/path"
# "foo/bar:bar" -> "foo/bar"
directory, _, name = no_toolchain.partition(":")
if not name:
# "foo/bar" -> "foo/bar"
rule = directory
elif directory.rpartition("/")[2] == name:
# "foo/bar:bar" -> "foo/bar"
rule = directory
else:
# "foo/bar:qux" -> "foo/bar/qux"
rule = directory + "/" + name
phony_rule_to_test_name[rule + " is dirty"].add(entry["test"]["name"])
# Touch all modified files so they're newer than any rules
git_base = subprocess.check_output(
["git", "rev-parse", "--show-toplevel"], encoding="UTF-8").strip()
for path in modified.splitlines():
p = pathlib.Path(git_base, path)
if p.exists() and not path.endswith("BUILD.gn"):
p.touch()
# Find all stale rules
ninja = []
for buildtype in ("", ".zircon"):
ninja += subprocess.check_output(
[
"fx",
"ninja",
"-C",
args.out_dir + buildtype,
"-d",
"explain",
"-n",
],
encoding="UTF-8",
stderr=subprocess.STDOUT,
).splitlines()
# Check stale rules against test names
affected_tests = set()
for line in ninja:
if not line.startswith("ninja explain: "):
continue
rule = line.partition("phony/")[2]
if not rule:
continue
affected_tests.update(phony_rule_to_test_name[rule])
if not affected_tests:
if args.verbose:
print("No affected tests")
return 0
if args.verbose:
print("Affected tests:")
print("\n".join(sorted(affected_tests)))
print()
# Run affected tests
if args.dry_run:
if args.verbose:
print("Not running tests (--dry-run)")
return 0
return subprocess.run(
["fx", "test"] + sorted(affected_tests) + args.test_args,
).returncode
if __name__ == "__main__":
try:
sys.exit(main())
except subprocess.CalledProcessError as e:
sys.exit(1)
except KeyboardInterrupt:
sys.exit(0)