| OLD | NEW |
| 1 # Copyright (c) 2012 Google Inc. All rights reserved. | 1 # Copyright (c) 2012 Google Inc. All rights reserved. |
| 2 # Use of this source code is governed by a BSD-style license that can be | 2 # Use of this source code is governed by a BSD-style license that can be |
| 3 # found in the LICENSE file. | 3 # found in the LICENSE file. |
| 4 | 4 |
| 5 """ | 5 """ |
| 6 TestGyp.py: a testing framework for GYP integration tests. | 6 TestGyp.py: a testing framework for GYP integration tests. |
| 7 """ | 7 """ |
| 8 | 8 |
| 9 import collections | 9 import collections |
| 10 from contextlib import contextmanager | 10 from contextlib import contextmanager |
| 11 import itertools | 11 import itertools |
| 12 import os | 12 import os |
| 13 import random | |
| 14 import re | 13 import re |
| 15 import shutil | 14 import shutil |
| 16 import stat | |
| 17 import subprocess | 15 import subprocess |
| 18 import sys | 16 import sys |
| 19 import tempfile | 17 import tempfile |
| 20 import time | |
| 21 | 18 |
| 22 import TestCmd | 19 import TestCmd |
| 23 import TestCommon | 20 import TestCommon |
| 24 from TestCommon import __all__ | 21 from TestCommon import __all__ |
| 25 | 22 |
| 26 __all__.extend([ | 23 __all__.extend([ |
| 27 'TestGyp', | 24 'TestGyp', |
| 28 ]) | 25 ]) |
| 29 | 26 |
| 30 | 27 |
| (...skipping 373 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 404 class TestGypCustom(TestGypBase): | 401 class TestGypCustom(TestGypBase): |
| 405 """ | 402 """ |
| 406 Subclass for testing the GYP with custom generator | 403 Subclass for testing the GYP with custom generator |
| 407 """ | 404 """ |
| 408 | 405 |
| 409 def __init__(self, gyp=None, *args, **kw): | 406 def __init__(self, gyp=None, *args, **kw): |
| 410 self.format = kw.pop("format") | 407 self.format = kw.pop("format") |
| 411 super(TestGypCustom, self).__init__(*args, **kw) | 408 super(TestGypCustom, self).__init__(*args, **kw) |
| 412 | 409 |
| 413 | 410 |
| 414 class TestGypAndroid(TestGypBase): | |
| 415 """ | |
| 416 Subclass for testing the GYP Android makefile generator. Note that | |
| 417 build/envsetup.sh and lunch must have been run before running tests. | |
| 418 """ | |
| 419 format = 'android' | |
| 420 | |
| 421 # Note that we can't use mmm as the build tool because ... | |
| 422 # - it builds all targets, whereas we need to pass a target | |
| 423 # - it is a function, whereas the test runner assumes the build tool is a file | |
| 424 # Instead we use make and duplicate the logic from mmm. | |
| 425 build_tool_list = ['make'] | |
| 426 | |
| 427 # We use our custom target 'gyp_all_modules', as opposed to the 'all_modules' | |
| 428 # target used by mmm, to build only those targets which are part of the gyp | |
| 429 # target 'all'. | |
| 430 ALL = 'gyp_all_modules' | |
| 431 | |
| 432 def __init__(self, gyp=None, *args, **kw): | |
| 433 # Android requires build and test output to be inside its source tree. | |
| 434 # We use the following working directory for the test's source, but the | |
| 435 # test's build output still goes to $ANDROID_PRODUCT_OUT. | |
| 436 # Note that some tests explicitly set format='gypd' to invoke the gypd | |
| 437 # backend. This writes to the source tree, but there's no way around this. | |
| 438 kw['workdir'] = os.path.join('/tmp', 'gyptest', | |
| 439 kw.get('workdir', 'testworkarea')) | |
| 440 # We need to remove all gyp outputs from out/. Ths is because some tests | |
| 441 # don't have rules to regenerate output, so they will simply re-use stale | |
| 442 # output if present. Since the test working directory gets regenerated for | |
| 443 # each test run, this can confuse things. | |
| 444 # We don't have a list of build outputs because we don't know which | |
| 445 # dependent targets were built. Instead we delete all gyp-generated output. | |
| 446 # This may be excessive, but should be safe. | |
| 447 out_dir = os.environ['ANDROID_PRODUCT_OUT'] | |
| 448 obj_dir = os.path.join(out_dir, 'obj') | |
| 449 shutil.rmtree(os.path.join(obj_dir, 'GYP'), ignore_errors = True) | |
| 450 for x in ['EXECUTABLES', 'STATIC_LIBRARIES', 'SHARED_LIBRARIES']: | |
| 451 for d in os.listdir(os.path.join(obj_dir, x)): | |
| 452 if d.endswith('_gyp_intermediates'): | |
| 453 shutil.rmtree(os.path.join(obj_dir, x, d), ignore_errors = True) | |
| 454 for x in [os.path.join('obj', 'lib'), os.path.join('system', 'lib')]: | |
| 455 for d in os.listdir(os.path.join(out_dir, x)): | |
| 456 if d.endswith('_gyp.so'): | |
| 457 os.remove(os.path.join(out_dir, x, d)) | |
| 458 | |
| 459 super(TestGypAndroid, self).__init__(*args, **kw) | |
| 460 self._adb_path = os.path.join(os.environ['ANDROID_HOST_OUT'], 'bin', 'adb') | |
| 461 self._device_serial = None | |
| 462 adb_devices_out = self._call_adb(['devices']) | |
| 463 devices = [l.split()[0] for l in adb_devices_out.splitlines()[1:-1] | |
| 464 if l.split()[1] == 'device'] | |
| 465 if len(devices) == 0: | |
| 466 self._device_serial = None | |
| 467 else: | |
| 468 if len(devices) > 1: | |
| 469 self._device_serial = random.choice(devices) | |
| 470 else: | |
| 471 self._device_serial = devices[0] | |
| 472 self._call_adb(['root']) | |
| 473 self._to_install = set() | |
| 474 | |
| 475 def target_name(self, target): | |
| 476 if target == self.ALL: | |
| 477 return self.ALL | |
| 478 # The default target is 'droid'. However, we want to use our special target | |
| 479 # to build only the gyp target 'all'. | |
| 480 if target in (None, self.DEFAULT): | |
| 481 return self.ALL | |
| 482 return target | |
| 483 | |
| 484 _INSTALLABLE_PREFIX = 'Install: ' | |
| 485 | |
| 486 def build(self, gyp_file, target=None, **kw): | |
| 487 """ | |
| 488 Runs a build using the Android makefiles generated from the specified | |
| 489 gyp_file. This logic is taken from Android's mmm. | |
| 490 """ | |
| 491 arguments = kw.get('arguments', [])[:] | |
| 492 arguments.append(self.target_name(target)) | |
| 493 arguments.append('-C') | |
| 494 arguments.append(os.environ['ANDROID_BUILD_TOP']) | |
| 495 kw['arguments'] = arguments | |
| 496 chdir = kw.get('chdir', '') | |
| 497 makefile = os.path.join(self.workdir, chdir, 'GypAndroid.mk') | |
| 498 os.environ['ONE_SHOT_MAKEFILE'] = makefile | |
| 499 result = self.run(program=self.build_tool, **kw) | |
| 500 for l in self.stdout().splitlines(): | |
| 501 if l.startswith(TestGypAndroid._INSTALLABLE_PREFIX): | |
| 502 self._to_install.add(os.path.abspath(os.path.join( | |
| 503 os.environ['ANDROID_BUILD_TOP'], | |
| 504 l[len(TestGypAndroid._INSTALLABLE_PREFIX):]))) | |
| 505 del os.environ['ONE_SHOT_MAKEFILE'] | |
| 506 return result | |
| 507 | |
| 508 def android_module(self, group, name, subdir): | |
| 509 if subdir: | |
| 510 name = '%s_%s' % (subdir, name) | |
| 511 if group == 'SHARED_LIBRARIES': | |
| 512 name = 'lib_%s' % name | |
| 513 return '%s_gyp' % name | |
| 514 | |
| 515 def intermediates_dir(self, group, module_name): | |
| 516 return os.path.join(os.environ['ANDROID_PRODUCT_OUT'], 'obj', group, | |
| 517 '%s_intermediates' % module_name) | |
| 518 | |
| 519 def built_file_path(self, name, type=None, **kw): | |
| 520 """ | |
| 521 Returns a path to the specified file name, of the specified type, | |
| 522 as built by Android. Note that we don't support the configuration | |
| 523 parameter. | |
| 524 """ | |
| 525 # Built files are in $ANDROID_PRODUCT_OUT. This requires copying logic from | |
| 526 # the Android build system. | |
| 527 if type == None or type == self.EXECUTABLE: | |
| 528 return os.path.join(os.environ['ANDROID_PRODUCT_OUT'], 'obj', 'GYP', | |
| 529 'shared_intermediates', name) | |
| 530 subdir = kw.get('subdir') | |
| 531 if type == self.STATIC_LIB: | |
| 532 group = 'STATIC_LIBRARIES' | |
| 533 module_name = self.android_module(group, name, subdir) | |
| 534 return os.path.join(self.intermediates_dir(group, module_name), | |
| 535 '%s.a' % module_name) | |
| 536 if type == self.SHARED_LIB: | |
| 537 group = 'SHARED_LIBRARIES' | |
| 538 module_name = self.android_module(group, name, subdir) | |
| 539 return os.path.join(self.intermediates_dir(group, module_name), 'LINKED', | |
| 540 '%s.so' % module_name) | |
| 541 assert False, 'Unhandled type' | |
| 542 | |
| 543 def _adb_failure(self, command, msg, stdout, stderr): | |
| 544 """ Reports a failed adb command and fails the containing test. | |
| 545 | |
| 546 Args: | |
| 547 command: The adb command that failed. | |
| 548 msg: The error description. | |
| 549 stdout: The standard output. | |
| 550 stderr: The standard error. | |
| 551 """ | |
| 552 print '%s failed%s' % (' '.join(command), ': %s' % msg if msg else '') | |
| 553 print self.banner('STDOUT ') | |
| 554 stdout.seek(0) | |
| 555 print stdout.read() | |
| 556 print self.banner('STDERR ') | |
| 557 stderr.seek(0) | |
| 558 print stderr.read() | |
| 559 self.fail_test() | |
| 560 | |
| 561 def _call_adb(self, command, timeout=15, retry=3): | |
| 562 """ Calls the provided adb command. | |
| 563 | |
| 564 If the command fails, the test fails. | |
| 565 | |
| 566 Args: | |
| 567 command: The adb command to call. | |
| 568 Returns: | |
| 569 The command's output. | |
| 570 """ | |
| 571 with tempfile.TemporaryFile(bufsize=0) as adb_out: | |
| 572 with tempfile.TemporaryFile(bufsize=0) as adb_err: | |
| 573 adb_command = [self._adb_path] | |
| 574 if self._device_serial: | |
| 575 adb_command += ['-s', self._device_serial] | |
| 576 is_shell = (command[0] == 'shell') | |
| 577 if is_shell: | |
| 578 command = [command[0], '%s; echo "\n$?";' % ' '.join(command[1:])] | |
| 579 adb_command += command | |
| 580 | |
| 581 for attempt in xrange(1, retry + 1): | |
| 582 adb_out.seek(0) | |
| 583 adb_out.truncate(0) | |
| 584 adb_err.seek(0) | |
| 585 adb_err.truncate(0) | |
| 586 proc = subprocess.Popen(adb_command, stdout=adb_out, stderr=adb_err) | |
| 587 deadline = time.time() + timeout | |
| 588 timed_out = False | |
| 589 while proc.poll() is None and not timed_out: | |
| 590 time.sleep(1) | |
| 591 timed_out = time.time() > deadline | |
| 592 if timed_out: | |
| 593 print 'Timeout for command %s (attempt %d of %s)' % ( | |
| 594 adb_command, attempt, retry) | |
| 595 try: | |
| 596 proc.kill() | |
| 597 except: | |
| 598 pass | |
| 599 else: | |
| 600 break | |
| 601 | |
| 602 if proc.returncode != 0: # returncode is None in the case of a timeout. | |
| 603 self._adb_failure( | |
| 604 adb_command, 'retcode=%s' % proc.returncode, adb_out, adb_err) | |
| 605 return | |
| 606 | |
| 607 adb_out.seek(0) | |
| 608 output = adb_out.read() | |
| 609 if is_shell: | |
| 610 output = output.splitlines(True) | |
| 611 try: | |
| 612 output[-2] = output[-2].rstrip('\r\n') | |
| 613 output, rc = (''.join(output[:-1]), int(output[-1])) | |
| 614 except ValueError: | |
| 615 self._adb_failure(adb_command, 'unexpected output format', | |
| 616 adb_out, adb_err) | |
| 617 if rc != 0: | |
| 618 self._adb_failure(adb_command, 'exited with %d' % rc, adb_out, | |
| 619 adb_err) | |
| 620 return output | |
| 621 | |
| 622 def run_built_executable(self, name, *args, **kw): | |
| 623 """ | |
| 624 Runs an executable program built from a gyp-generated configuration. | |
| 625 """ | |
| 626 match = kw.pop('match', self.match) | |
| 627 | |
| 628 executable_file = self.built_file_path(name, type=self.EXECUTABLE, **kw) | |
| 629 if executable_file not in self._to_install: | |
| 630 self.fail_test() | |
| 631 | |
| 632 if not self._device_serial: | |
| 633 self.skip_test(message='No devices attached.\n') | |
| 634 | |
| 635 storage = self._call_adb(['shell', 'echo', '$ANDROID_DATA']).strip() | |
| 636 if not len(storage): | |
| 637 self.fail_test() | |
| 638 | |
| 639 installed = set() | |
| 640 try: | |
| 641 for i in self._to_install: | |
| 642 a = os.path.abspath( | |
| 643 os.path.join(os.environ['ANDROID_BUILD_TOP'], i)) | |
| 644 dest = '%s/%s' % (storage, os.path.basename(a)) | |
| 645 self._call_adb(['push', os.path.abspath(a), dest]) | |
| 646 installed.add(dest) | |
| 647 if i == executable_file: | |
| 648 device_executable = dest | |
| 649 self._call_adb(['shell', 'chmod', '755', device_executable]) | |
| 650 | |
| 651 out = self._call_adb( | |
| 652 ['shell', 'LD_LIBRARY_PATH=$LD_LIBRARY_PATH:%s' % storage, | |
| 653 device_executable], | |
| 654 timeout=60, | |
| 655 retry=1) | |
| 656 out = out.replace('\r\n', '\n') | |
| 657 self._complete(out, kw.pop('stdout', None), None, None, None, match) | |
| 658 finally: | |
| 659 if len(installed): | |
| 660 self._call_adb(['shell', 'rm'] + list(installed)) | |
| 661 | |
| 662 def match_single_line(self, lines = None, expected_line = None): | |
| 663 """ | |
| 664 Checks that specified line appears in the text. | |
| 665 """ | |
| 666 for line in lines.split('\n'): | |
| 667 if line == expected_line: | |
| 668 return 1 | |
| 669 return | |
| 670 | |
| 671 def up_to_date(self, gyp_file, target=None, **kw): | |
| 672 """ | |
| 673 Verifies that a build of the specified target is up to date. | |
| 674 """ | |
| 675 kw['stdout'] = ("make: Nothing to be done for `%s'." % | |
| 676 self.target_name(target)) | |
| 677 | |
| 678 # We need to supply a custom matcher, since we don't want to depend on the | |
| 679 # exact stdout string. | |
| 680 kw['match'] = self.match_single_line | |
| 681 return self.build(gyp_file, target, **kw) | |
| 682 | |
| 683 | |
| 684 class TestGypCMake(TestGypBase): | 411 class TestGypCMake(TestGypBase): |
| 685 """ | 412 """ |
| 686 Subclass for testing the GYP CMake generator, using cmake's ninja backend. | 413 Subclass for testing the GYP CMake generator, using cmake's ninja backend. |
| 687 """ | 414 """ |
| 688 format = 'cmake' | 415 format = 'cmake' |
| 689 build_tool_list = ['cmake'] | 416 build_tool_list = ['cmake'] |
| 690 ALL = 'all' | 417 ALL = 'all' |
| 691 | 418 |
| 692 def cmake_build(self, gyp_file, target=None, **kw): | 419 def cmake_build(self, gyp_file, target=None, **kw): |
| 693 arguments = kw.get('arguments', [])[:] | 420 arguments = kw.get('arguments', [])[:] |
| (...skipping 698 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 1392 """ | 1119 """ |
| 1393 configuration = self.configuration_dirname() | 1120 configuration = self.configuration_dirname() |
| 1394 os.environ['DYLD_LIBRARY_PATH'] = os.path.join('out', configuration) | 1121 os.environ['DYLD_LIBRARY_PATH'] = os.path.join('out', configuration) |
| 1395 # Enclosing the name in a list avoids prepending the original dir. | 1122 # Enclosing the name in a list avoids prepending the original dir. |
| 1396 program = [self.built_file_path(name, type=self.EXECUTABLE, **kw)] | 1123 program = [self.built_file_path(name, type=self.EXECUTABLE, **kw)] |
| 1397 return self.run(program=program, *args, **kw) | 1124 return self.run(program=program, *args, **kw) |
| 1398 | 1125 |
| 1399 | 1126 |
| 1400 format_class_list = [ | 1127 format_class_list = [ |
| 1401 TestGypGypd, | 1128 TestGypGypd, |
| 1402 TestGypAndroid, | |
| 1403 TestGypCMake, | 1129 TestGypCMake, |
| 1404 TestGypMake, | 1130 TestGypMake, |
| 1405 TestGypMSVS, | 1131 TestGypMSVS, |
| 1406 TestGypMSVSNinja, | 1132 TestGypMSVSNinja, |
| 1407 TestGypNinja, | 1133 TestGypNinja, |
| 1408 TestGypXcode, | 1134 TestGypXcode, |
| 1409 TestGypXcodeNinja, | 1135 TestGypXcodeNinja, |
| 1410 ] | 1136 ] |
| 1411 | 1137 |
| 1412 def TestGyp(*args, **kw): | 1138 def TestGyp(*args, **kw): |
| 1413 """ | 1139 """ |
| 1414 Returns an appropriate TestGyp* instance for a specified GYP format. | 1140 Returns an appropriate TestGyp* instance for a specified GYP format. |
| 1415 """ | 1141 """ |
| 1416 format = kw.pop('format', os.environ.get('TESTGYP_FORMAT')) | 1142 format = kw.pop('format', os.environ.get('TESTGYP_FORMAT')) |
| 1417 for format_class in format_class_list: | 1143 for format_class in format_class_list: |
| 1418 if format == format_class.format: | 1144 if format == format_class.format: |
| 1419 return format_class(*args, **kw) | 1145 return format_class(*args, **kw) |
| 1420 raise Exception, "unknown format %r" % format | 1146 raise Exception, "unknown format %r" % format |
| OLD | NEW |