Index: tests/startup_regression/startup_regression.py |
diff --git a/tests/startup_regression/startup_regression.py b/tests/startup_regression/startup_regression.py |
new file mode 100755 |
index 0000000000000000000000000000000000000000..292c18f252aa80329381b9985f569df5494fa335 |
--- /dev/null |
+++ b/tests/startup_regression/startup_regression.py |
@@ -0,0 +1,397 @@ |
+#!/usr/bin/python |
+# Copyright (c) 2012 The Native Client Authors. All rights reserved. |
+# Use of this source code is governed by a BSD-style license that can be |
+# found in the LICENSE file. |
+ |
+import codecs |
+import hashlib |
+import json |
+import optparse |
+import os |
+import re |
+import subprocess |
+import sys |
+import tempfile |
+import threading |
+import time |
+import zipfile |
+ |
+ |
+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
|
+ # Bad manifest |
+ '2f97cec9f13b0f774d1f49490f26f32213e4e0a5', |
+ 'ced1fea90b71b0a8da08c1a1e6cb35975cc84f52', |
+ '3d6832749c8c1346c65b30f4b191930dec5f04a3', |
+ '0937b653af5553856532454ec340d0e0075bc0b4', |
+ '09ffe3793113fe564b71800a5844189c00bd8210', |
+ '81a4a3de69dd4ad169b1d4a7268b44c78ea5ffa8', |
+ '612a5aaa821b4b636168025f027e721c0f046e7c', |
+ '14f389a8c406d60e0fc05a1ec0189a652a1f006e', |
+ 'a8aa42d699dbef3e1403e4fdc49325e89a91f653', |
+ 'c6d40d4f3c8dccc710d8c09bfd074b2d20a504d2', |
+ # Bad permissions |
+ '8de65668cc7280ffb70ffd2fa5b2a22112156966', |
+ # Snap |
+ 'b458cd57c8b4e6c313b18f370fad59779f573afc', |
+ # No nacl module |
+ '57be161e5ff7011d2283e507a70f9005c448002b', |
+ '4beecff67651f13e013c12a5bf3661041ded323c', |
+ '1f861c0d8c173b64df3e70cfa1a5cd710ba59430', |
+ 'cfd62adf6790eed0520da2deb2246fc02e70c57e', |
+ ]) |
+ |
+ |
+SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__)) |
+TESTS_DIR = os.path.dirname(SCRIPT_DIR) |
+NACL_DIR = os.path.dirname(TESTS_DIR) |
+ |
+ |
+# Imports from the build directory. |
+sys.path.insert(0, os.path.join(NACL_DIR, 'build')) |
+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.
|
+ |
+ |
+def GsutilCopySilent(src, dst): |
+ """Invoke gsutil cp, swallowing the output, with retry. |
+ |
+ Args: |
+ src: src url. |
+ dst: dst path. |
+ """ |
+ for _ in range(3): |
+ env = os.environ.copy() |
+ 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.
|
+ process = subprocess.Popen( |
+ ['gsutil', 'cp', src, dst], |
+ env=env, stdout=subprocess.PIPE, stderr=subprocess.PIPE) |
+ process_stdout, process_stderr = process.communicate() |
+ if process.returncode == 0: |
+ return |
+ print 'Unexpected return code: %s' % process.returncode |
+ print '>>> STDOUT' |
+ print process_stdout |
+ print '>>> STDERR' |
+ print process_stderr |
+ print '-' * 70 |
+ 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.
|
+ |
+ |
+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.
|
+ """Download list of all archived files. |
+ |
+ Args: |
+ list_filename: destination filename (kept around for debugging). |
+ """ |
+ GsutilCopySilent('gs://nativeclient-snaps/naclapps.all', list_filename) |
+ fh = open(list_filename) |
+ filenames = fh.read().splitlines() |
+ fh.close() |
+ return [f for f in filenames if f.endswith('.crx')] |
+ |
+ |
+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.
|
+ """Download a file from our snapshot. |
+ |
+ Args: |
+ src_path: datastore relative path to download from. |
+ dst_filename: destination filename. |
+ """ |
+ GsutilCopySilent('gs://nativeclient-snaps/%s' % src_path, dst_filename) |
+ |
+ |
+def Sha1Sum(path): |
Nick Bray
2012/04/02 20:31:41
Sha1Digest? Sha1FileDigest?
bradn
2012/04/02 22:43:59
Done.
|
+ """Determine the sha1 hash of a file's contents given its path.""" |
+ m = hashlib.sha1() |
+ fh = open(path, 'rb') |
+ m.update(fh.read()) |
+ fh.close() |
+ return m.hexdigest() |
+ |
+ |
+def Hex2Alpha(ch): |
+ """Convert a hexadecimal digit from 0-9 / a-f to a-p. |
+ |
+ Args: |
+ ch: a character in 0-9 / a-f. |
+ Returns: |
+ A character in a-p. |
+ """ |
+ if ch >= '0' and ch <= '9': |
+ return chr(ord(ch) - ord('0') + ord('a')) |
+ else: |
+ return chr(ord(ch) + 10) |
+ |
+ |
+def ChromeAppIdFromPath(path): |
+ """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.
|
+ |
+ Args: |
+ path: Path to an unpacked extension. |
+ Returns: |
+ A 32 character chrome extension app id. |
+ """ |
+ hasher = hashlib.sha256() |
+ hasher.update(os.path.realpath(path)) |
+ hexhash = hasher.hexdigest()[:32] |
+ return ''.join([Hex2Alpha(ch) for ch in hexhash]) |
+ |
+ |
+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.
|
+ """Run the validator on a nexe, check if the result is expected. |
+ |
+ Args: |
+ options: bag of options. |
+ crx_path: path to the crx. |
+ app_path: path to the extracted crx. |
+ profile_path: path to a temporary profile dir. |
+ """ |
+ manifest = LoadManifest(app_path) |
+ start_path = manifest.get('app', {}).get('launch', {}).get('local_path') |
+ if not start_path: |
+ print '-' * 70 |
+ print 'Testing: %s' % crx_path |
+ print 'Browser: %s' % options.browser |
+ print 'BAD MANIFEST!' |
+ print '-' * 70 |
+ # Halt on first failure. |
+ sys.exit(1) |
+ start_url = 'chrome-extension://%s/%s' % ( |
+ ChromeAppIdFromPath(app_path), start_path) |
+ cmd = [options.browser, |
+ '--enable-nacl', |
+ '--load-extension=' + app_path, |
+ '--user-data-dir=' + profile_path, start_url] |
+ 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.
|
+ stdout=subprocess.PIPE, |
+ stderr=subprocess.PIPE) |
+ def GatherOutput(fh, dst): |
+ dst.append(fh.read()) |
+ # Gather stdout. |
+ stdout_output = [] |
+ stdout_thread = threading.Thread( |
+ target=GatherOutput, args=(process.stdout, stdout_output)) |
+ stdout_thread.setDaemon(True) |
+ stdout_thread.start() |
+ # Gather stderr. |
+ stderr_output = [] |
+ stderr_thread = threading.Thread( |
+ target=GatherOutput, args=(process.stderr, stderr_output)) |
+ stderr_thread.setDaemon(True) |
+ stderr_thread.start() |
+ # Wait for a small span for the app to load. |
+ time.sleep(options.duration) |
+ process.kill() |
+ time.sleep(1) |
Nick Bray
2012/04/02 20:31:41
Why sleep?
bradn
2012/04/02 22:43:59
Oops, switched to wait.
|
+ process.poll() |
+ # Join up. |
+ stdout_thread.join() |
+ stderr_thread.join() |
+ # Pick out result. |
+ 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.
|
+ 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.
|
+ # Check for errors we don't like. |
+ failure = None |
+ if 'NaClMakePcrelThunk:' not in process_stderr: |
+ failure = 'nacl module not started' |
+ if 'NaCl process exited with' in process_stderr: |
+ failure = 'nacl module crashed' |
+ errs = re.findall(':ERROR:[^\n]+', process_stderr) |
+ for err in errs: |
+ if ('extension_prefs.cc' not in err and |
+ 'gles2_cmd_decoder.cc' not in err): |
+ failure = 'unknown error: ' + err |
+ break |
+ # Check if result is what we expect. |
+ if failure: |
+ print '-' * 70 |
+ print 'Testing: %s' % crx_path |
+ print 'Browser: %s' % options.browser |
+ print 'Failure: %s' % failure |
+ print '>>> STDOUT' |
+ print process_stdout |
+ print '>>> STDERR' |
+ print process_stderr |
+ print '-' * 70 |
+ # Halt on first failure. |
+ sys.exit(1) |
+ |
+ |
+def LoadManifest(app_path): |
+ try: |
+ manifest_data = codecs.open(os.path.join(app_path, 'manifest.json'), |
+ 'r', encoding='utf-8').read() |
+ manifest_data = manifest_data.replace('\r', '') |
+ 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.
|
+ manifest_data = manifest_data.replace(u'\uffee', '') |
+ return json.loads(manifest_data) |
+ except: |
+ 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
|
+ |
+ |
+def IsBad(path): |
+ """Checks a blacklist to decide if we should startup test this app. |
+ |
+ Args: |
+ path: path to the nexe. |
+ Returns: |
+ Boolean indicating if we should test this app. |
+ """ |
+ return os.path.splitext(os.path.basename(path))[0] in KNOWN_BAD |
+ |
+ |
+def CachedPath(options, filename): |
+ """Find the full path of a cached file, a cache root relative path. |
+ |
+ Args: |
+ options: bags of options. |
+ filename: filename relative to the top of the download url / cache. |
+ Returns: |
+ Absolute path of where the file goes in the cache. |
+ """ |
+ return os.path.join(options.cache_dir, 'nacl_startup_test_cache', filename) |
+ |
+ |
+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.
|
+ """Get the expected sha1 of a file path. |
+ |
+ Throughout we use the convention that files are store to a name of the form: |
+ <path_to_file>/<sha1hex>[.<some_extention>] |
+ This function extracts the expected sha1. |
+ |
+ Args: |
+ filename: filename to extract. |
+ Returns: |
+ Excepted sha1. |
+ """ |
+ return os.path.splitext(os.path.basename(filename))[0] |
+ |
+ |
+def PrimeCache(options, filename): |
Nick Bray
2012/04/02 20:31:41
Dito.
bradn
2012/04/02 22:43:59
Done.
|
+ """Attempt to add a file to the cache directory if its not already there. |
+ |
+ Args: |
+ options: bag of options. |
+ filename: filename relative to the top of the download url / cache. |
+ """ |
+ dpath = CachedPath(options, filename) |
+ if not os.path.exists(dpath) or Sha1Sum(dpath) != Sha1FromFilename(filename): |
+ # Try to make the directory, fail is ok, let the download fail instead. |
+ try: |
+ os.makedirs(os.path.basename(dpath)) |
+ except OSError: |
+ pass |
+ DownloadFile(filename, dpath) |
+ |
+ |
+def ExtractFromCache(options, source, dest): |
+ """Extract a crx from the cache. |
+ |
+ Args: |
+ options: bag of options. |
+ source: crx file to extract (cache relative). |
+ dest: location to extract to. |
+ """ |
+ 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.
|
+ dpath = CachedPath(options, source) |
+ assert os.path.exists(dpath) |
+ zf = zipfile.ZipFile(dpath, 'r') |
+ os.makedirs(dest) |
+ for info in zf.infolist(): |
+ 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.
|
+ if info.filename.endswith('/'): |
+ os.makedirs(tpath) |
+ else: |
+ zf.extract(info, dest) |
+ zf.close() |
+ |
+ |
+def TestApps(options, work_dir): |
+ """Test a browser on a corpus of crxs. |
+ |
+ Args: |
+ options: bag of options. |
+ work_dir: directory to operate in. |
+ """ |
+ profile_path = os.path.join(work_dir, 'profile_temp') |
+ app_path = os.path.join(work_dir, 'app_temp') |
+ |
+ list_filename = os.path.join(work_dir, 'naclapps.all') |
+ filenames = DownloadTotalList(list_filename) |
+ filenames = filenames[24:] |
+ |
+ count = 0 |
+ start = time.time() |
+ count = len(filenames) |
+ for index, filename in enumerate(filenames): |
+ tm = time.time() |
+ 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.
|
+ eta = (count - index) * (tm - start) / index |
+ eta_minutes = int(eta / 60) |
+ eta_seconds = int(eta - eta_minutes * 60) |
+ eta_str = ' (ETA %d:%02d)' % (eta_minutes, eta_seconds) |
+ else: |
+ eta_str = '' |
+ print 'Processing %d of %d%s...' % (index + 1, count, eta_str) |
+ # Skip if known bad. |
+ if IsBad(filename): |
+ continue |
+ PrimeCache(options, filename) |
+ # Stop here if downloading only. |
+ if options.download_only: |
+ continue |
+ # Unzip the app. |
+ ExtractFromCache(options, filename, app_path) |
+ try: |
+ TestAppStartup(options, filename, app_path, profile_path) |
+ count += 1 |
+ finally: |
+ download_utils.RemoveDir(app_path) |
+ download_utils.RemoveDir(profile_path) |
+ print 'Ran tests on %d of %d CRXs' % (count, len(filenames)) |
+ print 'SUCCESS' |
+ |
+ |
+def Main(): |
+ # Decide a default cache directory. |
+ # Prefer /b (for the bots) |
+ # Failing that, use scons-out. |
+ # Failing that, use the current users's home dir. |
+ 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.
|
+ if not os.path.isdir(default_cache_dir): |
+ default_cache_dir = os.path.join(NACL_DIR, 'scons-out') |
+ if not os.path.isdir(default_cache_dir): |
+ default_cache_dir = os.path.expanduser('~/') |
+ default_cache_dir = os.path.abspath(default_cache_dir) |
+ assert os.path.isdir(default_cache_dir) |
+ |
+ parser = optparse.OptionParser() |
+ parser.add_option( |
+ '--cache-dir', dest='cache_dir', default=default_cache_dir, |
+ help='directory to cache downloads in') |
+ parser.add_option( |
+ '--download-only', dest='download_only', |
+ default=False, action='store_true', |
+ help='download to cache without running the tests') |
+ parser.add_option( |
+ '--duration', dest='duration', default=30, |
+ help='how long to run each app for') |
+ parser.add_option( |
+ '--browser', dest='browser', |
+ help='browser to run') |
+ options, args = parser.parse_args() |
+ if args: |
+ parser.error('unused arguments') |
+ if not options.download_only: |
+ if not options.browser: |
+ parser.error('no browser specified') |
+ |
+ work_dir = tempfile.mkdtemp(suffix='startup_crxs', prefix='tmp') |
+ work_dir = os.path.realpath(work_dir) |
+ try: |
+ TestApps(options, work_dir) |
+ finally: |
+ download_utils.RemoveDir(work_dir) |
+ |
+ |
+if __name__ == '__main__': |
+ Main() |