Chromium Code Reviews| 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 |