| Index: tests/abi_corpus/corpus_utils.py
|
| diff --git a/tests/abi_corpus/corpus_utils.py b/tests/abi_corpus/corpus_utils.py
|
| new file mode 100755
|
| index 0000000000000000000000000000000000000000..0cd4a78b4a8a5a0c3ad50dfe60acfff3498a0d52
|
| --- /dev/null
|
| +++ b/tests/abi_corpus/corpus_utils.py
|
| @@ -0,0 +1,369 @@
|
| +#!/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 math
|
| +import os
|
| +import shutil
|
| +import struct
|
| +import subprocess
|
| +import sys
|
| +import threading
|
| +import time
|
| +import zipfile
|
| +
|
| +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'))
|
| +from download_utils import RemoveDir
|
| +
|
| +
|
| +class DownloadError(Exception):
|
| + """Indicates a download failed."""
|
| + pass
|
| +
|
| +
|
| +class FailedTests(Exception):
|
| + """Indicates a test run failed."""
|
| + pass
|
| +
|
| +
|
| +def GsutilCopySilent(src, dst):
|
| + """Invoke gsutil cp, swallowing the output, with retry.
|
| +
|
| + Args:
|
| + src: src url.
|
| + dst: dst path.
|
| + """
|
| + env = os.environ.copy()
|
| + env['PATH'] = '/b/build/scripts/slave' + os.pathsep + env['PATH']
|
| + # Retry to compensate for storage flake.
|
| + for attempt in range(3):
|
| + 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
|
| + time.sleep(math.pow(2, attempt + 1) * 5)
|
| + raise DownloadError(
|
| + 'Unexpected return code: %s\n'
|
| + '>>> STDOUT\n%s\n'
|
| + '>>> STDERR\n%s\n' % (
|
| + process.returncode, process_stdout, process_stderr))
|
| +
|
| +
|
| +def DownloadFileFromCorpus(src_path, dst_filename):
|
| + """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 DownloadCorpusCRXList(list_filename):
|
| + """Download list of all crx files in test corpus.
|
| +
|
| + Args:
|
| + list_filename: destination filename (kept around for debugging).
|
| + Returns:
|
| + List of CRXs.
|
| + """
|
| + DownloadFileFromCorpus('naclapps.all', list_filename)
|
| + fh = open(list_filename)
|
| + filenames = fh.read().splitlines()
|
| + fh.close()
|
| + crx_filenames = [f for f in filenames if f.endswith('.crx')]
|
| + return crx_filenames
|
| +
|
| +
|
| +def DownloadNexeList(filename):
|
| + """Download list of NEXEs.
|
| +
|
| + Args:
|
| + filename: destination filename.
|
| + Returns:
|
| + List of NEXEs.
|
| + """
|
| + DownloadFileFromCorpus('naclapps.list', filename)
|
| + fh = open(filename)
|
| + filenames = fh.read().splitlines()
|
| + fh.close()
|
| + return filenames
|
| +
|
| +
|
| +def Sha1Digest(path):
|
| + """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.
|
| +
|
| + A stable but semi-undocumented property of unpacked chrome extensions is
|
| + that they are assigned an app-id based on the first 32 characters of the
|
| + sha256 digest of the absolute symlink expanded path of the extension.
|
| + Instead of hexadecimal digits, characters a-p.
|
| + From discussion with webstore team + inspection of extensions code.
|
| + 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 RunWithTimeout(cmd, timeout):
|
| + """Run a program, capture output, allowing to run up to a timeout.
|
| +
|
| + Args:
|
| + cmd: List of strings containing command to run.
|
| + timeout: Duration to timeout.
|
| + Returns:
|
| + Tuple of stdout, stderr, returncode.
|
| + """
|
| + process = subprocess.Popen(cmd,
|
| + stdout=subprocess.PIPE,
|
| + stderr=subprocess.PIPE)
|
| + # Put the read in another thread so the buffer doesn't fill up.
|
| + 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.start()
|
| + # Gather stderr.
|
| + stderr_output = []
|
| + stderr_thread = threading.Thread(
|
| + target=GatherOutput, args=(process.stderr, stderr_output))
|
| + stderr_thread.start()
|
| + # Wait for a small span for the app to load.
|
| + time.sleep(timeout)
|
| + process.kill()
|
| + # Join up.
|
| + process.wait()
|
| + stdout_thread.join()
|
| + stderr_thread.join()
|
| + # Pick out result.
|
| + return stdout_output[0], stderr_output[0], process.returncode
|
| +
|
| +
|
| +def LoadManifest(app_path):
|
| + manifest_data = codecs.open(os.path.join(app_path, 'manifest.json'),
|
| + 'r', encoding='utf-8').read()
|
| + # Ignore CRs as they confuse json.loads.
|
| + manifest_data = manifest_data.replace('\r', '')
|
| + # Ignore unicode endian markers as they confuse json.loads.
|
| + manifest_data = manifest_data.replace(u'\ufeff', '')
|
| + manifest_data = manifest_data.replace(u'\uffee', '')
|
| + return json.loads(manifest_data)
|
| +
|
| +
|
| +def CachedPath(cache_dir, filename):
|
| + """Find the full path of a cached file, a cache root relative path.
|
| +
|
| + Args:
|
| + cache_dir: directory to keep the cache in.
|
| + 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(cache_dir, 'nacl_abi_corpus_cache', filename)
|
| +
|
| +
|
| +def Sha1FromFilename(filename):
|
| + """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(cache_dir, filename):
|
| + """Attempt to add a file to the cache directory if its not already there.
|
| +
|
| + Args:
|
| + cache_dir: directory to keep the cache in.
|
| + filename: filename relative to the top of the download url / cache.
|
| + """
|
| + dpath = CachedPath(cache_dir, filename)
|
| + if (not os.path.exists(dpath) or
|
| + Sha1Digest(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
|
| + DownloadFileFromCorpus(filename, dpath)
|
| +
|
| +
|
| +def CopyFromCache(cache_dir, filename, dest_filename):
|
| + """Copy an item from the cache.
|
| +
|
| + Args:
|
| + cache_dir: directory to keep the cache in.
|
| + filename: filename relative to the top of the download url / cache.
|
| + dest_filename: location to copy the file to.
|
| + """
|
| + dpath = CachedPath(cache_dir, filename)
|
| + shutil.copy(dpath, dest_filename)
|
| + assert Sha1Digest(dest_filename) == Sha1FromFilename(filename)
|
| +
|
| +
|
| +def ExtractFromCache(cache_dir, source, dest):
|
| + """Extract a crx from the cache.
|
| +
|
| + Args:
|
| + cache_dir: directory to keep the cache in.
|
| + source: crx file to extract (cache relative).
|
| + dest: location to extract to.
|
| + """
|
| + # We don't want to accidentally extract two extensions on top of each other.
|
| + # Assert that the destination doesn't yet exist.
|
| + assert not os.path.exists(dest)
|
| + dpath = CachedPath(cache_dir, source)
|
| + # The cached location must exist.
|
| + assert os.path.exists(dpath)
|
| + zf = zipfile.ZipFile(dpath, 'r')
|
| + os.makedirs(dest)
|
| + for info in zf.infolist():
|
| + # Skip directories.
|
| + if info.filename.endswith('/'):
|
| + continue
|
| + # Do not support absolute paths or paths containing ..
|
| + if not os.path.isabs(info.filename) or '..' in info.filename:
|
| + raise Exception('Unacceptable zip filename %s' % info.filename)
|
| + tpath = os.path.join(dest, info.filename)
|
| + tdir = os.path.dirname(tpath)
|
| + if not os.path.exists(tdir):
|
| + os.makedirs(tdir)
|
| + zf.extract(info, dest)
|
| + zf.close()
|
| +
|
| +
|
| +def DefaultCacheDirectory():
|
| + """Decide a default cache directory.
|
| +
|
| + Decide a default cache directory.
|
| + Prefer /b (for the bots)
|
| + Failing that, use scons-out.
|
| + Failing that, use the current users's home dir.
|
| + Returns:
|
| + Default to use for a corpus cache directory.
|
| + """
|
| + default_cache_dir = '/b'
|
| + 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.realpath(default_cache_dir)
|
| + assert os.path.isdir(default_cache_dir)
|
| + assert os.path.realpath('.') != default_cache_dir
|
| + return default_cache_dir
|
| +
|
| +
|
| +def NexeArchitecture(filename):
|
| + """Decide the architecture of a nexe.
|
| +
|
| + Args:
|
| + filename: filename of the nexe.
|
| + Returns:
|
| + Architecture string (x86-32 / x86-64) or None.
|
| + """
|
| + fh = open(filename, 'rb')
|
| + head = fh.read(20)
|
| + # Must not be too short.
|
| + if len(head) != 20:
|
| + print 'ERROR - header too short'
|
| + return None
|
| + # Must have ELF header.
|
| + if head[0:4] != '\x7fELF':
|
| + print 'ERROR - no elf header'
|
| + return None
|
| + # Decode e_machine
|
| + machine = struct.unpack('<H', head[18:])[0]
|
| + return {
|
| + 3: 'x86-32',
|
| + #40: 'arm', # TODO(bradnelson): handle arm.
|
| + 62: 'x86-64',
|
| + }.get(machine)
|
| +
|
| +
|
| +class Progress(object):
|
| + def __init__(self, total):
|
| + self.total = total
|
| + self.count = 0
|
| + self.successes = 0
|
| + self.failures = 0
|
| + self.start = time.time()
|
| +
|
| + def Tally(self):
|
| + if self.count > 0:
|
| + tm = time.time()
|
| + eta = (self.total - self.count) * (tm - self.start) / self.count
|
| + eta_minutes = int(eta / 60)
|
| + eta_seconds = int(eta - eta_minutes * 60)
|
| + eta_str = ' (ETA %d:%02d)' % (eta_minutes, eta_seconds)
|
| + else:
|
| + eta_str = ''
|
| + self.count += 1
|
| + print 'Processing %d of %d%s...' % (self.count, self.total, eta_str)
|
| +
|
| + def Result(self, success):
|
| + if success:
|
| + self.successes += 1
|
| + else:
|
| + self.failures += 1
|
| +
|
| + def Summary(self, warn_only=False):
|
| + print 'Ran tests on %d of %d items.' % (
|
| + self.successes + self.failures, self.total)
|
| + if self.failures:
|
| + # Our alternate validators don't currently cover everything.
|
| + # For now, don't fail just emit warning (and a tally of failures).
|
| + print '@@@STEP_TEXT@FAILED %d times (%.1f%% are incorrect)@@@' % (
|
| + self.failures, self.failures * 100 / (self.successes + self.failures))
|
| + if warn_only:
|
| + print '@@@STEP_WARNINGS@@@'
|
| + else:
|
| + raise FailedTests('FAILED %d tests' % self.failures)
|
| + else:
|
| + print 'SUCCESS'
|
|
|