diff options
Diffstat (limited to 'tools/isolate/run_test_from_archive.py')
-rwxr-xr-x | tools/isolate/run_test_from_archive.py | 965 |
1 files changed, 0 insertions, 965 deletions
diff --git a/tools/isolate/run_test_from_archive.py b/tools/isolate/run_test_from_archive.py deleted file mode 100755 index 92abce2..0000000 --- a/tools/isolate/run_test_from_archive.py +++ /dev/null @@ -1,965 +0,0 @@ -#!/usr/bin/env python -# Copyright (c) 2012 The Chromium Authors. All rights reserved. -# Use of this source code is governed by a BSD-style license that can be -# found in the LICENSE file. - -"""Reads a manifest, creates a tree of hardlinks and runs the test. - -Keeps a local cache. -""" - -import ctypes -import hashlib -import json -import logging -import optparse -import os -import Queue -import re -import shutil -import stat -import subprocess -import sys -import tempfile -import threading -import time -import urllib - - -# Types of action accepted by recreate_tree(). -HARDLINK, SYMLINK, COPY = range(1, 4) - -RE_IS_SHA1 = re.compile(r'^[a-fA-F0-9]{40}$') - - -class ConfigError(ValueError): - """Generic failure to load a manifest.""" - pass - - -class MappingError(OSError): - """Failed to recreate the tree.""" - pass - - -def get_flavor(): - """Returns the system default flavor. Copied from gyp/pylib/gyp/common.py.""" - flavors = { - 'cygwin': 'win', - 'win32': 'win', - 'darwin': 'mac', - 'sunos5': 'solaris', - 'freebsd7': 'freebsd', - 'freebsd8': 'freebsd', - } - return flavors.get(sys.platform, 'linux') - - -def os_link(source, link_name): - """Add support for os.link() on Windows.""" - if sys.platform == 'win32': - if not ctypes.windll.kernel32.CreateHardLinkW( - unicode(link_name), unicode(source), 0): - raise OSError() - else: - os.link(source, link_name) - - -def readable_copy(outfile, infile): - """Makes a copy of the file that is readable by everyone.""" - shutil.copy(infile, outfile) - read_enabled_mode = (os.stat(outfile).st_mode | stat.S_IRUSR | - stat.S_IRGRP | stat.S_IROTH) - os.chmod(outfile, read_enabled_mode) - - -def link_file(outfile, infile, action): - """Links a file. The type of link depends on |action|.""" - logging.debug('Mapping %s to %s' % (infile, outfile)) - if action not in (HARDLINK, SYMLINK, COPY): - raise ValueError('Unknown mapping action %s' % action) - if not os.path.isfile(infile): - raise MappingError('%s is missing' % infile) - if os.path.isfile(outfile): - raise MappingError( - '%s already exist; insize:%d; outsize:%d' % - (outfile, os.stat(infile).st_size, os.stat(outfile).st_size)) - - if action == COPY: - readable_copy(outfile, infile) - elif action == SYMLINK and sys.platform != 'win32': - # On windows, symlink are converted to hardlink and fails over to copy. - os.symlink(infile, outfile) - else: - try: - os_link(infile, outfile) - except OSError: - # Probably a different file system. - logging.warn( - 'Failed to hardlink, failing back to copy %s to %s' % ( - infile, outfile)) - readable_copy(outfile, infile) - - -def _set_write_bit(path, read_only): - """Sets or resets the executable bit on a file or directory.""" - mode = os.lstat(path).st_mode - if read_only: - mode = mode & 0500 - else: - mode = mode | 0200 - if hasattr(os, 'lchmod'): - os.lchmod(path, mode) # pylint: disable=E1101 - else: - if stat.S_ISLNK(mode): - # Skip symlink without lchmod() support. - logging.debug('Can\'t change +w bit on symlink %s' % path) - return - - # TODO(maruel): Implement proper DACL modification on Windows. - os.chmod(path, mode) - - -def make_writable(root, read_only): - """Toggle the writable bit on a directory tree.""" - root = os.path.abspath(root) - for dirpath, dirnames, filenames in os.walk(root, topdown=True): - for filename in filenames: - _set_write_bit(os.path.join(dirpath, filename), read_only) - - for dirname in dirnames: - _set_write_bit(os.path.join(dirpath, dirname), read_only) - - -def rmtree(root): - """Wrapper around shutil.rmtree() to retry automatically on Windows.""" - make_writable(root, False) - if sys.platform == 'win32': - for i in range(3): - try: - shutil.rmtree(root) - break - except WindowsError: # pylint: disable=E0602 - delay = (i+1)*2 - print >> sys.stderr, ( - 'The test has subprocess outliving it. Sleep %d seconds.' % delay) - time.sleep(delay) - else: - shutil.rmtree(root) - - -def is_same_filesystem(path1, path2): - """Returns True if both paths are on the same filesystem. - - This is required to enable the use of hardlinks. - """ - assert os.path.isabs(path1), path1 - assert os.path.isabs(path2), path2 - if sys.platform == 'win32': - # If the drive letter mismatches, assume it's a separate partition. - # TODO(maruel): It should look at the underlying drive, a drive letter could - # be a mount point to a directory on another drive. - assert re.match(r'^[a-zA-Z]\:\\.*', path1), path1 - assert re.match(r'^[a-zA-Z]\:\\.*', path2), path2 - if path1[0].lower() != path2[0].lower(): - return False - return os.stat(path1).st_dev == os.stat(path2).st_dev - - -def get_free_space(path): - """Returns the number of free bytes.""" - if sys.platform == 'win32': - free_bytes = ctypes.c_ulonglong(0) - ctypes.windll.kernel32.GetDiskFreeSpaceExW( - ctypes.c_wchar_p(path), None, None, ctypes.pointer(free_bytes)) - return free_bytes.value - f = os.statvfs(path) - return f.f_bfree * f.f_frsize - - -def make_temp_dir(prefix, root_dir): - """Returns a temporary directory on the same file system as root_dir.""" - base_temp_dir = None - if not is_same_filesystem(root_dir, tempfile.gettempdir()): - base_temp_dir = os.path.dirname(root_dir) - return tempfile.mkdtemp(prefix=prefix, dir=base_temp_dir) - - -def load_manifest(content): - """Verifies the manifest is valid and loads this object with the json data. - """ - try: - data = json.loads(content) - except ValueError: - raise ConfigError('Failed to parse: %s...' % content[:100]) - - if not isinstance(data, dict): - raise ConfigError('Expected dict, got %r' % data) - - for key, value in data.iteritems(): - if key == 'command': - if not isinstance(value, list): - raise ConfigError('Expected list, got %r' % value) - for subvalue in value: - if not isinstance(subvalue, basestring): - raise ConfigError('Expected string, got %r' % subvalue) - - elif key == 'files': - if not isinstance(value, dict): - raise ConfigError('Expected dict, got %r' % value) - for subkey, subvalue in value.iteritems(): - if not isinstance(subkey, basestring): - raise ConfigError('Expected string, got %r' % subkey) - if not isinstance(subvalue, dict): - raise ConfigError('Expected dict, got %r' % subvalue) - for subsubkey, subsubvalue in subvalue.iteritems(): - if subsubkey == 'link': - if not isinstance(subsubvalue, basestring): - raise ConfigError('Expected string, got %r' % subsubvalue) - elif subsubkey == 'mode': - if not isinstance(subsubvalue, int): - raise ConfigError('Expected int, got %r' % subsubvalue) - elif subsubkey == 'sha-1': - if not RE_IS_SHA1.match(subsubvalue): - raise ConfigError('Expected sha-1, got %r' % subsubvalue) - elif subsubkey == 'size': - if not isinstance(subsubvalue, int): - raise ConfigError('Expected int, got %r' % subsubvalue) - elif subsubkey == 'timestamp': - if not isinstance(subsubvalue, int): - raise ConfigError('Expected int, got %r' % subsubvalue) - elif subsubkey == 'touched_only': - if not isinstance(subsubvalue, bool): - raise ConfigError('Expected bool, got %r' % subsubvalue) - else: - raise ConfigError('Unknown subsubkey %s' % subsubkey) - if bool('sha-1' in subvalue) and bool('link' in subvalue): - raise ConfigError( - 'Did not expect both \'sha-1\' and \'link\', got: %r' % subvalue) - - elif key == 'includes': - if not isinstance(value, list): - raise ConfigError('Expected list, got %r' % value) - for subvalue in value: - if not RE_IS_SHA1.match(subvalue): - raise ConfigError('Expected sha-1, got %r' % subvalue) - - elif key == 'read_only': - if not isinstance(value, bool): - raise ConfigError('Expected bool, got %r' % value) - - elif key == 'relative_cwd': - if not isinstance(value, basestring): - raise ConfigError('Expected string, got %r' % value) - - elif key == 'os': - if value != get_flavor(): - raise ConfigError( - 'Expected \'os\' to be \'%s\' but got \'%s\'' % - (get_flavor(), value)) - - else: - raise ConfigError('Unknown key %s' % key) - - return data - - -def fix_python_path(cmd): - """Returns the fixed command line to call the right python executable.""" - out = cmd[:] - if out[0] == 'python': - out[0] = sys.executable - elif out[0].endswith('.py'): - out.insert(0, sys.executable) - return out - - -class Profiler(object): - def __init__(self, name): - self.name = name - self.start_time = None - - def __enter__(self): - self.start_time = time.time() - return self - - def __exit__(self, _exc_type, _exec_value, _traceback): - time_taken = time.time() - self.start_time - logging.info('Profiling: Section %s took %3.3f seconds', - self.name, time_taken) - - -class Remote(object): - """Priority based worker queue to fetch or upload files from a - content-address server. Any function may be given as the fetcher/upload, - as long as it takes two inputs (the item contents, and their relative - destination). - - Supports local file system, CIFS or http remotes. - - When the priority of items is equals, works in strict FIFO mode. - """ - # Initial and maximum number of worker threads. - INITIAL_WORKERS = 2 - MAX_WORKERS = 16 - # Priorities. - LOW, MED, HIGH = (1<<8, 2<<8, 3<<8) - INTERNAL_PRIORITY_BITS = (1<<8) - 1 - RETRIES = 5 - - def __init__(self, destination_root): - # Function to fetch a remote object or upload to a remote location.. - self._do_item = self.get_file_handler(destination_root) - # Contains tuple(priority, index, obj, destination). - self._queue = Queue.PriorityQueue() - # Contains tuple(priority, index, obj). - self._done = Queue.PriorityQueue() - - # Contains generated exceptions that haven't been handled yet. - self._exceptions = Queue.Queue() - - # To keep FIFO ordering in self._queue. It is assumed xrange's iterator is - # thread-safe. - self._next_index = xrange(0, 1<<30).__iter__().next - - # Control access to the following member. - self._ready_lock = threading.Lock() - # Number of threads in wait state. - self._ready = 0 - - # Control access to the following member. - self._workers_lock = threading.Lock() - self._workers = [] - for _ in range(self.INITIAL_WORKERS): - self._add_worker() - - def join(self): - """Blocks until the queue is empty.""" - self._queue.join() - - def next_exception(self): - """Returns the next unhandled exception, or None if there is - no exception.""" - try: - return self._exceptions.get_nowait() - except Queue.Empty: - return None - - def add_item(self, priority, obj, dest): - """Retrieves an object from the remote data store. - - The smaller |priority| gets fetched first. - - Thread-safe. - """ - assert (priority & self.INTERNAL_PRIORITY_BITS) == 0 - self._add_to_queue(priority, obj, dest) - - def get_result(self): - """Returns the next file that was successfully fetched.""" - r = self._done.get() - if r[0] == -1: - # It's an exception. - raise r[2][0], r[2][1], r[2][2] - return r[2] - - def _add_to_queue(self, priority, obj, dest): - with self._ready_lock: - start_new_worker = not self._ready - self._queue.put((priority, self._next_index(), obj, dest)) - if start_new_worker: - self._add_worker() - - def _add_worker(self): - """Add one worker thread if there isn't too many. Thread-safe.""" - with self._workers_lock: - if len(self._workers) >= self.MAX_WORKERS: - return False - worker = threading.Thread(target=self._run) - self._workers.append(worker) - worker.daemon = True - worker.start() - - def _step_done(self, result): - """Worker helper function""" - self._done.put(result) - self._queue.task_done() - if result[0] == -1: - self._exceptions.put(sys.exc_info()) - - def _run(self): - """Worker thread loop.""" - while True: - try: - with self._ready_lock: - self._ready += 1 - item = self._queue.get() - finally: - with self._ready_lock: - self._ready -= 1 - if not item: - return - priority, index, obj, dest = item - try: - self._do_item(obj, dest) - except IOError: - # Retry a few times, lowering the priority. - if (priority & self.INTERNAL_PRIORITY_BITS) < self.RETRIES: - self._add_to_queue(priority + 1, obj, dest) - self._queue.task_done() - continue - # Transfers the exception back. It has maximum priority. - self._step_done((-1, 0, sys.exc_info())) - except: - # Transfers the exception back. It has maximum priority. - self._step_done((-1, 0, sys.exc_info())) - else: - self._step_done((priority, index, obj)) - - @staticmethod - def get_file_handler(file_or_url): - """Returns a object to retrieve objects from a remote.""" - if re.match(r'^https?://.+$', file_or_url): - file_or_url = file_or_url.rstrip('/') + '/' - def download_file(item, dest): - # TODO(maruel): Reuse HTTP connections. The stdlib doesn't make this - # easy. - source = file_or_url + item - logging.debug('download_file(%s, %s)', source, dest) - urllib.urlretrieve(source, dest) - return download_file - - def copy_file(item, dest): - source = os.path.join(file_or_url, item) - logging.debug('copy_file(%s, %s)', source, dest) - shutil.copy(source, dest) - return copy_file - - -class CachePolicies(object): - def __init__(self, max_cache_size, min_free_space, max_items): - """ - Arguments: - - max_cache_size: Trim if the cache gets larger than this value. If 0, the - cache is effectively a leak. - - min_free_space: Trim if disk free space becomes lower than this value. If - 0, it unconditionally fill the disk. - - max_items: Maximum number of items to keep in the cache. If 0, do not - enforce a limit. - """ - self.max_cache_size = max_cache_size - self.min_free_space = min_free_space - self.max_items = max_items - - -class Cache(object): - """Stateful LRU cache. - - Saves its state as json file. - """ - STATE_FILE = 'state.json' - - def __init__(self, cache_dir, remote, policies): - """ - Arguments: - - cache_dir: Directory where to place the cache. - - remote: Remote where to fetch items from. - - policies: cache retention policies. - """ - self.cache_dir = cache_dir - self.remote = remote - self.policies = policies - self.state_file = os.path.join(cache_dir, self.STATE_FILE) - # The tuple(file, size) are kept as an array in a LRU style. E.g. - # self.state[0] is the oldest item. - self.state = [] - # A lookup map to speed up searching. - self._lookup = {} - self._dirty = False - - # Items currently being fetched. Keep it local to reduce lock contention. - self._pending_queue = set() - - # Profiling values. - self._added = [] - self._removed = [] - self._free_disk = 0 - - if not os.path.isdir(self.cache_dir): - os.makedirs(self.cache_dir) - if os.path.isfile(self.state_file): - try: - self.state = json.load(open(self.state_file, 'r')) - except (IOError, ValueError), e: - # Too bad. The file will be overwritten and the cache cleared. - logging.error( - 'Broken state file %s, ignoring.\n%s' % (self.STATE_FILE, e)) - if (not isinstance(self.state, list) or - not all( - isinstance(i, (list, tuple)) and len(i) == 2 for i in self.state)): - # Discard. - self.state = [] - self._dirty = True - - # Ensure that all files listed in the state still exist and add new ones. - previous = set(filename for filename, _ in self.state) - if len(previous) != len(self.state): - logging.warn('Cache state is corrupted') - self._dirty = True - self.state = [] - else: - added = 0 - for filename in os.listdir(self.cache_dir): - if filename == self.STATE_FILE: - continue - if filename in previous: - previous.remove(filename) - continue - # An untracked file. - self._dirty = True - if not RE_IS_SHA1.match(filename): - logging.warn('Removing unknown file %s from cache', filename) - os.remove(self.path(filename)) - else: - # Insert as the oldest file. It will be deleted eventually if not - # accessed. - self._add(filename, False) - added += 1 - if added: - logging.warn('Added back %d unknown files', added) - self.state = [ - (filename, size) for filename, size in self.state - if filename not in previous - ] - self._update_lookup() - - with Profiler('SetupTrimming'): - self.trim() - - def __enter__(self): - return self - - def __exit__(self, _exc_type, _exec_value, _traceback): - with Profiler('CleanupTrimming'): - self.trim() - - logging.info( - '%4d (%7dkb) added', len(self._added), sum(self._added) / 1024) - logging.info( - '%4d (%7dkb) current', - len(self.state), - sum(i[1] for i in self.state) / 1024) - logging.info( - '%4d (%7dkb) removed', len(self._removed), sum(self._removed) / 1024) - logging.info('%7dkb free', self._free_disk / 1024) - - def remove_lru_file(self): - """Removes the last recently used file.""" - try: - filename, size = self.state.pop(0) - del self._lookup[filename] - self._removed.append(size) - os.remove(self.path(filename)) - self._dirty = True - except OSError as e: - logging.error('Error attempting to delete a file\n%s' % e) - - def trim(self): - """Trims anything we don't know, make sure enough free space exists.""" - # Ensure maximum cache size. - if self.policies.max_cache_size and self.state: - while sum(i[1] for i in self.state) > self.policies.max_cache_size: - self.remove_lru_file() - - # Ensure maximum number of items in the cache. - if self.policies.max_items and self.state: - while len(self.state) > self.policies.max_items: - self.remove_lru_file() - - # Ensure enough free space. - self._free_disk = get_free_space(self.cache_dir) - while ( - self.policies.min_free_space and - self.state and - self._free_disk < self.policies.min_free_space): - self.remove_lru_file() - self._free_disk = get_free_space(self.cache_dir) - - self.save() - - def retrieve(self, priority, item): - """Retrieves a file from the remote, if not already cached, and adds it to - the cache. - """ - assert not '/' in item - path = self.path(item) - index = self._lookup.get(item) - if index is None: - if item in self._pending_queue: - # Already pending. The same object could be referenced multiple times. - return - self.remote.add_item(priority, item, path) - self._pending_queue.add(item) - else: - if index != len(self.state) - 1: - # Was already in cache. Update it's LRU value by putting it at the end. - self.state.append(self.state.pop(index)) - self._dirty = True - self._update_lookup() - - def add(self, filepath, obj): - """Forcibly adds a file to the cache.""" - if not obj in self._lookup: - link_file(self.path(obj), filepath, HARDLINK) - self._add(obj, True) - - def path(self, item): - """Returns the path to one item.""" - return os.path.join(self.cache_dir, item) - - def save(self): - """Saves the LRU ordering.""" - json.dump(self.state, open(self.state_file, 'wb'), separators=(',',':')) - - def wait_for(self, items): - """Starts a loop that waits for at least one of |items| to be retrieved. - - Returns the first item retrieved. - """ - # Flush items already present. - for item in items: - if item in self._lookup: - return item - - assert all(i in self._pending_queue for i in items), ( - items, self._pending_queue) - # Note that: - # len(self._pending_queue) == - # ( len(self.remote._workers) - self.remote._ready + - # len(self._remote._queue) + len(self._remote.done)) - # There is no lock-free way to verify that. - while self._pending_queue: - item = self.remote.get_result() - self._pending_queue.remove(item) - self._add(item, True) - if item in items: - return item - - def _add(self, item, at_end): - """Adds an item in the internal state. - - If |at_end| is False, self._lookup becomes inconsistent and - self._update_lookup() must be called. - """ - size = os.stat(self.path(item)).st_size - self._added.append(size) - if at_end: - self.state.append((item, size)) - self._lookup[item] = len(self.state) - 1 - else: - self.state.insert(0, (item, size)) - self._dirty = True - - def _update_lookup(self): - self._lookup = dict( - (filename, index) for index, (filename, _) in enumerate(self.state)) - - - -class Manifest(object): - """Represents a single parsed manifest, e.g. a .results file.""" - def __init__(self, obj_hash): - """|obj_hash| is really the sha-1 of the file.""" - logging.debug('Manifest(%s)' % obj_hash) - self.obj_hash = obj_hash - # Set once all the left-side of the tree is parsed. 'Tree' here means the - # manifest and all the manifest recursively included by it with 'includes' - # key. The order of each manifest sha-1 in 'includes' is important, as the - # later ones are not processed until the firsts are retrieved and read. - self.can_fetch = False - - # Raw data. - self.data = {} - # A Manifest instance, one per object in self.includes. - self.children = [] - - # Set once the manifest is loaded. - self._manifest_parsed = False - # Set once the files are fetched. - self.files_fetched = False - - def load(self, content): - """Verifies the manifest is valid and loads this object with the json data. - """ - logging.debug('Manifest.load(%s)' % self.obj_hash) - assert not self._manifest_parsed - self.data = load_manifest(content) - self.children = [Manifest(i) for i in self.data.get('includes', [])] - self._manifest_parsed = True - - def fetch_files(self, cache, files): - """Adds files in this manifest not present in files dictionary. - - Preemptively request files. - - Note that |files| is modified by this function. - """ - assert self.can_fetch - if not self._manifest_parsed or self.files_fetched: - return - logging.debug('fetch_files(%s)' % self.obj_hash) - for filepath, properties in self.data.get('files', {}).iteritems(): - # Root manifest has priority on the files being mapped. In particular, - # overriden files must not be fetched. - if filepath not in files: - files[filepath] = properties - if 'sha-1' in properties: - # Preemptively request files. - logging.debug('fetching %s' % filepath) - cache.retrieve(Remote.MED, properties['sha-1']) - self.files_fetched = True - - -class Settings(object): - """Results of a completely parsed manifest.""" - def __init__(self): - self.command = [] - self.files = {} - self.read_only = None - self.relative_cwd = None - # The main manifest. - self.root = None - logging.debug('Settings') - - def load(self, cache, root_manifest_hash): - """Loads the manifest and all the included manifests asynchronously. - - It enables support for included manifest. They are processed in strict order - but fetched asynchronously from the cache. This is important so that a file - in an included manifest that is overridden by an embedding manifest is not - fetched neededlessly. The includes are fetched in one pass and the files are - fetched as soon as all the manifests on the left-side of the tree were - fetched. - - The prioritization is very important here for nested manifests. 'includes' - have the highest priority and the algorithm is optimized for both deep and - wide manifests. A deep one is a long link of manifest referenced one at a - time by one item in 'includes'. A wide one has a large number of 'includes' - in a single manifest. 'left' is defined as an included manifest earlier in - the 'includes' list. So the order of the elements in 'includes' is - important. - """ - self.root = Manifest(root_manifest_hash) - cache.retrieve(Remote.HIGH, root_manifest_hash) - pending = {root_manifest_hash: self.root} - # Keeps the list of retrieved items to refuse recursive includes. - retrieved = [root_manifest_hash] - - def update_self(node): - node.fetch_files(cache, self.files) - # Grabs properties. - if not self.command and node.data.get('command'): - self.command = node.data['command'] - if self.read_only is None and node.data.get('read_only') is not None: - self.read_only = node.data['read_only'] - if (self.relative_cwd is None and - node.data.get('relative_cwd') is not None): - self.relative_cwd = node.data['relative_cwd'] - - def traverse_tree(node): - if node.can_fetch: - if not node.files_fetched: - update_self(node) - will_break = False - for i in node.children: - if not i.can_fetch: - if will_break: - break - # Automatically mark the first one as fetcheable. - i.can_fetch = True - will_break = True - traverse_tree(i) - - while pending: - item_hash = cache.wait_for(pending) - item = pending.pop(item_hash) - item.load(open(cache.path(item_hash), 'r').read()) - if item_hash == root_manifest_hash: - # It's the root item. - item.can_fetch = True - - for new_child in item.children: - h = new_child.obj_hash - if h in retrieved: - raise ConfigError('Manifest %s is retrieved recursively' % h) - pending[h] = new_child - cache.retrieve(Remote.HIGH, h) - - # Traverse the whole tree to see if files can now be fetched. - traverse_tree(self.root) - def check(n): - return all(check(x) for x in n.children) and n.files_fetched - assert check(self.root) - self.relative_cwd = self.relative_cwd or '' - self.read_only = self.read_only or False - - -def run_tha_test(manifest_hash, cache_dir, remote, policies): - """Downloads the dependencies in the cache, hardlinks them into a temporary - directory and runs the executable. - """ - settings = Settings() - with Cache(cache_dir, Remote(remote), policies) as cache: - outdir = make_temp_dir('run_tha_test', cache_dir) - try: - # Initiate all the files download. - with Profiler('GetManifests') as _prof: - # Optionally support local files. - if not RE_IS_SHA1.match(manifest_hash): - # Adds it in the cache. While not strictly necessary, this simplifies - # the rest. - h = hashlib.sha1(open(manifest_hash, 'r').read()).hexdigest() - cache.add(manifest_hash, h) - manifest_hash = h - settings.load(cache, manifest_hash) - - if not settings.command: - print >> sys.stderr, 'No command to run' - return 1 - - with Profiler('GetRest') as _prof: - logging.debug('Creating directories') - # Creates the tree of directories to create. - directories = set(os.path.dirname(f) for f in settings.files) - for item in list(directories): - while item: - directories.add(item) - item = os.path.dirname(item) - for d in sorted(directories): - if d: - os.mkdir(os.path.join(outdir, d)) - - # Creates the links if necessary. - for filepath, properties in settings.files.iteritems(): - if 'link' not in properties: - continue - outfile = os.path.join(outdir, filepath) - os.symlink(properties['link'], outfile) - if 'mode' in properties: - # It's not set on Windows. - os.chmod(outfile, properties['mode']) - - # Remaining files to be processed. - # Note that files could still be not be downloaded yet here. - remaining = dict() - for filepath, props in settings.files.iteritems(): - if 'sha-1' in props: - remaining.setdefault(props['sha-1'], []).append((filepath, props)) - - # Do bookkeeping while files are being downloaded in the background. - cwd = os.path.join(outdir, settings.relative_cwd) - if not os.path.isdir(cwd): - os.makedirs(cwd) - cmd = settings.command[:] - # Ensure paths are correctly separated on windows. - cmd[0] = cmd[0].replace('/', os.path.sep) - cmd = fix_python_path(cmd) - - # Now block on the remaining files to be downloaded and mapped. - while remaining: - obj = cache.wait_for(remaining) - for filepath, properties in remaining.pop(obj): - outfile = os.path.join(outdir, filepath) - link_file(outfile, cache.path(obj), HARDLINK) - if 'mode' in properties: - # It's not set on Windows. - os.chmod(outfile, properties['mode']) - - if settings.read_only: - make_writable(outdir, True) - logging.info('Running %s, cwd=%s' % (cmd, cwd)) - try: - with Profiler('RunTest') as _prof: - return subprocess.call(cmd, cwd=cwd) - except OSError: - print >> sys.stderr, 'Failed to run %s; cwd=%s' % (cmd, cwd) - raise - finally: - rmtree(outdir) - - -def main(): - parser = optparse.OptionParser( - usage='%prog <options>', description=sys.modules[__name__].__doc__) - parser.add_option( - '-v', '--verbose', action='count', default=0, help='Use multiple times') - parser.add_option('--no-run', action='store_true', help='Skip the run part') - - group = optparse.OptionGroup(parser, 'Data source') - group.add_option( - '-m', '--manifest', - metavar='FILE', - help='File/url describing what to map or run') - group.add_option( - '-H', '--hash', - help='Hash of the manifest to grab from the hash table') - parser.add_option_group(group) - - group.add_option( - '-r', '--remote', metavar='URL', help='Remote where to get the items') - group = optparse.OptionGroup(parser, 'Cache management') - group.add_option( - '--cache', - default='cache', - metavar='DIR', - help='Cache directory, default=%default') - group.add_option( - '--max-cache-size', - type='int', - metavar='NNN', - default=20*1024*1024*1024, - help='Trim if the cache gets larger than this value, default=%default') - group.add_option( - '--min-free-space', - type='int', - metavar='NNN', - default=1*1024*1024*1024, - help='Trim if disk free space becomes lower than this value, ' - 'default=%default') - group.add_option( - '--max-items', - type='int', - metavar='NNN', - default=100000, - help='Trim if more than this number of items are in the cache ' - 'default=%default') - parser.add_option_group(group) - - options, args = parser.parse_args() - level = [logging.ERROR, logging.INFO, logging.DEBUG][min(2, options.verbose)] - logging.basicConfig( - level=level, - format='%(levelname)5s %(module)15s(%(lineno)3d): %(message)s') - - if bool(options.manifest) == bool(options.hash): - parser.error('One and only one of --manifest or --hash is required.') - if not options.remote: - parser.error('--remote is required.') - if args: - parser.error('Unsupported args %s' % ' '.join(args)) - - policies = CachePolicies( - options.max_cache_size, options.min_free_space, options.max_items) - try: - return run_tha_test( - options.manifest or options.hash, - os.path.abspath(options.cache), - options.remote, - policies) - except (ConfigError, MappingError), e: - print >> sys.stderr, str(e) - return 1 - - -if __name__ == '__main__': - sys.exit(main()) |