diff options
| author | Allan Sandfeld Jensen <allan.jensen@digia.com> | 2013-09-13 12:51:20 +0200 |
|---|---|---|
| committer | The Qt Project <gerrit-noreply@qt-project.org> | 2013-09-19 20:50:05 +0200 |
| commit | d441d6f39bb846989d95bcf5caf387b42414718d (patch) | |
| tree | e367e64a75991c554930278175d403c072de6bb8 /Tools/Scripts/webkitpy/common/system | |
| parent | 0060b2994c07842f4c59de64b5e3e430525c4b90 (diff) | |
| download | qtwebkit-d441d6f39bb846989d95bcf5caf387b42414718d.tar.gz | |
Import Qt5x2 branch of QtWebkit for Qt 5.2
Importing a new snapshot of webkit.
Change-Id: I2d01ad12cdc8af8cb015387641120a9d7ea5f10c
Reviewed-by: Allan Sandfeld Jensen <allan.jensen@digia.com>
Diffstat (limited to 'Tools/Scripts/webkitpy/common/system')
29 files changed, 597 insertions, 157 deletions
diff --git a/Tools/Scripts/webkitpy/common/system/autoinstall.py b/Tools/Scripts/webkitpy/common/system/autoinstall.py index 9d1f8cb2f..2e15887bb 100755..100644 --- a/Tools/Scripts/webkitpy/common/system/autoinstall.py +++ b/Tools/Scripts/webkitpy/common/system/autoinstall.py @@ -35,10 +35,11 @@ import codecs import logging import os import shutil +import stat import sys import tarfile import tempfile -import urllib +import urllib2 import urlparse import zipfile @@ -173,7 +174,7 @@ class AutoInstaller(object): return scratch_dir def _url_downloaded_path(self, target_name): - return os.path.join(self._target_dir, ".%s.url" % target_name) + return os.path.join(self._target_dir, ".%s.url" % target_name.replace('/', '_')) def _is_downloaded(self, target_name, url): version_path = self._url_downloaded_path(target_name) @@ -283,17 +284,27 @@ class AutoInstaller(object): return new_path def _download_to_stream(self, url, stream): - try: - netstream = urllib.urlopen(url) - except IOError, err: - # Append existing Error message to new Error. - message = ('Could not download Python modules from URL "%s".\n' - " Make sure you are connected to the internet.\n" - " You must be connected to the internet when " - "downloading needed modules for the first time.\n" - " --> Inner message: %s" - % (url, err)) - raise IOError(message) + failures = 0 + while True: + try: + netstream = urllib2.urlopen(url) + break + except IOError, err: + # Try multiple times + if failures < 5: + _log.warning("Failed to download %s, %s retrying" % ( + url, err)) + failures += 1 + continue + + # Append existing Error message to new Error. + message = ('Could not download Python modules from URL "%s".\n' + " Make sure you are connected to the internet.\n" + " You must be connected to the internet when " + "downloading needed modules for the first time.\n" + " --> Inner message: %s" + % (url, err)) + raise IOError(message) code = 200 if hasattr(netstream, "getcode"): code = netstream.getcode() @@ -319,8 +330,7 @@ class AutoInstaller(object): return target_path - def _install(self, scratch_dir, package_name, target_path, url, - url_subpath): + def _install(self, scratch_dir, package_name, target_path, url, url_subpath, files_to_remove): """Install a python package from an URL. This internal method overwrites the target path if the target @@ -335,6 +345,13 @@ class AutoInstaller(object): else: source_path = os.path.join(path, url_subpath) + for filename in files_to_remove: + path = os.path.join(source_path, filename.replace('/', os.sep)) + if os.path.exists(path): + # Pre-emptively change the permissions to #0777 to try and work around win32 permissions issues. + os.chmod(path, stat.S_IRWXU | stat.S_IRWXG | stat.S_IRWXO) + os.remove(path) + if os.path.exists(target_path): if os.path.isdir(target_path): shutil.rmtree(target_path, ignore_errors=True) @@ -354,7 +371,7 @@ class AutoInstaller(object): self._record_url_downloaded(package_name, url) def install(self, url, should_refresh=False, target_name=None, - url_subpath=None): + url_subpath=None, files_to_remove=None): """Install a python package from an URL. Args: @@ -382,10 +399,11 @@ class AutoInstaller(object): url_subpath = os.path.normpath(url_subpath) target_name = os.path.basename(url_subpath) - target_path = os.path.join(self._target_dir, target_name) + target_path = os.path.join(self._target_dir, target_name.replace('/', os.sep)) if not should_refresh and self._is_downloaded(target_name, url): return False + files_to_remove = files_to_remove or [] package_name = target_name.replace(os.sep, '.') _log.info("Auto-installing package: %s" % package_name) @@ -399,7 +417,8 @@ class AutoInstaller(object): target_path=target_path, scratch_dir=scratch_dir, url=url, - url_subpath=url_subpath) + url_subpath=url_subpath, + files_to_remove=files_to_remove) except Exception, err: # Append existing Error message to new Error. message = ("Error auto-installing the %s package to:\n" diff --git a/Tools/Scripts/webkitpy/common/system/crashlogs.py b/Tools/Scripts/webkitpy/common/system/crashlogs.py index 270ca81ed..7ebe52241 100644 --- a/Tools/Scripts/webkitpy/common/system/crashlogs.py +++ b/Tools/Scripts/webkitpy/common/system/crashlogs.py @@ -26,16 +26,23 @@ # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +import codecs import re class CrashLogs(object): - def __init__(self, host): + + PID_LINE_REGEX = re.compile(r'\s+Global\s+PID:\s+\[(?P<pid>\d+)\]') + + def __init__(self, host, results_directory=None): self._host = host + self._results_directory = results_directory def find_newest_log(self, process_name, pid=None, include_errors=False, newer_than=None): if self._host.platform.is_mac(): return self._find_newest_log_darwin(process_name, pid, include_errors, newer_than) + elif self._host.platform.is_win(): + return self._find_newest_log_win(process_name, pid, include_errors, newer_than) return None def _log_directory_darwin(self): @@ -72,3 +79,35 @@ class CrashLogs(object): if include_errors and errors: return errors return None + + def _find_newest_log_win(self, process_name, pid, include_errors, newer_than): + def is_crash_log(fs, dirpath, basename): + return basename.startswith("CrashLog") + + logs = self._host.filesystem.files_under(self._results_directory, file_filter=is_crash_log) + errors = '' + for path in reversed(sorted(logs)): + try: + if not newer_than or self._host.filesystem.mtime(path) > newer_than: + log_file = self._host.filesystem.read_binary_file(path).decode('utf8', 'ignore') + match = self.PID_LINE_REGEX.search(log_file) + if match is None: + continue + if int(match.group('pid')) == pid: + return errors + log_file + except IOError, e: + print "IOError %s" % str(e) + if include_errors: + errors += "ERROR: Failed to read '%s': %s\n" % (path, str(e)) + except OSError, e: + print "OSError %s" % str(e) + if include_errors: + errors += "ERROR: Failed to read '%s': %s\n" % (path, str(e)) + except UnicodeDecodeError, e: + print "UnicodeDecodeError %s" % str(e) + if include_errors: + errors += "ERROR: Failed to decode '%s' as utf8: %s\n" % (path, str(e)) + + if include_errors and errors: + return errors + return None diff --git a/Tools/Scripts/webkitpy/common/system/crashlogs_unittest.py b/Tools/Scripts/webkitpy/common/system/crashlogs_unittest.py index 1f5c40a09..48034e806 100644 --- a/Tools/Scripts/webkitpy/common/system/crashlogs_unittest.py +++ b/Tools/Scripts/webkitpy/common/system/crashlogs_unittest.py @@ -21,7 +21,7 @@ # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF # THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -import unittest +import unittest2 as unittest from webkitpy.common.system.crashlogs import CrashLogs from webkitpy.common.system.filesystem_mock import MockFileSystem @@ -29,6 +29,8 @@ from webkitpy.common.system.systemhost import SystemHost from webkitpy.common.system.systemhost_mock import MockSystemHost from webkitpy.thirdparty.mock import Mock +# Needed to support Windows port tests +from webkitpy.port.win import WinPort def make_mock_crash_report_darwin(process_name, pid): return """Process: {process_name} [{pid}] @@ -68,14 +70,169 @@ PCI Card: NVIDIA GeForce GT 120, sppci_displaycontroller, MXM-Slot Serial ATA Device: OPTIARC DVD RW AD-5670S """.format(process_name=process_name, pid=pid) -class CrashLogsTest(unittest.TestCase): - def assertLinesEqual(self, a, b): - if hasattr(self, 'assertMultiLineEqual'): - self.assertMultiLineEqual(a, b) - else: - self.assertEqual(a.splitlines(), b.splitlines()) +def make_mock_crash_report_win(process_name, pid): + return """Opened log file 'C:\Projects\WebKit\OpenSource\WebKitBuild\Release\bin32\layout-test-results\CrashLog_1d58_2013-06-03_12-21-20-110.txt' +0:000> .srcpath "C:\Projects\WebKit\OpenSource" +Source search path is: C:\Projects\WebKit\OpenSource +0:000> !analyze -vv +******************************************************************************* +* * +* Exception Analysis * +* * +******************************************************************************* + +*** ERROR: Symbol file could not be found. Defaulted to export symbols for C:\Projects\WebKit\OpenSource\WebKitBuild\Release\bin32\libdispatch.dll - +*** ERROR: Symbol file could not be found. Defaulted to export symbols for C:\Windows\SYSTEM32\atiumdag.dll - + +FAULTING_IP: +JavaScriptCore!JSC::JSActivation::getOwnPropertySlot+0 [c:\projects\webkit\opensource\source\javascriptcore\runtime\jsactivation.cpp @ 146] +01e3d070 55 push ebp + +EXCEPTION_RECORD: 00092cc8 -- (.exr 0x92cc8) +.exr 0x92cc8 +ExceptionAddress: 01e3d070 (JavaScriptCore!JSC::JSActivation::getOwnPropertySlot) + ExceptionCode: c00000fd (Stack overflow) + ExceptionFlags: 00000000 +NumberParameters: 2 + Parameter[0]: 00000001 + Parameter[1]: 00092ffc + +FAULTING_THREAD: 00000e68 +PROCESS_NAME: {process_name} +ERROR_CODE: (NTSTATUS) 0xc0000005 - The instruction at 0x%08lx referenced memory at 0x%08lx. The memory could not be %s. +EXCEPTION_CODE: (NTSTATUS) 0xc0000005 - The instruction at 0x%08lx referenced memory at 0x%08lx. The memory could not be %s. +EXCEPTION_CODE_STR: c0000005 +EXCEPTION_PARAMETER1: 00000000 +EXCEPTION_PARAMETER2: 00090000 +READ_ADDRESS: 00090000 + +FOLLOWUP_IP: +JavaScriptCore!JSC::JSActivation::getOwnPropertySlot+0 [c:\projects\webkit\opensource\source\javascriptcore\runtime\jsactivation.cpp @ 146] +01e3d070 55 push ebp + +WATSON_BKT_PROCSTAMP: 51a8f979 +WATSON_BKT_MODULE: MSVCR100.dll +WATSON_BKT_MODVER: 10.0.40219.325 +WATSON_BKT_MODSTAMP: 4df2be1e +WATSON_BKT_MODOFFSET: 160d7 +MODULE_VER_PRODUCT: Microsoft(R) Visual Studio(R) 2010 +BUILD_VERSION_STRING: 6.2.9200.16384 (win8_rtm.120725-1247) +NTGLOBALFLAG: 0 +APPLICATION_VERIFIER_FLAGS: 0 +APP: {process_name} + +ANALYSIS_SESSION_HOST: FULGBR-PC + +ANALYSIS_SESSION_TIME: 06-03-2013 12:21:20.0111 + +CONTEXT: 00092d18 -- (.cxr 0x92d18) +.cxr 0x92d18 +eax=01e3d070 ebx=000930bc ecx=7fe03ed0 edx=0751e168 esi=07a7ff98 edi=0791ff78 +eip=01e3d070 esp=00093000 ebp=0009306c iopl=0 nv up ei ng nz ac po cy +cs=0023 ss=002b ds=002b es=002b fs=0053 gs=002b efl=00210293 +JavaScriptCore!JSC::JSActivation::getOwnPropertySlot: +01e3d070 55 push ebp +.cxr +Resetting default scope + +RECURRING_STACK: From frames 0x14 to 0x1d + +THREAD_ATTRIBUTES: + +[ GLOBAL ] + + Global PID: [{pid}] + Global Thread_Count: [19] + Global PageSize: [4096] + Global ModList_SHA1_Hash: [aacef4e7e83b9bddc9cd0cc094dac88d531ea4a3] + Global CommandLine: [C:\Projects\WebKit\OpenSource\WebKitBuild\Release\bin32\{process_name} -] + Global Desktop_Name: [Winsta0\Default] + Global ProcessName: [{process_name}] + Global Debugger_CPU_Architecture: [X86] + Global CPU_ProcessorCount: [24] + Global CPU_MHZ: [1596] + Global CPU_Architecture: [X86] + Global CPU_Family: [6] + Global CPU_Model: [12] + Global CPU_Stepping: [2] + Global CPU_VendorString: [GenuineIntel] + Global LoadedModule_Count: [82] + Global ProcessBeingDebugged + Global GFlags: [0] + Global Application_Verifer_Flags: [0] + Global FinalExh: [2012093943] + Global SystemUpTime: [3 days 23:52:56.000] + Global SystemUpTime: [345176] + Global ProcessUpTime: [0 days 0:00:00.000] + Global ProcessUpTime: [0] + Global CurrentTimeDate: [Mon Jun 3 12:21:20.000 2013 (UTC - 7:00)] + Global CurrentTimeDate: [1370287280] + Global ProductType: [1] + Global SuiteMask: [272] + Global ApplicationName: [{process_name}] + Global ASLR_Enabled + Global SafeSEH_Enabled + +FAULT_INSTR_CODE: 83ec8b55 + +FAULTING_SOURCE_LINE: c:\projects\webkit\opensource\source\javascriptcore\runtime\jsactivation.cpp + +FAULTING_SOURCE_FILE: c:\projects\webkit\opensource\source\javascriptcore\runtime\jsactivation.cpp + +FAULTING_SOURCE_LINE_NUMBER: 146 + +SYMBOL_STACK_INDEX: 0 + +SYMBOL_NAME: javascriptcore!JSC::JSActivation::getOwnPropertySlot+92ffc + +FOLLOWUP_NAME: MachineOwner + +MODULE_NAME: JavaScriptCore + +IMAGE_NAME: JavaScriptCore.dll + +DEBUG_FLR_IMAGE_TIMESTAMP: 51ace473 + +STACK_COMMAND: .cxr 00092D18 ; kb ; dps 93000 ; kb +FAILURE_BUCKET_ID: STACK_OVERFLOW_c0000005_JavaScriptCore.dll!JSC::JSActivation::getOwnPropertySlot + +BUCKET_ID: APPLICATION_FAULT_STACK_OVERFLOW_INVALID_POINTER_READ_javascriptcore!JSC::JSActivation::getOwnPropertySlot+92ffc + +ANALYSIS_SESSION_ELAPSED_TIME: 18df + +Followup: MachineOwner +--------- + +0:000> ~*kpn + +. 0 Id: 18e0.e68 Suspend: 1 Teb: 7ffdd000 Unfrozen + # ChildEBP RetAddr +00 00092a08 7261ece1 MSVCR100!_alloca_probe+0x27 +01 00092a4c 7261a5d0 MSVCR100!_write+0x95 +02 00092a6c 7261ef6b MSVCR100!_flush+0x3b +03 00092a7c 7261ef1c MSVCR100!_fflush_nolock+0x1c +04 00092ab4 1000f814 MSVCR100!fflush+0x30 +05 00092ac8 77c0084e DumpRenderTree_10000000!exceptionFilter(struct _EXCEPTION_POINTERS * __formal = 0x852ac807)+0x24 [c:\projects\webkit\opensource\tools\dumprendertree\win\dumprendertree.cpp @ 1281] +06 00092b60 77e8bf2c KERNELBASE!UnhandledExceptionFilter+0x164 +07 00092b68 77e530b4 ntdll!__RtlUserThreadStart+0x57 +08 00092b7c 77e15246 ntdll!_EH4_CallFilterFunc+0x12 +09 00092ba4 77e151b1 ntdll!_except_handler4_common+0x8e +0a 00092bc4 77e52e71 ntdll!_except_handler4+0x20 +0b 00092be8 77e52e43 ntdll!ExecuteHandler2+0x26 +0c 00092cb0 77e52cbb ntdll!ExecuteHandler+0x24 +0d 00092cb0 01e3d070 ntdll!KiUserExceptionDispatcher+0xf +0e 00092ffc 01e67d25 JavaScriptCore!JSC::JSActivation::getOwnPropertySlot(class JSC::JSCell * cell = 0x07a7ff98, class JSC::ExecState * exec = 0x0751e168, class JSC::PropertyName propertyName = class JSC::PropertyName, class JSC::PropertySlot * slot = 0x000930bc) [c:\projects\webkit\opensource\source\javascriptcore\runtime\jsactivation.cpp @ 146] +0f 0009306c 01e68837 JavaScriptCore!JSC::JSScope::resolveContainingScopeInternal<1,2>(class JSC::ExecState * callFrame = 0x0751e168, class JSC::Identifier * identifier = 0x7fe0ebc0, class JSC::PropertySlot * slot = 0x7fe03ed0, class WTF::Vector<JSC::ResolveOperation,0,WTF::CrashOnOverflow> * operations = 0x7fda16c0, struct JSC::PutToBaseOperation * putToBaseOperation = 0x00000000, bool __formal = false)+0x205 [c:\projects\webkit\opensource\source\javascriptcore\runtime\jsscope.cpp @ 247] +10 00093090 01e65860 JavaScriptCore!JSC::JSScope::resolveContainingScope<1>(class JSC::ExecState * callFrame = 0x0751e168, class JSC::Identifier * identifier = 0x7fe0ebc0, class JSC::PropertySlot * slot = 0x000930bc, class WTF::Vector<JSC::ResolveOperation,0,WTF::CrashOnOverflow> * operations = 0x7fda16c0, struct JSC::PutToBaseOperation * putToBaseOperation = 0x00000000, bool isStrict = false)+0x27 [c:\projects\webkit\opensource\source\javascriptcore\runtime\jsscope.cpp @ 427] +11 00093104 01dceeff JavaScriptCore!JSC::JSScope::resolve(class JSC::ExecState * callFrame = 0x0751e168, class JSC::Identifier * identifier = 0x7fe0ebc0, class WTF::Vector<JSC::ResolveOperation,0,WTF::CrashOnOverflow> * operations = 0x7fda16c0)+0xc0 [c:\projects\webkit\opensource\source\javascriptcore\runtime\jsscope.cpp @ 447] + +0:000> q +quit: +""".format(process_name=process_name, pid=pid) + +class CrashLogsTest(unittest.TestCase): def test_find_log_darwin(self): if not SystemHost().platform.is_mac(): return @@ -95,15 +252,15 @@ class CrashLogsTest(unittest.TestCase): filesystem = MockFileSystem(files) crash_logs = CrashLogs(MockSystemHost(filesystem=filesystem)) log = crash_logs.find_newest_log("DumpRenderTree") - self.assertLinesEqual(log, newer_mock_crash_report) + self.assertMultiLineEqual(log, newer_mock_crash_report) log = crash_logs.find_newest_log("DumpRenderTree", 28529) - self.assertLinesEqual(log, newer_mock_crash_report) + self.assertMultiLineEqual(log, newer_mock_crash_report) log = crash_logs.find_newest_log("DumpRenderTree", 28530) - self.assertLinesEqual(log, mock_crash_report) + self.assertMultiLineEqual(log, mock_crash_report) log = crash_logs.find_newest_log("DumpRenderTree", 28531) - self.assertEqual(log, None) + self.assertIsNone(log) log = crash_logs.find_newest_log("DumpRenderTree", newer_than=1.0) - self.assertEqual(log, None) + self.assertIsNone(log) def bad_read(path): raise IOError('IOError: No such file or directory') @@ -113,10 +270,47 @@ class CrashLogsTest(unittest.TestCase): filesystem.read_text_file = bad_read log = crash_logs.find_newest_log("DumpRenderTree", 28531, include_errors=True) - self.assertTrue('IOError: No such file or directory' in log) + self.assertIn('IOError: No such file or directory', log) filesystem = MockFileSystem(files) crash_logs = CrashLogs(MockSystemHost(filesystem=filesystem)) filesystem.mtime = bad_mtime log = crash_logs.find_newest_log("DumpRenderTree", newer_than=1.0, include_errors=True) - self.assertTrue('OSError: No such file or directory' in log) + self.assertIn('OSError: No such file or directory', log) + + def test_find_log_win(self): + if not SystemHost().platform.is_win(): + return + + older_mock_crash_report = make_mock_crash_report_win('DumpRenderTree', 28528) + mock_crash_report = make_mock_crash_report_win('DumpRenderTree', 28530) + newer_mock_crash_report = make_mock_crash_report_win('DumpRenderTree', 28529) + other_process_mock_crash_report = make_mock_crash_report_win('FooProcess', 28527) + misformatted_mock_crash_report = 'Junk that should not appear in a crash report' + make_mock_crash_report_win('DumpRenderTree', 28526)[200:] + files = {} + files['~/CrashLog_1d58_2013-06-03_12-21-20-110.txt'] = older_mock_crash_report + files['~/CrashLog_abcd_2013-06-03_12-22-19-129.txt'] = mock_crash_report + files['~/CrashLog_2eff_2013-06-03_12-23-20-150.txt'] = newer_mock_crash_report + files['~/CrashLog_31a0_2013-06-03_12-24-22-119.txt'] = None + files['~/CrashLog_01a3_2013-06-03_12-25-23-120.txt'] = other_process_mock_crash_report + files['~/CrashLog_aadd_2013-06-03_12-26-24-121.txt'] = misformatted_mock_crash_report + filesystem = MockFileSystem(files) + mock_host = MockSystemHost(os_name='win', filesystem=filesystem) + crash_logs = CrashLogs(mock_host, "~") + + log = crash_logs.find_newest_log("DumpRenderTree", 28529) + self.assertMultiLineEqual(log, newer_mock_crash_report) + log = crash_logs.find_newest_log("DumpRenderTree", 28530) + self.assertMultiLineEqual(log, mock_crash_report) + log = crash_logs.find_newest_log("DumpRenderTree", 28531) + self.assertIsNone(log) + log = crash_logs.find_newest_log("DumpRenderTree", newer_than=1.0) + self.assertIsNone(log) + + def bad_read(path): + raise IOError('IOError: No such file or directory') + + filesystem.read_text_file = bad_read + filesystem.read_binary_file = bad_read + log = crash_logs.find_newest_log("DumpRenderTree", 28531, include_errors=True) + self.assertIn('IOError: No such file or directory', log) diff --git a/Tools/Scripts/webkitpy/common/system/environment_unittest.py b/Tools/Scripts/webkitpy/common/system/environment_unittest.py index 6558b51df..2868a65d2 100644 --- a/Tools/Scripts/webkitpy/common/system/environment_unittest.py +++ b/Tools/Scripts/webkitpy/common/system/environment_unittest.py @@ -26,7 +26,7 @@ # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -import unittest +import unittest2 as unittest from .environment import Environment diff --git a/Tools/Scripts/webkitpy/common/system/executive.py b/Tools/Scripts/webkitpy/common/system/executive.py index 42a8122d3..ca45f2f35 100644 --- a/Tools/Scripts/webkitpy/common/system/executive.py +++ b/Tools/Scripts/webkitpy/common/system/executive.py @@ -46,14 +46,6 @@ _log = logging.getLogger(__name__) class ScriptError(Exception): - # This is a custom List.__str__ implementation to allow size limiting. - def _string_from_args(self, args, limit=100): - args_string = unicode(args) - # We could make this much fancier, but for now this is OK. - if len(args_string) > limit: - return args_string[:limit - 3] + "..." - return args_string - def __init__(self, message=None, script_args=None, @@ -61,7 +53,7 @@ class ScriptError(Exception): output=None, cwd=None): if not message: - message = 'Failed to run "%s"' % self._string_from_args(script_args) + message = 'Failed to run "%s"' % repr(script_args) if exit_code: message += " exit_code: %d" % exit_code if cwd: @@ -92,6 +84,9 @@ class Executive(object): PIPE = subprocess.PIPE STDOUT = subprocess.STDOUT + def __init__(self): + self.pid_to_system_pid = {} + def _should_close_fds(self): # We need to pass close_fds=True to work around Python bug #2320 # (otherwise we can hang when we kill DumpRenderTree when we are running @@ -101,9 +96,6 @@ class Executive(object): return sys.platform not in ('win32', 'cygwin') def _run_command_with_teed_output(self, args, teed_output, **kwargs): - args = map(unicode, args) # Popen will throw an exception if args are non-strings (like int()) - args = map(self._encode_argument_if_needed, args) - child_process = self.popen(args, stdout=self.PIPE, stderr=self.STDOUT, @@ -153,6 +145,12 @@ class Executive(object): return child_output def cpu_count(self): + try: + cpus = int(os.environ.get('NUMBER_OF_PROCESSORS')) + if cpus > 0: + return cpus + except (ValueError, TypeError): + pass return multiprocessing.cpu_count() @staticmethod @@ -272,26 +270,37 @@ class Executive(object): return False def running_pids(self, process_name_filter=None): + if sys.platform == "win32": + # FIXME: running_pids isn't implemented on native Windows yet... + return [] + if not process_name_filter: process_name_filter = lambda process_name: True running_pids = [] - - if sys.platform in ("win32", "cygwin"): - # FIXME: running_pids isn't implemented on Windows yet... - return [] - - ps_process = self.popen(['ps', '-eo', 'pid,comm'], stdout=self.PIPE, stderr=self.PIPE) - stdout, _ = ps_process.communicate() - for line in stdout.splitlines(): - try: - # In some cases the line can contain one or more - # leading white-spaces, so strip it before split. - pid, process_name = line.strip().split(' ', 1) - if process_name_filter(process_name): - running_pids.append(int(pid)) - except ValueError, e: - pass + if sys.platform in ("cygwin"): + ps_process = self.run_command(['ps', '-e'], error_handler=Executive.ignore_error) + for line in ps_process.splitlines(): + tokens = line.strip().split() + try: + pid, ppid, pgid, winpid, tty, uid, stime, process_name = tokens + if process_name_filter(process_name): + running_pids.append(int(pid)) + self.pid_to_system_pid[int(pid)] = int(winpid) + except ValueError, e: + pass + else: + ps_process = self.popen(['ps', '-eo', 'pid,comm'], stdout=self.PIPE, stderr=self.PIPE) + stdout, _ = ps_process.communicate() + for line in stdout.splitlines(): + try: + # In some cases the line can contain one or more + # leading white-spaces, so strip it before split. + pid, process_name = line.strip().split(' ', 1) + if process_name_filter(process_name): + running_pids.append(int(pid)) + except ValueError, e: + pass return sorted(running_pids) @@ -307,6 +316,13 @@ class Executive(object): while self.check_running_pid(pid): time.sleep(0.25) + def wait_limited(self, pid, limit_in_seconds=None, check_frequency_in_seconds=None): + seconds_left = limit_in_seconds or 10 + sleep_length = check_frequency_in_seconds or 1 + while seconds_left > 0 and self.check_running_pid(pid): + seconds_left -= sleep_length + time.sleep(sleep_length) + def _windows_image_name(self, process_name): name, extension = os.path.splitext(process_name) if not extension: @@ -315,6 +331,17 @@ class Executive(object): process_name = "%s.exe" % name return process_name + def interrupt(self, pid): + interrupt_signal = signal.SIGINT + # FIXME: The python docs seem to imply that platform == 'win32' may need to use signal.CTRL_C_EVENT + # http://docs.python.org/2/library/signal.html + try: + os.kill(pid, interrupt_signal) + except OSError: + # Silently ignore when the pid doesn't exist. + # It's impossible for callers to avoid race conditions with process shutdown. + pass + def kill_all(self, process_name): """Attempts to kill processes matching process_name. Will fail silently if no process are found.""" @@ -365,9 +392,10 @@ class Executive(object): input = input.encode(self._child_process_encoding()) return (self.PIPE, input) - def _command_for_printing(self, args): + def command_for_printing(self, args): """Returns a print-ready string representing command args. The string should be copy/paste ready for execution in a shell.""" + args = self._stringify_args(args) escaped_args = [] for arg in args: if isinstance(arg, unicode): @@ -390,8 +418,6 @@ class Executive(object): """Popen wrapper for convenience and to work around python bugs.""" assert(isinstance(args, list) or isinstance(args, tuple)) start_time = time.time() - args = map(unicode, args) # Popen will throw an exception if args are non-strings (like int()) - args = map(self._encode_argument_if_needed, args) stdin, string_to_communicate = self._compute_stdin(input) stderr = self.STDOUT if return_stderr else None @@ -413,7 +439,7 @@ class Executive(object): # http://bugs.python.org/issue1731717 exit_code = process.wait() - _log.debug('"%s" took %.2fs' % (self._command_for_printing(args), time.time() - start_time)) + _log.debug('"%s" took %.2fs' % (self.command_for_printing(args), time.time() - start_time)) if return_exit_code: return exit_code @@ -457,8 +483,23 @@ class Executive(object): return argument return argument.encode(self._child_process_encoding()) - def popen(self, *args, **kwargs): - return subprocess.Popen(*args, **kwargs) + def _stringify_args(self, args): + # Popen will throw an exception if args are non-strings (like int()) + string_args = map(unicode, args) + # The Windows implementation of Popen cannot handle unicode strings. :( + return map(self._encode_argument_if_needed, string_args) + + # The only required arugment to popen is named "args", the rest are optional keyword arguments. + def popen(self, args, **kwargs): + # FIXME: We should always be stringifying the args, but callers who pass shell=True + # expect that the exact bytes passed will get passed to the shell (even if they're wrongly encoded). + # shell=True is wrong for many other reasons, and we should remove this + # hack as soon as we can fix all callers to not use shell=True. + if kwargs.get('shell') == True: + string_args = args + else: + string_args = self._stringify_args(args) + return subprocess.Popen(string_args, **kwargs) def run_in_parallel(self, command_lines_and_cwds, processes=None): """Runs a list of (cmd_line list, cwd string) tuples in parallel and returns a list of (retcode, stdout, stderr) tuples.""" diff --git a/Tools/Scripts/webkitpy/common/system/executive_mock.py b/Tools/Scripts/webkitpy/common/system/executive_mock.py index a83f5b245..a3870b131 100644 --- a/Tools/Scripts/webkitpy/common/system/executive_mock.py +++ b/Tools/Scripts/webkitpy/common/system/executive_mock.py @@ -63,6 +63,7 @@ class MockExecutive(object): self._running_pids = {'test-webkitpy': os.getpid()} self._proc = None self.calls = [] + self.pid_to_system_pid = {} def check_running_pid(self, pid): return pid in self._running_pids.values() @@ -86,6 +87,10 @@ class MockExecutive(object): raise ScriptError("Exception for %s" % args, output="MOCK command output") return "MOCK output of child process" + def command_for_printing(self, args): + string_args = map(unicode, args) + return " ".join(string_args) + def run_command(self, args, cwd=None, @@ -108,6 +113,10 @@ class MockExecutive(object): input_string = ", input=%s" % input _log.info("MOCK run_command: %s, cwd=%s%s%s" % (args, cwd, env_string, input_string)) output = "MOCK output of child process" + + if self._should_throw_when_run.intersection(args): + raise ScriptError("Exception for %s" % args, output="MOCK command output") + if self._should_throw: raise ScriptError("MOCK ScriptError", output=output) return output @@ -170,7 +179,7 @@ class MockExecutive2(MockExecutive): self.calls.append(args) assert(isinstance(args, list) or isinstance(args, tuple)) if self._exception: - raise self._exception # pylint: disable-msg=E0702 + raise self._exception # pylint: disable=E0702 if self._run_command_fn: return self._run_command_fn(args) if return_exit_code: diff --git a/Tools/Scripts/webkitpy/common/system/executive_unittest.py b/Tools/Scripts/webkitpy/common/system/executive_unittest.py index 755955d34..f71201a04 100644 --- a/Tools/Scripts/webkitpy/common/system/executive_unittest.py +++ b/Tools/Scripts/webkitpy/common/system/executive_unittest.py @@ -33,34 +33,36 @@ import signal import subprocess import sys import time -import unittest # Since we execute this script directly as part of the unit tests, we need to ensure # that Tools/Scripts is in sys.path for the next imports to work correctly. script_dir = os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))) if script_dir not in sys.path: sys.path.append(script_dir) +third_party_py = os.path.join(script_dir, "webkitpy", "thirdparty", "autoinstalled") +if third_party_py not in sys.path: + sys.path.append(third_party_py) + +import unittest2 as unittest from webkitpy.common.system.executive import Executive, ScriptError from webkitpy.common.system.filesystem_mock import MockFileSystem class ScriptErrorTest(unittest.TestCase): - def test_string_from_args(self): - error = ScriptError() - self.assertEqual(error._string_from_args(None), 'None') - self.assertEqual(error._string_from_args([]), '[]') - self.assertEqual(error._string_from_args(map(str, range(30))), "['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12', '13', '14', '15', '16', '17'...") - def test_message_with_output(self): error = ScriptError('My custom message!', '', -1) self.assertEqual(error.message_with_output(), 'My custom message!') error = ScriptError('My custom message!', '', -1, 'My output.') self.assertEqual(error.message_with_output(), 'My custom message!\n\nMy output.') error = ScriptError('', 'my_command!', -1, 'My output.', '/Users/username/blah') - self.assertEqual(error.message_with_output(), 'Failed to run "my_command!" exit_code: -1 cwd: /Users/username/blah\n\nMy output.') + self.assertEqual(error.message_with_output(), 'Failed to run "\'my_command!\'" exit_code: -1 cwd: /Users/username/blah\n\nMy output.') error = ScriptError('', 'my_command!', -1, 'ab' + '1' * 499) - self.assertEqual(error.message_with_output(), 'Failed to run "my_command!" exit_code: -1\n\nLast 500 characters of output:\nb' + '1' * 499) + self.assertEqual(error.message_with_output(), 'Failed to run "\'my_command!\'" exit_code: -1\n\nLast 500 characters of output:\nb' + '1' * 499) + + def test_message_with_tuple(self): + error = ScriptError('', ('my', 'command'), -1, 'My output.', '/Users/username/blah') + self.assertEqual(error.message_with_output(), 'Failed to run "(\'my\', \'command\')" exit_code: -1 cwd: /Users/username/blah\n\nMy output.') def never_ending_command(): """Arguments for a command that will never end (useful for testing process @@ -113,6 +115,17 @@ class ExecutiveTest(unittest.TestCase): executive.run_command(command_line('echo', 'foo')) executive.run_command(tuple(command_line('echo', 'foo'))) + def test_auto_stringify_args(self): + executive = Executive() + executive.run_command(command_line('echo', 1)) + executive.popen(command_line('echo', 1), stdout=executive.PIPE).wait() + self.assertEqual('echo 1', executive.command_for_printing(['echo', 1])) + + def test_popen_args(self): + executive = Executive() + # Explicitly naming the 'args' argument should not thow an exception. + executive.popen(args=command_line('echo', 1), stdout=executive.PIPE).wait() + def test_run_command_with_unicode(self): """Validate that it is safe to pass unicode() objects to Executive.run* methods, and they will return unicode() @@ -161,11 +174,11 @@ class ExecutiveTest(unittest.TestCase): if sys.platform == "win32": # FIXME: https://bugs.webkit.org/show_bug.cgi?id=54790 # We seem to get either 0 or 1 here for some reason. - self.assertTrue(process.wait() in (0, 1)) + self.assertIn(process.wait(), (0, 1)) elif sys.platform == "cygwin": # FIXME: https://bugs.webkit.org/show_bug.cgi?id=98196 # cygwin seems to give us either SIGABRT or SIGKILL - self.assertTrue(process.wait() in (-signal.SIGABRT, -signal.SIGKILL)) + self.assertIn(process.wait(), (-signal.SIGABRT, -signal.SIGKILL)) else: expected_exit_code = -signal.SIGKILL self.assertEqual(process.wait(), expected_exit_code) @@ -176,7 +189,7 @@ class ExecutiveTest(unittest.TestCase): def serial_test_kill_all(self): executive = Executive() process = subprocess.Popen(never_ending_command(), stdout=subprocess.PIPE) - self.assertEqual(process.poll(), None) # Process is running + self.assertIsNone(process.poll()) # Process is running executive.kill_all(never_ending_command()[0]) # Note: Can't use a ternary since signal.SIGTERM is undefined for sys.platform == "win32" if sys.platform == "cygwin": @@ -185,7 +198,7 @@ class ExecutiveTest(unittest.TestCase): elif sys.platform == "win32": # FIXME: https://bugs.webkit.org/show_bug.cgi?id=54790 # We seem to get either 0 or 1 here for some reason. - self.assertTrue(process.wait() in (0, 1)) + self.assertIn(process.wait(), (0, 1)) else: expected_exit_code = -signal.SIGTERM self.assertEqual(process.wait(), expected_exit_code) @@ -218,7 +231,7 @@ class ExecutiveTest(unittest.TestCase): executive = Executive() pids = executive.running_pids() - self.assertTrue(os.getpid() in pids) + self.assertIn(os.getpid(), pids) def serial_test_run_in_parallel(self): # We run this test serially to avoid overloading the machine and throwing off the timing. diff --git a/Tools/Scripts/webkitpy/common/system/file_lock.py b/Tools/Scripts/webkitpy/common/system/file_lock.py index c542777f2..3ca8b3cba 100644 --- a/Tools/Scripts/webkitpy/common/system/file_lock.py +++ b/Tools/Scripts/webkitpy/common/system/file_lock.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python # Copyright (C) 2010 Gabor Rapcsanyi (rgabor@inf.u-szeged.hu), University of Szeged # # All rights reserved. diff --git a/Tools/Scripts/webkitpy/common/system/file_lock_integrationtest.py b/Tools/Scripts/webkitpy/common/system/file_lock_integrationtest.py index 5cd27d11d..7b1b42695 100644 --- a/Tools/Scripts/webkitpy/common/system/file_lock_integrationtest.py +++ b/Tools/Scripts/webkitpy/common/system/file_lock_integrationtest.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python # Copyright (C) 2010 Gabor Rapcsanyi (rgabor@inf.u-szeged.hu), University of Szeged # # All rights reserved. @@ -26,7 +25,7 @@ import os import tempfile -import unittest +import unittest2 as unittest from webkitpy.common.system.file_lock import FileLock diff --git a/Tools/Scripts/webkitpy/common/system/file_lock_mock.py b/Tools/Scripts/webkitpy/common/system/file_lock_mock.py index e2c1d5cdf..f53081d1c 100644 --- a/Tools/Scripts/webkitpy/common/system/file_lock_mock.py +++ b/Tools/Scripts/webkitpy/common/system/file_lock_mock.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python # Copyright (c) 2012 Google Inc. All rights reserved. # # All rights reserved. @@ -30,7 +29,7 @@ class MockFileLock(object): pass def acquire_lock(self): - pass + return True def release_lock(self): - pass + return True diff --git a/Tools/Scripts/webkitpy/common/system/filesystem_mock.py b/Tools/Scripts/webkitpy/common/system/filesystem_mock.py index 16e9fadaa..ee0664ea0 100644 --- a/Tools/Scripts/webkitpy/common/system/filesystem_mock.py +++ b/Tools/Scripts/webkitpy/common/system/filesystem_mock.py @@ -395,7 +395,7 @@ class MockFileSystem(object): def splitext(self, path): idx = path.rfind('.') if idx == -1: - idx = 0 + idx = len(path) return (path[0:idx], path[idx:]) @@ -452,7 +452,7 @@ class ReadableBinaryFileObject(object): class ReadableTextFileObject(ReadableBinaryFileObject): def __init__(self, fs, path, data): - super(ReadableTextFileObject, self).__init__(fs, path, StringIO.StringIO(data)) + super(ReadableTextFileObject, self).__init__(fs, path, StringIO.StringIO(data.decode("utf-8"))) def close(self): self.data.close() diff --git a/Tools/Scripts/webkitpy/common/system/filesystem_mock_unittest.py b/Tools/Scripts/webkitpy/common/system/filesystem_mock_unittest.py index 391c1d954..a5983320a 100644 --- a/Tools/Scripts/webkitpy/common/system/filesystem_mock_unittest.py +++ b/Tools/Scripts/webkitpy/common/system/filesystem_mock_unittest.py @@ -28,7 +28,7 @@ import os import re -import unittest +import unittest2 as unittest from webkitpy.common.system import filesystem_mock @@ -82,7 +82,3 @@ class MockFileSystemTest(unittest.TestCase, filesystem_unittest.GenericFileSyste 'foo/../bar', 'foo/../bar/baz', '../foo') - - -if __name__ == '__main__': - unittest.main() diff --git a/Tools/Scripts/webkitpy/common/system/filesystem_unittest.py b/Tools/Scripts/webkitpy/common/system/filesystem_unittest.py index d656b2580..cd4ad6e4a 100644 --- a/Tools/Scripts/webkitpy/common/system/filesystem_unittest.py +++ b/Tools/Scripts/webkitpy/common/system/filesystem_unittest.py @@ -35,7 +35,7 @@ import os import stat import sys import tempfile -import unittest +import unittest2 as unittest from filesystem import FileSystem @@ -209,6 +209,8 @@ class RealFileSystemTest(unittest.TestCase, GenericFileSystemTests): unicode_text_string = u'\u016An\u012Dc\u014Dde\u033D' hex_equivalent = '\xC5\xAA\x6E\xC4\xAD\x63\xC5\x8D\x64\x65\xCC\xBD' + malformed_text_hex = '\x4D\x69\x63\x72\x6F\x73\x6F\x66\x74\xAE\x20\x56\x69\x73\x75\x61\x6C\x20\x53\x74\x75\x64\x69\x6F\xAE\x20\x32\x30\x31\x30\x0D\x0A' + malformed_ignored_text_hex = '\x4D\x69\x63\x72\x6F\x73\x6F\x66\x74\x20\x56\x69\x73\x75\x61\x6C\x20\x53\x74\x75\x64\x69\x6F\x20\x32\x30\x31\x30\x0D\x0A' try: text_path = tempfile.mktemp(prefix='tree_unittest_') binary_path = tempfile.mktemp(prefix='tree_unittest_') @@ -219,6 +221,12 @@ class RealFileSystemTest(unittest.TestCase, GenericFileSystemTests): fs.write_binary_file(binary_path, hex_equivalent) text_contents = fs.read_text_file(binary_path) self.assertEqual(text_contents, unicode_text_string) + + self.assertRaises(ValueError, fs.write_text_file, binary_path, malformed_text_hex) + fs.write_binary_file(binary_path, malformed_text_hex) + self.assertRaises(ValueError, fs.read_text_file, binary_path) + text_contents = fs.read_binary_file(binary_path).decode('utf8', 'ignore') + self.assertEquals(text_contents, malformed_ignored_text_hex) finally: if text_path and fs.isfile(text_path): os.remove(text_path) @@ -254,7 +262,3 @@ class RealFileSystemTest(unittest.TestCase, GenericFileSystemTests): self.assertEqual(fs.sep, os.sep) self.assertEqual(fs.join("foo", "bar"), os.path.join("foo", "bar")) - - -if __name__ == '__main__': - unittest.main() diff --git a/Tools/Scripts/webkitpy/common/system/logtesting.py b/Tools/Scripts/webkitpy/common/system/logtesting.py index 0cfa6cb0a..1aba1726a 100644 --- a/Tools/Scripts/webkitpy/common/system/logtesting.py +++ b/Tools/Scripts/webkitpy/common/system/logtesting.py @@ -32,7 +32,7 @@ see the TestLogStream class, and perhaps also the LogTesting class. """ import logging -import unittest +import unittest2 as unittest class TestLogStream(object): diff --git a/Tools/Scripts/webkitpy/common/system/logutils_unittest.py b/Tools/Scripts/webkitpy/common/system/logutils_unittest.py index 6d7cc4da4..252ebf4cc 100644 --- a/Tools/Scripts/webkitpy/common/system/logutils_unittest.py +++ b/Tools/Scripts/webkitpy/common/system/logutils_unittest.py @@ -24,7 +24,7 @@ import logging import os -import unittest +import unittest2 as unittest from webkitpy.common.system.logtesting import LogTesting from webkitpy.common.system.logtesting import TestLogStream diff --git a/Tools/Scripts/webkitpy/common/system/outputcapture.py b/Tools/Scripts/webkitpy/common/system/outputcapture.py index 26670d214..893b5e528 100644 --- a/Tools/Scripts/webkitpy/common/system/outputcapture.py +++ b/Tools/Scripts/webkitpy/common/system/outputcapture.py @@ -29,8 +29,8 @@ # Class for unittest support. Used for capturing stderr/stdout. import logging +import unittest # Don't use unittest2 here as the autoinstaller may not have it yet. import sys -import unittest from StringIO import StringIO @@ -94,15 +94,22 @@ class OutputCapture(object): finally: (stdout_string, stderr_string, logs_string) = self.restore_output() - testcase.assertEqual(stdout_string, expected_stdout) - testcase.assertEqual(stderr_string, expected_stderr) + if hasattr(testcase, 'assertMultiLineEqual'): + testassert = testcase.assertMultiLineEqual + else: + testassert = testcase.assertEqual + + testassert(stdout_string, expected_stdout) + testassert(stderr_string, expected_stderr) if expected_logs is not None: - testcase.assertEqual(logs_string, expected_logs) + testassert(logs_string, expected_logs) # This is a little strange, but I don't know where else to return this information. return return_value class OutputCaptureTestCaseBase(unittest.TestCase): + maxDiff = None + def setUp(self): unittest.TestCase.setUp(self) self.output_capture = OutputCapture() diff --git a/Tools/Scripts/webkitpy/common/system/outputcapture_unittest.py b/Tools/Scripts/webkitpy/common/system/outputcapture_unittest.py index da4347c8d..7ef2e247a 100644 --- a/Tools/Scripts/webkitpy/common/system/outputcapture_unittest.py +++ b/Tools/Scripts/webkitpy/common/system/outputcapture_unittest.py @@ -21,7 +21,7 @@ # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. import logging -import unittest +import unittest2 as unittest from webkitpy.common.system.outputcapture import OutputCapture @@ -43,7 +43,7 @@ class OutputCaptureTest(unittest.TestCase): actual_stdout, actual_stderr, actual_logs = self.output.restore_output() self.assertEqual('', actual_stdout) self.assertEqual('', actual_stderr) - self.assertEqual(expected_logs, actual_logs) + self.assertMultiLineEqual(expected_logs, actual_logs) def test_initial_log_level(self): self.output.capture_output() diff --git a/Tools/Scripts/webkitpy/common/system/outputtee_unittest.py b/Tools/Scripts/webkitpy/common/system/outputtee_unittest.py index 6a509f0c2..8d06916f8 100644 --- a/Tools/Scripts/webkitpy/common/system/outputtee_unittest.py +++ b/Tools/Scripts/webkitpy/common/system/outputtee_unittest.py @@ -27,7 +27,7 @@ # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. import StringIO -import unittest +import unittest2 as unittest from webkitpy.common.system.outputtee import Tee, OutputTee diff --git a/Tools/Scripts/webkitpy/common/system/path_unittest.py b/Tools/Scripts/webkitpy/common/system/path_unittest.py index 7a719584d..118546e68 100644 --- a/Tools/Scripts/webkitpy/common/system/path_unittest.py +++ b/Tools/Scripts/webkitpy/common/system/path_unittest.py @@ -26,7 +26,7 @@ # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -import unittest +import unittest2 as unittest import sys from webkitpy.common.system.systemhost import SystemHost diff --git a/Tools/Scripts/webkitpy/common/system/platforminfo.py b/Tools/Scripts/webkitpy/common/system/platforminfo.py index b2451f5f9..582e1996f 100644 --- a/Tools/Scripts/webkitpy/common/system/platforminfo.py +++ b/Tools/Scripts/webkitpy/common/system/platforminfo.py @@ -155,7 +155,7 @@ class PlatformInfo(object): def _win_version_tuple_from_cmd(self): # Note that this should only ever be called on windows, so this should always work. - ver_output = self._executive.run_command(['cmd', '/c', 'ver']) + ver_output = self._executive.run_command(['cmd', '/c', 'ver'], decode_output=False) match_object = re.search(r'(?P<major>\d)\.(?P<minor>\d)\.(?P<build>\d+)', ver_output) assert match_object, 'cmd returned an unexpected version string: ' + ver_output return tuple(map(int, match_object.groups())) diff --git a/Tools/Scripts/webkitpy/common/system/platforminfo_unittest.py b/Tools/Scripts/webkitpy/common/system/platforminfo_unittest.py index 327229eb9..bdb0f8661 100644 --- a/Tools/Scripts/webkitpy/common/system/platforminfo_unittest.py +++ b/Tools/Scripts/webkitpy/common/system/platforminfo_unittest.py @@ -28,7 +28,7 @@ import platform import sys -import unittest +import unittest2 as unittest from webkitpy.common.system.executive import Executive from webkitpy.common.system.executive_mock import MockExecutive, MockExecutive2 @@ -79,12 +79,12 @@ class TestPlatformInfo(unittest.TestCase): self.assertNotEquals(info.os_version, '') self.assertNotEquals(info.display_name(), '') self.assertTrue(info.is_mac() or info.is_win() or info.is_linux() or info.is_freebsd()) - self.assertNotEquals(info.terminal_width(), None) + self.assertIsNotNone(info.terminal_width()) if info.is_mac(): self.assertTrue(info.total_bytes_memory() > 0) else: - self.assertEqual(info.total_bytes_memory(), None) + self.assertIsNone(info.total_bytes_memory()) def test_os_name_and_wrappers(self): info = self.make_info(fake_sys('linux2')) @@ -172,14 +172,10 @@ class TestPlatformInfo(unittest.TestCase): self.assertEqual(info.total_bytes_memory(), 1234) info = self.make_info(fake_sys('win32', tuple([6, 1, 7600]))) - self.assertEqual(info.total_bytes_memory(), None) + self.assertIsNone(info.total_bytes_memory()) info = self.make_info(fake_sys('linux2')) - self.assertEqual(info.total_bytes_memory(), None) + self.assertIsNone(info.total_bytes_memory()) info = self.make_info(fake_sys('freebsd9')) - self.assertEqual(info.total_bytes_memory(), None) - - -if __name__ == '__main__': - unittest.main() + self.assertIsNone(info.total_bytes_memory()) diff --git a/Tools/Scripts/webkitpy/common/system/profiler.py b/Tools/Scripts/webkitpy/common/system/profiler.py index 264a4e238..0208cf898 100644 --- a/Tools/Scripts/webkitpy/common/system/profiler.py +++ b/Tools/Scripts/webkitpy/common/system/profiler.py @@ -28,19 +28,44 @@ import logging import re +import itertools _log = logging.getLogger(__name__) class ProfilerFactory(object): @classmethod - def create_profiler(cls, host, executable_path, output_dir, identifier=None): - if host.platform.is_mac(): - return Instruments(host, executable_path, output_dir, identifier) - return GooglePProf(host, executable_path, output_dir, identifier) + def create_profiler(cls, host, executable_path, output_dir, profiler_name=None, identifier=None): + profilers = cls.profilers_for_platform(host.platform) + if not profilers: + return None + profiler_name = profiler_name or cls.default_profiler_name(host.platform) + profiler_class = next(itertools.ifilter(lambda profiler: profiler.name == profiler_name, profilers), None) + if not profiler_class: + return None + return profilers[0](host, executable_path, output_dir, identifier) + + @classmethod + def default_profiler_name(cls, platform): + profilers = cls.profilers_for_platform(platform) + return profilers[0].name if profilers else None + + @classmethod + def profilers_for_platform(cls, platform): + # GooglePProf requires TCMalloc/google-perftools, but is available everywhere. + profilers_by_os_name = { + 'mac': [IProfiler, Sample, GooglePProf], + 'linux': [Perf, GooglePProf], + # Note: freebsd, win32 have no profilers defined yet, thus --profile will be ignored + # by default, but a profiler can be selected with --profiler=PROFILER explicitly. + } + return profilers_by_os_name.get(platform.os_name, []) class Profiler(object): + # Used by ProfilerFactory to lookup a profiler from the --profiler=NAME option. + name = None + def __init__(self, host, executable_path, output_dir, identifier=None): self._host = host self._executable_path = executable_path @@ -61,10 +86,14 @@ class Profiler(object): class SingleFileOutputProfiler(Profiler): def __init__(self, host, executable_path, output_dir, output_suffix, identifier=None): super(SingleFileOutputProfiler, self).__init__(host, executable_path, output_dir, identifier) - self._output_path = self._host.workspace.find_unused_filename(self._output_dir, self._identifier, output_suffix) + # FIXME: Currently all reports are kept as test.*, until we fix that, search up to 1000 names before giving up. + self._output_path = self._host.workspace.find_unused_filename(self._output_dir, self._identifier, output_suffix, search_limit=1000) + assert(self._output_path) class GooglePProf(SingleFileOutputProfiler): + name = 'pprof' + def __init__(self, host, executable_path, output_dir, identifier=None): super(GooglePProf, self).__init__(host, executable_path, output_dir, "pprof", identifier) @@ -76,24 +105,106 @@ class GooglePProf(SingleFileOutputProfiler): match = re.search("^Total:[^\n]*\n((?:[^\n]*\n){0,10})", pprof_output, re.MULTILINE) return match.group(1) if match else None - def profile_after_exit(self): + def _pprof_path(self): # FIXME: We should have code to find the right google-pprof executable, some Googlers have # google-pprof installed as "pprof" on their machines for them. - # FIXME: Similarly we should find the right perl! - pprof_args = ['/usr/bin/perl', '/usr/bin/google-pprof', '--text', self._executable_path, self._output_path] + return '/usr/bin/google-pprof' + + def profile_after_exit(self): + # google-pprof doesn't check its arguments, so we have to. + if not (self._host.filesystem.exists(self._output_path)): + print "Failed to gather profile, %s does not exist." % self._output_path + return + + pprof_args = [self._pprof_path(), '--text', self._executable_path, self._output_path] profile_text = self._host.executive.run_command(pprof_args) + print "First 10 lines of pprof --text:" print self._first_ten_lines_of_profile(profile_text) + print "http://google-perftools.googlecode.com/svn/trunk/doc/cpuprofile.html documents output." + print + print "To interact with the the full profile, including produce graphs:" + print ' '.join([self._pprof_path(), self._executable_path, self._output_path]) + +class Perf(SingleFileOutputProfiler): + name = 'perf' -# FIXME: iprofile is a newer commandline interface to replace /usr/bin/instruments. -class Instruments(SingleFileOutputProfiler): def __init__(self, host, executable_path, output_dir, identifier=None): - super(Instruments, self).__init__(host, executable_path, output_dir, "trace", identifier) + super(Perf, self).__init__(host, executable_path, output_dir, "data", identifier) + self._perf_process = None + self._pid_being_profiled = None - # FIXME: We may need a way to find this tracetemplate on the disk - _time_profile = "/Applications/Xcode.app/Contents/Applications/Instruments.app/Contents/Resources/templates/Time Profiler.tracetemplate" + def _perf_path(self): + # FIXME: We may need to support finding the perf binary in other locations. + return 'perf' def attach_to_pid(self, pid): - cmd = ["instruments", "-t", self._time_profile, "-D", self._output_path, "-p", pid] - cmd = map(unicode, cmd) - self._host.executive.popen(cmd) + assert(not self._perf_process and not self._pid_being_profiled) + self._pid_being_profiled = pid + cmd = [self._perf_path(), "record", "--call-graph", "--pid", pid, "--output", self._output_path] + self._perf_process = self._host.executive.popen(cmd) + + def _first_ten_lines_of_profile(self, perf_output): + match = re.search("^#[^\n]*\n((?: [^\n]*\n){1,10})", perf_output, re.MULTILINE) + return match.group(1) if match else None + + def profile_after_exit(self): + # Perf doesn't automatically watch the attached pid for death notifications, + # so we have to do it for it, and then tell it its time to stop sampling. :( + self._host.executive.wait_limited(self._pid_being_profiled, limit_in_seconds=10) + perf_exitcode = self._perf_process.poll() + if perf_exitcode is None: # This should always be the case, unless perf error'd out early. + self._host.executive.interrupt(self._perf_process.pid) + + perf_exitcode = self._perf_process.wait() + if perf_exitcode not in (0, -2): # The exit code should always be -2, as we're always interrupting perf. + print "'perf record' failed (exit code: %i), can't process results:" % perf_exitcode + return + + perf_args = [self._perf_path(), 'report', '--call-graph', 'none', '--input', self._output_path] + print "First 10 lines of 'perf report --call-graph=none':" + + print " ".join(perf_args) + perf_output = self._host.executive.run_command(perf_args) + print self._first_ten_lines_of_profile(perf_output) + + print "To view the full profile, run:" + print ' '.join([self._perf_path(), 'report', '-i', self._output_path]) + print # An extra line between tests looks nicer. + + +class Sample(SingleFileOutputProfiler): + name = 'sample' + + def __init__(self, host, executable_path, output_dir, identifier=None): + super(Sample, self).__init__(host, executable_path, output_dir, "txt", identifier) + self._profiler_process = None + + def attach_to_pid(self, pid): + cmd = ["sample", pid, "-mayDie", "-file", self._output_path] + self._profiler_process = self._host.executive.popen(cmd) + + def profile_after_exit(self): + self._profiler_process.wait() + + +class IProfiler(SingleFileOutputProfiler): + name = 'iprofiler' + + def __init__(self, host, executable_path, output_dir, identifier=None): + super(IProfiler, self).__init__(host, executable_path, output_dir, "dtps", identifier) + self._profiler_process = None + + def attach_to_pid(self, pid): + # FIXME: iprofiler requires us to pass the directory separately + # from the basename of the file, with no control over the extension. + fs = self._host.filesystem + cmd = ["iprofiler", "-timeprofiler", "-a", pid, + "-d", fs.dirname(self._output_path), "-o", fs.splitext(fs.basename(self._output_path))[0]] + # FIXME: Consider capturing instead of letting instruments spam to stderr directly. + self._profiler_process = self._host.executive.popen(cmd) + + def profile_after_exit(self): + # It seems like a nicer user experiance to wait on the profiler to exit to prevent + # it from spewing to stderr at odd times. + self._profiler_process.wait() diff --git a/Tools/Scripts/webkitpy/common/system/profiler_unittest.py b/Tools/Scripts/webkitpy/common/system/profiler_unittest.py index 059b7cfa1..22bc2df6e 100644 --- a/Tools/Scripts/webkitpy/common/system/profiler_unittest.py +++ b/Tools/Scripts/webkitpy/common/system/profiler_unittest.py @@ -26,25 +26,41 @@ # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -import unittest +import unittest2 as unittest +from webkitpy.common.system.platforminfo_mock import MockPlatformInfo from webkitpy.common.system.systemhost_mock import MockSystemHost -from .profiler import ProfilerFactory, Instruments, GooglePProf +from .profiler import ProfilerFactory, GooglePProf class ProfilerFactoryTest(unittest.TestCase): - def test_basic(self): + def _assert_default_profiler_name(self, os_name, expected_profiler_name): + profiler_name = ProfilerFactory.default_profiler_name(MockPlatformInfo(os_name)) + self.assertEqual(profiler_name, expected_profiler_name) + + def test_default_profilers(self): + self._assert_default_profiler_name('mac', 'iprofiler') + self._assert_default_profiler_name('linux', 'perf') + self._assert_default_profiler_name('win32', None) + self._assert_default_profiler_name('freebsd', None) + + def test_default_profiler_output(self): host = MockSystemHost() self.assertFalse(host.filesystem.exists("/tmp/output")) + + # Default mocks are Mac, so iprofile should be default. profiler = ProfilerFactory.create_profiler(host, '/bin/executable', '/tmp/output') self.assertTrue(host.filesystem.exists("/tmp/output")) - self.assertEquals(profiler._output_path, "/tmp/output/test.trace") + self.assertEqual(profiler._output_path, "/tmp/output/test.dtps") + # Linux defaults to perf. host.platform.os_name = 'linux' profiler = ProfilerFactory.create_profiler(host, '/bin/executable', '/tmp/output') - self.assertEquals(profiler._output_path, "/tmp/output/test.pprof") + self.assertEqual(profiler._output_path, "/tmp/output/test.data") + +class GooglePProfTest(unittest.TestCase): def test_pprof_output_regexp(self): pprof_output = """ sometimes @@ -84,4 +100,4 @@ Total: 3770 samples """ host = MockSystemHost() profiler = GooglePProf(host, '/bin/executable', '/tmp/output') - self.assertEquals(profiler._first_ten_lines_of_profile(pprof_output), expected_first_ten_lines) + self.assertEqual(profiler._first_ten_lines_of_profile(pprof_output), expected_first_ten_lines) diff --git a/Tools/Scripts/webkitpy/common/system/stack_utils_unittest.py b/Tools/Scripts/webkitpy/common/system/stack_utils_unittest.py index 625acf2b3..3050adc99 100644 --- a/Tools/Scripts/webkitpy/common/system/stack_utils_unittest.py +++ b/Tools/Scripts/webkitpy/common/system/stack_utils_unittest.py @@ -27,7 +27,7 @@ # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. import sys -import unittest +import unittest2 as unittest from webkitpy.common.system import outputcapture from webkitpy.common.system import stack_utils @@ -42,11 +42,11 @@ class StackUtilsTest(unittest.TestCase): def test_find_thread_stack_found(self): thread_id = current_thread_id() found_stack = stack_utils._find_thread_stack(thread_id) - self.assertNotEqual(found_stack, None) + self.assertIsNotNone(found_stack) def test_find_thread_stack_not_found(self): found_stack = stack_utils._find_thread_stack(0) - self.assertEqual(found_stack, None) + self.assertIsNone(found_stack) def test_log_thread_state(self): msgs = [] diff --git a/Tools/Scripts/webkitpy/common/system/user_unittest.py b/Tools/Scripts/webkitpy/common/system/user_unittest.py index bd86d228f..49810b2e0 100644 --- a/Tools/Scripts/webkitpy/common/system/user_unittest.py +++ b/Tools/Scripts/webkitpy/common/system/user_unittest.py @@ -26,7 +26,7 @@ # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -import unittest +import unittest2 as unittest from webkitpy.common.system.outputcapture import OutputCapture from webkitpy.common.system.user import User diff --git a/Tools/Scripts/webkitpy/common/system/workspace.py b/Tools/Scripts/webkitpy/common/system/workspace.py index 686837619..1d92aca13 100644 --- a/Tools/Scripts/webkitpy/common/system/workspace.py +++ b/Tools/Scripts/webkitpy/common/system/workspace.py @@ -67,7 +67,7 @@ class Workspace(object): try: self._executive.run_command(['zip', '-9', '-r', zip_path, '.'], cwd=source_path) except ScriptError, e: - _log.error("Workspace.create_zip failed:\n%s" % e.message_with_output()) + _log.error("Workspace.create_zip failed in %s:\n%s" % (source_path, e.message_with_output())) return None return zip_class(zip_path) diff --git a/Tools/Scripts/webkitpy/common/system/workspace_mock.py b/Tools/Scripts/webkitpy/common/system/workspace_mock.py index 005f86cf3..02a5f4c29 100644 --- a/Tools/Scripts/webkitpy/common/system/workspace_mock.py +++ b/Tools/Scripts/webkitpy/common/system/workspace_mock.py @@ -32,4 +32,6 @@ class MockWorkspace(object): return "%s/%s.%s" % (directory, name, extension) def create_zip(self, zip_path, source_path): + self.zip_path = zip_path + self.source_path = source_path return object() # Something that is not None diff --git a/Tools/Scripts/webkitpy/common/system/workspace_unittest.py b/Tools/Scripts/webkitpy/common/system/workspace_unittest.py index eca386ac3..8262f6cf1 100644 --- a/Tools/Scripts/webkitpy/common/system/workspace_unittest.py +++ b/Tools/Scripts/webkitpy/common/system/workspace_unittest.py @@ -26,7 +26,7 @@ # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -import unittest +import unittest2 as unittest from webkitpy.common.system.filesystem_mock import MockFileSystem from webkitpy.common.system.outputcapture import OutputCapture @@ -60,7 +60,7 @@ class WorkspaceTest(unittest.TestCase): def test_create_zip_exception(self): workspace = Workspace(None, MockExecutive(should_log=True, should_throw=True)) expected_logs = """MOCK run_command: ['zip', '-9', '-r', '/zip/path', '.'], cwd=/source/path -Workspace.create_zip failed: +Workspace.create_zip failed in /source/path: MOCK ScriptError MOCK output of child process @@ -69,4 +69,4 @@ MOCK output of child process def __init__(self, path): self.filename = path archive = OutputCapture().assert_outputs(self, workspace.create_zip, ["/zip/path", "/source/path", MockZipFile], expected_logs=expected_logs) - self.assertEqual(archive, None) + self.assertIsNone(archive) diff --git a/Tools/Scripts/webkitpy/common/system/zipfileset_unittest.py b/Tools/Scripts/webkitpy/common/system/zipfileset_unittest.py index 22ba72082..1a0603c9e 100644 --- a/Tools/Scripts/webkitpy/common/system/zipfileset_unittest.py +++ b/Tools/Scripts/webkitpy/common/system/zipfileset_unittest.py @@ -23,7 +23,7 @@ import shutil import tempfile -import unittest +import unittest2 as unittest import zipfile from webkitpy.common.system.filesystem_mock import MockFileSystem @@ -92,7 +92,3 @@ class ZipFileSetTest(unittest.TestCase): def test_namelist(self): self.assertTrue('some-file' in self._zip.namelist()) - - -if __name__ == '__main__': - unittest.main() |
