Merge "Build CRT objects using just-built toolchain."
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 3aa6448..3d723a3 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -50,6 +50,18 @@
  * Updated Clang to r339409.
  * [Issue 780]: A complete NDK toolchain is now installed to `$NDK/toolchain`.
    See the announcements section for more information.
+ * ndk-build no longer removes artifacts from `NDK_LIBS_OUT` for ABIs not
+   present in `APP_ABI`. This enables workflows like the following:
+
+   ```bash
+   for abi in armeabi-v7a arm64-v8a x86 x86_64; do
+       ndk-build APP_ABI=$abi
+   done
+   ```
+
+   Prior to this change, the above workflow would remove the previously built
+   ABI's artifacts on each successive build, resulting in only x86_64 being
+   present at the end of the loop.
 
 Known Issues
 ------------
diff --git a/build/core/setup-app.mk b/build/core/setup-app.mk
index 9d2eebb..dbd25e1 100644
--- a/build/core/setup-app.mk
+++ b/build/core/setup-app.mk
@@ -86,17 +86,28 @@
     $(call __ndk_warning,Support for these ABIs will be removed in a future NDK release.)
 endif
 
-# Clear all installed binaries for this application
-# This ensures that if the build fails, you're not going to mistakenly
-# package an obsolete version of it. Or if you change the ABIs you're targetting,
-# you're not going to leave a stale shared library for the old one.
+# Clear all installed binaries for this application. This ensures that if the
+# build fails or if you remove a module, you're not going to mistakenly package
+# an obsolete version.
 #
+# Historically this would clear every ABI, meaning that the following workflow
+# would leave only x86_64 present in the lib dir on completion:
+#
+#     for abi in armeabi-v7a arm64-v8a x86 x86_64; do
+#         ndk-build APP_ABI=$abi
+#     done
+#
+# This is the workflow used by gradle. They currently override NDK_ALL_ABIS (an
+# internal variable) to workaround this behavior. Changing this behavior allows
+# them to remove their workaround and stop clobbering our implementation
+# details.
 ifeq ($(NDK_APP.$(_app).cleaned_binaries),)
     NDK_APP.$(_app).cleaned_binaries := true
-    clean-installed-binaries::
-	$(hide) $(call host-rm,$(NDK_ALL_ABIS:%=$(NDK_APP_LIBS_OUT)/%/*))
-	$(hide) $(call host-rm,$(NDK_ALL_ABIS:%=$(NDK_APP_LIBS_OUT)/%/gdbserver))
-	$(hide) $(call host-rm,$(NDK_ALL_ABIS:%=$(NDK_APP_LIBS_OUT)/%/gdb.setup))
+
+clean-installed-binaries::
+	$(hide) $(call host-rm,$(NDK_APP_ABI:%=$(NDK_APP_LIBS_OUT)/%/*))
+	$(hide) $(call host-rm,$(NDK_APP_ABI:%=$(NDK_APP_LIBS_OUT)/%/gdbserver))
+	$(hide) $(call host-rm,$(NDK_APP_ABI:%=$(NDK_APP_LIBS_OUT)/%/gdb.setup))
 endif
 
 # Renderscript
diff --git a/ndk/run_tests.py b/ndk/run_tests.py
index 588914b..ea49996 100755
--- a/ndk/run_tests.py
+++ b/ndk/run_tests.py
@@ -37,19 +37,19 @@
 import ndk.notify
 import ndk.paths
 import ndk.test.builder
+import ndk.test.config
 import ndk.test.devices
 import ndk.test.filters
 import ndk.test.printers
 import ndk.test.report
 import ndk.test.result
 import ndk.test.spec
+import ndk.test.types
 import ndk.test.ui
 import ndk.timer
 import ndk.ui
 import ndk.workqueue
 
-import tests.testlib as testlib
-
 
 DEVICE_TEST_BASE_DIR = '/data/local/tmp/tests'
 
@@ -67,7 +67,8 @@
         return 1, traceback.format_exc(ex), ''
 
 
-# TODO: Extract a common interface from this and testlib.Test for the printer.
+# TODO: Extract a common interface from this and ndk.test.types.Test for the
+# printer.
 class TestCase(object):
     """A test case found in the dist directory.
 
@@ -115,7 +116,7 @@
         # handled by a different LibcxxTest. We can safely assume that anything
         # here is in tests/device.
         test_dir = os.path.join(self.test_src_dir, 'device', self.suite)
-        return testlib.DeviceTestConfig.from_test_dir(test_dir)
+        return ndk.test.config.DeviceTestConfig.from_test_dir(test_dir)
 
     def check_unsupported(self, device):
         return self.get_test_config().run_unsupported(
@@ -165,7 +166,7 @@
     def get_test_config(self):
         _, _, test_subdir = self.suite.partition('/')
         test_dir = os.path.join(self.test_src_dir, 'libc++/test', test_subdir)
-        return testlib.LibcxxTestConfig.from_test_dir(test_dir)
+        return ndk.test.config.LibcxxTestConfig.from_test_dir(test_dir)
 
     def check_unsupported(self, device):
         # Executable is foo.pass.cpp.exe, we want foo.pass.
@@ -523,7 +524,7 @@
 
 
 def flake_filter(result):
-    if isinstance(result, testlib.UnexpectedSuccess):
+    if isinstance(result, ndk.test.result.UnexpectedSuccess):
         # There are no flaky successes.
         return False
 
diff --git a/ndk/test/builder.py b/ndk/test/builder.py
index 9d55edc..62228f5 100644
--- a/ndk/test/builder.py
+++ b/ndk/test/builder.py
@@ -21,15 +21,20 @@
 import multiprocessing
 import os
 import pickle
+import random
 import shutil
+import sys
+import traceback
 
 import ndk.abis
 import ndk.test.filters
+import ndk.test.report
+import ndk.test.scanner
 import ndk.test.spec
+import ndk.test.suites
+import ndk.test.ui
 import ndk.workqueue
 
-import tests.testlib as testlib
-
 
 def logger():
     """Returns the module logger."""
@@ -39,52 +44,117 @@
 def test_spec_from_config(test_config):
     """Returns a TestSpec based on the test config file."""
     abis = test_config.get('abis', ndk.abis.ALL_ABIS)
-    suites = test_config.get('suites', testlib.ALL_SUITES)
+    suites = test_config.get('suites', ndk.test.suites.ALL_SUITES)
 
     return ndk.test.spec.TestSpec(abis, suites)
 
 
-def build_test_runner(test_spec, test_options, printer):
-    runner = testlib.TestRunner(printer)
-
-    scanner = testlib.BuildTestScanner(test_options.ndk_path)
-    nodist_scanner = testlib.BuildTestScanner(
-        test_options.ndk_path, dist=False)
-    libcxx_scanner = testlib.LibcxxTestScanner(test_options.ndk_path)
-    for abi in test_spec.abis:
-        build_api_level = None  # Always use the default.
-
-        scanner.add_build_configuration(abi, build_api_level)
-        nodist_scanner.add_build_configuration(abi, build_api_level)
-        libcxx_scanner.add_build_configuration(abi, build_api_level)
-
-    if 'build' in test_spec.suites:
-        test_src = os.path.join(test_options.src_dir, 'build')
-        runner.add_suite('build', test_src, nodist_scanner)
-    if 'device' in test_spec.suites:
-        test_src = os.path.join(test_options.src_dir, 'device')
-        runner.add_suite('device', test_src, scanner)
-    if 'libc++' in test_spec.suites:
-        test_src = os.path.join(test_options.src_dir, 'libc++')
-        runner.add_suite('libc++', test_src, libcxx_scanner)
-
-    return runner
-
-
 def write_build_report(build_report, results):
     with open(build_report, 'w') as build_report_file:
         pickle.dump(results, build_report_file)
 
 
+def scan_test_suite(suite_dir, test_scanner):
+    tests = []
+    for dentry in os.listdir(suite_dir):
+        path = os.path.join(suite_dir, dentry)
+        if os.path.isdir(path):
+            test_name = os.path.basename(path)
+            tests.extend(test_scanner.find_tests(path, test_name))
+    return tests
+
+
+def _fixup_expected_failure(result, config, bug):
+    if isinstance(result, ndk.test.result.Failure):
+        return ndk.test.result.ExpectedFailure(result.test, config, bug)
+    elif isinstance(result, ndk.test.result.Success):
+        return ndk.test.result.UnexpectedSuccess(result.test, config, bug)
+    else:  # Skipped, UnexpectedSuccess, or ExpectedFailure.
+        return result
+
+
+def _fixup_negative_test(result):
+    if isinstance(result, ndk.test.result.Failure):
+        return ndk.test.result.Success(result.test)
+    elif isinstance(result, ndk.test.result.Success):
+        return ndk.test.result.Failure(
+            result.test, 'negative test case succeeded')
+    else:  # Skipped, UnexpectedSuccess, or ExpectedFailure.
+        return result
+
+
+def _run_test(worker, suite, test, obj_dir, dist_dir, test_filters):
+    """Runs a given test according to the given filters.
+
+    Args:
+        worker: The worker that invoked this task.
+        suite: Name of the test suite the test belongs to.
+        test: The test to be run.
+        obj_dir: Out directory for intermediate build artifacts.
+        dist_dir: Out directory for build artifacts needed for running.
+        test_filters: Filters to apply when running tests.
+
+    Returns: Tuple of (suite, TestResult, [Test]). The [Test] element is a list
+             of additional tests to be run.
+    """
+    worker.status = 'Building {}'.format(test)
+
+    config = test.check_unsupported()
+    if config is not None:
+        message = 'test unsupported for {}'.format(config)
+        return suite, ndk.test.result.Skipped(test, message), []
+
+    try:
+        result, additional_tests = test.run(obj_dir, dist_dir, test_filters)
+        if test.is_negative_test():
+            result = _fixup_negative_test(result)
+        config, bug = test.check_broken()
+        if config is not None:
+            # We need to check change each pass/fail to either an
+            # ExpectedFailure or an UnexpectedSuccess as necessary.
+            result = _fixup_expected_failure(result, config, bug)
+    except Exception:  # pylint: disable=broad-except
+        result = ndk.test.result.Failure(test, traceback.format_exc())
+        additional_tests = []
+    return suite, result, additional_tests
+
+
 class TestBuilder(object):
     def __init__(self, test_spec, test_options, printer):
-        self.runner = build_test_runner(test_spec, test_options, printer)
+        self.printer = printer
+        self.tests = {}
+        self.build_dirs = {}
 
         self.test_options = test_options
 
         self.obj_dir = os.path.join(self.test_options.out_dir, 'obj')
         self.dist_dir = os.path.join(self.test_options.out_dir, 'dist')
 
+        self.find_tests(test_spec)
+
+    def find_tests(self, test_spec):
+        scanner = ndk.test.scanner.BuildTestScanner(self.test_options.ndk_path)
+        nodist_scanner = ndk.test.scanner.BuildTestScanner(
+            self.test_options.ndk_path, dist=False)
+        libcxx_scanner = ndk.test.scanner.LibcxxTestScanner(
+            self.test_options.ndk_path)
+        for abi in test_spec.abis:
+            build_api_level = None  # Always use the default.
+
+            scanner.add_build_configuration(abi, build_api_level)
+            nodist_scanner.add_build_configuration(abi, build_api_level)
+            libcxx_scanner.add_build_configuration(abi, build_api_level)
+
+        if 'build' in test_spec.suites:
+            test_src = os.path.join(self.test_options.src_dir, 'build')
+            self.add_suite('build', test_src, nodist_scanner)
+        if 'device' in test_spec.suites:
+            test_src = os.path.join(self.test_options.src_dir, 'device')
+            self.add_suite('device', test_src, scanner)
+        if 'libc++' in test_spec.suites:
+            test_src = os.path.join(self.test_options.src_dir, 'libc++')
+            self.add_suite('libc++', test_src, libcxx_scanner)
+
     @classmethod
     def from_config_file(cls, config_path, test_options, printer):
         with open(config_path) as test_config_file:
@@ -92,6 +162,23 @@
         spec = test_spec_from_config(test_config)
         return cls(spec, test_options, printer)
 
+    def add_suite(self, name, path, test_scanner):
+        if name in self.tests:
+            raise KeyError('suite {} already exists'.format(name))
+        new_tests = scan_test_suite(path, test_scanner)
+        self.check_no_overlapping_build_dirs(name, new_tests)
+        self.tests[name] = new_tests
+
+    def check_no_overlapping_build_dirs(self, suite, new_tests):
+        for test in new_tests:
+            build_dir = test.get_build_dir('')
+            if build_dir in self.build_dirs:
+                dup_suite, dup_test = self.build_dirs[build_dir]
+                raise RuntimeError(
+                    'Found duplicate build directory:\n{} {}\n{} {}'.format(
+                        dup_suite, dup_test, suite, test))
+            self.build_dirs[build_dir] = (suite, test)
+
     def make_out_dirs(self):
         if not os.path.exists(self.obj_dir):
             os.makedirs(self.obj_dir)
@@ -109,11 +196,70 @@
 
         test_filters = ndk.test.filters.TestFilter.from_string(
             self.test_options.test_filter)
-        result = self.runner.run(self.obj_dir, self.dist_dir, test_filters)
+        result = self.do_build(test_filters)
         if self.test_options.build_report:
             write_build_report(self.test_options.build_report, result)
         return result
 
+    def do_build(self, test_filters):
+        workqueue = ndk.test.builder.LoadRestrictingWorkQueue()
+        try:
+            for suite, tests in self.tests.items():
+                # Each test configuration was expanded when each test was
+                # discovered, so the current order has all the largest tests
+                # right next to each other. Spread them out to try to avoid
+                # having too many heavy builds happening simultaneously.
+                random.shuffle(tests)
+                for test in tests:
+                    if not test_filters.filter(test.name):
+                        continue
+
+                    if test.name == 'libc++':
+                        workqueue.add_load_restricted_task(
+                            _run_test, suite, test, self.obj_dir,
+                            self.dist_dir, test_filters)
+                    else:
+                        workqueue.add_task(
+                            _run_test, suite, test, self.obj_dir,
+                            self.dist_dir, test_filters)
+
+            report = ndk.test.report.Report()
+            self.wait_for_results(report, workqueue, test_filters)
+
+            return report
+        finally:
+            workqueue.terminate()
+            workqueue.join()
+
+    def wait_for_results(self, report, workqueue, test_filters):
+        console = ndk.ansi.get_console()
+        ui = ndk.test.ui.get_test_build_progress_ui(console, workqueue)
+        with ndk.ansi.disable_terminal_echo(sys.stdin):
+            with console.cursor_hide_context():
+                while not workqueue.finished():
+                    suite, result, additional_tests = workqueue.get_result()
+                    # Filtered test. Skip them entirely to avoid polluting
+                    # --show-all results.
+                    if result is None:
+                        assert len(additional_tests) == 0
+                        ui.draw()
+                        continue
+
+                    assert result.passed() or len(additional_tests) == 0
+                    for test in additional_tests:
+                        workqueue.add_task(
+                            _run_test, suite, test, self.obj_dir,
+                            self.dist_dir, test_filters)
+                    if logger().isEnabledFor(logging.INFO):
+                        ui.clear()
+                        self.printer.print_result(result)
+                    elif result.failed():
+                        ui.clear()
+                        self.printer.print_result(result)
+                    report.add_result(suite, result)
+                    ui.draw()
+                ui.clear()
+
 
 class LoadRestrictingWorkQueue(object):
     """Specialized work queue for building tests.
diff --git a/ndk/test/config.py b/ndk/test/config.py
new file mode 100644
index 0000000..c750556
--- /dev/null
+++ b/ndk/test/config.py
@@ -0,0 +1,223 @@
+#
+# Copyright (C) 2015 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+import imp
+import os
+
+
+class TestConfig(object):
+    """Describes the status of a test.
+
+    Each test directory can contain a "test_config.py" file that describes
+    the configurations a test is not expected to pass for. Previously this
+    information could be captured in one of two places: the Application.mk
+    file, or a BROKEN_BUILD/BROKEN_RUN file.
+
+    Application.mk was used to state that a test was only to be run for a
+    specific platform version, specific toolchain, or a set of ABIs.
+    Unfortunately Application.mk could only specify a single toolchain or
+    platform, not a set.
+
+    BROKEN_BUILD/BROKEN_RUN files were too general. An empty file meant the
+    test should always be skipped regardless of configuration. Any change that
+    would put a test in that situation should be reverted immediately. These
+    also didn't make it clear if the test was actually broken (and thus should
+    be fixed) or just not applicable.
+
+    A test_config.py file is more flexible. It is a Python module that defines
+    at least one function by the same name as one in TestConfig.NullTestConfig.
+    If a function is not defined the null implementation (not broken,
+    supported), will be used.
+    """
+
+    class NullTestConfig(object):
+        def __init__(self):
+            pass
+
+        # pylint: disable=unused-argument
+        @staticmethod
+        def build_broken(abi, platform):
+            """Tests if a given configuration is known broken.
+
+            A broken test is a known failing test that should be fixed.
+
+            Any test with a non-empty broken section requires a "bug" entry
+            with a link to either an internal bug (http://b/BUG_NUMBER) or a
+            public bug (http://b.android.com/BUG_NUMBER).
+
+            These tests will still be built and run. If the test succeeds, it
+            will be reported as an error.
+
+            Returns: A tuple of (broken_configuration, bug) or (None, None).
+            """
+            return None, None
+
+        @staticmethod
+        def build_unsupported(abi, platform):
+            """Tests if a given configuration is unsupported.
+
+            An unsupported test is a test that do not make sense to run for a
+            given configuration. Testing x86 assembler on MIPS, for example.
+
+            These tests will not be built or run.
+
+            Returns: The string unsupported_configuration or None.
+            """
+            return None
+
+        @staticmethod
+        def extra_cmake_flags():
+            return []
+
+        @staticmethod
+        def extra_ndk_build_flags():
+            """Returns extra flags that should be passed to ndk-build."""
+            return []
+
+        @staticmethod
+        def is_negative_test():
+            """Returns True if this test should pass if the build fails.
+
+            Note that this is different from build_broken. Use build_broken to
+            indicate a bug and use is_negative_test to indicate a test that
+            should fail if things are working.
+
+            Also note that check_broken and is_negative_test can be layered. If
+            a build is expected to fail, but doesn't for armeabi, the
+            test_config could contain:
+
+                def is_negative_test():
+                    return True
+
+
+                def build_broken(abi, api):
+                    if abi == 'armeabi':
+                        return abi, bug_url
+                    return None, None
+            """
+            return False
+        # pylint: enable=unused-argument
+
+    def __init__(self, file_path):
+        # Note that this namespace isn't actually meaningful from our side;
+        # it's only what the loaded module's __name__ gets set to.
+        dirname = os.path.dirname(file_path)
+        namespace = '.'.join([dirname, 'test_config'])
+
+        try:
+            self.module = imp.load_source(namespace, file_path)
+        except IOError:
+            self.module = None
+
+        try:
+            self.build_broken = self.module.build_broken
+        except AttributeError:
+            self.build_broken = self.NullTestConfig.build_broken
+
+        try:
+            self.build_unsupported = self.module.build_unsupported
+        except AttributeError:
+            self.build_unsupported = self.NullTestConfig.build_unsupported
+
+        try:
+            self.extra_cmake_flags = self.module.extra_cmake_flags
+        except AttributeError:
+            self.extra_cmake_flags = self.NullTestConfig.extra_cmake_flags
+
+        try:
+            self.extra_ndk_build_flags = self.module.extra_ndk_build_flags
+        except AttributeError:
+            ntc = self.NullTestConfig
+            self.extra_ndk_build_flags = ntc.extra_ndk_build_flags
+
+        try:
+            self.is_negative_test = self.module.is_negative_test
+        except AttributeError:
+            self.is_negative_test = self.NullTestConfig.is_negative_test
+
+    @classmethod
+    def from_test_dir(cls, test_dir):
+        path = os.path.join(test_dir, 'test_config.py')
+        return cls(path)
+
+
+class DeviceTestConfig(TestConfig):
+    """Specialization of test_config.py that includes device API level.
+
+    We need to mark some tests as broken or unsupported based on what device
+    they are running on, as opposed to just what they were built for.
+    """
+    class NullTestConfig(TestConfig.NullTestConfig):
+        # pylint: disable=unused-argument
+        @staticmethod
+        def run_broken(abi, device_api, subtest):
+            return None, None
+
+        @staticmethod
+        def run_unsupported(abi, device_api, subtest):
+            return None
+
+        @staticmethod
+        def extra_cmake_flags():
+            return []
+        # pylint: enable=unused-argument
+
+    def __init__(self, file_path):
+        super(DeviceTestConfig, self).__init__(file_path)
+
+        try:
+            self.run_broken = self.module.run_broken
+        except AttributeError:
+            self.run_broken = self.NullTestConfig.run_broken
+
+        try:
+            self.run_unsupported = self.module.run_unsupported
+        except AttributeError:
+            self.run_unsupported = self.NullTestConfig.run_unsupported
+
+        if hasattr(self.module, 'is_negative_test'):
+            # If the build is expected to fail, then it should just be a build
+            # test since the test should never be run.
+            #
+            # If the run is expected to fail, just fix the test to pass for
+            # thatr case. Gtest death tests can handle the more complicated
+            # cases.
+            raise RuntimeError('is_negative_test is invalid for device tests')
+
+
+class LibcxxTestConfig(DeviceTestConfig):
+    """Specialization of test_config.py for libc++.
+
+    The libc++ tests have multiple tests in a single directory, so we need to
+    pass the test name for build_broken too.
+    """
+    class NullTestConfig(TestConfig.NullTestConfig):
+        # pylint: disable=unused-argument,arguments-differ
+        @staticmethod
+        def build_unsupported(abi, api, name):
+            return None
+
+        @staticmethod
+        def build_broken(abi, api, name):
+            return None, None
+
+        @staticmethod
+        def run_unsupported(abi, device_api, name):
+            return None
+
+        @staticmethod
+        def run_broken(abi, device_api, name):
+            return None, None
+        # pylint: enable=unused-argument,arguments-differ
diff --git a/ndk/test/scanner.py b/ndk/test/scanner.py
new file mode 100644
index 0000000..d94b3b0
--- /dev/null
+++ b/ndk/test/scanner.py
@@ -0,0 +1,142 @@
+#
+# Copyright (C) 2018 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+from __future__ import absolute_import
+
+import os
+
+import ndk.test.spec
+import ndk.test.types
+
+
+class TestScanner(object):
+    """Creates a Test objects for a given test directory.
+
+    A test scanner is used to turn a test directory into a list of Tests for
+    any of the test types found in the directory.
+    """
+    def find_tests(self, path, name):
+        """Searches a directory for tests.
+
+        Args:
+            path: Path to the test directory.
+            name: Name of the test.
+
+        Returns: List of Tests, possibly empty.
+        """
+        raise NotImplementedError
+
+
+class BuildTestScanner(TestScanner):
+    def __init__(self, ndk_path, dist=True):
+        self.ndk_path = ndk_path
+        self.dist = dist
+        self.build_configurations = set()
+
+    def add_build_configuration(self, abi, api):
+        self.build_configurations.add(ndk.test.spec.BuildConfiguration(
+            abi, api))
+
+    def find_tests(self, path, name):
+        # If we have a build.sh, that takes precedence over the Android.mk.
+        build_sh_path = os.path.join(path, 'build.sh')
+        if os.path.exists(build_sh_path):
+            return self.make_build_sh_tests(path, name)
+
+        # Same for test.py
+        build_sh_path = os.path.join(path, 'test.py')
+        if os.path.exists(build_sh_path):
+            return self.make_test_py_tests(path, name)
+
+        # But we can have both ndk-build and cmake tests in the same directory.
+        tests = []
+        android_mk_path = os.path.join(path, 'jni/Android.mk')
+        if os.path.exists(android_mk_path):
+            tests.extend(self.make_ndk_build_tests(path, name))
+
+        cmake_lists_path = os.path.join(path, 'CMakeLists.txt')
+        if os.path.exists(cmake_lists_path):
+            tests.extend(self.make_cmake_tests(path, name))
+        return tests
+
+    def make_build_sh_tests(self, path, name):
+        tests = []
+        for config in self.build_configurations:
+            test = ndk.test.types.ShellBuildTest(
+                name, path, config, self.ndk_path)
+            tests.append(test)
+        return tests
+
+    def make_test_py_tests(self, path, name):
+        tests = []
+        for config in self.build_configurations:
+            test = ndk.test.types.PythonBuildTest(
+                name, path, config, self.ndk_path)
+            tests.append(test)
+        return tests
+
+    def make_ndk_build_tests(self, path, name):
+        tests = []
+        for config in self.build_configurations:
+            test = ndk.test.types.NdkBuildTest(
+                name, path, config, self.ndk_path, self.dist)
+            tests.append(test)
+        return tests
+
+    def make_cmake_tests(self, path, name):
+        tests = []
+        for config in self.build_configurations:
+            test = ndk.test.types.CMakeBuildTest(
+                name, path, config, self.ndk_path, self.dist)
+            tests.append(test)
+        return tests
+
+
+class LibcxxTestScanner(TestScanner):
+    ALL_TESTS = []
+
+    def __init__(self, ndk_path):
+        self.ndk_path = ndk_path
+        self.build_configurations = set()
+        LibcxxTestScanner.find_all_libcxx_tests(self.ndk_path)
+
+    def add_build_configuration(self, abi, api):
+        self.build_configurations.add(ndk.test.spec.BuildConfiguration(
+            abi, api))
+
+    def find_tests(self, path, name):
+        tests = []
+        for config in self.build_configurations:
+            tests.append(ndk.test.types.LibcxxTest(
+                'libc++', path, config, self.ndk_path))
+        return tests
+
+    @classmethod
+    def find_all_libcxx_tests(cls, ndk_path):
+        # If we instantiate multiple LibcxxTestScanners, we still only need to
+        # initialize this once. We only create these in the main thread, so
+        # there's no risk of race.
+        if len(cls.ALL_TESTS) != 0:
+            return
+
+        test_base_dir = os.path.join(
+            ndk_path, 'sources/cxx-stl/llvm-libc++/test')
+
+        for root, _dirs, files in os.walk(test_base_dir):
+            for test_file in files:
+                if test_file.endswith('.cpp'):
+                    test_path = ndk.paths.to_posix_path(os.path.relpath(
+                        os.path.join(root, test_file), test_base_dir))
+                    cls.ALL_TESTS.append(test_path)
diff --git a/ndk/test/suites.py b/ndk/test/suites.py
new file mode 100644
index 0000000..c3e176a
--- /dev/null
+++ b/ndk/test/suites.py
@@ -0,0 +1,22 @@
+#
+# Copyright (C) 2015 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+
+ALL_SUITES = (
+    'build',
+    'device',
+    'libc++',
+)
diff --git a/ndk/test/test_report.py b/ndk/test/test_report.py
index 414f4b4..eb5b976 100644
--- a/ndk/test/test_report.py
+++ b/ndk/test/test_report.py
@@ -17,30 +17,44 @@
 import unittest
 
 import ndk.test.report
-import tests.testlib
 
 
 class MockTest(object):
-    is_flaky = True
+    def __init__(self, name=''):
+        self.name = name
 
 
 class ReportTest(unittest.TestCase):
     def test_remove_all_failing_flaky(self):
         report = ndk.test.report.Report()
-        report.add_result('build', tests.testlib.Success(MockTest()))
-        report.add_result('build', tests.testlib.Failure(MockTest(), 'failed'))
-        report.add_result('build', tests.testlib.Failure(
-            MockTest(), 'Did not receive exit status from test.'))
-        report.add_result('build', tests.testlib.Failure(
-            MockTest(), 'text busy'))
-        report.add_result('build', tests.testlib.Failure(
-            MockTest(), 'Text file busy'))
-        report.add_result('build', tests.testlib.Skipped(
+        # Success. Not filtered.
+        report.add_result('build', ndk.test.result.Success(MockTest()))
+
+        # Normal failure. Not filtered.
+        report.add_result('build', ndk.test.result.Failure(
+            MockTest(), 'failed'))
+
+        # Skipped test. Not filtered.
+        report.add_result('build', ndk.test.result.Skipped(
             MockTest(), 'skipped'))
-        report.add_result('build', tests.testlib.ExpectedFailure(
-            MockTest(), 'bug', 'config'))
-        report.add_result('build', tests.testlib.UnexpectedSuccess(
+
+        # Expected failure. Not filtered.
+        report.add_result('build', ndk.test.result.ExpectedFailure(
             MockTest(), 'bug', 'config'))
 
-        results = report.remove_all_failing_flaky(tests.testlib.flake_filter)
+        # Unexpected success. Not filtered.
+        report.add_result('build', ndk.test.result.UnexpectedSuccess(
+            MockTest(), 'bug', 'config'))
+
+        # adb didn't tell us anything. Filtered.
+        report.add_result('build', ndk.test.result.Failure(
+            MockTest(), 'Could not find exit status in shell output.'))
+
+        # Flaky libc++ tests. Filtered.
+        report.add_result('build', ndk.test.result.Failure(
+            MockTest('libc++.libcxx/thread/foo'), ''))
+        report.add_result('build', ndk.test.result.Failure(
+            MockTest('libc++.std/thread/foo'), ''))
+
+        results = report.remove_all_failing_flaky(ndk.run_tests.flake_filter)
         self.assertEqual(3, len(results))
diff --git a/ndk/test/types.py b/ndk/test/types.py
new file mode 100644
index 0000000..90384e2
--- /dev/null
+++ b/ndk/test/types.py
@@ -0,0 +1,694 @@
+#
+# Copyright (C) 2015 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+import fnmatch
+import imp
+import logging
+import multiprocessing
+import os
+import re
+import shutil
+import subprocess
+import xml.etree.ElementTree
+
+import ndk.abis
+import ndk.ansi
+import ndk.ext.os
+import ndk.ext.shutil
+import ndk.ext.subprocess
+import ndk.hosts
+import ndk.ndkbuild
+import ndk.test.config
+import ndk.test.result
+
+
+def logger():
+    """Return the logger for this module."""
+    return logging.getLogger(__name__)
+
+
+def _get_jobs_args():
+    cpus = multiprocessing.cpu_count()
+    return ['-j{}'.format(cpus), '-l{}'.format(cpus)]
+
+
+def _prep_build_dir(src_dir, out_dir):
+    if os.path.exists(out_dir):
+        shutil.rmtree(out_dir)
+    shutil.copytree(src_dir, out_dir)
+
+
+def _run_build_sh_test(test, build_dir, test_dir, ndk_path, ndk_build_flags,
+                       abi, platform):
+    _prep_build_dir(test_dir, build_dir)
+    with ndk.ext.os.cd(build_dir):
+        build_cmd = ['bash', 'build.sh'] + _get_jobs_args() + ndk_build_flags
+        test_env = dict(os.environ)
+        test_env['NDK'] = ndk_path
+        if abi is not None:
+            test_env['APP_ABI'] = abi
+        test_env['APP_PLATFORM'] = 'android-{}'.format(platform)
+        rc, out = ndk.ext.subprocess.call_output(build_cmd, env=test_env)
+        if rc == 0:
+            return ndk.test.result.Success(test)
+        else:
+            return ndk.test.result.Failure(test, out)
+
+
+def _run_ndk_build_test(test, obj_dir, dist_dir, test_dir, ndk_path,
+                        ndk_build_flags, abi, platform):
+    _prep_build_dir(test_dir, obj_dir)
+    with ndk.ext.os.cd(obj_dir):
+        args = [
+            'APP_ABI=' + abi,
+            'NDK_LIBS_OUT=' + dist_dir,
+        ]
+        args.extend(_get_jobs_args())
+        if platform is not None:
+            args.append('APP_PLATFORM=android-{}'.format(platform))
+        rc, out = ndk.ndkbuild.build(ndk_path, args + ndk_build_flags)
+        if rc == 0:
+            return ndk.test.result.Success(test)
+        else:
+            return ndk.test.result.Failure(test, out)
+
+
+def _run_cmake_build_test(test, obj_dir, dist_dir, test_dir, ndk_path,
+                          cmake_flags, abi, platform):
+    _prep_build_dir(test_dir, obj_dir)
+
+    # Add prebuilts to PATH.
+    prebuilts_host_tag = ndk.hosts.get_default_host() + '-x86'
+    prebuilts_bin = ndk.paths.android_path(
+        'prebuilts', 'cmake', prebuilts_host_tag, 'bin')
+    env_path = prebuilts_bin + os.pathsep + os.environ['PATH']
+
+    # Fail if we don't have a working cmake executable, either from the
+    # prebuilts, or from the SDK, or if a new enough version is installed.
+    cmake_bin = ndk.ext.shutil.which('cmake', path=env_path)
+    if cmake_bin is None:
+        return ndk.test.result.Failure(test, 'cmake executable not found')
+
+    out = subprocess.check_output([cmake_bin, '--version']).decode('utf-8')
+    version_pattern = r'cmake version (\d+)\.(\d+)\.'
+    version = [int(v) for v in re.match(version_pattern, out).groups()]
+    if version < [3, 6]:
+        return ndk.test.result.Failure(test, 'cmake 3.6 or above required')
+
+    # Also require a working ninja executable.
+    ninja_bin = ndk.ext.shutil.which('ninja', path=env_path)
+    if ninja_bin is None:
+        return ndk.test.result.Failure(test, 'ninja executable not found')
+    rc, _ = ndk.ext.subprocess.call_output([ninja_bin, '--version'])
+    if rc != 0:
+        return ndk.test.result.Failure(test, 'ninja --version failed')
+
+    toolchain_file = os.path.join(ndk_path, 'build', 'cmake',
+                                  'android.toolchain.cmake')
+    objs_dir = os.path.join(obj_dir, abi)
+    libs_dir = os.path.join(dist_dir, abi)
+    args = [
+        '-H' + obj_dir,
+        '-B' + objs_dir,
+        '-DCMAKE_TOOLCHAIN_FILE=' + toolchain_file,
+        '-DANDROID_ABI=' + abi,
+        '-DCMAKE_RUNTIME_OUTPUT_DIRECTORY=' + libs_dir,
+        '-DCMAKE_LIBRARY_OUTPUT_DIRECTORY=' + libs_dir,
+        '-GNinja',
+        '-DCMAKE_MAKE_PROGRAM=' + ninja_bin,
+    ]
+    if platform is not None:
+        args.append('-DANDROID_PLATFORM=android-{}'.format(platform))
+    rc, out = ndk.ext.subprocess.call_output(
+        [cmake_bin] + cmake_flags + args)
+    if rc != 0:
+        return ndk.test.result.Failure(test, out)
+    rc, out = ndk.ext.subprocess.call_output(
+        [cmake_bin, '--build', objs_dir, '--'] + _get_jobs_args())
+    if rc != 0:
+        return ndk.test.result.Failure(test, out)
+    return ndk.test.result.Success(test)
+
+
+class Test(object):
+    def __init__(self, name, test_dir, config, ndk_path):
+        self.name = name
+        self.test_dir = test_dir
+        self.config = config
+        self.ndk_path = ndk_path
+
+    def get_test_config(self):
+        return ndk.test.config.TestConfig.from_test_dir(self.test_dir)
+
+    def run(self, obj_dir, dist_dir, test_filters):
+        raise NotImplementedError
+
+    def __str__(self):
+        return '{} [{}]'.format(self.name, self.config)
+
+
+class BuildTest(Test):
+    def __init__(self, name, test_dir, config, ndk_path):
+        super(BuildTest, self).__init__(name, test_dir, config, ndk_path)
+
+        if self.api is None:
+            raise ValueError
+
+    @property
+    def abi(self):
+        return self.config.abi
+
+    @property
+    def api(self):
+        return self.config.api
+
+    @property
+    def platform(self):
+        return self.api
+
+    @property
+    def ndk_build_flags(self):
+        flags = self.config.get_extra_ndk_build_flags()
+        if flags is None:
+            flags = []
+        return flags + self.get_extra_ndk_build_flags()
+
+    @property
+    def cmake_flags(self):
+        flags = self.config.get_extra_cmake_flags()
+        if flags is None:
+            flags = []
+        return flags + self.get_extra_cmake_flags()
+
+    def run(self, obj_dir, dist_dir, _test_filters):
+        raise NotImplementedError
+
+    def check_broken(self):
+        return self.get_test_config().build_broken(self.abi, self.platform)
+
+    def check_unsupported(self):
+        return self.get_test_config().build_unsupported(
+            self.abi, self.platform)
+
+    def is_negative_test(self):
+        return self.get_test_config().is_negative_test()
+
+    def get_extra_cmake_flags(self):
+        return self.get_test_config().extra_cmake_flags()
+
+    def get_extra_ndk_build_flags(self):
+        return self.get_test_config().extra_ndk_build_flags()
+
+
+class PythonBuildTest(BuildTest):
+    """A test that is implemented by test.py.
+
+    A test.py test has a test.py file in its root directory. This module
+    contains a run_test function which returns a tuple of `(boolean_success,
+    string_failure_message)` and takes the following kwargs (all of which
+    default to None):
+
+    abi: ABI to test as a string.
+    platform: Platform to build against as a string.
+    ndk_build_flags: Additional build flags that should be passed to ndk-build
+                     if invoked as a list of strings.
+    """
+    def __init__(self, name, test_dir, config, ndk_path):
+        api = config.api
+        if api is None:
+            api = ndk.abis.min_api_for_abi(config.abi)
+        config = ndk.test.spec.BuildConfiguration(config.abi, api)
+        super(PythonBuildTest, self).__init__(name, test_dir, config, ndk_path)
+
+        if self.abi not in ndk.abis.ALL_ABIS:
+            raise ValueError('{} is not a valid ABI'.format(self.abi))
+
+        try:
+            int(self.platform)
+        except ValueError:
+            raise ValueError(
+                '{} is not a valid platform number'.format(self.platform))
+
+        # Not a ValueError for this one because it should be impossible. This
+        # is actually a computed result from the config we're passed.
+        assert self.ndk_build_flags is not None
+
+    def get_build_dir(self, out_dir):
+        return os.path.join(out_dir, str(self.config), 'test.py', self.name)
+
+    def run(self, obj_dir, _dist_dir, _test_filters):
+        build_dir = self.get_build_dir(obj_dir)
+        logger().info('Building test: %s', self.name)
+        _prep_build_dir(self.test_dir, build_dir)
+        with ndk.ext.os.cd(build_dir):
+            module = imp.load_source('test', 'test.py')
+            success, failure_message = module.run_test(
+                self.ndk_path, self.abi, self.platform, self.ndk_build_flags)
+            if success:
+                return ndk.test.result.Success(self), []
+            else:
+                return ndk.test.result.Failure(self, failure_message), []
+
+
+class ShellBuildTest(BuildTest):
+    def __init__(self, name, test_dir, config, ndk_path):
+        api = config.api
+        if api is None:
+            api = ndk.abis.min_api_for_abi(config.abi)
+        config = ndk.test.spec.BuildConfiguration(config.abi, api)
+        super(ShellBuildTest, self).__init__(name, test_dir, config, ndk_path)
+
+    def get_build_dir(self, out_dir):
+        return os.path.join(out_dir, str(self.config), 'build.sh', self.name)
+
+    def run(self, obj_dir, _dist_dir, _test_filters):
+        build_dir = self.get_build_dir(obj_dir)
+        logger().info('Building test: %s', self.name)
+        if os.name == 'nt':
+            reason = 'build.sh tests are not supported on Windows'
+            return ndk.test.result.Skipped(self, reason), []
+        else:
+            result = _run_build_sh_test(
+                self, build_dir, self.test_dir, self.ndk_path,
+                self.ndk_build_flags, self.abi, self.platform)
+            return result, []
+
+
+def _platform_from_application_mk(test_dir):
+    """Determine target API level from a test's Application.mk.
+
+    Args:
+        test_dir: Directory of the test to read.
+
+    Returns:
+        Integer portion of APP_PLATFORM if found, else None.
+
+    Raises:
+        ValueError: Found an unexpected value for APP_PLATFORM.
+    """
+    application_mk = os.path.join(test_dir, 'jni/Application.mk')
+    if not os.path.exists(application_mk):
+        return None
+
+    with open(application_mk) as application_mk_file:
+        for line in application_mk_file:
+            if line.startswith('APP_PLATFORM'):
+                _, platform_str = line.split(':=')
+                break
+        else:
+            return None
+
+    platform_str = platform_str.strip()
+    if not platform_str.startswith('android-'):
+        raise ValueError(platform_str)
+
+    _, api_level_str = platform_str.split('-')
+    return int(api_level_str)
+
+
+def _get_or_infer_app_platform(platform_from_user, test_dir, abi):
+    """Determines the platform level to use for a test using ndk-build.
+
+    Choose the platform level from, in order of preference:
+    1. Value given as argument.
+    2. APP_PLATFORM from jni/Application.mk.
+    3. Default value for the target ABI.
+
+    Args:
+        platform_from_user: A user provided platform level or None.
+        test_dir: The directory containing the ndk-build project.
+        abi: The ABI being targeted.
+
+    Returns:
+        The platform version the test should build against.
+    """
+    if platform_from_user is not None:
+        return platform_from_user
+
+    minimum_version = ndk.abis.min_api_for_abi(abi)
+    platform_from_application_mk = _platform_from_application_mk(test_dir)
+    if platform_from_application_mk is not None:
+        if platform_from_application_mk >= minimum_version:
+            return platform_from_application_mk
+
+    return minimum_version
+
+
+class NdkBuildTest(BuildTest):
+    def __init__(self, name, test_dir, config, ndk_path, dist):
+        api = _get_or_infer_app_platform(config.api, test_dir, config.abi)
+        config = ndk.test.spec.BuildConfiguration(config.abi, api)
+        super(NdkBuildTest, self).__init__(name, test_dir, config, ndk_path)
+        self.dist = dist
+
+    def get_dist_dir(self, obj_dir, dist_dir):
+        if self.dist:
+            return self.get_build_dir(dist_dir)
+        else:
+            return os.path.join(self.get_build_dir(obj_dir), 'dist')
+
+    def get_build_dir(self, out_dir):
+        return os.path.join(out_dir, str(self.config), 'ndk-build', self.name)
+
+    def run(self, obj_dir, dist_dir, _test_filters):
+        logger().info('Building test: %s', self.name)
+        obj_dir = self.get_build_dir(obj_dir)
+        dist_dir = self.get_dist_dir(obj_dir, dist_dir)
+        result = _run_ndk_build_test(
+            self, obj_dir, dist_dir, self.test_dir, self.ndk_path,
+            self.ndk_build_flags, self.abi, self.platform)
+        return result, []
+
+
+class CMakeBuildTest(BuildTest):
+    def __init__(self, name, test_dir, config, ndk_path, dist):
+        api = _get_or_infer_app_platform(config.api, test_dir, config.abi)
+        config = ndk.test.spec.BuildConfiguration(config.abi, api)
+        super(CMakeBuildTest, self).__init__(name, test_dir, config, ndk_path)
+        self.dist = dist
+
+    def get_dist_dir(self, obj_dir, dist_dir):
+        if self.dist:
+            return self.get_build_dir(dist_dir)
+        else:
+            return os.path.join(self.get_build_dir(obj_dir), 'dist')
+
+    def get_build_dir(self, out_dir):
+        return os.path.join(out_dir, str(self.config), 'cmake', self.name)
+
+    def run(self, obj_dir, dist_dir, _test_filters):
+        obj_dir = self.get_build_dir(obj_dir)
+        dist_dir = self.get_dist_dir(obj_dir, dist_dir)
+        logger().info('Building test: %s', self.name)
+        result = _run_cmake_build_test(
+            self, obj_dir, dist_dir, self.test_dir, self.ndk_path,
+            self.cmake_flags, self.abi, self.platform)
+        return result, []
+
+
+def get_xunit_reports(xunit_file, test_base_dir, config, ndk_path):
+    tree = xml.etree.ElementTree.parse(xunit_file)
+    root = tree.getroot()
+    cases = root.findall('.//testcase')
+
+    reports = []
+    for test_case in cases:
+        mangled_test_dir = test_case.get('classname')
+
+        # The classname is the path from the root of the libc++ test directory
+        # to the directory containing the test (prefixed with 'libc++.')...
+        mangled_path = '/'.join([mangled_test_dir, test_case.get('name')])
+
+        # ... that has had '.' in its path replaced with '_' because xunit.
+        test_matches = find_original_libcxx_test(mangled_path, ndk_path)
+        if len(test_matches) == 0:
+            raise RuntimeError('Found no matches for test ' + mangled_path)
+        if len(test_matches) > 1:
+            raise RuntimeError('Found multiple matches for test {}: {}'.format(
+                mangled_path, test_matches))
+        assert len(test_matches) == 1
+
+        # We found a unique path matching the xunit class/test name.
+        name = test_matches[0]
+        test_dir = os.path.dirname(name)[len('libc++.'):]
+
+        failure_nodes = test_case.findall('failure')
+        if len(failure_nodes) == 0:
+            reports.append(XunitSuccess(
+                name, test_base_dir, test_dir, config, ndk_path))
+            continue
+
+        if len(failure_nodes) != 1:
+            msg = ('Could not parse XUnit output: test case does not have a '
+                   'unique failure node: {}'.format(name))
+            raise RuntimeError(msg)
+
+        failure_node = failure_nodes[0]
+        failure_text = failure_node.text
+        reports.append(XunitFailure(
+            name, test_base_dir, test_dir, failure_text, config, ndk_path))
+    return reports
+
+
+def get_lit_cmd():
+    # The build server doesn't install lit to a virtualenv, so use it from the
+    # source location if possible.
+    lit_path = ndk.paths.android_path('external/llvm/utils/lit/lit.py')
+    if os.path.exists(lit_path):
+        return ['python', lit_path]
+    elif ndk.ext.shutil.which('lit'):
+        return ['lit']
+    return None
+
+
+def find_original_libcxx_test(name, ndk_path):
+    """Finds the original libc++ test file given the xunit test name.
+
+    LIT mangles test names to replace all periods with underscores because
+    xunit. This returns all tests that could possibly match the xunit test
+    name.
+    """
+
+    name = ndk.paths.to_posix_path(name)
+
+    # LIT special cases tests in the root of the test directory (such as
+    # test/nothing_to_do.pass.cpp) as "libc++.libc++/$TEST_FILE.pass.cpp" for
+    # some reason. Strip it off so we can find the tests.
+    if name.startswith('libc++.libc++/'):
+        name = 'libc++.' + name[len('libc++.libc++/'):]
+
+    test_prefix = 'libc++.'
+    if not name.startswith(test_prefix):
+        raise ValueError('libc++ test name must begin with "libc++."')
+
+    name = name[len(test_prefix):]
+    test_pattern = name.replace('_', '?')
+    matches = []
+
+    # On Windows, a multiprocessing worker process does not inherit ALL_TESTS,
+    # so we must scan libc++ tests in each worker.
+    ndk.test.scanner.LibcxxTestScanner.find_all_libcxx_tests(ndk_path)
+
+    all_libcxx_tests = ndk.test.scanner.LibcxxTestScanner.ALL_TESTS
+    for match in fnmatch.filter(all_libcxx_tests, test_pattern):
+        matches.append(test_prefix + match)
+    return matches
+
+
+class LibcxxTest(Test):
+    def __init__(self, name, test_dir, config, ndk_path):
+        if config.api is None:
+            config.api = ndk.abis.min_api_for_abi(config.abi)
+
+        super(LibcxxTest, self).__init__(name, test_dir, config, ndk_path)
+
+    @property
+    def abi(self):
+        return self.config.abi
+
+    @property
+    def api(self):
+        return self.config.api
+
+    def get_build_dir(self, out_dir):
+        return os.path.join(out_dir, str(self.config), 'libcxx', self.name)
+
+    def run_lit(self, build_dir, filters):
+        libcxx_dir = os.path.join(self.ndk_path, 'sources/cxx-stl/llvm-libc++')
+        device_dir = '/data/local/tmp/libcxx'
+
+        arch = ndk.abis.abi_to_arch(self.abi)
+        host_tag = ndk.hosts.get_host_tag(self.ndk_path)
+        triple = ndk.abis.arch_to_triple(arch)
+        toolchain = ndk.abis.arch_to_toolchain(arch)
+
+        replacements = [
+            ('abi', self.abi),
+            ('api', self.api),
+            ('arch', arch),
+            ('host_tag', host_tag),
+            ('toolchain', toolchain),
+            ('triple', '{}{}'.format(triple, self.api)),
+            ('use_pie', True),
+            ('build_dir', build_dir),
+        ]
+        lit_cfg_args = []
+        for key, value in replacements:
+            lit_cfg_args.append('--param={}={}'.format(key, value))
+
+        shutil.copy2(os.path.join(libcxx_dir, 'test/lit.ndk.cfg.in'),
+                     os.path.join(libcxx_dir, 'test/lit.site.cfg'))
+
+        xunit_output = os.path.join(build_dir, 'xunit.xml')
+
+        lit_args = get_lit_cmd() + [
+            '-sv',
+            '--param=device_dir=' + device_dir,
+            '--param=unified_headers=True',
+            '--param=build_only=True',
+            '--no-progress-bar',
+            '--show-all',
+            '--xunit-xml-output=' + xunit_output,
+        ] + lit_cfg_args
+
+        default_test_path = os.path.join(libcxx_dir, 'test')
+        test_paths = list(filters)
+        if len(test_paths) == 0:
+            test_paths.append(default_test_path)
+        for test_path in test_paths:
+            lit_args.append(test_path)
+
+        # Ignore the exit code. We do most XFAIL processing outside the test
+        # runner so expected failures in the test runner will still cause a
+        # non-zero exit status. This "test" only fails if we encounter a Python
+        # exception. Exceptions raised from our code are already caught by the
+        # test runner. If that happens in LIT, the xunit output will not be
+        # valid and we'll fail get_xunit_reports and raise an exception anyway.
+        with open(os.devnull, 'w') as dev_null:
+            stdout = dev_null
+            stderr = dev_null
+            if logger().isEnabledFor(logging.INFO):
+                stdout = None
+                stderr = None
+            env = dict(os.environ)
+            env['NDK'] = self.ndk_path
+            subprocess.call(lit_args, env=env, stdout=stdout, stderr=stderr)
+
+    def run(self, obj_dir, dist_dir, test_filters):
+        if get_lit_cmd() is None:
+            return ndk.test.result.Failure(self, 'Could not find lit'), []
+
+        build_dir = self.get_build_dir(dist_dir)
+
+        if not os.path.exists(build_dir):
+            os.makedirs(build_dir)
+
+        xunit_output = os.path.join(build_dir, 'xunit.xml')
+        libcxx_subpath = 'sources/cxx-stl/llvm-libc++'
+        libcxx_path = os.path.join(self.ndk_path, libcxx_subpath)
+        libcxx_so_path = os.path.join(
+            libcxx_path, 'libs', self.config.abi, 'libc++_shared.so')
+        libcxx_test_path = os.path.join(libcxx_path, 'test')
+        shutil.copy2(libcxx_so_path, build_dir)
+
+        # The libc++ test runner's filters are path based. Assemble the path to
+        # the test based on the late_filters (early filters for a libc++ test
+        # would be simply "libc++", so that's not interesting at this stage).
+        filters = []
+        for late_filter in test_filters.late_filters:
+            filter_pattern = late_filter.pattern
+            if not filter_pattern.startswith('libc++.'):
+                continue
+
+            _, _, path = filter_pattern.partition('.')
+            if not os.path.isabs(path):
+                path = os.path.join(libcxx_test_path, path)
+
+            # If we have a filter like "libc++.std", we'll run everything in
+            # std, but all our XunitReport "tests" will be filtered out.  Make
+            # sure we have something usable.
+            if path.endswith('*'):
+                # But the libc++ test runner won't like that, so strip it.
+                path = path[:-1]
+            else:
+                assert os.path.isfile(path)
+
+            filters.append(path)
+        self.run_lit(build_dir, filters)
+
+        for root, _, files in os.walk(libcxx_test_path):
+            for test_file in files:
+                if not test_file.endswith('.dat'):
+                    continue
+                test_relpath = os.path.relpath(root, libcxx_test_path)
+                dest_dir = os.path.join(build_dir, test_relpath)
+                if not os.path.exists(dest_dir):
+                    continue
+
+                shutil.copy2(os.path.join(root, test_file), dest_dir)
+
+        # We create a bunch of fake tests that report the status of each
+        # individual test in the xunit report.
+        test_reports = get_xunit_reports(
+            xunit_output, self.test_dir, self.config, self.ndk_path)
+
+        return ndk.test.result.Success(self), test_reports
+
+    # pylint: disable=no-self-use
+    def check_broken(self):
+        # Actual results are reported individually by pulling them out of the
+        # xunit output. This just reports the status of the overall test run,
+        # which should be passing.
+        return None, None
+
+    def check_unsupported(self):
+        return None
+
+    def is_negative_test(self):
+        return False
+    # pylint: enable=no-self-use
+
+
+class XunitResult(Test):
+    """Fake tests so we can show a result for each libc++ test.
+
+    We create these by parsing the xunit XML output from the libc++ test
+    runner. For each result, we create an XunitResult "test" that simply
+    returns a result for the xunit status.
+
+    We don't have an ExpectedFailure form of the XunitResult because that is
+    already handled for us by the libc++ test runner.
+    """
+    def __init__(self, name, test_base_dir, test_dir, config, ndk_path):
+        super(XunitResult, self).__init__(name, test_dir, config, ndk_path)
+        self.test_base_dir = test_base_dir
+
+    def run(self, _out_dir, _dist_dir, _test_filters):
+        raise NotImplementedError
+
+    def get_test_config(self):
+        test_config_dir = os.path.join(self.test_base_dir, self.test_dir)
+        return ndk.test.config.LibcxxTestConfig.from_test_dir(test_config_dir)
+
+    def check_broken(self):
+        name = os.path.splitext(os.path.basename(self.name))[0]
+        config, bug = self.get_test_config().build_broken(
+            self.config.abi, self.config.api, name)
+        if config is not None:
+            return config, bug
+        return None, None
+
+    # pylint: disable=no-self-use
+    def check_unsupported(self):
+        return None
+
+    def is_negative_test(self):
+        return False
+    # pylint: enable=no-self-use
+
+
+class XunitSuccess(XunitResult):
+    def run(self, _out_dir, _dist_dir, _test_filters):
+        return ndk.test.result.Success(self), []
+
+
+class XunitFailure(XunitResult):
+    def __init__(self, name, test_base_dir, test_dir, text, config, ndk_path):
+        super(XunitFailure, self).__init__(
+            name, test_base_dir, test_dir, config, ndk_path)
+        self.text = text
+
+    def run(self, _out_dir, _dist_dir, _test_filters):
+        return ndk.test.result.Failure(self, self.text), []
diff --git a/tests/testlib.py b/tests/testlib.py
deleted file mode 100644
index 631f842..0000000
--- a/tests/testlib.py
+++ /dev/null
@@ -1,1187 +0,0 @@
-#
-# Copyright (C) 2015 The Android Open Source Project
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-#      http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-#
-from __future__ import absolute_import
-from __future__ import print_function
-
-import fnmatch
-import imp
-import logging
-import multiprocessing
-import os
-import random
-import re
-import shutil
-import subprocess
-import sys
-import traceback
-import xml.etree.ElementTree
-
-import ndk.abis
-import ndk.ansi
-import ndk.ext.os
-import ndk.ext.shutil
-import ndk.ext.subprocess
-import ndk.hosts
-import ndk.ndkbuild
-import ndk.paths
-import ndk.test.report
-from ndk.test.result import (Success, Failure, Skipped, ExpectedFailure,
-                             UnexpectedSuccess)
-import ndk.test.spec
-import ndk.test.ui
-import ndk.test.builder
-
-# pylint: disable=no-self-use
-
-
-ALL_SUITES = (
-    'build',
-    'device',
-    'libc++',
-)
-
-
-def logger():
-    """Return the logger for this module."""
-    return logging.getLogger(__name__)
-
-
-def _get_jobs_args():
-    cpus = multiprocessing.cpu_count()
-    return ['-j{}'.format(cpus), '-l{}'.format(cpus)]
-
-
-class TestScanner(object):
-    """Creates a Test objects for a given test directory.
-
-    A test scanner is used to turn a test directory into a list of Tests for
-    any of the test types found in the directory.
-    """
-    def find_tests(self, path, name):
-        """Searches a directory for tests.
-
-        Args:
-            path: Path to the test directory.
-            name: Name of the test.
-
-        Returns: List of Tests, possibly empty.
-        """
-        raise NotImplementedError
-
-
-class BuildTestScanner(TestScanner):
-    def __init__(self, ndk_path, dist=True):
-        self.ndk_path = ndk_path
-        self.dist = dist
-        self.build_configurations = set()
-
-    def add_build_configuration(self, abi, api):
-        self.build_configurations.add(ndk.test.spec.BuildConfiguration(
-            abi, api))
-
-    def find_tests(self, path, name):
-        # If we have a build.sh, that takes precedence over the Android.mk.
-        build_sh_path = os.path.join(path, 'build.sh')
-        if os.path.exists(build_sh_path):
-            return self.make_build_sh_tests(path, name)
-
-        # Same for test.py
-        build_sh_path = os.path.join(path, 'test.py')
-        if os.path.exists(build_sh_path):
-            return self.make_test_py_tests(path, name)
-
-        # But we can have both ndk-build and cmake tests in the same directory.
-        tests = []
-        android_mk_path = os.path.join(path, 'jni/Android.mk')
-        if os.path.exists(android_mk_path):
-            tests.extend(self.make_ndk_build_tests(path, name))
-
-        cmake_lists_path = os.path.join(path, 'CMakeLists.txt')
-        if os.path.exists(cmake_lists_path):
-            tests.extend(self.make_cmake_tests(path, name))
-        return tests
-
-    def make_build_sh_tests(self, path, name):
-        tests = []
-        for config in self.build_configurations:
-            test = ShellBuildTest(name, path, config, self.ndk_path)
-            tests.append(test)
-        return tests
-
-    def make_test_py_tests(self, path, name):
-        tests = []
-        for config in self.build_configurations:
-            test = PythonBuildTest(name, path, config, self.ndk_path)
-            tests.append(test)
-        return tests
-
-    def make_ndk_build_tests(self, path, name):
-        tests = []
-        for config in self.build_configurations:
-            test = NdkBuildTest(name, path, config, self.ndk_path, self.dist)
-            tests.append(test)
-        return tests
-
-    def make_cmake_tests(self, path, name):
-        tests = []
-        for config in self.build_configurations:
-            test = CMakeBuildTest(name, path, config, self.ndk_path, self.dist)
-            tests.append(test)
-        return tests
-
-
-class LibcxxTestScanner(TestScanner):
-    ALL_TESTS = []
-
-    def __init__(self, ndk_path):
-        self.ndk_path = ndk_path
-        self.build_configurations = set()
-        LibcxxTestScanner.find_all_libcxx_tests(self.ndk_path)
-
-    def add_build_configuration(self, abi, api):
-        self.build_configurations.add(ndk.test.spec.BuildConfiguration(
-            abi, api))
-
-    def find_tests(self, path, name):
-        tests = []
-        for config in self.build_configurations:
-            tests.append(LibcxxTest('libc++', path, config, self.ndk_path))
-        return tests
-
-    @classmethod
-    def find_all_libcxx_tests(cls, ndk_path):
-        # If we instantiate multiple LibcxxTestScanners, we still only need to
-        # initialize this once. We only create these in the main thread, so
-        # there's no risk of race.
-        if len(cls.ALL_TESTS) != 0:
-            return
-
-        test_base_dir = os.path.join(
-            ndk_path, 'sources/cxx-stl/llvm-libc++/test')
-
-        for root, _dirs, files in os.walk(test_base_dir):
-            for test_file in files:
-                if test_file.endswith('.cpp'):
-                    test_path = ndk.paths.to_posix_path(os.path.relpath(
-                        os.path.join(root, test_file), test_base_dir))
-                    cls.ALL_TESTS.append(test_path)
-
-
-def _fixup_expected_failure(result, config, bug):
-    if isinstance(result, Failure):
-        return ExpectedFailure(result.test, config, bug)
-    elif isinstance(result, Success):
-        return UnexpectedSuccess(result.test, config, bug)
-    else:  # Skipped, UnexpectedSuccess, or ExpectedFailure.
-        return result
-
-
-def _fixup_negative_test(result):
-    if isinstance(result, Failure):
-        return Success(result.test)
-    elif isinstance(result, Success):
-        return Failure(result.test, 'negative test case succeeded')
-    else:  # Skipped, UnexpectedSuccess, or ExpectedFailure.
-        return result
-
-
-def _run_test(worker, suite, test, obj_dir, dist_dir, test_filters):
-    """Runs a given test according to the given filters.
-
-    Args:
-        worker: The worker that invoked this task.
-        suite: Name of the test suite the test belongs to.
-        test: The test to be run.
-        obj_dir: Out directory for intermediate build artifacts.
-        dist_dir: Out directory for build artifacts needed for running.
-        test_filters: Filters to apply when running tests.
-
-    Returns: Tuple of (suite, TestResult, [Test]). The [Test] element is a list
-             of additional tests to be run.
-    """
-    worker.status = 'Building {}'.format(test)
-
-    config = test.check_unsupported()
-    if config is not None:
-        message = 'test unsupported for {}'.format(config)
-        return suite, Skipped(test, message), []
-
-    try:
-        result, additional_tests = test.run(obj_dir, dist_dir, test_filters)
-        if test.is_negative_test():
-            result = _fixup_negative_test(result)
-        config, bug = test.check_broken()
-        if config is not None:
-            # We need to check change each pass/fail to either an
-            # ExpectedFailure or an UnexpectedSuccess as necessary.
-            result = _fixup_expected_failure(result, config, bug)
-    except Exception:  # pylint: disable=broad-except
-        result = Failure(test, traceback.format_exc())
-        additional_tests = []
-    return suite, result, additional_tests
-
-
-class TestRunner(object):
-    def __init__(self, printer):
-        self.printer = printer
-        self.tests = {}
-        self.build_dirs = {}
-
-    def add_suite(self, name, path, test_scanner):
-        if name in self.tests:
-            raise KeyError('suite {} already exists'.format(name))
-        new_tests = self.scan_test_suite(path, test_scanner)
-        self.check_no_overlapping_build_dirs(name, new_tests)
-        self.tests[name] = new_tests
-
-    def scan_test_suite(self, suite_dir, test_scanner):
-        tests = []
-        for dentry in os.listdir(suite_dir):
-            path = os.path.join(suite_dir, dentry)
-            if os.path.isdir(path):
-                test_name = os.path.basename(path)
-                tests.extend(test_scanner.find_tests(path, test_name))
-        return tests
-
-    def check_no_overlapping_build_dirs(self, suite, new_tests):
-        for test in new_tests:
-            build_dir = test.get_build_dir('')
-            if build_dir in self.build_dirs:
-                dup_suite, dup_test = self.build_dirs[build_dir]
-                raise RuntimeError(
-                    'Found duplicate build directory:\n{} {}\n{} {}'.format(
-                        dup_suite, dup_test, suite, test))
-            self.build_dirs[build_dir] = (suite, test)
-
-    def run(self, obj_dir, dist_dir, test_filters):
-        workqueue = ndk.test.builder.LoadRestrictingWorkQueue()
-        try:
-            for suite, tests in self.tests.items():
-                # Each test configuration was expanded when each test was
-                # discovered, so the current order has all the largest tests
-                # right next to each other. Spread them out to try to avoid
-                # having too many heavy builds happening simultaneously.
-                random.shuffle(tests)
-                for test in tests:
-                    if not test_filters.filter(test.name):
-                        continue
-
-                    if test.name == 'libc++':
-                        workqueue.add_load_restricted_task(
-                            _run_test, suite, test, obj_dir, dist_dir,
-                            test_filters)
-                    else:
-                        workqueue.add_task(
-                            _run_test, suite, test, obj_dir, dist_dir,
-                            test_filters)
-
-            report = ndk.test.report.Report()
-            self.wait_for_results(
-                report, workqueue, obj_dir, dist_dir, test_filters)
-
-            return report
-        finally:
-            workqueue.terminate()
-            workqueue.join()
-
-    def wait_for_results(self, report, workqueue, obj_dir, dist_dir,
-                         test_filters):
-        console = ndk.ansi.get_console()
-        ui = ndk.test.ui.get_test_build_progress_ui(console, workqueue)
-        with ndk.ansi.disable_terminal_echo(sys.stdin):
-            with console.cursor_hide_context():
-                while not workqueue.finished():
-                    suite, result, additional_tests = workqueue.get_result()
-                    # Filtered test. Skip them entirely to avoid polluting
-                    # --show-all results.
-                    if result is None:
-                        assert len(additional_tests) == 0
-                        ui.draw()
-                        continue
-
-                    assert result.passed() or len(additional_tests) == 0
-                    for test in additional_tests:
-                        workqueue.add_task(
-                            _run_test, suite, test, obj_dir, dist_dir,
-                            test_filters)
-                    if logger().isEnabledFor(logging.INFO):
-                        ui.clear()
-                        self.printer.print_result(result)
-                    elif result.failed():
-                        ui.clear()
-                        self.printer.print_result(result)
-                    report.add_result(suite, result)
-                    ui.draw()
-                ui.clear()
-
-
-class Test(object):
-    def __init__(self, name, test_dir, config, ndk_path):
-        self.name = name
-        self.test_dir = test_dir
-        self.config = config
-        self.ndk_path = ndk_path
-
-    @property
-    def is_flaky(self):
-        return False
-
-    def get_test_config(self):
-        return TestConfig.from_test_dir(self.test_dir)
-
-    def run(self, obj_dir, dist_dir, test_filters):
-        raise NotImplementedError
-
-    def __str__(self):
-        return '{} [{}]'.format(self.name, self.config)
-
-
-def _prep_build_dir(src_dir, out_dir):
-    if os.path.exists(out_dir):
-        shutil.rmtree(out_dir)
-    shutil.copytree(src_dir, out_dir)
-
-
-class TestConfig(object):
-    """Describes the status of a test.
-
-    Each test directory can contain a "test_config.py" file that describes
-    the configurations a test is not expected to pass for. Previously this
-    information could be captured in one of two places: the Application.mk
-    file, or a BROKEN_BUILD/BROKEN_RUN file.
-
-    Application.mk was used to state that a test was only to be run for a
-    specific platform version, specific toolchain, or a set of ABIs.
-    Unfortunately Application.mk could only specify a single toolchain or
-    platform, not a set.
-
-    BROKEN_BUILD/BROKEN_RUN files were too general. An empty file meant the
-    test should always be skipped regardless of configuration. Any change that
-    would put a test in that situation should be reverted immediately. These
-    also didn't make it clear if the test was actually broken (and thus should
-    be fixed) or just not applicable.
-
-    A test_config.py file is more flexible. It is a Python module that defines
-    at least one function by the same name as one in TestConfig.NullTestConfig.
-    If a function is not defined the null implementation (not broken,
-    supported), will be used.
-    """
-
-    class NullTestConfig(object):
-        def __init__(self):
-            pass
-
-        # pylint: disable=unused-argument
-        @staticmethod
-        def build_broken(abi, platform):
-            """Tests if a given configuration is known broken.
-
-            A broken test is a known failing test that should be fixed.
-
-            Any test with a non-empty broken section requires a "bug" entry
-            with a link to either an internal bug (http://b/BUG_NUMBER) or a
-            public bug (http://b.android.com/BUG_NUMBER).
-
-            These tests will still be built and run. If the test succeeds, it
-            will be reported as an error.
-
-            Returns: A tuple of (broken_configuration, bug) or (None, None).
-            """
-            return None, None
-
-        @staticmethod
-        def build_unsupported(abi, platform):
-            """Tests if a given configuration is unsupported.
-
-            An unsupported test is a test that do not make sense to run for a
-            given configuration. Testing x86 assembler on MIPS, for example.
-
-            These tests will not be built or run.
-
-            Returns: The string unsupported_configuration or None.
-            """
-            return None
-
-        @staticmethod
-        def extra_cmake_flags():
-            return []
-
-        @staticmethod
-        def extra_ndk_build_flags():
-            """Returns extra flags that should be passed to ndk-build."""
-            return []
-
-        @staticmethod
-        def is_negative_test():
-            """Returns True if this test should pass if the build fails.
-
-            Note that this is different from build_broken. Use build_broken to
-            indicate a bug and use is_negative_test to indicate a test that
-            should fail if things are working.
-
-            Also note that check_broken and is_negative_test can be layered. If
-            a build is expected to fail, but doesn't for armeabi, the
-            test_config could contain:
-
-                def is_negative_test():
-                    return True
-
-
-                def build_broken(abi, api):
-                    if abi == 'armeabi':
-                        return abi, bug_url
-                    return None, None
-            """
-            return False
-        # pylint: enable=unused-argument
-
-    def __init__(self, file_path):
-        # Note that this namespace isn't actually meaningful from our side;
-        # it's only what the loaded module's __name__ gets set to.
-        dirname = os.path.dirname(file_path)
-        namespace = '.'.join([dirname, 'test_config'])
-
-        try:
-            self.module = imp.load_source(namespace, file_path)
-        except IOError:
-            self.module = None
-
-        try:
-            self.build_broken = self.module.build_broken
-        except AttributeError:
-            self.build_broken = self.NullTestConfig.build_broken
-
-        try:
-            self.build_unsupported = self.module.build_unsupported
-        except AttributeError:
-            self.build_unsupported = self.NullTestConfig.build_unsupported
-
-        try:
-            self.extra_cmake_flags = self.module.extra_cmake_flags
-        except AttributeError:
-            self.extra_cmake_flags = self.NullTestConfig.extra_cmake_flags
-
-        try:
-            self.extra_ndk_build_flags = self.module.extra_ndk_build_flags
-        except AttributeError:
-            ntc = self.NullTestConfig
-            self.extra_ndk_build_flags = ntc.extra_ndk_build_flags
-
-        try:
-            self.is_negative_test = self.module.is_negative_test
-        except AttributeError:
-            self.is_negative_test = self.NullTestConfig.is_negative_test
-
-    @classmethod
-    def from_test_dir(cls, test_dir):
-        path = os.path.join(test_dir, 'test_config.py')
-        return cls(path)
-
-
-class DeviceTestConfig(TestConfig):
-    """Specialization of test_config.py that includes device API level.
-
-    We need to mark some tests as broken or unsupported based on what device
-    they are running on, as opposed to just what they were built for.
-    """
-    class NullTestConfig(TestConfig.NullTestConfig):
-        # pylint: disable=unused-argument
-        @staticmethod
-        def run_broken(abi, device_api, subtest):
-            return None, None
-
-        @staticmethod
-        def run_unsupported(abi, device_api, subtest):
-            return None
-
-        @staticmethod
-        def extra_cmake_flags():
-            return []
-        # pylint: enable=unused-argument
-
-    def __init__(self, file_path):
-        super(DeviceTestConfig, self).__init__(file_path)
-
-        try:
-            self.run_broken = self.module.run_broken
-        except AttributeError:
-            self.run_broken = self.NullTestConfig.run_broken
-
-        try:
-            self.run_unsupported = self.module.run_unsupported
-        except AttributeError:
-            self.run_unsupported = self.NullTestConfig.run_unsupported
-
-        if hasattr(self.module, 'is_negative_test'):
-            # If the build is expected to fail, then it should just be a build
-            # test since the test should never be run.
-            #
-            # If the run is expected to fail, just fix the test to pass for
-            # thatr case. Gtest death tests can handle the more complicated
-            # cases.
-            raise RuntimeError('is_negative_test is invalid for device tests')
-
-
-def _run_build_sh_test(test, build_dir, test_dir, ndk_path, ndk_build_flags,
-                       abi, platform):
-    _prep_build_dir(test_dir, build_dir)
-    with ndk.ext.os.cd(build_dir):
-        build_cmd = ['bash', 'build.sh'] + _get_jobs_args() + ndk_build_flags
-        test_env = dict(os.environ)
-        test_env['NDK'] = ndk_path
-        if abi is not None:
-            test_env['APP_ABI'] = abi
-        test_env['APP_PLATFORM'] = 'android-{}'.format(platform)
-        rc, out = ndk.ext.subprocess.call_output(build_cmd, env=test_env)
-        if rc == 0:
-            return Success(test)
-        else:
-            return Failure(test, out)
-
-
-def _run_ndk_build_test(test, obj_dir, dist_dir, test_dir, ndk_path,
-                        ndk_build_flags, abi, platform):
-    _prep_build_dir(test_dir, obj_dir)
-    with ndk.ext.os.cd(obj_dir):
-        args = [
-            'APP_ABI=' + abi,
-            'NDK_LIBS_OUT=' + dist_dir,
-        ]
-        args.extend(_get_jobs_args())
-        if platform is not None:
-            args.append('APP_PLATFORM=android-{}'.format(platform))
-        rc, out = ndk.ndkbuild.build(ndk_path, args + ndk_build_flags)
-        if rc == 0:
-            return Success(test)
-        else:
-            return Failure(test, out)
-
-
-def _run_cmake_build_test(test, obj_dir, dist_dir, test_dir, ndk_path,
-                          cmake_flags, abi, platform):
-    _prep_build_dir(test_dir, obj_dir)
-
-    # Add prebuilts to PATH.
-    prebuilts_host_tag = ndk.hosts.get_default_host() + '-x86'
-    prebuilts_bin = ndk.paths.android_path(
-        'prebuilts', 'cmake', prebuilts_host_tag, 'bin')
-    env_path = prebuilts_bin + os.pathsep + os.environ['PATH']
-
-    # Fail if we don't have a working cmake executable, either from the
-    # prebuilts, or from the SDK, or if a new enough version is installed.
-    cmake_bin = ndk.ext.shutil.which('cmake', path=env_path)
-    if cmake_bin is None:
-        return Failure(test, 'cmake executable not found')
-
-    out = subprocess.check_output([cmake_bin, '--version']).decode('utf-8')
-    version_pattern = r'cmake version (\d+)\.(\d+)\.'
-    version = [int(v) for v in re.match(version_pattern, out).groups()]
-    if version < [3, 6]:
-        return Failure(test, 'cmake 3.6 or above required')
-
-    # Also require a working ninja executable.
-    ninja_bin = ndk.ext.shutil.which('ninja', path=env_path)
-    if ninja_bin is None:
-        return Failure(test, 'ninja executable not found')
-    rc, _ = ndk.ext.subprocess.call_output([ninja_bin, '--version'])
-    if rc != 0:
-        return Failure(test, 'ninja --version failed')
-
-    toolchain_file = os.path.join(ndk_path, 'build', 'cmake',
-                                  'android.toolchain.cmake')
-    objs_dir = os.path.join(obj_dir, abi)
-    libs_dir = os.path.join(dist_dir, abi)
-    args = [
-        '-H' + obj_dir,
-        '-B' + objs_dir,
-        '-DCMAKE_TOOLCHAIN_FILE=' + toolchain_file,
-        '-DANDROID_ABI=' + abi,
-        '-DCMAKE_RUNTIME_OUTPUT_DIRECTORY=' + libs_dir,
-        '-DCMAKE_LIBRARY_OUTPUT_DIRECTORY=' + libs_dir,
-        '-GNinja',
-        '-DCMAKE_MAKE_PROGRAM=' + ninja_bin,
-    ]
-    if platform is not None:
-        args.append('-DANDROID_PLATFORM=android-{}'.format(platform))
-    rc, out = ndk.ext.subprocess.call_output(
-        [cmake_bin] + cmake_flags + args)
-    if rc != 0:
-        return Failure(test, out)
-    rc, out = ndk.ext.subprocess.call_output(
-        [cmake_bin, '--build', objs_dir, '--'] + _get_jobs_args())
-    if rc != 0:
-        return Failure(test, out)
-    return Success(test)
-
-
-class BuildTest(Test):
-    def __init__(self, name, test_dir, config, ndk_path):
-        super(BuildTest, self).__init__(name, test_dir, config, ndk_path)
-
-        if self.api is None:
-            raise ValueError
-
-    @property
-    def abi(self):
-        return self.config.abi
-
-    @property
-    def api(self):
-        return self.config.api
-
-    @property
-    def platform(self):
-        return self.api
-
-    @property
-    def ndk_build_flags(self):
-        flags = self.config.get_extra_ndk_build_flags()
-        if flags is None:
-            flags = []
-        return flags + self.get_extra_ndk_build_flags()
-
-    @property
-    def cmake_flags(self):
-        flags = self.config.get_extra_cmake_flags()
-        if flags is None:
-            flags = []
-        return flags + self.get_extra_cmake_flags()
-
-    def run(self, obj_dir, dist_dir, _test_filters):
-        raise NotImplementedError
-
-    def check_broken(self):
-        return self.get_test_config().build_broken(self.abi, self.platform)
-
-    def check_unsupported(self):
-        return self.get_test_config().build_unsupported(
-            self.abi, self.platform)
-
-    def is_negative_test(self):
-        return self.get_test_config().is_negative_test()
-
-    def get_extra_cmake_flags(self):
-        return self.get_test_config().extra_cmake_flags()
-
-    def get_extra_ndk_build_flags(self):
-        return self.get_test_config().extra_ndk_build_flags()
-
-
-class PythonBuildTest(BuildTest):
-    """A test that is implemented by test.py.
-
-    A test.py test has a test.py file in its root directory. This module
-    contains a run_test function which returns a tuple of `(boolean_success,
-    string_failure_message)` and takes the following kwargs (all of which
-    default to None):
-
-    abi: ABI to test as a string.
-    platform: Platform to build against as a string.
-    ndk_build_flags: Additional build flags that should be passed to ndk-build
-                     if invoked as a list of strings.
-    """
-    def __init__(self, name, test_dir, config, ndk_path):
-        api = config.api
-        if api is None:
-            api = ndk.abis.min_api_for_abi(config.abi)
-        config = ndk.test.spec.BuildConfiguration(config.abi, api)
-        super(PythonBuildTest, self).__init__(name, test_dir, config, ndk_path)
-
-        if self.abi not in ndk.abis.ALL_ABIS:
-            raise ValueError('{} is not a valid ABI'.format(self.abi))
-
-        try:
-            int(self.platform)
-        except ValueError:
-            raise ValueError(
-                '{} is not a valid platform number'.format(self.platform))
-
-        # Not a ValueError for this one because it should be impossible. This
-        # is actually a computed result from the config we're passed.
-        assert self.ndk_build_flags is not None
-
-    def get_build_dir(self, out_dir):
-        return os.path.join(out_dir, str(self.config), 'test.py', self.name)
-
-    def run(self, obj_dir, _dist_dir, _test_filters):
-        build_dir = self.get_build_dir(obj_dir)
-        logger().info('Building test: %s', self.name)
-        _prep_build_dir(self.test_dir, build_dir)
-        with ndk.ext.os.cd(build_dir):
-            module = imp.load_source('test', 'test.py')
-            success, failure_message = module.run_test(
-                self.ndk_path, self.abi, self.platform, self.ndk_build_flags)
-            if success:
-                return Success(self), []
-            else:
-                return Failure(self, failure_message), []
-
-
-class ShellBuildTest(BuildTest):
-    def __init__(self, name, test_dir, config, ndk_path):
-        api = config.api
-        if api is None:
-            api = ndk.abis.min_api_for_abi(config.abi)
-        config = ndk.test.spec.BuildConfiguration(config.abi, api)
-        super(ShellBuildTest, self).__init__(name, test_dir, config, ndk_path)
-
-    def get_build_dir(self, out_dir):
-        return os.path.join(out_dir, str(self.config), 'build.sh', self.name)
-
-    def run(self, obj_dir, _dist_dir, _test_filters):
-        build_dir = self.get_build_dir(obj_dir)
-        logger().info('Building test: %s', self.name)
-        if os.name == 'nt':
-            reason = 'build.sh tests are not supported on Windows'
-            return Skipped(self, reason), []
-        else:
-            result = _run_build_sh_test(
-                self, build_dir, self.test_dir, self.ndk_path,
-                self.ndk_build_flags, self.abi, self.platform)
-            return result, []
-
-
-def _platform_from_application_mk(test_dir):
-    """Determine target API level from a test's Application.mk.
-
-    Args:
-        test_dir: Directory of the test to read.
-
-    Returns:
-        Integer portion of APP_PLATFORM if found, else None.
-
-    Raises:
-        ValueError: Found an unexpected value for APP_PLATFORM.
-    """
-    application_mk = os.path.join(test_dir, 'jni/Application.mk')
-    if not os.path.exists(application_mk):
-        return None
-
-    with open(application_mk) as application_mk_file:
-        for line in application_mk_file:
-            if line.startswith('APP_PLATFORM'):
-                _, platform_str = line.split(':=')
-                break
-        else:
-            return None
-
-    platform_str = platform_str.strip()
-    if not platform_str.startswith('android-'):
-        raise ValueError(platform_str)
-
-    _, api_level_str = platform_str.split('-')
-    return int(api_level_str)
-
-
-def _get_or_infer_app_platform(platform_from_user, test_dir, abi):
-    """Determines the platform level to use for a test using ndk-build.
-
-    Choose the platform level from, in order of preference:
-    1. Value given as argument.
-    2. APP_PLATFORM from jni/Application.mk.
-    3. Default value for the target ABI.
-
-    Args:
-        platform_from_user: A user provided platform level or None.
-        test_dir: The directory containing the ndk-build project.
-        abi: The ABI being targeted.
-
-    Returns:
-        The platform version the test should build against.
-    """
-    if platform_from_user is not None:
-        return platform_from_user
-
-    minimum_version = ndk.abis.min_api_for_abi(abi)
-    platform_from_application_mk = _platform_from_application_mk(test_dir)
-    if platform_from_application_mk is not None:
-        if platform_from_application_mk >= minimum_version:
-            return platform_from_application_mk
-
-    return minimum_version
-
-
-class NdkBuildTest(BuildTest):
-    def __init__(self, name, test_dir, config, ndk_path, dist):
-        api = _get_or_infer_app_platform(config.api, test_dir, config.abi)
-        config = ndk.test.spec.BuildConfiguration(config.abi, api)
-        super(NdkBuildTest, self).__init__(name, test_dir, config, ndk_path)
-        self.dist = dist
-
-    def get_dist_dir(self, obj_dir, dist_dir):
-        if self.dist:
-            return self.get_build_dir(dist_dir)
-        else:
-            return os.path.join(self.get_build_dir(obj_dir), 'dist')
-
-    def get_build_dir(self, out_dir):
-        return os.path.join(out_dir, str(self.config), 'ndk-build', self.name)
-
-    def run(self, obj_dir, dist_dir, _test_filters):
-        logger().info('Building test: %s', self.name)
-        obj_dir = self.get_build_dir(obj_dir)
-        dist_dir = self.get_dist_dir(obj_dir, dist_dir)
-        result = _run_ndk_build_test(
-            self, obj_dir, dist_dir, self.test_dir, self.ndk_path,
-            self.ndk_build_flags, self.abi, self.platform)
-        return result, []
-
-
-class CMakeBuildTest(BuildTest):
-    def __init__(self, name, test_dir, config, ndk_path, dist):
-        api = _get_or_infer_app_platform(config.api, test_dir, config.abi)
-        config = ndk.test.spec.BuildConfiguration(config.abi, api)
-        super(CMakeBuildTest, self).__init__(name, test_dir, config, ndk_path)
-        self.dist = dist
-
-    def get_dist_dir(self, obj_dir, dist_dir):
-        if self.dist:
-            return self.get_build_dir(dist_dir)
-        else:
-            return os.path.join(self.get_build_dir(obj_dir), 'dist')
-
-    def get_build_dir(self, out_dir):
-        return os.path.join(out_dir, str(self.config), 'cmake', self.name)
-
-    def run(self, obj_dir, dist_dir, _test_filters):
-        obj_dir = self.get_build_dir(obj_dir)
-        dist_dir = self.get_dist_dir(obj_dir, dist_dir)
-        logger().info('Building test: %s', self.name)
-        result = _run_cmake_build_test(
-            self, obj_dir, dist_dir, self.test_dir, self.ndk_path,
-            self.cmake_flags, self.abi, self.platform)
-        return result, []
-
-
-def get_xunit_reports(xunit_file, test_base_dir, config, ndk_path):
-    tree = xml.etree.ElementTree.parse(xunit_file)
-    root = tree.getroot()
-    cases = root.findall('.//testcase')
-
-    reports = []
-    for test_case in cases:
-        mangled_test_dir = test_case.get('classname')
-
-        # The classname is the path from the root of the libc++ test directory
-        # to the directory containing the test (prefixed with 'libc++.')...
-        mangled_path = '/'.join([mangled_test_dir, test_case.get('name')])
-
-        # ... that has had '.' in its path replaced with '_' because xunit.
-        test_matches = find_original_libcxx_test(mangled_path, ndk_path)
-        if len(test_matches) == 0:
-            raise RuntimeError('Found no matches for test ' + mangled_path)
-        if len(test_matches) > 1:
-            raise RuntimeError('Found multiple matches for test {}: {}'.format(
-                mangled_path, test_matches))
-        assert len(test_matches) == 1
-
-        # We found a unique path matching the xunit class/test name.
-        name = test_matches[0]
-        test_dir = os.path.dirname(name)[len('libc++.'):]
-
-        failure_nodes = test_case.findall('failure')
-        if len(failure_nodes) == 0:
-            reports.append(XunitSuccess(
-                name, test_base_dir, test_dir, config, ndk_path))
-            continue
-
-        if len(failure_nodes) != 1:
-            msg = ('Could not parse XUnit output: test case does not have a '
-                   'unique failure node: {}'.format(name))
-            raise RuntimeError(msg)
-
-        failure_node = failure_nodes[0]
-        failure_text = failure_node.text
-        reports.append(XunitFailure(
-            name, test_base_dir, test_dir, failure_text, config, ndk_path))
-    return reports
-
-
-def get_lit_cmd():
-    # The build server doesn't install lit to a virtualenv, so use it from the
-    # source location if possible.
-    lit_path = ndk.paths.android_path('external/llvm/utils/lit/lit.py')
-    if os.path.exists(lit_path):
-        return ['python', lit_path]
-    elif ndk.ext.shutil.which('lit'):
-        return ['lit']
-    return None
-
-
-class LibcxxTest(Test):
-    def __init__(self, name, test_dir, config, ndk_path):
-        if config.api is None:
-            config.api = ndk.abis.min_api_for_abi(config.abi)
-
-        super(LibcxxTest, self).__init__(name, test_dir, config, ndk_path)
-
-    @property
-    def abi(self):
-        return self.config.abi
-
-    @property
-    def api(self):
-        return self.config.api
-
-    def get_build_dir(self, out_dir):
-        return os.path.join(out_dir, str(self.config), 'libcxx', self.name)
-
-    def run_lit(self, build_dir, filters):
-        libcxx_dir = os.path.join(self.ndk_path, 'sources/cxx-stl/llvm-libc++')
-        device_dir = '/data/local/tmp/libcxx'
-
-        arch = ndk.abis.abi_to_arch(self.abi)
-        host_tag = ndk.hosts.get_host_tag(self.ndk_path)
-        triple = ndk.abis.arch_to_triple(arch)
-        toolchain = ndk.abis.arch_to_toolchain(arch)
-
-        replacements = [
-            ('abi', self.abi),
-            ('api', self.api),
-            ('arch', arch),
-            ('host_tag', host_tag),
-            ('toolchain', toolchain),
-            ('triple', '{}{}'.format(triple, self.api)),
-            ('use_pie', True),
-            ('build_dir', build_dir),
-        ]
-        lit_cfg_args = []
-        for key, value in replacements:
-            lit_cfg_args.append('--param={}={}'.format(key, value))
-
-        shutil.copy2(os.path.join(libcxx_dir, 'test/lit.ndk.cfg.in'),
-                     os.path.join(libcxx_dir, 'test/lit.site.cfg'))
-
-        xunit_output = os.path.join(build_dir, 'xunit.xml')
-
-        lit_args = get_lit_cmd() + [
-            '-sv',
-            '--param=device_dir=' + device_dir,
-            '--param=unified_headers=True',
-            '--param=build_only=True',
-            '--no-progress-bar',
-            '--show-all',
-            '--xunit-xml-output=' + xunit_output,
-        ] + lit_cfg_args
-
-        default_test_path = os.path.join(libcxx_dir, 'test')
-        test_paths = list(filters)
-        if len(test_paths) == 0:
-            test_paths.append(default_test_path)
-        for test_path in test_paths:
-            lit_args.append(test_path)
-
-        # Ignore the exit code. We do most XFAIL processing outside the test
-        # runner so expected failures in the test runner will still cause a
-        # non-zero exit status. This "test" only fails if we encounter a Python
-        # exception. Exceptions raised from our code are already caught by the
-        # test runner. If that happens in LIT, the xunit output will not be
-        # valid and we'll fail get_xunit_reports and raise an exception anyway.
-        with open(os.devnull, 'w') as dev_null:
-            stdout = dev_null
-            stderr = dev_null
-            if logger().isEnabledFor(logging.INFO):
-                stdout = None
-                stderr = None
-            env = dict(os.environ)
-            env['NDK'] = self.ndk_path
-            subprocess.call(lit_args, env=env, stdout=stdout, stderr=stderr)
-
-    def run(self, obj_dir, dist_dir, test_filters):
-        if get_lit_cmd() is None:
-            return Failure(self, 'Could not find lit'), []
-
-        build_dir = self.get_build_dir(dist_dir)
-
-        if not os.path.exists(build_dir):
-            os.makedirs(build_dir)
-
-        xunit_output = os.path.join(build_dir, 'xunit.xml')
-        libcxx_subpath = 'sources/cxx-stl/llvm-libc++'
-        libcxx_path = os.path.join(self.ndk_path, libcxx_subpath)
-        libcxx_so_path = os.path.join(
-            libcxx_path, 'libs', self.config.abi, 'libc++_shared.so')
-        libcxx_test_path = os.path.join(libcxx_path, 'test')
-        shutil.copy2(libcxx_so_path, build_dir)
-
-        # The libc++ test runner's filters are path based. Assemble the path to
-        # the test based on the late_filters (early filters for a libc++ test
-        # would be simply "libc++", so that's not interesting at this stage).
-        filters = []
-        for late_filter in test_filters.late_filters:
-            filter_pattern = late_filter.pattern
-            if not filter_pattern.startswith('libc++.'):
-                continue
-
-            _, _, path = filter_pattern.partition('.')
-            if not os.path.isabs(path):
-                path = os.path.join(libcxx_test_path, path)
-
-            # If we have a filter like "libc++.std", we'll run everything in
-            # std, but all our XunitReport "tests" will be filtered out.  Make
-            # sure we have something usable.
-            if path.endswith('*'):
-                # But the libc++ test runner won't like that, so strip it.
-                path = path[:-1]
-            else:
-                assert os.path.isfile(path)
-
-            filters.append(path)
-        self.run_lit(build_dir, filters)
-
-        for root, _, files in os.walk(libcxx_test_path):
-            for test_file in files:
-                if not test_file.endswith('.dat'):
-                    continue
-                test_relpath = os.path.relpath(root, libcxx_test_path)
-                dest_dir = os.path.join(build_dir, test_relpath)
-                if not os.path.exists(dest_dir):
-                    continue
-
-                shutil.copy2(os.path.join(root, test_file), dest_dir)
-
-        # We create a bunch of fake tests that report the status of each
-        # individual test in the xunit report.
-        test_reports = get_xunit_reports(
-            xunit_output, self.test_dir, self.config, self.ndk_path)
-
-        return Success(self), test_reports
-
-    def check_broken(self):
-        # Actual results are reported individually by pulling them out of the
-        # xunit output. This just reports the status of the overall test run,
-        # which should be passing.
-        return None, None
-
-    def check_unsupported(self):
-        return None
-
-    def is_negative_test(self):
-        return False
-
-
-class LibcxxTestConfig(DeviceTestConfig):
-    """Specialization of test_config.py for libc++.
-
-    The libc++ tests have multiple tests in a single directory, so we need to
-    pass the test name for build_broken too.
-    """
-    class NullTestConfig(TestConfig.NullTestConfig):
-        # pylint: disable=unused-argument,arguments-differ
-        @staticmethod
-        def build_unsupported(abi, api, name):
-            return None
-
-        @staticmethod
-        def build_broken(abi, api, name):
-            return None, None
-
-        @staticmethod
-        def run_unsupported(abi, device_api, name):
-            return None
-
-        @staticmethod
-        def run_broken(abi, device_api, name):
-            return None, None
-        # pylint: enable=unused-argument,arguments-differ
-
-
-def find_original_libcxx_test(name, ndk_path):
-    """Finds the original libc++ test file given the xunit test name.
-
-    LIT mangles test names to replace all periods with underscores because
-    xunit. This returns all tests that could possibly match the xunit test
-    name.
-    """
-
-    name = ndk.paths.to_posix_path(name)
-
-    # LIT special cases tests in the root of the test directory (such as
-    # test/nothing_to_do.pass.cpp) as "libc++.libc++/$TEST_FILE.pass.cpp" for
-    # some reason. Strip it off so we can find the tests.
-    if name.startswith('libc++.libc++/'):
-        name = 'libc++.' + name[len('libc++.libc++/'):]
-
-    test_prefix = 'libc++.'
-    if not name.startswith(test_prefix):
-        raise ValueError('libc++ test name must begin with "libc++."')
-
-    name = name[len(test_prefix):]
-    test_pattern = name.replace('_', '?')
-    matches = []
-
-    # On Windows, a multiprocessing worker process does not inherit ALL_TESTS,
-    # so we must scan libc++ tests in each worker.
-    LibcxxTestScanner.find_all_libcxx_tests(ndk_path)
-
-    for match in fnmatch.filter(LibcxxTestScanner.ALL_TESTS, test_pattern):
-        matches.append(test_prefix + match)
-    return matches
-
-
-class XunitResult(Test):
-    """Fake tests so we can show a result for each libc++ test.
-
-    We create these by parsing the xunit XML output from the libc++ test
-    runner. For each result, we create an XunitResult "test" that simply
-    returns a result for the xunit status.
-
-    We don't have an ExpectedFailure form of the XunitResult because that is
-    already handled for us by the libc++ test runner.
-    """
-    def __init__(self, name, test_base_dir, test_dir, config, ndk_path):
-        super(XunitResult, self).__init__(name, test_dir, config, ndk_path)
-        self.test_base_dir = test_base_dir
-
-    def run(self, _out_dir, _dist_dir, _test_filters):
-        raise NotImplementedError
-
-    @property
-    def is_flaky(self):
-        return True
-
-    def get_test_config(self):
-        test_config_dir = os.path.join(self.test_base_dir, self.test_dir)
-        return LibcxxTestConfig.from_test_dir(test_config_dir)
-
-    def check_broken(self):
-        name = os.path.splitext(os.path.basename(self.name))[0]
-        config, bug = self.get_test_config().build_broken(
-            self.config.abi, self.config.api, name)
-        if config is not None:
-            return config, bug
-        return None, None
-
-    def check_unsupported(self):
-        return None
-
-    def is_negative_test(self):
-        return False
-
-
-class XunitSuccess(XunitResult):
-    def run(self, _out_dir, _dist_dir, _test_filters):
-        return Success(self), []
-
-
-class XunitFailure(XunitResult):
-    def __init__(self, name, test_base_dir, test_dir, text, config, ndk_path):
-        super(XunitFailure, self).__init__(
-            name, test_base_dir, test_dir, config, ndk_path)
-        self.text = text
-
-    def run(self, _out_dir, _dist_dir, _test_filters):
-        return Failure(self, self.text), []