<filename>tools/testing/affectedtests/testanalyze.py
#!/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())

<filename>tools/testing/affectedtests/testanalyze.py
#!/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())

#!/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. 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. # TODO: ARM, ASAN, etc. # 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. # TODO: Handle other toolchain's tests, or otherwise signal failure # cleanly and indicate that "everything" should be tested. #meta/fbl-test.cmx', #meta/fostr_unittests.cmx', #meta/conformance_test.cmx', #meta/fidl_cpp_unittests.cmx', #meta/conformance_test.cmx', #meta/fidl_cpp_unittests.cmx', #meta/log_listener_bin_test.cmx', #meta/log_listener_return_code_test.cmx',