Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(32)

Unified Diff: ios/build/bots/scripts/test_runner.py

Issue 2659493002: Replace src/ios copy with DEPS mirror. (Closed)
Patch Set: Adding src/ios to .gitignore Created 3 years, 11 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View side-by-side diff with in-line comments
Download patch
« no previous file with comments | « ios/build/bots/scripts/run.py ('k') | ios/build/bots/scripts/test_runner_test.py » ('j') | no next file with comments »
Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
Index: ios/build/bots/scripts/test_runner.py
diff --git a/ios/build/bots/scripts/test_runner.py b/ios/build/bots/scripts/test_runner.py
deleted file mode 100644
index 8027d65a9e8da3d3683aabe1eeabde49b8901344..0000000000000000000000000000000000000000
--- a/ios/build/bots/scripts/test_runner.py
+++ /dev/null
@@ -1,688 +0,0 @@
-# Copyright 2016 The Chromium Authors. All rights reserved.
-# Use of this source code is governed by a BSD-style license that can be
-# found in the LICENSE file.
-
-"""Test runners for iOS."""
-
-import argparse
-import collections
-import errno
-import os
-import shutil
-import subprocess
-import sys
-import tempfile
-import time
-
-import find_xcode
-import gtest_utils
-import xctest_utils
-
-
-XCTEST_PROJECT = os.path.abspath(os.path.join(
- os.path.dirname(__file__),
- 'TestProject',
- 'TestProject.xcodeproj',
-))
-
-XCTEST_SCHEME = 'TestProject'
-
-
-class Error(Exception):
- """Base class for errors."""
- pass
-
-
-class TestRunnerError(Error):
- """Base class for TestRunner-related errors."""
- pass
-
-
-class AppLaunchError(TestRunnerError):
- """The app failed to launch."""
- pass
-
-
-class AppNotFoundError(TestRunnerError):
- """The requested app was not found."""
- def __init__(self, app_path):
- super(AppNotFoundError, self).__init__(
- 'App does not exist: %s' % app_path)
-
-
-class DeviceDetectionError(TestRunnerError):
- """Unexpected number of devices detected."""
- def __init__(self, udids):
- super(DeviceDetectionError, self).__init__(
- 'Expected one device, found %s:\n%s' % (len(udids), '\n'.join(udids)))
-
-
-class PlugInsNotFoundError(TestRunnerError):
- """The PlugIns directory was not found."""
- def __init__(self, plugins_dir):
- super(PlugInsNotFoundError, self).__init__(
- 'PlugIns directory does not exist: %s' % plugins_dir)
-
-
-class SimulatorNotFoundError(TestRunnerError):
- """The given simulator binary was not found."""
- def __init__(self, iossim_path):
- super(SimulatorNotFoundError, self).__init__(
- 'Simulator does not exist: %s' % iossim_path)
-
-
-class XcodeVersionNotFoundError(TestRunnerError):
- """The requested version of Xcode was not found."""
- def __init__(self, xcode_version):
- super(XcodeVersionNotFoundError, self).__init__(
- 'Xcode version not found: %s', xcode_version)
-
-
-class XCTestPlugInNotFoundError(TestRunnerError):
- """The .xctest PlugIn was not found."""
- def __init__(self, xctest_path):
- super(XCTestPlugInNotFoundError, self).__init__(
- 'XCTest not found: %s', xctest_path)
-
-
-def get_kif_test_filter(tests, invert=False):
- """Returns the KIF test filter to filter the given test cases.
-
- Args:
- tests: List of test cases to filter.
- invert: Whether to invert the filter or not. Inverted, the filter will match
- everything except the given test cases.
-
- Returns:
- A string which can be supplied to GKIF_SCENARIO_FILTER.
- """
- # A pipe-separated list of test cases with the "KIF." prefix omitted.
- # e.g. NAME:a|b|c matches KIF.a, KIF.b, KIF.c.
- # e.g. -NAME:a|b|c matches everything except KIF.a, KIF.b, KIF.c.
- test_filter = '|'.join(test.split('KIF.', 1)[-1] for test in tests)
- if invert:
- return '-NAME:%s' % test_filter
- return 'NAME:%s' % test_filter
-
-
-def get_gtest_filter(tests, invert=False):
- """Returns the GTest filter to filter the given test cases.
-
- Args:
- tests: List of test cases to filter.
- invert: Whether to invert the filter or not. Inverted, the filter will match
- everything except the given test cases.
-
- Returns:
- A string which can be supplied to --gtest_filter.
- """
- # A colon-separated list of tests cases.
- # e.g. a:b:c matches a, b, c.
- # e.g. -a:b:c matches everything except a, b, c.
- test_filter = ':'.join(test for test in tests)
- if invert:
- return '-%s' % test_filter
- return test_filter
-
-
-class TestRunner(object):
- """Base class containing common functionality."""
-
- def __init__(
- self,
- app_path,
- xcode_version,
- out_dir,
- env_vars=None,
- test_args=None,
- xctest=False,
- ):
- """Initializes a new instance of this class.
-
- Args:
- app_path: Path to the compiled .app to run.
- xcode_version: Version of Xcode to use when running the test.
- out_dir: Directory to emit test data into.
- env_vars: List of environment variables to pass to the test itself.
- test_args: List of strings to pass as arguments to the test when
- launching.
- xctest: Whether or not this is an XCTest.
-
- Raises:
- AppNotFoundError: If the given app does not exist.
- PlugInsNotFoundError: If the PlugIns directory does not exist for XCTests.
- XcodeVersionNotFoundError: If the given Xcode version does not exist.
- XCTestPlugInNotFoundError: If the .xctest PlugIn does not exist.
- """
- app_path = os.path.abspath(app_path)
- if not os.path.exists(app_path):
- raise AppNotFoundError(app_path)
-
- if not find_xcode.find_xcode(xcode_version)['found']:
- raise XcodeVersionNotFoundError(xcode_version)
-
- if not os.path.exists(out_dir):
- os.makedirs(out_dir)
-
- self.app_name = os.path.splitext(os.path.split(app_path)[-1])[0]
- self.app_path = app_path
- self.cfbundleid = subprocess.check_output([
- '/usr/libexec/PlistBuddy',
- '-c', 'Print:CFBundleIdentifier',
- os.path.join(app_path, 'Info.plist'),
- ]).rstrip()
- self.env_vars = env_vars or []
- self.logs = collections.OrderedDict()
- self.out_dir = out_dir
- self.test_args = test_args or []
- self.xcode_version = xcode_version
- self.xctest_path = ''
-
- if xctest:
- plugins_dir = os.path.join(self.app_path, 'PlugIns')
- if not os.path.exists(plugins_dir):
- raise PlugInsNotFoundError(plugins_dir)
- for plugin in os.listdir(plugins_dir):
- if plugin.endswith('.xctest'):
- self.xctest_path = os.path.join(plugins_dir, plugin)
- if not os.path.exists(self.xctest_path):
- raise XCTestPlugInNotFoundError(self.xctest_path)
-
- def get_launch_command(self, test_filter=None, invert=False):
- """Returns the command that can be used to launch the test app.
-
- Args:
- test_filter: List of test cases to filter.
- invert: Whether to invert the filter or not. Inverted, the filter will
- match everything except the given test cases.
-
- Returns:
- A list of strings forming the command to launch the test.
- """
- raise NotImplementedError
-
- def get_launch_env(self):
- """Returns a dict of environment variables to use to launch the test app.
-
- Returns:
- A dict of environment variables.
- """
- return os.environ.copy()
-
- def set_up(self):
- """Performs setup actions which must occur prior to every test launch."""
- raise NotImplementedError
-
- def tear_down(self):
- """Performs cleanup actions which must occur after every test launch."""
- raise NotImplementedError
-
- def screenshot_desktop(self):
- """Saves a screenshot of the desktop in the output directory."""
- subprocess.check_call([
- 'screencapture',
- os.path.join(self.out_dir, 'desktop_%s.png' % time.time()),
- ])
-
- def _run(self, cmd):
- """Runs the specified command, parsing GTest output.
-
- Args:
- cmd: List of strings forming the command to run.
-
- Returns:
- GTestResult instance.
- """
- print ' '.join(cmd)
- print
-
- result = gtest_utils.GTestResult(cmd)
- if self.xctest_path:
- parser = xctest_utils.XCTestLogParser()
- else:
- parser = gtest_utils.GTestLogParser()
-
- proc = subprocess.Popen(
- cmd,
- env=self.get_launch_env(),
- stdout=subprocess.PIPE,
- stderr=subprocess.STDOUT,
- )
-
- while True:
- line = proc.stdout.readline()
- if not line:
- break
- line = line.rstrip()
- parser.ProcessLine(line)
- print line
- sys.stdout.flush()
-
- proc.wait()
- sys.stdout.flush()
-
- for test in parser.FailedTests(include_flaky=True):
- # Test cases are named as <test group>.<test case>. If the test case
- # is prefixed with "FLAKY_", it should be reported as flaked not failed.
- if '.' in test and test.split('.', 1)[1].startswith('FLAKY_'):
- result.flaked_tests[test] = parser.FailureDescription(test)
- else:
- result.failed_tests[test] = parser.FailureDescription(test)
-
- result.passed_tests.extend(parser.PassedTests(include_flaky=True))
-
- print '%s returned %s' % (cmd[0], proc.returncode)
- print
-
- # iossim can return 5 if it exits noncleanly even if all tests passed.
- # Therefore we cannot rely on process exit code to determine success.
- result.finalize(proc.returncode, parser.CompletedWithoutFailure())
- return result
-
- def launch(self):
- """Launches the test app."""
- self.set_up()
- cmd = self.get_launch_command()
- try:
- result = self._run(cmd)
- if result.crashed and not result.crashed_test:
- # If the app crashed but not during any particular test case, assume
- # it crashed on startup. Try one more time.
- print 'Crashed on startup, retrying...'
- print
- result = self._run(cmd)
-
- if result.crashed and not result.crashed_test:
- raise AppLaunchError
-
- passed = result.passed_tests
- failed = result.failed_tests
- flaked = result.flaked_tests
-
- try:
- # XCTests cannot currently be resumed at the next test case.
- while not self.xctest_path and result.crashed and result.crashed_test:
- # If the app crashes during a specific test case, then resume at the
- # next test case. This is achieved by filtering out every test case
- # which has already run.
- print 'Crashed during %s, resuming...' % result.crashed_test
- print
- result = self._run(self.get_launch_command(
- test_filter=passed + failed.keys() + flaked.keys(), invert=True,
- ))
- passed.extend(result.passed_tests)
- failed.update(result.failed_tests)
- flaked.update(result.flaked_tests)
- except OSError as e:
- if e.errno == errno.E2BIG:
- print 'Too many test cases to resume.'
- print
- else:
- raise
-
- self.logs['passed tests'] = passed
- for test, log_lines in failed.iteritems():
- self.logs[test] = log_lines
- for test, log_lines in flaked.iteritems():
- self.logs[test] = log_lines
-
- return not failed
- finally:
- self.tear_down()
-
-
-class SimulatorTestRunner(TestRunner):
- """Class for running tests on iossim."""
-
- def __init__(
- self,
- app_path,
- iossim_path,
- platform,
- version,
- xcode_version,
- out_dir,
- env_vars=None,
- test_args=None,
- xctest=False,
- ):
- """Initializes a new instance of this class.
-
- Args:
- app_path: Path to the compiled .app or .ipa to run.
- iossim_path: Path to the compiled iossim binary to use.
- platform: Name of the platform to simulate. Supported values can be found
- by running "iossim -l". e.g. "iPhone 5s", "iPad Retina".
- version: Version of iOS the platform should be running. Supported values
- can be found by running "iossim -l". e.g. "9.3", "8.2", "7.1".
- xcode_version: Version of Xcode to use when running the test.
- out_dir: Directory to emit test data into.
- env_vars: List of environment variables to pass to the test itself.
- test_args: List of strings to pass as arguments to the test when
- launching.
- xctest: Whether or not this is an XCTest.
-
- Raises:
- AppNotFoundError: If the given app does not exist.
- PlugInsNotFoundError: If the PlugIns directory does not exist for XCTests.
- XcodeVersionNotFoundError: If the given Xcode version does not exist.
- XCTestPlugInNotFoundError: If the .xctest PlugIn does not exist.
- """
- super(SimulatorTestRunner, self).__init__(
- app_path,
- xcode_version,
- out_dir,
- env_vars=env_vars,
- test_args=test_args,
- xctest=xctest,
- )
-
- iossim_path = os.path.abspath(iossim_path)
- if not os.path.exists(iossim_path):
- raise SimulatorNotFoundError(iossim_path)
-
- self.homedir = ''
- self.iossim_path = iossim_path
- self.platform = platform
- self.start_time = None
- self.version = version
-
- @staticmethod
- def kill_simulators():
- """Kills all running simulators."""
- try:
- subprocess.check_call([
- 'pkill',
- '-9',
- '-x',
- # The simulator's name varies by Xcode version.
- 'iPhone Simulator', # Xcode 5
- 'iOS Simulator', # Xcode 6
- 'Simulator', # Xcode 7+
- 'simctl', # https://crbug.com/637429
- ])
- # If a signal was sent, wait for the simulators to actually be killed.
- time.sleep(5)
- except subprocess.CalledProcessError as e:
- if e.returncode != 1:
- # Ignore a 1 exit code (which means there were no simulators to kill).
- raise
-
- def wipe_simulator(self):
- """Wipes the simulator."""
- subprocess.check_call([
- self.iossim_path,
- '-d', self.platform,
- '-s', self.version,
- '-w',
- ])
-
- def get_home_directory(self):
- """Returns the simulator's home directory."""
- return subprocess.check_output([
- self.iossim_path,
- '-d', self.platform,
- '-p',
- '-s', self.version,
- ]).rstrip()
-
- def set_up(self):
- """Performs setup actions which must occur prior to every test launch."""
- self.kill_simulators()
- self.wipe_simulator()
- self.homedir = self.get_home_directory()
- # Crash reports have a timestamp in their file name, formatted as
- # YYYY-MM-DD-HHMMSS. Save the current time in the same format so
- # we can compare and fetch crash reports from this run later on.
- self.start_time = time.strftime('%Y-%m-%d-%H%M%S', time.localtime())
-
- def extract_test_data(self):
- """Extracts data emitted by the test."""
- # Find the Documents directory of the test app. The app directory names
- # don't correspond with any known information, so we have to examine them
- # all until we find one with a matching CFBundleIdentifier.
- apps_dir = os.path.join(
- self.homedir, 'Containers', 'Data', 'Application')
- if os.path.exists(apps_dir):
- for appid_dir in os.listdir(apps_dir):
- docs_dir = os.path.join(apps_dir, appid_dir, 'Documents')
- metadata_plist = os.path.join(
- apps_dir,
- appid_dir,
- '.com.apple.mobile_container_manager.metadata.plist',
- )
- if os.path.exists(docs_dir) and os.path.exists(metadata_plist):
- cfbundleid = subprocess.check_output([
- '/usr/libexec/PlistBuddy',
- '-c', 'Print:MCMMetadataIdentifier',
- metadata_plist,
- ]).rstrip()
- if cfbundleid == self.cfbundleid:
- shutil.copytree(docs_dir, os.path.join(self.out_dir, 'Documents'))
- return
-
- def retrieve_crash_reports(self):
- """Retrieves crash reports produced by the test."""
- # A crash report's naming scheme is [app]_[timestamp]_[hostname].crash.
- # e.g. net_unittests_2014-05-13-15-0900_vm1-a1.crash.
- crash_reports_dir = os.path.expanduser(os.path.join(
- '~', 'Library', 'Logs', 'DiagnosticReports'))
-
- if not os.path.exists(crash_reports_dir):
- return
-
- for crash_report in os.listdir(crash_reports_dir):
- report_name, ext = os.path.splitext(crash_report)
- if report_name.startswith(self.app_name) and ext == '.crash':
- report_time = report_name[len(self.app_name) + 1:].split('_')[0]
-
- # The timestamp format in a crash report is big-endian and therefore
- # a staight string comparison works.
- if report_time > self.start_time:
- with open(os.path.join(crash_reports_dir, crash_report)) as f:
- self.logs['crash report (%s)' % report_time] = (
- f.read().splitlines())
-
- def tear_down(self):
- """Performs cleanup actions which must occur after every test launch."""
- self.extract_test_data()
- self.retrieve_crash_reports()
- self.screenshot_desktop()
- self.kill_simulators()
- self.wipe_simulator()
- if os.path.exists(self.homedir):
- shutil.rmtree(self.homedir, ignore_errors=True)
- self.homedir = ''
-
- def get_launch_command(self, test_filter=None, invert=False):
- """Returns the command that can be used to launch the test app.
-
- Args:
- test_filter: List of test cases to filter.
- invert: Whether to invert the filter or not. Inverted, the filter will
- match everything except the given test cases.
-
- Returns:
- A list of strings forming the command to launch the test.
- """
- cmd = [
- self.iossim_path,
- '-d', self.platform,
- '-s', self.version,
- ]
-
- if test_filter:
- kif_filter = get_kif_test_filter(test_filter, invert=invert)
- gtest_filter = get_gtest_filter(test_filter, invert=invert)
- cmd.extend(['-e', 'GKIF_SCENARIO_FILTER=%s' % kif_filter])
- cmd.extend(['-c', '--gtest_filter=%s' % gtest_filter])
-
- for env_var in self.env_vars:
- cmd.extend(['-e', env_var])
-
- for test_arg in self.test_args:
- cmd.extend(['-c', test_arg])
-
- cmd.append(self.app_path)
- if self.xctest_path:
- cmd.append(self.xctest_path)
- return cmd
-
- def get_launch_env(self):
- """Returns a dict of environment variables to use to launch the test app.
-
- Returns:
- A dict of environment variables.
- """
- env = super(SimulatorTestRunner, self).get_launch_env()
- if self.xctest_path:
- env['NSUnbufferedIO'] = 'YES'
- return env
-
-
-class DeviceTestRunner(TestRunner):
- """Class for running tests on devices."""
-
- def __init__(
- self,
- app_path,
- xcode_version,
- out_dir,
- env_vars=None,
- test_args=None,
- xctest=False,
- ):
- """Initializes a new instance of this class.
-
- Args:
- app_path: Path to the compiled .app to run.
- xcode_version: Version of Xcode to use when running the test.
- out_dir: Directory to emit test data into.
- env_vars: List of environment variables to pass to the test itself.
- test_args: List of strings to pass as arguments to the test when
- launching.
- xctest: Whether or not this is an XCTest.
-
- Raises:
- AppNotFoundError: If the given app does not exist.
- PlugInsNotFoundError: If the PlugIns directory does not exist for XCTests.
- XcodeVersionNotFoundError: If the given Xcode version does not exist.
- XCTestPlugInNotFoundError: If the .xctest PlugIn does not exist.
- """
- super(DeviceTestRunner, self).__init__(
- app_path,
- xcode_version,
- out_dir,
- env_vars=env_vars,
- test_args=test_args,
- xctest=xctest,
- )
-
- self.udid = subprocess.check_output(['idevice_id', '--list']).rstrip()
- if len(self.udid.splitlines()) != 1:
- raise DeviceDetectionError(self.udid)
-
- def uninstall_apps(self):
- """Uninstalls all apps found on the device."""
- for app in subprocess.check_output(
- ['idevicefs', '--udid', self.udid, 'ls', '@']).splitlines():
- subprocess.check_call(
- ['ideviceinstaller', '--udid', self.udid, '--uninstall', app])
-
- def install_app(self):
- """Installs the app."""
- subprocess.check_call(
- ['ideviceinstaller', '--udid', self.udid, '--install', self.app_path])
-
- def set_up(self):
- """Performs setup actions which must occur prior to every test launch."""
- self.uninstall_apps()
- self.install_app()
-
- def extract_test_data(self):
- """Extracts data emitted by the test."""
- subprocess.check_call([
- 'idevicefs',
- '--udid', self.udid,
- 'pull',
- '@%s/Documents' % self.cfbundleid,
- os.path.join(self.out_dir, 'Documents'),
- ])
-
- def retrieve_crash_reports(self):
- """Retrieves crash reports produced by the test."""
- logs_dir = os.path.join(self.out_dir, 'Logs')
- os.mkdir(logs_dir)
- subprocess.check_call([
- 'idevicecrashreport',
- '--extract',
- '--udid', self.udid,
- logs_dir,
- ])
-
- def tear_down(self):
- """Performs cleanup actions which must occur after every test launch."""
- self.extract_test_data()
- self.retrieve_crash_reports()
- self.screenshot_desktop()
- self.uninstall_apps()
-
- def get_launch_command(self, test_filter=None, invert=False):
- """Returns the command that can be used to launch the test app.
-
- Args:
- test_filter: List of test cases to filter.
- invert: Whether to invert the filter or not. Inverted, the filter will
- match everything except the given test cases.
-
- Returns:
- A list of strings forming the command to launch the test.
- """
- if self.xctest_path:
- return [
- 'xcodebuild',
- 'test-without-building',
- 'BUILT_PRODUCTS_DIR=%s' % os.path.dirname(self.app_path),
- '-destination', 'id=%s' % self.udid,
- '-project', XCTEST_PROJECT,
- '-scheme', XCTEST_SCHEME,
- ]
-
- cmd = [
- 'idevice-app-runner',
- '--udid', self.udid,
- '--start', self.cfbundleid,
- ]
- args = []
-
- if test_filter:
- kif_filter = get_kif_test_filter(test_filter, invert=invert)
- gtest_filter = get_gtest_filter(test_filter, invert=invert)
- cmd.extend(['-D', 'GKIF_SCENARIO_FILTER=%s' % kif_filter])
- args.append('--gtest-filter=%s' % gtest_filter)
-
- for env_var in self.env_vars:
- cmd.extend(['-D', env_var])
-
- if args or self.test_args:
- cmd.append('--args')
- cmd.extend(self.test_args)
- cmd.extend(args)
-
- return cmd
-
- def get_launch_env(self):
- """Returns a dict of environment variables to use to launch the test app.
-
- Returns:
- A dict of environment variables.
- """
- env = super(DeviceTestRunner, self).get_launch_env()
- if self.xctest_path:
- env['NSUnbufferedIO'] = 'YES'
- # e.g. ios_web_shell_egtests
- env['APP_TARGET_NAME'] = os.path.splitext(
- os.path.basename(self.app_path))[0]
- # e.g. ios_web_shell_egtests_module
- env['TEST_TARGET_NAME'] = env['APP_TARGET_NAME'] + '_module'
- return env
« no previous file with comments | « ios/build/bots/scripts/run.py ('k') | ios/build/bots/scripts/test_runner_test.py » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698