blob: bb3cf6446dde41cf04166802b90f05060a466865 [file] [log] [blame]
#!/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.
from __future__ import print_function
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)
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 DownloadCorpusList(list_filename):
"""Download list of all files in test corpus.
Args:
list_filename: destination filename (kept around for debugging).
Returns:
List of files.
"""
DownloadFileFromCorpus('naclapps.all', list_filename)
fh = open(list_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.dirname(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 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.skips = 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 % 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 Skip(self):
self.skips += 1
def Summary(self, warn_only=False):
print('Ran tests on %d of %d items.' % (self.successes + self.failures,
self.total))
if self.skips:
print('%d tests were skipped.' % self.skips)
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')