blob: 0f3ebd7546fc5dad42e767e32f6306af7d346f0a [file] [log] [blame]
#
# Copyright (C) 2017 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.
#
"""APIs for enumerating and building NDK tests."""
from __future__ import absolute_import
import json
import logging
import os
import pickle
import random
import shutil
import sys
import traceback
from typing import (
Dict,
List,
Tuple,
)
import ndk.abis
from ndk.test.filters import TestFilter
from ndk.test.printers import Printer
from ndk.test.report import Report
from ndk.test.scanner import TestScanner
import ndk.test.spec
import ndk.test.suites
from ndk.test.types import Test
import ndk.test.ui
from ndk.toolchains import LinkerOption
from ndk.workqueue import LoadRestrictingWorkQueue, Worker
def logger() -> logging.Logger:
"""Returns the module logger."""
return logging.getLogger(__name__)
def test_spec_from_config(test_config: Dict) -> ndk.test.spec.TestSpec:
"""Returns a TestSpec based on the test config file."""
abis = test_config.get('abis', ndk.abis.ALL_ABIS)
suites = test_config.get('suites', ndk.test.suites.ALL_SUITES)
linkers_str = test_config.get('linkers', None)
if linkers_str is None:
linkers = list(LinkerOption)
else:
linkers = [LinkerOption(l) for l in linkers_str]
return ndk.test.spec.TestSpec(abis, linkers, suites)
def write_build_report(build_report: str, results: Report) -> None:
with open(build_report, 'wb') as build_report_file:
pickle.dump(results, build_report_file)
def scan_test_suite(suite_dir: str, test_scanner: TestScanner) -> List[Test]:
tests: List[Test] = []
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: ndk.test.result.TestResult, config: str,
bug: str) -> ndk.test.result.TestResult:
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: ndk.test.result.TestResult) -> ndk.test.result.TestResult:
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: Worker, suite: str, test: Test,
obj_dir: str, dist_dir: str, test_filters: TestFilter
) -> Tuple[str, ndk.test.result.TestResult, List[Test]]:
"""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.
assert bug is not None
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:
def __init__(self, test_spec: ndk.test.spec.TestSpec,
test_options: ndk.test.spec.TestOptions,
printer: Printer) -> None:
self.printer = printer
self.tests: Dict[str, List[Test]] = {}
self.build_dirs: Dict[str, Tuple[str, Test]] = {}
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: ndk.test.spec.TestSpec) -> None:
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:
for linker in test_spec.linkers:
build_api_level = None # Always use the default.
scanner.add_build_configuration(abi, build_api_level, linker)
nodist_scanner.add_build_configuration(abi, build_api_level,
linker)
libcxx_scanner.add_build_configuration(abi, build_api_level,
linker)
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: str,
test_options: ndk.test.spec.TestOptions,
printer: Printer) -> 'TestBuilder':
with open(config_path) as test_config_file:
test_config = json.load(test_config_file)
spec = test_spec_from_config(test_config)
return cls(spec, test_options, printer)
def add_suite(self, name: str, path: str,
test_scanner: TestScanner) -> None:
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: str,
new_tests: List[Test]) -> None:
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) -> None:
if not os.path.exists(self.obj_dir):
os.makedirs(self.obj_dir)
if not os.path.exists(self.dist_dir):
os.makedirs(self.dist_dir)
def clean_out_dir(self) -> None:
if os.path.exists(self.test_options.out_dir):
shutil.rmtree(self.test_options.out_dir)
def build(self) -> Report:
if self.test_options.clean:
self.clean_out_dir()
self.make_out_dirs()
test_filters = TestFilter.from_string(self.test_options.test_filter)
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: TestFilter) -> Report:
workqueue = 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 = Report()
self.wait_for_results(report, workqueue, test_filters)
return report
finally:
workqueue.terminate()
workqueue.join()
def wait_for_results(self, report: Report,
workqueue: LoadRestrictingWorkQueue,
test_filters: TestFilter) -> None:
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 not additional_tests
ui.draw()
continue
assert result.passed() or not additional_tests
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()