| #!/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 subprocess | |
| import sys | |
| import tempfile | |
| import unittest | |
| def analyze(gn_path, tests_json, build_dir, changed_files): | |
| """ | |
| gn_path: Local path to gn binary (e.g. /work/fuchsia/prebuilt/linux64/gn) | |
| tests_json: Local path to build's tests.json (e.g. out/default/tests.json) | |
| build_dir: Fuchsia build dir (e.g. out/default) | |
| changed_files: List of files to check against, relative to source root. | |
| Returns: List of test URLs and host test paths that should be run. | |
| """ | |
| with open(tests_json) as f: | |
| tests_obj = json.load(f) | |
| label_to_test_packages = collections.defaultdict(list) | |
| label_to_host_tests = collections.defaultdict(list) | |
| test_labels = [] | |
| for t in tests_obj: | |
| full_label = t['test']['label'] | |
| label_no_toolchain, _, toolchain = full_label.partition('(') | |
| toolchain = toolchain[:-1] | |
| # TODO: ARM, ASAN, etc. | |
| if toolchain == '//build/toolchain/fuchsia:x64': | |
| test_labels.append(full_label) | |
| # This is the default toolchain, so when emitting these, GN drops | |
| # the (//toolchain), so we need store these by toolchain-less-name, | |
| # even though the tests.json name includes fully-qualified names. | |
| label_to_test_packages[label_no_toolchain].append( | |
| t['test']['package_url']) | |
| elif toolchain == '//build/toolchain:host_x64': | |
| test_labels.append(full_label) | |
| label_to_host_tests[full_label].append(t['test']['path']) | |
| # TODO: Handle other toolchain's tests, or otherwise signal failure | |
| # cleanly and indicate that "everything" should be tested. | |
| with tempfile.NamedTemporaryFile(suffix='.json', mode='wt') as tmp: | |
| analyze_input = { | |
| 'files': ['//' + x for x in changed_files], | |
| 'test_targets': sorted(set(test_labels)), | |
| 'additional_compile_targets': ["all"], | |
| } | |
| tmp.write(json.dumps(analyze_input)) | |
| result = json.loads( | |
| subprocess.check_output( | |
| [gn_path, 'analyze', build_dir, tmp.name, '-'])) | |
| if 'error' in result: | |
| print(result['error'], file=sys.stderr) | |
| print(result, file=sys.stderr) | |
| return None | |
| if result['status'] == 'No dependency': | |
| return [] | |
| else: | |
| ret = [] | |
| for tl in result['test_targets']: | |
| ret.extend(label_to_test_packages[tl]) | |
| ret.extend(label_to_host_tests[tl]) | |
| return ret | |
| def main(args): | |
| tests = analyze( | |
| args.gn_path, args.tests_json, args.build_dir, | |
| args.changed_files.splitlines()) | |
| if tests is None: | |
| return 1 | |
| for t in tests: | |
| print(t) | |
| return 0 | |
| def tests_for(sources): | |
| global BUILD_DIR_FOR_TESTS | |
| host_platform = ( | |
| 'linux-x64' if sys.platform.startswith('linux') else 'mac-x64') | |
| gn_path = os.path.join( | |
| BUILD_DIR_FOR_TESTS, os.pardir, os.pardir, 'prebuilt', 'third_party', | |
| 'gn', host_platform, 'gn') | |
| tests_json = os.path.join(BUILD_DIR_FOR_TESTS, 'tests.json') | |
| return analyze(gn_path, tests_json, BUILD_DIR_FOR_TESTS, sources) | |
| class Test(unittest.TestCase): | |
| def test_plain_cpp(self): | |
| self.assertIn( | |
| 'fuchsia-pkg://fuchsia.com/fbl#meta/fbl-test.cmx', | |
| tests_for(['zircon/system/ulib/fbl/test/slab_allocator_tests.cc'])) | |
| def test_cpp_header_change(self): | |
| self.assertIn( | |
| 'fuchsia-pkg://fuchsia.com/fostr_unittests#meta/fostr_unittests.cmx', | |
| tests_for(['garnet/public/lib/fostr/hex_dump.h'])) | |
| def test_multiple_packages_single_label(self): | |
| tests = tests_for(['sdk/lib/fidl/cpp/array_unittest.cc']) | |
| self.assertIn( | |
| 'fuchsia-pkg://fuchsia.com/fidl_tests#meta/conformance_test.cmx', | |
| tests) | |
| self.assertIn( | |
| 'fuchsia-pkg://fuchsia.com/fidl_tests#meta/fidl_cpp_unittests.cmx', | |
| tests) | |
| def test_host_tests(self): | |
| tests = tests_for(['sdk/lib/fidl/cpp/array_unittest.cc']) | |
| self.assertIn( | |
| 'fuchsia-pkg://fuchsia.com/fidl_tests#meta/conformance_test.cmx', | |
| tests) | |
| self.assertIn( | |
| 'fuchsia-pkg://fuchsia.com/fidl_tests#meta/fidl_cpp_unittests.cmx', | |
| tests) | |
| self.assertIn('host_x64/fidl_cpp_host_unittests', tests) | |
| @unittest.skip('.rs files are not listed in sources yet.') | |
| def test_plain_rust(self): | |
| self.assertEqual( | |
| [ | |
| 'fuchsia-pkg://fuchsia.com/log_listener_tests#meta/log_listener_bin_test.cmx', | |
| 'fuchsia-pkg://fuchsia.com/log_listener_tests#meta/log_listener_return_code_test.cmx', | |
| ], tests_for(['garnet/bin/log_listener/src/main.rs'])) | |
| def test(fake_argv, build_dir): | |
| global BUILD_DIR_FOR_TESTS | |
| BUILD_DIR_FOR_TESTS = build_dir | |
| unittest.main(argv=fake_argv, verbosity=2) | |
| def parse_args_and_dispatch(): | |
| parser = argparse.ArgumentParser( | |
| description='Determine affected tests, given a list of changed sources.' | |
| ) | |
| parser.add_argument('--gn-path', help='path to gn binary') | |
| parser.add_argument( | |
| '--tests-json', help='path to tests.json (e.g. out/default/tests.json)') | |
| parser.add_argument( | |
| '--build-dir', | |
| help='path to build dir (e.g. out/default)', | |
| required=True) | |
| parser.add_argument( | |
| '--changed-files', | |
| help='sources files to be checked, separated by newline') | |
| parser.add_argument('action', help='either selftest or analyze') | |
| args = parser.parse_args() | |
| if args.action == 'selftest': | |
| sys.exit(test([sys.argv[0]], args.build_dir)) | |
| elif args.action == 'analyze': | |
| sys.exit(main(args)) | |
| else: | |
| sys.exit(1) | |
| if __name__ == '__main__': | |
| sys.exit(parse_args_and_dispatch()) |