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 |
| 17 import browsertester.browserlauncher |
15 | 18 |
16 # This script extends browser_tester to check for the presence of | 19 # This script extends browser_tester to check for the presence of |
17 # Breakpad crash dumps. | 20 # Breakpad crash dumps. |
18 | 21 |
19 | 22 |
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. | 23 # This reads a file of lines containing 'key:value' pairs. |
57 # The file contains entries like the following: | 24 # The file contains entries like the following: |
58 # plat:Win32 | 25 # plat:Win32 |
59 # prod:Chromium | 26 # prod:Chromium |
60 # ptype:nacl-loader | 27 # ptype:nacl-loader |
61 # rept:crash svc | 28 # rept:crash svc |
62 def ReadDumpTxtFile(filename): | 29 def ReadDumpTxtFile(filename): |
63 dump_info = {} | 30 dump_info = {} |
64 fh = open(filename, 'r') | 31 fh = open(filename, 'r') |
65 for line in fh: | 32 for line in fh: |
66 if ':' in line: | 33 if ':' in line: |
67 key, value = line.rstrip().split(':', 1) | 34 key, value = line.rstrip().split(':', 1) |
68 dump_info[key] = value | 35 dump_info[key] = value |
69 fh.close() | 36 fh.close() |
70 return dump_info | 37 return dump_info |
71 | 38 |
72 | 39 |
73 def StartCrashService(browser_path): | 40 def StartCrashService(browser_path, dumps_dir, windows_pipe_name): |
74 if sys.platform == 'win32': | 41 if sys.platform == 'win32': |
75 # Find crash_service.exe relative to chrome.exe. This is a bit icky. | 42 # Find crash_service.exe relative to chrome.exe. This is a bit icky. |
76 browser_dir = os.path.dirname(browser_path) | 43 browser_dir = os.path.dirname(browser_path) |
77 proc = subprocess.Popen([os.path.join(browser_dir, 'crash_service.exe')]) | 44 proc = subprocess.Popen([os.path.join(browser_dir, 'crash_service.exe'), |
| 45 '--dumps-dir=%s' % dumps_dir, |
| 46 '--pipe-name=%s' % windows_pipe_name]) |
78 def Cleanup(): | 47 def Cleanup(): |
79 try: | 48 # Note that if the process has already exited, this will raise |
80 proc.terminate() | 49 # an 'Access is denied' WindowsError exception, but |
81 sys.stdout.write('crash_dump_tester: Stopped crash_service.exe\n') | 50 # crash_service.exe is not supposed to do this and such |
82 except WindowsError: | 51 # behaviour should make the test fail. |
83 # If the process has already exited, we will get an 'Access is | 52 proc.terminate() |
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() | 53 status = proc.wait() |
91 sys.stdout.write('crash_dump_tester: ' | 54 sys.stdout.write('crash_dump_tester: ' |
92 'crash_service.exe exited with status %s\n' % status) | 55 'crash_service.exe exited with status %s\n' % status) |
93 # We add a delay because there is probably a race condition: | 56 # We add a delay because there is probably a race condition: |
94 # crash_service.exe might not have finished doing | 57 # crash_service.exe might not have finished doing |
95 # CreateNamedPipe() before NaCl does a crash dump and tries to | 58 # CreateNamedPipe() before NaCl does a crash dump and tries to |
96 # connect to that pipe. | 59 # connect to that pipe. |
97 # TODO(mseaborn): We could change crash_service.exe to report when | 60 # TODO(mseaborn): We could change crash_service.exe to report when |
98 # it has successfully created the named pipe. | 61 # it has successfully created the named pipe. |
99 time.sleep(1) | 62 time.sleep(1) |
100 else: | 63 else: |
101 def Cleanup(): | 64 def Cleanup(): |
102 pass | 65 pass |
103 return Cleanup | 66 return Cleanup |
104 | 67 |
105 | 68 |
| 69 def GetDumpFiles(dumps_dir): |
| 70 all_files = [os.path.join(dumps_dir, dump_file) |
| 71 for dump_file in os.listdir(dumps_dir)] |
| 72 sys.stdout.write('crash_dump_tester: Found %i files\n' % len(all_files)) |
| 73 for dump_file in all_files: |
| 74 sys.stdout.write(' %s\n' % dump_file) |
| 75 return [dump_file for dump_file in all_files |
| 76 if dump_file.endswith('.dmp')] |
| 77 |
| 78 |
106 def Main(): | 79 def Main(): |
107 parser = browser_tester.BuildArgParser() | 80 parser = browser_tester.BuildArgParser() |
108 parser.add_option('--expected_crash_dumps', dest='expected_crash_dumps', | 81 parser.add_option('--expected_crash_dumps', dest='expected_crash_dumps', |
109 type=int, default=0, | 82 type=int, default=0, |
110 help='The number of crash dumps that we should expect') | 83 help='The number of crash dumps that we should expect') |
111 options, args = parser.parse_args() | 84 options, args = parser.parse_args() |
112 | 85 |
| 86 dumps_dir = tempfile.mkdtemp(prefix='nacl_crash_dump_tester_') |
| 87 # To get a guaranteed unique pipe name, use the base name of the |
| 88 # directory we just created. |
| 89 windows_pipe_name = r'\\.\pipe\%s_crash_service' % os.path.basename(dumps_dir) |
| 90 |
113 # This environment variable enables Breakpad crash dumping in | 91 # This environment variable enables Breakpad crash dumping in |
114 # non-official Windows builds of Chromium. | 92 # non-official Windows builds of Chromium. |
115 os.environ['CHROME_HEADLESS'] = '1' | 93 os.environ['CHROME_HEADLESS'] = '1' |
| 94 # Override the default (global) Windows pipe name that Chromium will |
| 95 # use for out-of-process crash reporting. |
| 96 os.environ['CHROME_BREAKPAD_PIPE_NAME'] = windows_pipe_name |
116 | 97 |
117 dumps_before = GetDumpFiles() | 98 cleanup_func = StartCrashService(options.browser_path, dumps_dir, |
118 PrintDumps('crash dump files before the test', dumps_before) | 99 windows_pipe_name) |
119 | |
120 cleanup_func = StartCrashService(options.browser_path) | |
121 try: | 100 try: |
122 result = browser_tester.Run(options.url, options) | 101 result = browser_tester.Run(options.url, options) |
123 finally: | 102 finally: |
124 cleanup_func() | 103 cleanup_func() |
125 | 104 |
126 dumps_after = GetDumpFiles() | 105 dmp_files = GetDumpFiles(dumps_dir) |
127 PrintDumps('crash dump files after the test', dumps_after) | |
128 # Find the new files. This is only necessary because we are not | |
129 # using a clean temp directory. This is subject to a race condition | |
130 # if running crash dump tests concurrently. | |
131 dumps_diff = sorted(set(dumps_after).difference(dumps_before)) | |
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')] | |
135 | |
136 failed = False | 106 failed = False |
137 msg = ('crash_dump_tester: ERROR: Got %i crash dumps but expected %i\n' % | 107 msg = ('crash_dump_tester: ERROR: Got %i crash dumps but expected %i\n' % |
138 (len(new_dumps), options.expected_crash_dumps)) | 108 (len(dmp_files), options.expected_crash_dumps)) |
139 if len(new_dumps) != options.expected_crash_dumps: | 109 if len(dmp_files) != options.expected_crash_dumps: |
140 sys.stdout.write(msg) | 110 sys.stdout.write(msg) |
141 failed = True | 111 failed = True |
142 for dump_file in new_dumps: | 112 for dump_file in dmp_files: |
143 # The crash dumps should come in pairs of a .dmp and .txt file. | 113 # The crash dumps should come in pairs of a .dmp and .txt file. |
144 second_file = dump_file[:-4] + '.txt' | 114 second_file = dump_file[:-4] + '.txt' |
145 msg = ('crash_dump_tester: ERROR: File %r is missing a corresponding ' | 115 msg = ('crash_dump_tester: ERROR: File %r is missing a corresponding ' |
146 '%r file\n' % (dump_file, second_file)) | 116 '%r file\n' % (dump_file, second_file)) |
147 if not os.path.exists(second_file): | 117 if not os.path.exists(second_file): |
148 sys.stdout.write(msg) | 118 sys.stdout.write(msg) |
149 failed = True | 119 failed = True |
150 continue | 120 continue |
151 # Check that the crash dump comes from the NaCl process. | 121 # Check that the crash dump comes from the NaCl process. |
152 dump_info = ReadDumpTxtFile(second_file) | 122 dump_info = ReadDumpTxtFile(second_file) |
153 if 'ptype' in dump_info: | 123 if 'ptype' in dump_info: |
154 msg = ('crash_dump_tester: ERROR: Unexpected ptype value: %r\n' | 124 msg = ('crash_dump_tester: ERROR: Unexpected ptype value: %r\n' |
155 % dump_info['ptype']) | 125 % dump_info['ptype']) |
156 if dump_info['ptype'] != 'nacl-loader': | 126 if dump_info['ptype'] != 'nacl-loader': |
157 sys.stdout.write(msg) | 127 sys.stdout.write(msg) |
158 failed = True | 128 failed = True |
159 else: | 129 else: |
160 sys.stdout.write('crash_dump_tester: ERROR: Missing ptype field\n') | 130 sys.stdout.write('crash_dump_tester: ERROR: Missing ptype field\n') |
161 failed = True | 131 failed = True |
162 # TODO(mseaborn): Ideally we would also check that a backtrace | 132 # TODO(mseaborn): Ideally we would also check that a backtrace |
163 # containing an expected function name can be extracted from the | 133 # containing an expected function name can be extracted from the |
164 # crash dump. | 134 # crash dump. |
165 | 135 |
166 if failed: | 136 if failed: |
167 sys.stdout.write('crash_dump_tester: FAILED\n') | 137 sys.stdout.write('crash_dump_tester: FAILED\n') |
168 result = 1 | 138 result = 1 |
169 else: | 139 else: |
170 sys.stdout.write('crash_dump_tester: PASSED\n') | 140 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 | 141 |
| 142 browsertester.browserlauncher.RemoveDirectory(dumps_dir) |
180 return result | 143 return result |
181 | 144 |
182 | 145 |
183 if __name__ == '__main__': | 146 if __name__ == '__main__': |
184 sys.exit(Main()) | 147 sys.exit(Main()) |
OLD | NEW |