OLD | NEW |
---|---|
(Empty) | |
1 #!/usr/bin/python | |
2 # Copyright (c) 2012 The Native Client Authors. All rights reserved. | |
3 # Use of this source code is governed by a BSD-style license that can be | |
4 # found in the LICENSE file. | |
5 | |
6 import codecs | |
7 import hashlib | |
8 import json | |
9 import optparse | |
10 import os | |
11 import re | |
12 import subprocess | |
13 import sys | |
14 import tempfile | |
15 import threading | |
16 import time | |
17 import zipfile | |
18 | |
19 | |
20 KNOWN_BAD = set([ | |
Nick Bray
2012/04/02 20:31:41
This is going to be visible in a publicly viewable
bradn
2012/04/02 22:43:59
So actually mainly I'm worried about app ids.
Thes
| |
21 # Bad manifest | |
22 '2f97cec9f13b0f774d1f49490f26f32213e4e0a5', | |
23 'ced1fea90b71b0a8da08c1a1e6cb35975cc84f52', | |
24 '3d6832749c8c1346c65b30f4b191930dec5f04a3', | |
25 '0937b653af5553856532454ec340d0e0075bc0b4', | |
26 '09ffe3793113fe564b71800a5844189c00bd8210', | |
27 '81a4a3de69dd4ad169b1d4a7268b44c78ea5ffa8', | |
28 '612a5aaa821b4b636168025f027e721c0f046e7c', | |
29 '14f389a8c406d60e0fc05a1ec0189a652a1f006e', | |
30 'a8aa42d699dbef3e1403e4fdc49325e89a91f653', | |
31 'c6d40d4f3c8dccc710d8c09bfd074b2d20a504d2', | |
32 # Bad permissions | |
33 '8de65668cc7280ffb70ffd2fa5b2a22112156966', | |
34 # Snap | |
35 'b458cd57c8b4e6c313b18f370fad59779f573afc', | |
36 # No nacl module | |
37 '57be161e5ff7011d2283e507a70f9005c448002b', | |
38 '4beecff67651f13e013c12a5bf3661041ded323c', | |
39 '1f861c0d8c173b64df3e70cfa1a5cd710ba59430', | |
40 'cfd62adf6790eed0520da2deb2246fc02e70c57e', | |
41 ]) | |
42 | |
43 | |
44 SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__)) | |
45 TESTS_DIR = os.path.dirname(SCRIPT_DIR) | |
46 NACL_DIR = os.path.dirname(TESTS_DIR) | |
47 | |
48 | |
49 # Imports from the build directory. | |
50 sys.path.insert(0, os.path.join(NACL_DIR, 'build')) | |
51 import download_utils | |
Nick Bray
2012/04/02 20:31:41
Move import hackery above KNOWN_BAD - keep it grou
bradn
2012/04/02 22:43:59
Done.
| |
52 | |
53 | |
54 def GsutilCopySilent(src, dst): | |
55 """Invoke gsutil cp, swallowing the output, with retry. | |
56 | |
57 Args: | |
58 src: src url. | |
59 dst: dst path. | |
60 """ | |
61 for _ in range(3): | |
62 env = os.environ.copy() | |
63 env['PATH'] = '/b/build/scripts/slave' + os.pathsep + env['PATH'] | |
Nick Bray
2012/04/02 20:31:41
* Lift env creation out of loop - makes intent mor
bradn
2012/04/02 22:43:59
Done.
| |
64 process = subprocess.Popen( | |
65 ['gsutil', 'cp', src, dst], | |
66 env=env, stdout=subprocess.PIPE, stderr=subprocess.PIPE) | |
67 process_stdout, process_stderr = process.communicate() | |
68 if process.returncode == 0: | |
69 return | |
70 print 'Unexpected return code: %s' % process.returncode | |
71 print '>>> STDOUT' | |
72 print process_stdout | |
73 print '>>> STDERR' | |
74 print process_stderr | |
75 print '-' * 70 | |
76 sys.exit(1) | |
Nick Bray
2012/04/02 20:31:41
Very optional: I am not a huge fan of exiting insi
bradn
2012/04/02 22:43:59
Done, due to later unrelated refactor.
| |
77 | |
78 | |
79 def DownloadTotalList(list_filename): | |
Nick Bray
2012/04/02 20:31:41
Bad name. List of what? Total of what?
bradn
2012/04/02 22:43:59
Done.
| |
80 """Download list of all archived files. | |
81 | |
82 Args: | |
83 list_filename: destination filename (kept around for debugging). | |
84 """ | |
85 GsutilCopySilent('gs://nativeclient-snaps/naclapps.all', list_filename) | |
86 fh = open(list_filename) | |
87 filenames = fh.read().splitlines() | |
88 fh.close() | |
89 return [f for f in filenames if f.endswith('.crx')] | |
90 | |
91 | |
92 def DownloadFile(src_path, dst_filename): | |
Nick Bray
2012/04/02 20:31:41
Bad name. DownloadFileFromSnapshot?
bradn
2012/04/02 22:43:59
Done.
| |
93 """Download a file from our snapshot. | |
94 | |
95 Args: | |
96 src_path: datastore relative path to download from. | |
97 dst_filename: destination filename. | |
98 """ | |
99 GsutilCopySilent('gs://nativeclient-snaps/%s' % src_path, dst_filename) | |
100 | |
101 | |
102 def Sha1Sum(path): | |
Nick Bray
2012/04/02 20:31:41
Sha1Digest? Sha1FileDigest?
bradn
2012/04/02 22:43:59
Done.
| |
103 """Determine the sha1 hash of a file's contents given its path.""" | |
104 m = hashlib.sha1() | |
105 fh = open(path, 'rb') | |
106 m.update(fh.read()) | |
107 fh.close() | |
108 return m.hexdigest() | |
109 | |
110 | |
111 def Hex2Alpha(ch): | |
112 """Convert a hexadecimal digit from 0-9 / a-f to a-p. | |
113 | |
114 Args: | |
115 ch: a character in 0-9 / a-f. | |
116 Returns: | |
117 A character in a-p. | |
118 """ | |
119 if ch >= '0' and ch <= '9': | |
120 return chr(ord(ch) - ord('0') + ord('a')) | |
121 else: | |
122 return chr(ord(ch) + 10) | |
123 | |
124 | |
125 def ChromeAppIdFromPath(path): | |
126 """Converts a path to the corrisponding chrome app id. | |
Nick Bray
2012/04/02 20:31:41
Path, in what context? On disk? In the web store
bradn
2012/04/02 22:43:59
Done.
| |
127 | |
128 Args: | |
129 path: Path to an unpacked extension. | |
130 Returns: | |
131 A 32 character chrome extension app id. | |
132 """ | |
133 hasher = hashlib.sha256() | |
134 hasher.update(os.path.realpath(path)) | |
135 hexhash = hasher.hexdigest()[:32] | |
136 return ''.join([Hex2Alpha(ch) for ch in hexhash]) | |
137 | |
138 | |
139 def TestAppStartup(options, crx_path, app_path, profile_path): | |
Nick Bray
2012/04/02 20:31:41
Big function. Is there a clean way to modularize
bradn
2012/04/02 22:43:59
Done.
| |
140 """Run the validator on a nexe, check if the result is expected. | |
141 | |
142 Args: | |
143 options: bag of options. | |
144 crx_path: path to the crx. | |
145 app_path: path to the extracted crx. | |
146 profile_path: path to a temporary profile dir. | |
147 """ | |
148 manifest = LoadManifest(app_path) | |
149 start_path = manifest.get('app', {}).get('launch', {}).get('local_path') | |
150 if not start_path: | |
151 print '-' * 70 | |
152 print 'Testing: %s' % crx_path | |
153 print 'Browser: %s' % options.browser | |
154 print 'BAD MANIFEST!' | |
155 print '-' * 70 | |
156 # Halt on first failure. | |
157 sys.exit(1) | |
158 start_url = 'chrome-extension://%s/%s' % ( | |
159 ChromeAppIdFromPath(app_path), start_path) | |
160 cmd = [options.browser, | |
161 '--enable-nacl', | |
162 '--load-extension=' + app_path, | |
163 '--user-data-dir=' + profile_path, start_url] | |
164 process = subprocess.Popen(cmd, | |
Nick Bray
2012/04/02 20:31:41
For example, this is essentially subprocess.commun
bradn
2012/04/02 22:43:59
Done.
| |
165 stdout=subprocess.PIPE, | |
166 stderr=subprocess.PIPE) | |
167 def GatherOutput(fh, dst): | |
168 dst.append(fh.read()) | |
169 # Gather stdout. | |
170 stdout_output = [] | |
171 stdout_thread = threading.Thread( | |
172 target=GatherOutput, args=(process.stdout, stdout_output)) | |
173 stdout_thread.setDaemon(True) | |
174 stdout_thread.start() | |
175 # Gather stderr. | |
176 stderr_output = [] | |
177 stderr_thread = threading.Thread( | |
178 target=GatherOutput, args=(process.stderr, stderr_output)) | |
179 stderr_thread.setDaemon(True) | |
180 stderr_thread.start() | |
181 # Wait for a small span for the app to load. | |
182 time.sleep(options.duration) | |
183 process.kill() | |
184 time.sleep(1) | |
Nick Bray
2012/04/02 20:31:41
Why sleep?
bradn
2012/04/02 22:43:59
Oops, switched to wait.
| |
185 process.poll() | |
186 # Join up. | |
187 stdout_thread.join() | |
188 stderr_thread.join() | |
189 # Pick out result. | |
190 process_stdout = stdout_output[0] | |
Nick Bray
2012/04/02 20:31:41
Scraping the output will not work on Windows. We
bradn
2012/04/02 22:43:59
Done.
| |
191 process_stderr = stderr_output[0] | |
Nick Bray
2012/04/02 20:31:41
Scraping for failiures => function (but not printi
bradn
2012/04/02 22:43:59
Added --verbose flag that does emit it.
| |
192 # Check for errors we don't like. | |
193 failure = None | |
194 if 'NaClMakePcrelThunk:' not in process_stderr: | |
195 failure = 'nacl module not started' | |
196 if 'NaCl process exited with' in process_stderr: | |
197 failure = 'nacl module crashed' | |
198 errs = re.findall(':ERROR:[^\n]+', process_stderr) | |
199 for err in errs: | |
200 if ('extension_prefs.cc' not in err and | |
201 'gles2_cmd_decoder.cc' not in err): | |
202 failure = 'unknown error: ' + err | |
203 break | |
204 # Check if result is what we expect. | |
205 if failure: | |
206 print '-' * 70 | |
207 print 'Testing: %s' % crx_path | |
208 print 'Browser: %s' % options.browser | |
209 print 'Failure: %s' % failure | |
210 print '>>> STDOUT' | |
211 print process_stdout | |
212 print '>>> STDERR' | |
213 print process_stderr | |
214 print '-' * 70 | |
215 # Halt on first failure. | |
216 sys.exit(1) | |
217 | |
218 | |
219 def LoadManifest(app_path): | |
220 try: | |
221 manifest_data = codecs.open(os.path.join(app_path, 'manifest.json'), | |
222 'r', encoding='utf-8').read() | |
223 manifest_data = manifest_data.replace('\r', '') | |
224 manifest_data = manifest_data.replace(u'\ufeff', '') | |
Nick Bray
2012/04/02 20:31:41
What are these characters? Document.
bradn
2012/04/02 22:43:59
Done.
| |
225 manifest_data = manifest_data.replace(u'\uffee', '') | |
226 return json.loads(manifest_data) | |
227 except: | |
228 return {} | |
Nick Bray
2012/04/02 20:31:41
This seems a little sketchy. Why return an empty
bradn
2012/04/02 22:43:59
Simplifies the logic of checking for a nested key
| |
229 | |
230 | |
231 def IsBad(path): | |
232 """Checks a blacklist to decide if we should startup test this app. | |
233 | |
234 Args: | |
235 path: path to the nexe. | |
236 Returns: | |
237 Boolean indicating if we should test this app. | |
238 """ | |
239 return os.path.splitext(os.path.basename(path))[0] in KNOWN_BAD | |
240 | |
241 | |
242 def CachedPath(options, filename): | |
243 """Find the full path of a cached file, a cache root relative path. | |
244 | |
245 Args: | |
246 options: bags of options. | |
247 filename: filename relative to the top of the download url / cache. | |
248 Returns: | |
249 Absolute path of where the file goes in the cache. | |
250 """ | |
251 return os.path.join(options.cache_dir, 'nacl_startup_test_cache', filename) | |
252 | |
253 | |
254 def Sha1FromFilename(filename): | |
Nick Bray
2012/04/02 20:31:41
Are there functions here that you should be sharin
bradn
2012/04/02 22:43:59
Done.
| |
255 """Get the expected sha1 of a file path. | |
256 | |
257 Throughout we use the convention that files are store to a name of the form: | |
258 <path_to_file>/<sha1hex>[.<some_extention>] | |
259 This function extracts the expected sha1. | |
260 | |
261 Args: | |
262 filename: filename to extract. | |
263 Returns: | |
264 Excepted sha1. | |
265 """ | |
266 return os.path.splitext(os.path.basename(filename))[0] | |
267 | |
268 | |
269 def PrimeCache(options, filename): | |
Nick Bray
2012/04/02 20:31:41
Dito.
bradn
2012/04/02 22:43:59
Done.
| |
270 """Attempt to add a file to the cache directory if its not already there. | |
271 | |
272 Args: | |
273 options: bag of options. | |
274 filename: filename relative to the top of the download url / cache. | |
275 """ | |
276 dpath = CachedPath(options, filename) | |
277 if not os.path.exists(dpath) or Sha1Sum(dpath) != Sha1FromFilename(filename): | |
278 # Try to make the directory, fail is ok, let the download fail instead. | |
279 try: | |
280 os.makedirs(os.path.basename(dpath)) | |
281 except OSError: | |
282 pass | |
283 DownloadFile(filename, dpath) | |
284 | |
285 | |
286 def ExtractFromCache(options, source, dest): | |
287 """Extract a crx from the cache. | |
288 | |
289 Args: | |
290 options: bag of options. | |
291 source: crx file to extract (cache relative). | |
292 dest: location to extract to. | |
293 """ | |
294 assert not os.path.exists(dest) | |
Nick Bray
2012/04/02 20:31:41
If you have an assert, have a message to help expl
bradn
2012/04/02 22:43:59
Done.
| |
295 dpath = CachedPath(options, source) | |
296 assert os.path.exists(dpath) | |
297 zf = zipfile.ZipFile(dpath, 'r') | |
298 os.makedirs(dest) | |
299 for info in zf.infolist(): | |
300 tpath = os.path.join(dest, info.filename) | |
Nick Bray
2012/04/02 20:31:41
Pwnage: Zipfile is from an untrusted source, filen
bradn
2012/04/02 22:43:59
Done.
| |
301 if info.filename.endswith('/'): | |
302 os.makedirs(tpath) | |
303 else: | |
304 zf.extract(info, dest) | |
305 zf.close() | |
306 | |
307 | |
308 def TestApps(options, work_dir): | |
309 """Test a browser on a corpus of crxs. | |
310 | |
311 Args: | |
312 options: bag of options. | |
313 work_dir: directory to operate in. | |
314 """ | |
315 profile_path = os.path.join(work_dir, 'profile_temp') | |
316 app_path = os.path.join(work_dir, 'app_temp') | |
317 | |
318 list_filename = os.path.join(work_dir, 'naclapps.all') | |
319 filenames = DownloadTotalList(list_filename) | |
320 filenames = filenames[24:] | |
321 | |
322 count = 0 | |
323 start = time.time() | |
324 count = len(filenames) | |
325 for index, filename in enumerate(filenames): | |
326 tm = time.time() | |
327 if index > 0: | |
Nick Bray
2012/04/02 20:31:41
ETA calculation and / or header printing in functi
bradn
2012/04/02 22:43:59
Done.
| |
328 eta = (count - index) * (tm - start) / index | |
329 eta_minutes = int(eta / 60) | |
330 eta_seconds = int(eta - eta_minutes * 60) | |
331 eta_str = ' (ETA %d:%02d)' % (eta_minutes, eta_seconds) | |
332 else: | |
333 eta_str = '' | |
334 print 'Processing %d of %d%s...' % (index + 1, count, eta_str) | |
335 # Skip if known bad. | |
336 if IsBad(filename): | |
337 continue | |
338 PrimeCache(options, filename) | |
339 # Stop here if downloading only. | |
340 if options.download_only: | |
341 continue | |
342 # Unzip the app. | |
343 ExtractFromCache(options, filename, app_path) | |
344 try: | |
345 TestAppStartup(options, filename, app_path, profile_path) | |
346 count += 1 | |
347 finally: | |
348 download_utils.RemoveDir(app_path) | |
349 download_utils.RemoveDir(profile_path) | |
350 print 'Ran tests on %d of %d CRXs' % (count, len(filenames)) | |
351 print 'SUCCESS' | |
352 | |
353 | |
354 def Main(): | |
355 # Decide a default cache directory. | |
356 # Prefer /b (for the bots) | |
357 # Failing that, use scons-out. | |
358 # Failing that, use the current users's home dir. | |
359 default_cache_dir = '/b' | |
Nick Bray
2012/04/02 20:31:41
Another bit of code that might do well shared.
bradn
2012/04/02 22:43:59
Done.
| |
360 if not os.path.isdir(default_cache_dir): | |
361 default_cache_dir = os.path.join(NACL_DIR, 'scons-out') | |
362 if not os.path.isdir(default_cache_dir): | |
363 default_cache_dir = os.path.expanduser('~/') | |
364 default_cache_dir = os.path.abspath(default_cache_dir) | |
365 assert os.path.isdir(default_cache_dir) | |
366 | |
367 parser = optparse.OptionParser() | |
368 parser.add_option( | |
369 '--cache-dir', dest='cache_dir', default=default_cache_dir, | |
370 help='directory to cache downloads in') | |
371 parser.add_option( | |
372 '--download-only', dest='download_only', | |
373 default=False, action='store_true', | |
374 help='download to cache without running the tests') | |
375 parser.add_option( | |
376 '--duration', dest='duration', default=30, | |
377 help='how long to run each app for') | |
378 parser.add_option( | |
379 '--browser', dest='browser', | |
380 help='browser to run') | |
381 options, args = parser.parse_args() | |
382 if args: | |
383 parser.error('unused arguments') | |
384 if not options.download_only: | |
385 if not options.browser: | |
386 parser.error('no browser specified') | |
387 | |
388 work_dir = tempfile.mkdtemp(suffix='startup_crxs', prefix='tmp') | |
389 work_dir = os.path.realpath(work_dir) | |
390 try: | |
391 TestApps(options, work_dir) | |
392 finally: | |
393 download_utils.RemoveDir(work_dir) | |
394 | |
395 | |
396 if __name__ == '__main__': | |
397 Main() | |
OLD | NEW |