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 |