blob: ca0763bbe6d1f4b13e409b3db04552f7ec525abf [file] [log] [blame]
#
# 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 difflib
import distutils.spawn
import filecmp
import glob
import imp
import multiprocessing
import os
import posixpath
import re
import shutil
import subprocess
import time
import build.lib.build_support
import ndk.workqueue as wq
import tests.ndk as ndk
import tests.util as util
# pylint: disable=no-self-use
def _get_jobs_arg():
return '-j{}'.format(multiprocessing.cpu_count() * 2)
def _make_subtest_name(test, case):
return '.'.join([test, case])
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 BuildConfiguration(object):
def __init__(self, abi, api, toolchain, force_pie, verbose):
self.abi = abi
self.api = api
self.toolchain = toolchain
self.force_pie = force_pie
self.verbose = verbose
def __eq__(self, other):
if self.abi != other.abi:
return False
if self.api != other.api:
return False
if self.toolchain != other.toolchain:
return False
if self.force_pie != other.force_pie:
return False
if self.verbose != other.verbose:
return False
return True
def get_extra_ndk_build_flags(self):
extra_flags = []
if self.force_pie:
extra_flags.append('APP_PIE=true')
if self.verbose:
extra_flags.append('V=1')
return extra_flags
def get_extra_cmake_flags(self):
extra_flags = []
if self.force_pie:
extra_flags.append('-DANDROID_PIE=TRUE')
if self.verbose:
extra_flags.append('-DCMAKE_VERBOSE_MAKEFILE=ON')
return extra_flags
class DeviceConfiguration(BuildConfiguration):
def __init__(self, abi, api, toolchain, force_pie, verbose, device,
device_api, skip_run):
super(DeviceConfiguration, self).__init__(
abi, api, toolchain, force_pie, verbose)
self.device = device
self.device_api = device_api
self.skip_run = skip_run
def __eq__(self, other):
if not super(DeviceConfiguration, self).__eq__(other):
return False
if self.device != other.device:
return False
if self.device_api != other.device_api:
return False
if self.skip_run != other.skip_run:
return False
return True
class AwkTestScanner(TestScanner):
def find_tests(self, path, name):
script_name = name + '.awk'
script = os.path.join(ndk.NDK_ROOT, 'build/awk', script_name)
if not os.path.isfile(script):
msg = '{} missing test script: {}'.format(name, script)
raise RuntimeError(msg)
tests = []
for test_case in glob.glob(os.path.join(path, '*.in')):
golden_path = re.sub(r'\.in$', '.out', test_case)
if not os.path.isfile(golden_path):
msg = '{} missing output: {}'.format(name, golden_path)
raise RuntimeError(msg)
case_name = os.path.splitext(os.path.basename(test_case))[0]
full_case_name = _make_subtest_name(name, case_name)
tests.append(
AwkTest(full_case_name, path, script, test_case, golden_path))
return tests
class BuildTestScanner(TestScanner):
def __init__(self):
self.build_configurations = set()
def add_build_configuration(self, abi, api, toolchain, force_pie, verbose):
self.build_configurations.add(BuildConfiguration(
abi, api, toolchain, force_pie, verbose))
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:
tests.append(ShellBuildTest(
name, path, config.abi, config.api, config.toolchain,
config.get_extra_ndk_build_flags()))
return tests
def make_test_py_tests(self, path, name):
tests = []
for config in self.build_configurations:
tests.append(PythonBuildTest(
name, path, config.abi, config.api, config.toolchain,
config.get_extra_ndk_build_flags()))
return tests
def make_ndk_build_tests(self, path, name):
tests = []
for config in self.build_configurations:
tests.append(NdkBuildTest(
name, path, config.abi, config.api, config.toolchain,
config.get_extra_ndk_build_flags()))
return tests
def make_cmake_tests(self, path, name):
tests = []
for config in self.build_configurations:
tests.append(CMakeBuildTest(
name, path, config.abi, config.api, config.toolchain,
config.get_extra_cmake_flags()))
return tests
class DeviceTestScanner(TestScanner):
def __init__(self):
self.device_configurations = set()
def add_device_configuration(self, abi, api, toolchain, force_pie, verbose,
device, device_api, skip_run):
self.device_configurations.add(DeviceConfiguration(
abi, api, toolchain, force_pie, verbose, device, device_api,
skip_run))
def find_tests(self, path, name):
# If we have a build.sh, that takes precedence over the Android.mk.
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_ndk_build_tests(self, path, name):
tests = []
for config in self.device_configurations:
tests.append(NdkBuildDeviceTest(
name, path, config.abi, config.api, config.toolchain,
config.get_extra_ndk_build_flags(), config.device,
config.device_api, config.skip_run))
return tests
def make_cmake_tests(self, path, name):
tests = []
for config in self.device_configurations:
tests.append(CMakeDeviceTest(
name, path, config.abi, config.api, config.toolchain,
config.get_extra_cmake_flags(), config.device,
config.device_api, config.skip_run))
return tests
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, Failure):
return ExpectedFailure(result.test_name, config, bug)
elif isinstance(result, Success):
return UnexpectedSuccess(result.test_name, config, bug)
else: # Skipped, UnexpectedSuccess, or ExpectedFailure.
return result
def _fixup_negative_test(result):
if isinstance(result, Failure):
return Success(result.test_name)
elif isinstance(result, Success):
return Failure(result.test_name, 'negative test case succeeded')
else: # Skipped, UnexpectedSuccess, or ExpectedFailure.
return result
def _run_test(suite, test, out_dir, test_filters):
"""Runs a given test according to the given filters.
Args:
suite: Name of the test suite the test belongs to.
test: The test to be run.
out_dir: Out directory for building tests.
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.
"""
if not test_filters.filter(test.name):
return suite, None, []
config = test.check_unsupported()
if config is not None:
message = 'test unsupported for {}'.format(config)
return suite, Skipped(test.name, message), []
result, additional_tests = test.run(out_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)
return suite, result, additional_tests
class TestRunner(object):
def __init__(self, printer):
self.printer = printer
self.tests = {}
def add_suite(self, name, path, test_scanner):
if name in self.tests:
raise KeyError('suite {} already exists'.format(name))
self.tests[name] = _scan_test_suite(path, test_scanner)
def run(self, out_dir, test_filters):
workqueue = wq.WorkQueue()
try:
for suite, tests in self.tests.items():
for test in tests:
workqueue.add_task(
_run_test, suite, test, out_dir, test_filters)
results = {suite: [] for suite in self.tests.keys()}
while not workqueue.finished():
suite, result, additional_tests = workqueue.get_result()
for test in additional_tests:
workqueue.add_task(
_run_test, suite, test, out_dir, test_filters)
# Filtered test. Skip them entirely to avoid polluting
# --show-all results.
if result is None:
continue
results[suite].append(result)
self.printer.print_result(result)
return results
finally:
workqueue.terminate()
workqueue.join()
class TestResult(object):
def __init__(self, test_name):
self.test_name = test_name
def __repr__(self):
return self.to_string(colored=False)
def passed(self):
raise NotImplementedError
def failed(self):
raise NotImplementedError
def to_string(self, colored=False):
raise NotImplementedError
class Failure(TestResult):
def __init__(self, test_name, message):
super(Failure, self).__init__(test_name)
self.message = message
def passed(self):
return False
def failed(self):
return True
def to_string(self, colored=False):
label = util.maybe_color('FAIL', 'red', colored)
return '{} {}: {}'.format(label, self.test_name, self.message)
class Success(TestResult):
def passed(self):
return True
def failed(self):
return False
def to_string(self, colored=False):
label = util.maybe_color('PASS', 'green', colored)
return '{} {}'.format(label, self.test_name)
class Skipped(TestResult):
def __init__(self, test_name, reason):
super(Skipped, self).__init__(test_name)
self.reason = reason
def passed(self):
return False
def failed(self):
return False
def to_string(self, colored=False):
label = util.maybe_color('SKIP', 'yellow', colored)
return '{} {}: {}'.format(label, self.test_name, self.reason)
class ExpectedFailure(TestResult):
def __init__(self, test_name, config, bug):
super(ExpectedFailure, self).__init__(test_name)
self.config = config
self.bug = bug
def passed(self):
return True
def failed(self):
return False
def to_string(self, colored=False):
label = util.maybe_color('KNOWN FAIL', 'yellow', colored)
return '{} {}: known failure for {} ({})'.format(
label, self.test_name, self.config, self.bug)
class UnexpectedSuccess(TestResult):
def __init__(self, test_name, config, bug):
super(UnexpectedSuccess, self).__init__(test_name)
self.config = config
self.bug = bug
def passed(self):
return False
def failed(self):
return True
def to_string(self, colored=False):
label = util.maybe_color('SHOULD FAIL', 'red', colored)
return '{} {}: unexpected success for {} ({})'.format(
label, self.test_name, self.config, self.bug)
class Test(object):
def __init__(self, name, test_dir):
self.name = name
self.test_dir = test_dir
def get_test_config(self):
return TestConfig.from_test_dir(self.test_dir)
def run(self, out_dir, test_filters):
raise NotImplementedError
class AwkTest(Test):
def __init__(self, name, test_dir, script, input_path, golden_out_path):
super(AwkTest, self).__init__(name, test_dir)
self.script = script
self.input_path = input_path
self.golden_out_path = golden_out_path
# Awk tests only run in a single configuration. Disabling them per ABI,
# platform, or toolchain has no meaning. Stub out the checks.
def check_broken(self):
return None, None
def check_unsupported(self):
return None
def is_negative_test(self):
return False
def run(self, out_dir, test_filters):
# We need a subdirectory named for our test to handle the case where
# multiple awk tests share names for test cases. If run simultaneously,
# the outputs will collide.
out_path = os.path.join(
out_dir, 'awk', self.name, os.path.basename(self.golden_out_path))
test_out_dir = os.path.dirname(out_path)
if not os.path.exists(test_out_dir):
os.makedirs(test_out_dir)
with open(self.input_path) as test_in, open(out_path, 'w') as out_file:
awk_path = ndk.get_tool('awk')
print('{} -f {} < {} > {}'.format(
awk_path, self.script, self.input_path, out_path))
rc = subprocess.call([awk_path, '-f', self.script], stdin=test_in,
stdout=out_file)
if rc != 0:
return Failure(self.name, 'awk failed'), []
if filecmp.cmp(out_path, self.golden_out_path):
return Success(self.name), []
else:
with open(out_path) as out_file:
out_lines = out_file.readlines()
with open(self.golden_out_path) as golden_out_file:
golden_lines = golden_out_file.readlines()
diff = ''.join(difflib.unified_diff(
golden_lines, out_lines, fromfile='expected', tofile='actual'))
message = 'output does not match expected:\n\n' + diff
return Failure(self.name, message), []
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, toolchain):
"""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, toolchain):
"""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, toolchain):
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, toolchain, subtest):
return None, None
@staticmethod
def run_unsupported(abi, device_api, toolchain, 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
try:
_ = 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')
except AttributeError:
pass
def _run_build_sh_test(test_name, build_dir, test_dir, ndk_build_flags, abi,
platform, toolchain):
_prep_build_dir(test_dir, build_dir)
with util.cd(build_dir):
build_cmd = ['bash', 'build.sh', _get_jobs_arg()] + ndk_build_flags
test_env = dict(os.environ)
if abi is not None:
test_env['APP_ABI'] = abi
test_env['APP_PLATFORM'] = 'android-{}'.format(platform)
assert toolchain is not None
test_env['NDK_TOOLCHAIN_VERSION'] = toolchain
rc, out = util.call_output(build_cmd, env=test_env)
if rc == 0:
return Success(test_name)
else:
return Failure(test_name, out)
def _run_ndk_build_test(test_name, build_dir, test_dir, ndk_build_flags, abi,
platform, toolchain):
_prep_build_dir(test_dir, build_dir)
with util.cd(build_dir):
args = [
'APP_ABI=' + abi,
'NDK_TOOLCHAIN_VERSION=' + toolchain,
_get_jobs_arg(),
]
if platform is not None:
args.append('APP_PLATFORM=android-{}'.format(platform))
rc, out = ndk.build(ndk_build_flags + args)
if rc == 0:
return Success(test_name)
else:
return Failure(test_name, out)
def _run_cmake_build_test(test_name, build_dir, test_dir, cmake_flags, abi,
platform, toolchain):
_prep_build_dir(test_dir, build_dir)
# Add prebuilts to PATH.
prebuilts_host_tag = build.lib.build_support.get_default_host() + '-x86'
prebuilts_bin = build.lib.build_support.android_path(
'prebuilts', 'cmake', prebuilts_host_tag, 'bin')
env = dict(os.environ)
env['PATH'] = prebuilts_bin + os.pathsep + os.environ['PATH']
# Skip 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.
if distutils.spawn.find_executable('cmake') is None:
return Skipped(test_name, 'cmake executable not found')
out = subprocess.check_output(['cmake', '--version'], env=env)
version_pattern = r'cmake version (\d+)\.(\d+)\.'
version = [int(v) for v in re.match(version_pattern, out).groups()]
if version < [3, 6]:
return Skipped(test_name, 'cmake 3.6 or above required')
toolchain_file = os.path.join(os.environ['NDK'], 'build', 'cmake',
'android.toolchain.cmake')
objs_dir = os.path.join(build_dir, 'objs', abi)
libs_dir = os.path.join(build_dir, 'libs', abi)
if toolchain != 'clang':
toolchain = 'gcc'
args = [
'-H' + build_dir,
'-B' + objs_dir,
'-DCMAKE_TOOLCHAIN_FILE=' + toolchain_file,
'-DANDROID_ABI=' + abi,
'-DANDROID_TOOLCHAIN=' + toolchain,
'-DCMAKE_RUNTIME_OUTPUT_DIRECTORY=' + libs_dir,
'-DCMAKE_LIBRARY_OUTPUT_DIRECTORY=' + libs_dir
]
rc, _ = util.call_output(['ninja', '--version'], env=env)
if rc == 0:
args += [
'-GNinja',
'-DCMAKE_MAKE_PROGRAM=ninja',
]
if platform is not None:
args.append('-DANDROID_PLATFORM=android-{}'.format(platform))
rc, out = util.call_output(['cmake'] + cmake_flags + args, env=env)
if rc != 0:
return Failure(test_name, out)
rc, out = util.call_output(['cmake', '--build', objs_dir,
'--', _get_jobs_arg()], env=env)
if rc != 0:
return Failure(test_name, out)
return Success(test_name)
class BuildTest(Test):
def __init__(self, name, test_dir, abi, platform, toolchain,
ndk_build_flags=None, cmake_flags=None):
super(BuildTest, self).__init__(name, test_dir)
if ndk_build_flags is None:
ndk_build_flags = []
if cmake_flags is None:
cmake_flags = []
if platform is None:
raise ValueError
self.abi = abi
self.platform = platform
self.toolchain = toolchain
self.ndk_build_flags = ndk_build_flags
self.ndk_build_flags += self.get_extra_ndk_build_flags()
self.cmake_flags = cmake_flags + self.get_extra_cmake_flags()
def run(self, out_dir, _):
raise NotImplementedError
def check_broken(self):
return self.get_test_config().build_broken(
self.abi, self.platform, self.toolchain)
def check_unsupported(self):
return self.get_test_config().build_unsupported(
self.abi, self.platform, self.toolchain)
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.
toolchain: Toolchain to use 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, abi, platform, toolchain,
ndk_build_flags):
if platform is None:
platform = build.lib.build_support.minimum_platform_level(abi)
super(PythonBuildTest, self).__init__(
name, test_dir, abi, platform, toolchain,
ndk_build_flags=ndk_build_flags)
def run(self, out_dir, _):
build_dir = os.path.join(out_dir, 'build/tests.py', self.name)
print('Building test: {}'.format(self.name))
_prep_build_dir(self.test_dir, build_dir)
with util.cd(build_dir):
module = imp.load_source('test', 'test.py')
success, failure_message = module.run_test(
abi=self.abi, platform=self.platform, toolchain=self.toolchain,
build_flags=self.ndk_build_flags)
if success:
return Success(self.name), []
else:
return Failure(self.name, failure_message), []
class ShellBuildTest(BuildTest):
def __init__(self, name, test_dir, abi, platform, toolchain,
ndk_build_flags):
if platform is None:
platform = build.lib.build_support.minimum_platform_level(abi)
super(ShellBuildTest, self).__init__(
name, test_dir, abi, platform, toolchain, ndk_build_flags)
def run(self, out_dir, _):
build_dir = os.path.join(out_dir, 'build/build.sh', self.name)
print('Building test: {}'.format(self.name))
if os.name == 'nt':
reason = 'build.sh tests are not supported on Windows'
return Skipped(self.name, reason), []
else:
result = _run_build_sh_test(
self.name, build_dir, self.test_dir, self.ndk_build_flags,
self.abi, self.platform, self.toolchain)
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
platform_from_application_mk = _platform_from_application_mk(test_dir)
if platform_from_application_mk is not None:
return platform_from_application_mk
return build.lib.build_support.minimum_platform_level(abi)
class NdkBuildTest(BuildTest):
def __init__(self, name, test_dir, abi, platform, toolchain,
ndk_build_flags):
platform = _get_or_infer_app_platform(platform, test_dir, abi)
super(NdkBuildTest, self).__init__(
name, test_dir, abi, platform, toolchain, ndk_build_flags)
def run(self, out_dir, _):
build_dir = os.path.join(out_dir, 'build/ndk-build', self.name)
print('Building test: {}'.format(self.name))
result = _run_ndk_build_test(
self.name, build_dir, self.test_dir, self.ndk_build_flags,
self.abi, self.platform, self.toolchain)
return result, []
class CMakeBuildTest(BuildTest):
def __init__(self, name, test_dir, abi, platform, toolchain, cmake_flags):
platform = _get_or_infer_app_platform(platform, test_dir, abi)
super(CMakeBuildTest, self).__init__(
name, test_dir, abi, platform, toolchain, cmake_flags=cmake_flags)
def run(self, out_dir, _):
build_dir = os.path.join(out_dir, 'build/cmake', self.name)
print('Building test: {}'.format(self.name))
result = _run_cmake_build_test(
self.name, build_dir, self.test_dir, self.cmake_flags, self.abi,
self.platform, self.toolchain)
return result, []
def is_text_busy(out):
# Anything longer than this isn't going to be a text busy message, so don't
# waste time scanning it.
if len(out) > 1024:
return False
# 'text busy' was printed on Gingerbread.
if 'text busy' in out:
return True
# 'Text file busy' was printed on Jelly Bean (not sure exactly when this
# changed).
if 'Text file busy' in out:
return True
return False
class DeviceTest(Test):
def __init__(self, name, test_dir, abi, platform, device, device_platform,
toolchain, skip_run):
super(DeviceTest, self).__init__(name, test_dir)
platform = _get_or_infer_app_platform(platform, test_dir, abi)
self.abi = abi
self.platform = platform
self.device = device
self.device_platform = device_platform
self.toolchain = toolchain
self.skip_run = skip_run
def check_broken(self):
return self.get_test_config().build_broken(
self.abi, self.platform, self.toolchain)
def check_unsupported(self):
return self.get_test_config().build_unsupported(
self.abi, self.platform, self.toolchain)
def is_negative_test(self):
return False
def run(self, out_dir, test_filters):
raise NotImplementedError
def get_device_subdir(self):
raise NotImplementedError
def get_device_dir(self):
return posixpath.join(
'/data/local/tmp', self.get_device_subdir(), self.name)
def copy_test_to_device(self, build_dir, test_filters):
# We have to use `ls foo || mkdir foo` because Gingerbread was lacking
# `mkdir -p`, the -d check for directory existence, stat, dirname, and
# every other thing I could think of to implement this aside from ls.
self.device.shell(
['ls {0} || mkdir {0}'.format(self.get_device_dir())])
abi_dir = os.path.join(build_dir, 'libs', self.abi)
if not os.path.isdir(abi_dir):
raise RuntimeError('No libraries for {}'.format(self.abi))
test_cases = []
for test_file in os.listdir(abi_dir):
if test_file in ('gdbserver', 'gdb.setup'):
continue
file_is_lib = True
if not test_file.endswith('.so'):
file_is_lib = False
case_name = _make_subtest_name(self.name, test_file)
if not test_filters.filter(case_name):
continue
test_cases.append(test_file)
# TODO(danalbert): Libs with the same name will clobber each other.
# This was the case with the old shell based script too. I'm trying
# not to change too much in the translation.
lib_path = os.path.join(abi_dir, test_file)
print('Pushing {} to {}...'.format(
lib_path, self.get_device_dir()))
self.device.push(lib_path, self.get_device_dir())
# Binaries pushed from Windows may not have execute permissions.
if not file_is_lib:
file_path = posixpath.join(self.get_device_dir(), test_file)
# Can't use +x because apparently old versions of Android
# didn't support that...
self.device.shell(['chmod', '777', file_path])
if len(test_cases) == 0:
raise RuntimeError('Could not find any test executables.')
return test_cases
def get_additional_tests(self, build_dir, test_filters):
executables = self.copy_test_to_device(build_dir, test_filters)
additional_tests = []
for exe in executables:
name = _make_subtest_name(self.name, exe)
additional_tests.append(
DeviceRunTest(
name, self.test_dir, exe, self.get_device_dir(), self.abi,
self.platform, self.device_platform, self.toolchain,
self.device))
return additional_tests
class NdkBuildDeviceTest(DeviceTest):
def __init__(self, name, test_dir, abi, platform, toolchain,
ndk_build_flags, device, device_api, skip_run):
super(NdkBuildDeviceTest, self).__init__(
name, test_dir, abi, platform, device, device_api, toolchain,
skip_run)
self.ndk_build_flags = ndk_build_flags
self.ndk_build_flags += self.get_extra_ndk_build_flags()
def get_extra_ndk_build_flags(self):
return self.get_test_config().extra_ndk_build_flags()
def get_device_subdir(self):
return 'ndk-tests'
def run(self, out_dir, test_filters):
print('Building device test with ndk-build: {}'.format(self.name))
build_dir = os.path.join(out_dir, 'device/ndk-build', self.name)
build_result = _run_ndk_build_test(self.name, build_dir, self.test_dir,
self.ndk_build_flags, self.abi,
self.platform, self.toolchain)
if not build_result.passed():
return build_result, []
if self.skip_run:
return build_result, []
return build_result, self.get_additional_tests(build_dir, test_filters)
class CMakeDeviceTest(DeviceTest):
def __init__(self, name, test_dir, abi, platform, toolchain, cmake_flags,
device, device_api, skip_run):
super(CMakeDeviceTest, self).__init__(
name, test_dir, abi, platform, device, device_api, toolchain,
skip_run)
self.cmake_flags = cmake_flags + self.get_extra_cmake_flags()
def get_extra_cmake_flags(self):
return self.get_test_config().extra_cmake_flags()
def get_device_subdir(self):
return 'cmake-tests'
def run(self, out_dir, test_filters):
print('Building device test with cmake: {}'.format(self.name))
build_dir = os.path.join(out_dir, 'device/cmake', self.name)
build_result = _run_cmake_build_test(self.name, build_dir,
self.test_dir, self.cmake_flags,
self.abi, self.platform,
self.toolchain)
if not build_result.passed():
return build_result, []
if self.skip_run:
return build_result, []
return build_result, self.get_additional_tests(build_dir, test_filters)
class DeviceRunTest(Test):
def __init__(self, name, test_dir, case_name, device_dir, abi, build_api,
device_api, toolchain, device):
super(DeviceRunTest, self).__init__(name, test_dir)
self.case_name = case_name
self.device_dir = device_dir
self.abi = abi
self.build_api = build_api
self.device_api = device_api
self.toolchain = toolchain
self.device = device
def get_test_config(self):
return DeviceTestConfig.from_test_dir(self.test_dir)
def check_broken(self):
return self.get_test_config().run_broken(
self.abi, self.device_api, self.toolchain, self.case_name)
def check_unsupported(self):
if self.build_api > self.device_api:
return 'device platform {} < build platform {}'.format(
self.device_api, self.build_api)
return self.get_test_config().run_unsupported(
self.abi, self.device_api, self.toolchain, self.case_name)
def is_negative_test(self):
return False
def run(self, out_dir, test_filters):
cmd = 'cd {} && LD_LIBRARY_PATH={} ./{} 2>&1'.format(
self.device_dir, self.device_dir, self.case_name)
for _ in range(8):
result, out, _ = self.device.shell_nocheck([cmd])
if result == 0:
break
if not is_text_busy(out):
break
time.sleep(1)
if result == 0:
return Success(self.name), []
else:
return Failure(self.name, out), []