Chromium Code Reviews| OLD | NEW |
|---|---|
| 1 #!/usr/bin/python | 1 #!/usr/bin/python |
| 2 # Copyright (c) 2011 The Native Client Authors. All rights reserved. | 2 # Copyright (c) 2011 The Native Client Authors. All rights reserved. |
| 3 # Use of this source code is governed by a BSD-style license that can be | 3 # Use of this source code is governed by a BSD-style license that can be |
| 4 # found in the LICENSE file. | 4 # found in the LICENSE file. |
| 5 | 5 |
| 6 import os | 6 import os |
| 7 import shutil | |
| 7 import subprocess | 8 import subprocess |
| 8 import sys | 9 import sys |
| 10 import tempfile | |
| 9 import time | 11 import time |
| 10 | 12 |
| 11 script_dir = os.path.dirname(__file__) | 13 script_dir = os.path.dirname(__file__) |
| 12 sys.path.append(os.path.join(script_dir, '../../tools/browser_tester')) | 14 sys.path.append(os.path.join(script_dir, '../../tools/browser_tester')) |
| 13 | 15 |
| 14 import browser_tester | 16 import browser_tester |
| 15 | 17 |
| 16 # This script extends browser_tester to check for the presence of | 18 # This script extends browser_tester to check for the presence of |
| 17 # Breakpad crash dumps. | 19 # Breakpad crash dumps. |
| 18 | 20 |
| 19 | 21 |
| 20 # TODO(mseaborn): Change Chromium's crash_service.exe so that we can | |
| 21 # tell it to put dumps in a per-test temp directory. | |
| 22 def GetDumpDirs(): | |
| 23 if sys.platform == 'win32': | |
| 24 # We duplicate Chromium's logic for deciding where its per-user | |
| 25 # directory should be on Windows. We can remove this if we use a | |
| 26 # temp directory instead. | |
| 27 import win32com.shell.shell | |
| 28 import win32com.shell.shellcon | |
| 29 # This typically returns a pathname like | |
| 30 # 'C:\Documents and Settings\Username\Local Settings\Application Data'. | |
| 31 appdata_dir = win32com.shell.shell.SHGetFolderPath( | |
| 32 0, win32com.shell.shellcon.CSIDL_LOCAL_APPDATA, 0, 0) | |
| 33 return [os.path.join(appdata_dir, app_name, 'User Data', 'Crash Reports') | |
| 34 for app_name in ('Chromium', os.path.join('Google', 'Chrome'))] | |
| 35 else: | |
| 36 # TODO(mseaborn): Handle other platforms when NaCl supports | |
| 37 # Breakpad crash reporting there. | |
| 38 return [] | |
| 39 | |
| 40 | |
| 41 def GetDumpFiles(): | |
| 42 file_list = [] | |
| 43 for dump_dir in GetDumpDirs(): | |
| 44 if os.path.exists(dump_dir): | |
| 45 for dump_file in sorted(os.listdir(dump_dir)): | |
| 46 file_list.append(os.path.join(dump_dir, dump_file)) | |
| 47 return file_list | |
| 48 | |
| 49 | |
| 50 def PrintDumps(desc, dump_files): | |
| 51 sys.stdout.write('crash_dump_tester: Found %i %s\n' % (len(dump_files), desc)) | |
| 52 for dump_file in dump_files: | |
| 53 sys.stdout.write(' %s\n' % dump_file) | |
| 54 | |
| 55 | |
| 56 # This reads a file of lines containing 'key:value' pairs. | 22 # This reads a file of lines containing 'key:value' pairs. |
| 57 # The file contains entries like the following: | 23 # The file contains entries like the following: |
| 58 # plat:Win32 | 24 # plat:Win32 |
| 59 # prod:Chromium | 25 # prod:Chromium |
| 60 # ptype:nacl-loader | 26 # ptype:nacl-loader |
| 61 # rept:crash svc | 27 # rept:crash svc |
| 62 def ReadDumpTxtFile(filename): | 28 def ReadDumpTxtFile(filename): |
| 63 dump_info = {} | 29 dump_info = {} |
| 64 fh = open(filename, 'r') | 30 fh = open(filename, 'r') |
| 65 for line in fh: | 31 for line in fh: |
| 66 if ':' in line: | 32 if ':' in line: |
| 67 key, value = line.rstrip().split(':', 1) | 33 key, value = line.rstrip().split(':', 1) |
| 68 dump_info[key] = value | 34 dump_info[key] = value |
| 69 fh.close() | 35 fh.close() |
| 70 return dump_info | 36 return dump_info |
| 71 | 37 |
| 72 | 38 |
| 73 def StartCrashService(browser_path): | 39 def StartCrashService(browser_path, dumps_dir, windows_pipe_name): |
| 74 if sys.platform == 'win32': | 40 if sys.platform == 'win32': |
| 75 # Find crash_service.exe relative to chrome.exe. This is a bit icky. | 41 # Find crash_service.exe relative to chrome.exe. This is a bit icky. |
| 76 browser_dir = os.path.dirname(browser_path) | 42 browser_dir = os.path.dirname(browser_path) |
| 77 proc = subprocess.Popen([os.path.join(browser_dir, 'crash_service.exe')]) | 43 proc = subprocess.Popen([os.path.join(browser_dir, 'crash_service.exe'), |
| 44 '--dumps-dir=%s' % dumps_dir, | |
| 45 '--pipe-name=%s' % windows_pipe_name]) | |
| 78 def Cleanup(): | 46 def Cleanup(): |
| 79 try: | 47 # Note that if the process has already exited, this will raise |
| 80 proc.terminate() | 48 # an 'Access is denied' WindowsError exception. |
|
Nick Bray
2011/08/12 21:46:27
... which should not happen in normal operation?
| |
| 81 sys.stdout.write('crash_dump_tester: Stopped crash_service.exe\n') | 49 proc.terminate() |
| 82 except WindowsError: | |
| 83 # If the process has already exited, we will get an 'Access is | |
| 84 # denied' error. This can happen if another instance of | |
| 85 # crash_service.exe was already running, because our instance | |
| 86 # will fail to claim the named pipe. | |
| 87 # TODO(mseaborn): We could change crash_service.exe to create | |
| 88 # unique pipe names for testing purposes. | |
| 89 pass | |
| 90 status = proc.wait() | 50 status = proc.wait() |
| 91 sys.stdout.write('crash_dump_tester: ' | 51 sys.stdout.write('crash_dump_tester: ' |
| 92 'crash_service.exe exited with status %s\n' % status) | 52 'crash_service.exe exited with status %s\n' % status) |
| 93 # We add a delay because there is probably a race condition: | 53 # We add a delay because there is probably a race condition: |
| 94 # crash_service.exe might not have finished doing | 54 # crash_service.exe might not have finished doing |
| 95 # CreateNamedPipe() before NaCl does a crash dump and tries to | 55 # CreateNamedPipe() before NaCl does a crash dump and tries to |
| 96 # connect to that pipe. | 56 # connect to that pipe. |
| 97 # TODO(mseaborn): We could change crash_service.exe to report when | 57 # TODO(mseaborn): We could change crash_service.exe to report when |
| 98 # it has successfully created the named pipe. | 58 # it has successfully created the named pipe. |
| 99 time.sleep(1) | 59 time.sleep(1) |
| 100 else: | 60 else: |
| 101 def Cleanup(): | 61 def Cleanup(): |
| 102 pass | 62 pass |
| 103 return Cleanup | 63 return Cleanup |
| 104 | 64 |
| 105 | 65 |
| 106 def Main(): | 66 def Main(): |
| 107 parser = browser_tester.BuildArgParser() | 67 parser = browser_tester.BuildArgParser() |
| 108 parser.add_option('--expected_crash_dumps', dest='expected_crash_dumps', | 68 parser.add_option('--expected_crash_dumps', dest='expected_crash_dumps', |
| 109 type=int, default=0, | 69 type=int, default=0, |
| 110 help='The number of crash dumps that we should expect') | 70 help='The number of crash dumps that we should expect') |
| 111 options, args = parser.parse_args() | 71 options, args = parser.parse_args() |
| 112 | 72 |
| 73 dumps_dir = tempfile.mkdtemp(prefix='nacl_crash_dump_tester_') | |
| 74 # To get a guaranteed unique pipe name, use the base name of the | |
| 75 # directory we just created. | |
| 76 windows_pipe_name = r'\\.\pipe\%s_crash_service' % os.path.basename(dumps_dir) | |
| 77 | |
| 113 # This environment variable enables Breakpad crash dumping in | 78 # This environment variable enables Breakpad crash dumping in |
| 114 # non-official Windows builds of Chromium. | 79 # non-official Windows builds of Chromium. |
| 115 os.environ['CHROME_HEADLESS'] = '1' | 80 os.environ['CHROME_HEADLESS'] = '1' |
| 81 # Override the default (global) Windows pipe name that Chromium will | |
| 82 # use for out-of-process crash reporting. | |
| 83 os.environ['CHROME_BREAKPAD_PIPE_NAME'] = windows_pipe_name | |
|
Nick Bray
2011/08/12 21:46:27
Hmmm. Everything else being equal, I'd rather see
| |
| 116 | 84 |
| 117 dumps_before = GetDumpFiles() | 85 cleanup_func = StartCrashService(options.browser_path, dumps_dir, |
| 118 PrintDumps('crash dump files before the test', dumps_before) | 86 windows_pipe_name) |
| 119 | |
| 120 cleanup_func = StartCrashService(options.browser_path) | |
| 121 try: | 87 try: |
| 122 result = browser_tester.Run(options.url, options) | 88 result = browser_tester.Run(options.url, options) |
| 123 finally: | 89 finally: |
| 124 cleanup_func() | 90 cleanup_func() |
| 125 | 91 |
| 126 dumps_after = GetDumpFiles() | 92 all_files = [os.path.join(dumps_dir, dump_file) |
|
Nick Bray
2011/08/12 21:46:27
Finding the dump files should be a function taking
| |
| 127 PrintDumps('crash dump files after the test', dumps_after) | 93 for dump_file in os.listdir(dumps_dir)] |
| 128 # Find the new files. This is only necessary because we are not | 94 sys.stdout.write('crash_dump_tester: Found %i files\n' % len(all_files)) |
| 129 # using a clean temp directory. This is subject to a race condition | 95 for dump_file in all_files: |
| 130 # if running crash dump tests concurrently. | 96 sys.stdout.write(' %s\n' % dump_file) |
| 131 dumps_diff = sorted(set(dumps_after).difference(dumps_before)) | 97 dmp_files = [dump_file for dump_file in all_files |
| 132 PrintDumps('new crash dump files', dumps_diff) | |
| 133 new_dumps = [dump_file for dump_file in dumps_diff | |
| 134 if dump_file.endswith('.dmp')] | 98 if dump_file.endswith('.dmp')] |
| 135 | 99 |
| 136 failed = False | 100 failed = False |
| 137 msg = ('crash_dump_tester: ERROR: Got %i crash dumps but expected %i\n' % | 101 msg = ('crash_dump_tester: ERROR: Got %i crash dumps but expected %i\n' % |
| 138 (len(new_dumps), options.expected_crash_dumps)) | 102 (len(dmp_files), options.expected_crash_dumps)) |
| 139 if len(new_dumps) != options.expected_crash_dumps: | 103 if len(dmp_files) != options.expected_crash_dumps: |
| 140 sys.stdout.write(msg) | 104 sys.stdout.write(msg) |
| 141 failed = True | 105 failed = True |
| 142 for dump_file in new_dumps: | 106 for dump_file in dmp_files: |
| 143 # The crash dumps should come in pairs of a .dmp and .txt file. | 107 # The crash dumps should come in pairs of a .dmp and .txt file. |
| 144 second_file = dump_file[:-4] + '.txt' | 108 second_file = dump_file[:-4] + '.txt' |
| 145 msg = ('crash_dump_tester: ERROR: File %r is missing a corresponding ' | 109 msg = ('crash_dump_tester: ERROR: File %r is missing a corresponding ' |
| 146 '%r file\n' % (dump_file, second_file)) | 110 '%r file\n' % (dump_file, second_file)) |
| 147 if not os.path.exists(second_file): | 111 if not os.path.exists(second_file): |
| 148 sys.stdout.write(msg) | 112 sys.stdout.write(msg) |
| 149 failed = True | 113 failed = True |
| 150 continue | 114 continue |
| 151 # Check that the crash dump comes from the NaCl process. | 115 # Check that the crash dump comes from the NaCl process. |
| 152 dump_info = ReadDumpTxtFile(second_file) | 116 dump_info = ReadDumpTxtFile(second_file) |
| 153 if 'ptype' in dump_info: | 117 if 'ptype' in dump_info: |
| 154 msg = ('crash_dump_tester: ERROR: Unexpected ptype value: %r\n' | 118 msg = ('crash_dump_tester: ERROR: Unexpected ptype value: %r\n' |
| 155 % dump_info['ptype']) | 119 % dump_info['ptype']) |
| 156 if dump_info['ptype'] != 'nacl-loader': | 120 if dump_info['ptype'] != 'nacl-loader': |
| 157 sys.stdout.write(msg) | 121 sys.stdout.write(msg) |
| 158 failed = True | 122 failed = True |
| 159 else: | 123 else: |
| 160 sys.stdout.write('crash_dump_tester: ERROR: Missing ptype field\n') | 124 sys.stdout.write('crash_dump_tester: ERROR: Missing ptype field\n') |
| 161 failed = True | 125 failed = True |
| 162 # TODO(mseaborn): Ideally we would also check that a backtrace | 126 # TODO(mseaborn): Ideally we would also check that a backtrace |
| 163 # containing an expected function name can be extracted from the | 127 # containing an expected function name can be extracted from the |
| 164 # crash dump. | 128 # crash dump. |
| 165 | 129 |
| 166 if failed: | 130 if failed: |
| 167 sys.stdout.write('crash_dump_tester: FAILED\n') | 131 sys.stdout.write('crash_dump_tester: FAILED\n') |
| 168 result = 1 | 132 result = 1 |
| 169 else: | 133 else: |
| 170 sys.stdout.write('crash_dump_tester: PASSED\n') | 134 sys.stdout.write('crash_dump_tester: PASSED\n') |
| 171 # Clean up the dump files only if we are sure we produced them. | |
| 172 for dump_file in dumps_diff: | |
| 173 try: | |
| 174 os.unlink(dump_file) | |
| 175 except Exception: | |
| 176 # Handle exception in case the file is locked. | |
| 177 sys.stdout.write('crash_dump_tester: Deleting %r failed, ' | |
| 178 'but continuing anyway\n' % dump_file) | |
| 179 | 135 |
| 136 shutil.rmtree(dumps_dir) | |
|
Nick Bray
2011/08/12 21:46:27
This may occasionally blow up on Windows. You shou
| |
| 180 return result | 137 return result |
| 181 | 138 |
| 182 | 139 |
| 183 if __name__ == '__main__': | 140 if __name__ == '__main__': |
| 184 sys.exit(Main()) | 141 sys.exit(Main()) |
| OLD | NEW |