1# 2# Copyright (C) 2015 The Android Open Source Project 3# 4# Licensed under the Apache License, Version 2.0 (the "License"); 5# you may not use this file except in compliance with the License. 6# You may obtain a copy of the License at 7# 8# http://www.apache.org/licenses/LICENSE-2.0 9# 10# Unless required by applicable law or agreed to in writing, software 11# distributed under the License is distributed on an "AS IS" BASIS, 12# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13# See the License for the specific language governing permissions and 14# limitations under the License. 15# 16from __future__ import print_function 17 18import difflib 19import filecmp 20import glob 21import imp 22import multiprocessing 23import os 24import posixpath 25import re 26import shutil 27import subprocess 28 29import adb 30import ndk 31import util 32 33# pylint: disable=no-self-use 34 35 36def _get_jobs_arg(): 37 return '-j{}'.format(multiprocessing.cpu_count() * 2) 38 39 40def _make_subtest_name(test, case): 41 return '.'.join([test, case]) 42 43 44def _scan_test_suite(suite_dir, test_class, *args): 45 tests = [] 46 for dentry in os.listdir(suite_dir): 47 path = os.path.join(suite_dir, dentry) 48 if os.path.isdir(path): 49 tests.append(test_class.from_dir(path, *args)) 50 return tests 51 52 53class TestRunner(object): 54 def __init__(self): 55 self.tests = {} 56 57 def add_suite(self, name, path, test_class, *args): 58 if name in self.tests: 59 raise KeyError('suite {} already exists'.format(name)) 60 self.tests[name] = _scan_test_suite(path, test_class, *args) 61 62 def _fixup_expected_failure(self, result, config, bug): 63 if isinstance(result, Failure): 64 return ExpectedFailure(result.test_name, config, bug) 65 elif isinstance(result, Success): 66 return UnexpectedSuccess(result.test_name, config, bug) 67 else: # Skipped, UnexpectedSuccess, or ExpectedFailure. 68 return result 69 70 def _run_test(self, test, out_dir, test_filters): 71 if not test_filters.filter(test.name): 72 return [] 73 74 config = test.check_unsupported() 75 if config is not None: 76 message = 'test unsupported for {}'.format(config) 77 return [Skipped(test.name, message)] 78 79 results = test.run(out_dir, test_filters) 80 config, bug = test.check_broken() 81 if config is None: 82 return results 83 84 # We need to check each individual test case for pass/fail and change 85 # it to either an ExpectedFailure or an UnexpectedSuccess as necessary. 86 return [self._fixup_expected_failure(r, config, bug) for r in results] 87 88 def run(self, out_dir, test_filters): 89 results = {suite: [] for suite in self.tests.keys()} 90 for suite, tests in self.tests.items(): 91 test_results = [] 92 for test in tests: 93 test_results.extend(self._run_test(test, out_dir, 94 test_filters)) 95 results[suite] = test_results 96 return results 97 98 99def _maybe_color(text, color, do_color): 100 return util.color_string(text, color) if do_color else text 101 102 103class TestResult(object): 104 def __init__(self, test_name): 105 self.test_name = test_name 106 107 def __repr__(self): 108 return self.to_string(colored=False) 109 110 def passed(self): 111 raise NotImplementedError 112 113 def failed(self): 114 raise NotImplementedError 115 116 def to_string(self, colored=False): 117 raise NotImplementedError 118 119 120class Failure(TestResult): 121 def __init__(self, test_name, message): 122 super(Failure, self).__init__(test_name) 123 self.message = message 124 125 def passed(self): 126 return False 127 128 def failed(self): 129 return True 130 131 def to_string(self, colored=False): 132 label = _maybe_color('FAIL', 'red', colored) 133 return '{} {}: {}'.format(label, self.test_name, self.message) 134 135 136class Success(TestResult): 137 def passed(self): 138 return True 139 140 def failed(self): 141 return False 142 143 def to_string(self, colored=False): 144 label = _maybe_color('PASS', 'green', colored) 145 return '{} {}'.format(label, self.test_name) 146 147 148class Skipped(TestResult): 149 def __init__(self, test_name, reason): 150 super(Skipped, self).__init__(test_name) 151 self.reason = reason 152 153 def passed(self): 154 return False 155 156 def failed(self): 157 return False 158 159 def to_string(self, colored=False): 160 label = _maybe_color('SKIP', 'yellow', colored) 161 return '{} {}: {}'.format(label, self.test_name, self.reason) 162 163 164class ExpectedFailure(TestResult): 165 def __init__(self, test_name, config, bug): 166 super(ExpectedFailure, self).__init__(test_name) 167 self.config = config 168 self.bug = bug 169 170 def passed(self): 171 return True 172 173 def failed(self): 174 return False 175 176 def to_string(self, colored=False): 177 label = _maybe_color('KNOWN FAIL', 'yellow', colored) 178 return '{} {}: known failure for {} ({})'.format( 179 label, self.test_name, self.config, self.bug) 180 181 182class UnexpectedSuccess(TestResult): 183 def __init__(self, test_name, config, bug): 184 super(UnexpectedSuccess, self).__init__(test_name) 185 self.config = config 186 self.bug = bug 187 188 def passed(self): 189 return False 190 191 def failed(self): 192 return True 193 194 def to_string(self, colored=False): 195 label = _maybe_color('SHOULD FAIL', 'red', colored) 196 return '{} {}: unexpected success for {} ({})'.format( 197 label, self.test_name, self.config, self.bug) 198 199 200class Test(object): 201 def __init__(self, name, test_dir): 202 self.name = name 203 self.test_dir = test_dir 204 self.config = self.get_test_config() 205 206 def get_test_config(self): 207 return TestConfig.from_test_dir(self.test_dir) 208 209 def run(self, out_dir, test_filters): 210 raise NotImplementedError 211 212 def check_broken(self): 213 return self.config.match_broken(self.abi, self.platform, 214 self.toolchain) 215 216 def check_unsupported(self): 217 return self.config.match_unsupported(self.abi, self.platform, 218 self.toolchain) 219 220 def check_subtest_broken(self, name): 221 return self.config.match_broken(self.abi, self.platform, 222 self.toolchain, subtest=name) 223 224 def check_subtest_unsupported(self, name): 225 return self.config.match_unsupported(self.abi, self.platform, 226 self.toolchain, subtest=name) 227 228 229class AwkTest(Test): 230 def __init__(self, name, test_dir, script): 231 super(AwkTest, self).__init__(name, test_dir) 232 self.script = script 233 234 @classmethod 235 def from_dir(cls, test_dir): 236 test_name = os.path.basename(test_dir) 237 script_name = test_name + '.awk' 238 script = os.path.join(ndk.NDK_ROOT, 'build/awk', script_name) 239 if not os.path.isfile(script): 240 msg = '{} missing test script: {}'.format(test_name, script) 241 raise RuntimeError(msg) 242 243 # Check that all of our test cases are valid. 244 for test_case in glob.glob(os.path.join(test_dir, '*.in')): 245 golden_path = re.sub(r'\.in$', '.out', test_case) 246 if not os.path.isfile(golden_path): 247 msg = '{} missing output: {}'.format(test_name, golden_path) 248 raise RuntimeError(msg) 249 return cls(test_name, test_dir, script) 250 251 # Awk tests only run in a single configuration. Disabling them per ABI, 252 # platform, or toolchain has no meaning. Stub out the checks. 253 def check_broken(self): 254 return None, None 255 256 def check_unsupported(self): 257 return None 258 259 def run(self, out_dir, test_filters): 260 results = [] 261 for test_case in glob.glob(os.path.join(self.test_dir, '*.in')): 262 golden_path = re.sub(r'\.in$', '.out', test_case) 263 result = self.run_case(out_dir, test_case, golden_path, 264 test_filters) 265 if result is not None: 266 results.append(result) 267 return results 268 269 def run_case(self, out_dir, test_case, golden_out_path, test_filters): 270 case_name = os.path.splitext(os.path.basename(test_case))[0] 271 name = _make_subtest_name(self.name, case_name) 272 273 if not test_filters.filter(name): 274 return None 275 276 out_path = os.path.join(out_dir, os.path.basename(golden_out_path)) 277 278 with open(test_case, 'r') as test_in, open(out_path, 'w') as out_file: 279 awk_path = ndk.get_tool('awk') 280 print('{} -f {} < {} > {}'.format( 281 awk_path, self.script, test_case, out_path)) 282 rc = subprocess.call([awk_path, '-f', self.script], stdin=test_in, 283 stdout=out_file) 284 if rc != 0: 285 return Failure(name, 'awk failed') 286 287 if filecmp.cmp(out_path, golden_out_path): 288 return Success(name) 289 else: 290 with open(out_path) as out_file: 291 out_lines = out_file.readlines() 292 with open(golden_out_path) as golden_out_file: 293 golden_lines = golden_out_file.readlines() 294 diff = ''.join(difflib.unified_diff( 295 golden_lines, out_lines, fromfile='expected', tofile='actual')) 296 message = 'output does not match expected:\n\n' + diff 297 return Failure(name, message) 298 299 300def _prep_build_dir(src_dir, out_dir): 301 if os.path.exists(out_dir): 302 shutil.rmtree(out_dir) 303 shutil.copytree(src_dir, out_dir) 304 305 306class TestConfig(object): 307 """Describes the status of a test. 308 309 Each test directory can contain a "test_config.py" file that describes 310 the configurations a test is not expected to pass for. Previously this 311 information could be captured in one of two places: the Application.mk 312 file, or a BROKEN_BUILD/BROKEN_RUN file. 313 314 Application.mk was used to state that a test was only to be run for a 315 specific platform version, specific toolchain, or a set of ABIs. 316 Unfortunately Application.mk could only specify a single toolchain or 317 platform, not a set. 318 319 BROKEN_BUILD/BROKEN_RUN files were too general. An empty file meant the 320 test should always be skipped regardless of configuration. Any change that 321 would put a test in that situation should be reverted immediately. These 322 also didn't make it clear if the test was actually broken (and thus should 323 be fixed) or just not applicable. 324 325 A test_config.py file is more flexible. It is a Python module that defines 326 at least one function by the same name as one in TestConfig.NullTestConfig. 327 If a function is not defined the null implementation (not broken, 328 supported), will be used. 329 """ 330 331 class NullTestConfig(object): 332 def __init__(self): 333 pass 334 335 # pylint: disable=unused-argument 336 @staticmethod 337 def match_broken(abi, platform, toolchain, subtest=None): 338 """Tests if a given configuration is known broken. 339 340 A broken test is a known failing test that should be fixed. 341 342 Any test with a non-empty broken section requires a "bug" entry 343 with a link to either an internal bug (http://b/BUG_NUMBER) or a 344 public bug (http://b.android.com/BUG_NUMBER). 345 346 These tests will still be built and run. If the test succeeds, it 347 will be reported as an error. 348 349 Returns: A tuple of (broken_configuration, bug) or (None, None). 350 """ 351 return None, None 352 353 @staticmethod 354 def match_unsupported(abi, platform, toolchain, subtest=None): 355 """Tests if a given configuration is unsupported. 356 357 An unsupported test is a test that do not make sense to run for a 358 given configuration. Testing x86 assembler on MIPS, for example. 359 360 These tests will not be built or run. 361 362 Returns: The string unsupported_configuration or None. 363 """ 364 return None 365 # pylint: enable=unused-argument 366 367 def __init__(self, file_path): 368 369 # Note that this namespace isn't actually meaningful from our side; 370 # it's only what the loaded module's __name__ gets set to. 371 dirname = os.path.dirname(file_path) 372 namespace = '.'.join([dirname, 'test_config']) 373 374 try: 375 self.module = imp.load_source(namespace, file_path) 376 except IOError: 377 self.module = None 378 379 try: 380 self.match_broken = self.module.match_broken 381 except AttributeError: 382 self.match_broken = self.NullTestConfig.match_broken 383 384 try: 385 self.match_unsupported = self.module.match_unsupported 386 except AttributeError: 387 self.match_unsupported = self.NullTestConfig.match_unsupported 388 389 @classmethod 390 def from_test_dir(cls, test_dir): 391 path = os.path.join(test_dir, 'test_config.py') 392 return cls(path) 393 394 395class DeviceTestConfig(TestConfig): 396 """Specialization of test_config.py that includes device API level. 397 398 We need to mark some tests as broken or unsupported based on what device 399 they are running on, as opposed to just what they were built for. 400 """ 401 class NullTestConfig(object): 402 def __init__(self): 403 pass 404 405 # pylint: disable=unused-argument 406 @staticmethod 407 def match_broken(abi, platform, device_platform, toolchain, 408 subtest=None): 409 return None, None 410 411 @staticmethod 412 def match_unsupported(abi, platform, device_platform, toolchain, 413 subtest=None): 414 return None 415 # pylint: enable=unused-argument 416 417 418def _run_build_sh_test(test_name, build_dir, test_dir, build_flags, abi, 419 platform, toolchain): 420 _prep_build_dir(test_dir, build_dir) 421 with util.cd(build_dir): 422 build_cmd = ['sh', 'build.sh', _get_jobs_arg()] + build_flags 423 test_env = dict(os.environ) 424 if abi is not None: 425 test_env['APP_ABI'] = abi 426 if platform is not None: 427 test_env['APP_PLATFORM'] = platform 428 assert toolchain is not None 429 test_env['NDK_TOOLCHAIN_VERSION'] = toolchain 430 rc, out = util.call_output(build_cmd, env=test_env) 431 if rc == 0: 432 return Success(test_name) 433 else: 434 return Failure(test_name, out) 435 436 437def _run_ndk_build_test(test_name, build_dir, test_dir, build_flags, abi, 438 platform, toolchain): 439 _prep_build_dir(test_dir, build_dir) 440 with util.cd(build_dir): 441 args = [ 442 'APP_ABI=' + abi, 443 'NDK_TOOLCHAIN_VERSION=' + toolchain, 444 _get_jobs_arg(), 445 ] 446 if platform is not None: 447 args.append('APP_PLATFORM=' + platform) 448 rc, out = ndk.build(build_flags + args) 449 if rc == 0: 450 return Success(test_name) 451 else: 452 return Failure(test_name, out) 453 454 455class PythonBuildTest(Test): 456 """A test that is implemented by test.py. 457 458 A test.py test has a test.py file in its root directory. This module 459 contains a run_test function which returns a tuple of `(boolean_success, 460 string_failure_message)` and takes the following kwargs (all of which 461 default to None): 462 463 abi: ABI to test as a string. 464 platform: Platform to build against as a string. 465 toolchain: Toolchain to use as a string. 466 build_flags: Additional build flags that should be passed to ndk-build if 467 invoked as a list of strings. 468 """ 469 def __init__(self, name, test_dir, abi, platform, toolchain, build_flags): 470 super(PythonBuildTest, self).__init__(name, test_dir) 471 self.abi = abi 472 self.platform = platform 473 self.toolchain = toolchain 474 self.build_flags = build_flags 475 476 def run(self, out_dir, _): 477 build_dir = os.path.join(out_dir, self.name) 478 print('Running build test: {}'.format(self.name)) 479 _prep_build_dir(self.test_dir, build_dir) 480 with util.cd(build_dir): 481 module = imp.load_source('test', 'test.py') 482 success, failure_message = module.run_test( 483 abi=self.abi, platform=self.platform, toolchain=self.toolchain, 484 build_flags=self.build_flags) 485 if success: 486 return [Success(self.name)] 487 else: 488 return [Failure(self.name, failure_message)] 489 490 491class ShellBuildTest(Test): 492 def __init__(self, name, test_dir, abi, platform, toolchain, build_flags): 493 super(ShellBuildTest, self).__init__(name, test_dir) 494 self.abi = abi 495 self.platform = platform 496 self.toolchain = toolchain 497 self.build_flags = build_flags 498 499 def run(self, out_dir, _): 500 build_dir = os.path.join(out_dir, self.name) 501 print('Running build test: {}'.format(self.name)) 502 if os.name == 'nt': 503 reason = 'build.sh tests are not supported on Windows' 504 return [Skipped(self.name, reason)] 505 return [_run_build_sh_test(self.name, build_dir, self.test_dir, 506 self.build_flags, self.abi, self.platform, 507 self.toolchain)] 508 509 510class NdkBuildTest(Test): 511 def __init__(self, name, test_dir, abi, platform, toolchain, build_flags): 512 super(NdkBuildTest, self).__init__(name, test_dir) 513 self.abi = abi 514 self.platform = platform 515 self.toolchain = toolchain 516 self.build_flags = build_flags 517 518 def run(self, out_dir, _): 519 build_dir = os.path.join(out_dir, self.name) 520 print('Running build test: {}'.format(self.name)) 521 return [_run_ndk_build_test(self.name, build_dir, self.test_dir, 522 self.build_flags, self.abi, 523 self.platform, self.toolchain)] 524 525 526class BuildTest(object): 527 @classmethod 528 def from_dir(cls, test_dir, abi, platform, toolchain, build_flags): 529 test_name = os.path.basename(test_dir) 530 531 if os.path.isfile(os.path.join(test_dir, 'test.py')): 532 return PythonBuildTest(test_name, test_dir, abi, platform, 533 toolchain, build_flags) 534 elif os.path.isfile(os.path.join(test_dir, 'build.sh')): 535 return ShellBuildTest(test_name, test_dir, abi, platform, 536 toolchain, build_flags) 537 else: 538 return NdkBuildTest(test_name, test_dir, abi, platform, 539 toolchain, build_flags) 540 541 542def _copy_test_to_device(build_dir, device_dir, abi, test_filters, test_name): 543 abi_dir = os.path.join(build_dir, 'libs', abi) 544 if not os.path.isdir(abi_dir): 545 raise RuntimeError('No libraries for {}'.format(abi)) 546 547 test_cases = [] 548 for test_file in os.listdir(abi_dir): 549 if test_file in ('gdbserver', 'gdb.setup'): 550 continue 551 552 file_is_lib = False 553 if not test_file.endswith('.so'): 554 file_is_lib = True 555 case_name = _make_subtest_name(test_name, test_file) 556 if not test_filters.filter(case_name): 557 continue 558 test_cases.append(test_file) 559 560 # TODO(danalbert): Libs with the same name will clobber each other. 561 # This was the case with the old shell based script too. I'm trying not 562 # to change too much in the translation. 563 lib_path = os.path.join(abi_dir, test_file) 564 print('\tPushing {} to {}...'.format(lib_path, device_dir)) 565 adb.push(lib_path, device_dir) 566 567 # Binaries pushed from Windows may not have execute permissions. 568 if not file_is_lib: 569 file_path = posixpath.join(device_dir, test_file) 570 adb.shell('chmod +x ' + file_path) 571 572 # TODO(danalbert): Sync data. 573 # The libc++ tests contain a DATA file that lists test names and their 574 # dependencies on file system data. These files need to be copied to 575 # the device. 576 577 if len(test_cases) == 0: 578 raise RuntimeError('Could not find any test executables.') 579 580 return test_cases 581 582 583class DeviceTest(Test): 584 def __init__(self, name, test_dir, abi, platform, device_platform, 585 toolchain, build_flags): 586 super(DeviceTest, self).__init__(name, test_dir) 587 self.abi = abi 588 self.platform = platform 589 self.device_platform = device_platform 590 self.toolchain = toolchain 591 self.build_flags = build_flags 592 593 @classmethod 594 def from_dir(cls, test_dir, abi, platform, device_platform, toolchain, 595 build_flags): 596 test_name = os.path.basename(test_dir) 597 return cls(test_name, test_dir, abi, platform, device_platform, 598 toolchain, build_flags) 599 600 def get_test_config(self): 601 return DeviceTestConfig.from_test_dir(self.test_dir) 602 603 def check_broken(self): 604 return self.config.match_broken(self.abi, self.platform, 605 self.device_platform, 606 self.toolchain) 607 608 def check_unsupported(self): 609 return self.config.match_unsupported(self.abi, self.platform, 610 self.device_platform, 611 self.toolchain) 612 613 def check_subtest_broken(self, name): 614 return self.config.match_broken(self.abi, self.platform, 615 self.device_platform, 616 self.toolchain, subtest=name) 617 618 def check_subtest_unsupported(self, name): 619 return self.config.match_unsupported(self.abi, self.platform, 620 self.device_platform, 621 self.toolchain, subtest=name) 622 623 def run(self, out_dir, test_filters): 624 print('Running device test: {}'.format(self.name)) 625 build_dir = os.path.join(out_dir, self.name) 626 build_result = _run_ndk_build_test(self.name, build_dir, self.test_dir, 627 self.build_flags, self.abi, 628 self.platform, self.toolchain) 629 if not build_result.passed(): 630 return [build_result] 631 632 device_dir = posixpath.join('/data/local/tmp/ndk-tests', self.name) 633 634 # We have to use `ls foo || mkdir foo` because Gingerbread was lacking 635 # `mkdir -p`, the -d check for directory existence, stat, dirname, and 636 # every other thing I could think of to implement this aside from ls. 637 result, out = adb.shell('ls {0} || mkdir {0}'.format(device_dir)) 638 if result != 0: 639 raise RuntimeError('mkdir failed:\n' + '\n'.join(out)) 640 641 results = [] 642 try: 643 test_cases = _copy_test_to_device( 644 build_dir, device_dir, self.abi, test_filters, self.name) 645 for case in test_cases: 646 case_name = _make_subtest_name(self.name, case) 647 if not test_filters.filter(case_name): 648 continue 649 650 config = self.check_subtest_unsupported(case) 651 if config is not None: 652 message = 'test unsupported for {}'.format(config) 653 results.append(Skipped(case_name, message)) 654 continue 655 656 cmd = 'cd {} && LD_LIBRARY_PATH={} ./{}'.format( 657 device_dir, device_dir, case) 658 print('\tExecuting {}...'.format(case_name)) 659 result, out = adb.shell(cmd) 660 661 config, bug = self.check_subtest_broken(case) 662 if config is None: 663 if result == 0: 664 results.append(Success(case_name)) 665 else: 666 results.append(Failure(case_name, '\n'.join(out))) 667 else: 668 if result == 0: 669 results.append(UnexpectedSuccess(case_name, config, 670 bug)) 671 else: 672 results.append(ExpectedFailure(case_name, config, bug)) 673 return results 674 finally: 675 adb.shell('rm -r {}'.format(device_dir)) 676