diff options
author | maruel@chromium.org <maruel@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2012-03-28 19:04:48 +0000 |
---|---|---|
committer | maruel@chromium.org <maruel@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2012-03-28 19:04:48 +0000 |
commit | 86c82fe1cc918fde83efe4a8e91455d075ae9ee5 (patch) | |
tree | a5f5882a375426ff4e1df5176780cf4fd033e996 /tools/isolate | |
parent | 3d5f3c13150e5cb5fa1fd779d26d9bca9b49acd3 (diff) | |
download | chromium_src-86c82fe1cc918fde83efe4a8e91455d075ae9ee5.zip chromium_src-86c82fe1cc918fde83efe4a8e91455d075ae9ee5.tar.gz chromium_src-86c82fe1cc918fde83efe4a8e91455d075ae9ee5.tar.bz2 |
Add tool to use the manifest, fetch and cache dependencies and run the test.
Only keep functions needed on the slave in tree_creator.py. Move the rest to
isolate.py.
R=rogerta@chromium.org
BUG=98834
TEST=Run from the ouput directory:
GYP_DEFINES="$GYP_DEFINES tests_run=hashtable" ../../build/gyp_chromium; \
ninja base_unittests_run; \
../../tools/isolate/tree_creator.py -m base_unittests.results --remote .
Review URL: https://chromiumcodereview.appspot.com/9834090
git-svn-id: svn://svn.chromium.org/chrome/trunk/src@129455 0039d316-1c4b-4281-b951-d872f2087c98
Diffstat (limited to 'tools/isolate')
-rwxr-xr-x | tools/isolate/isolate.py | 109 | ||||
-rwxr-xr-x[-rw-r--r--] | tools/isolate/tree_creator.py | 315 |
2 files changed, 316 insertions, 108 deletions
diff --git a/tools/isolate/isolate.py b/tools/isolate/isolate.py index 9a4dcdc..47925dc 100755 --- a/tools/isolate/isolate.py +++ b/tools/isolate/isolate.py @@ -19,11 +19,13 @@ See more information at http://dev.chromium.org/developers/testing/isolated-testing """ +import hashlib import json import logging import optparse import os import re +import stat import subprocess import sys import tempfile @@ -50,6 +52,98 @@ def to_relative(path, root, relative): return path +def expand_directories(indir, infiles, blacklist): + """Expands the directories, applies the blacklist and verifies files exist.""" + logging.debug('expand_directories(%s, %s, %s)' % (indir, infiles, blacklist)) + outfiles = [] + for relfile in infiles: + if os.path.isabs(relfile): + raise tree_creator.MappingError('Can\'t map absolute path %s' % relfile) + infile = os.path.normpath(os.path.join(indir, relfile)) + if not infile.startswith(indir): + raise tree_creator.MappingError( + 'Can\'t map file %s outside %s' % (infile, indir)) + + if relfile.endswith('/'): + if not os.path.isdir(infile): + raise tree_creator.MappingError( + 'Input directory %s must have a trailing slash' % infile) + for dirpath, dirnames, filenames in os.walk(infile): + # Convert the absolute path to subdir + relative subdirectory. + reldirpath = dirpath[len(indir)+1:] + outfiles.extend(os.path.join(reldirpath, f) for f in filenames) + for index, dirname in enumerate(dirnames): + # Do not process blacklisted directories. + if blacklist(os.path.join(reldirpath, dirname)): + del dirnames[index] + else: + if not os.path.isfile(infile): + raise tree_creator.MappingError('Input file %s doesn\'t exist' % infile) + outfiles.append(relfile) + return outfiles + + +def process_inputs(indir, infiles, need_hash, read_only): + """Returns a dictionary of input files, populated with the files' mode and + hash. + + The file mode is manipulated if read_only is True. In practice, we only save + one of 4 modes: 0755 (rwx), 0644 (rw), 0555 (rx), 0444 (r). + """ + outdict = {} + for infile in infiles: + filepath = os.path.join(indir, infile) + filemode = stat.S_IMODE(os.stat(filepath).st_mode) + # Remove write access for non-owner. + filemode &= ~(stat.S_IWGRP | stat.S_IWOTH) + if read_only: + filemode &= ~stat.S_IWUSR + if filemode & stat.S_IXUSR: + filemode |= (stat.S_IXGRP | stat.S_IXOTH) + else: + filemode &= ~(stat.S_IXGRP | stat.S_IXOTH) + outdict[infile] = { + 'mode': filemode, + } + if need_hash: + h = hashlib.sha1() + with open(filepath, 'rb') as f: + h.update(f.read()) + outdict[infile]['sha-1'] = h.hexdigest() + return outdict + + +def recreate_tree(outdir, indir, infiles, action): + """Creates a new tree with only the input files in it. + + Arguments: + outdir: Output directory to create the files in. + indir: Root directory the infiles are based in. + infiles: List of files to map from |indir| to |outdir|. + action: See assert below. + """ + logging.debug( + 'recreate_tree(%s, %s, %s, %s)' % (outdir, indir, infiles, action)) + logging.info('Mapping from %s to %s' % (indir, outdir)) + + assert action in ( + tree_creator.HARDLINK, tree_creator.SYMLINK, tree_creator.COPY) + outdir = os.path.normpath(outdir) + if not os.path.isdir(outdir): + logging.info ('Creating %s' % outdir) + os.makedirs(outdir) + # Do not call abspath until the directory exists. + outdir = os.path.abspath(outdir) + + for relfile in infiles: + infile = os.path.join(indir, relfile) + outfile = os.path.join(outdir, relfile) + outsubdir = os.path.dirname(outfile) + if not os.path.isdir(outsubdir): + os.makedirs(outsubdir) + tree_creator.link_file(outfile, infile, action) + + def separate_inputs_command(args, root, files): """Strips off the command line from the inputs. @@ -94,8 +188,8 @@ def isolate(outdir, resultfile, indir, infiles, mode, read_only, cmd, no_save): assert mode_fn assert os.path.isabs(resultfile) - infiles = tree_creator.expand_directories( - indir, infiles, lambda x: re.match(r'.*\.(svn|pyc)$', x)) + infiles = expand_directories( + indir, infiles, lambda x: re.match(r'.*\.(svn|pyc)$', x)) # Note the relative current directory. # In general, this path will be the path containing the gyp file where the @@ -116,8 +210,7 @@ def isolate(outdir, resultfile, indir, infiles, mode, read_only, cmd, no_save): cmd.insert(0, sys.executable) # Only hashtable mode really needs the sha-1. - dictfiles = tree_creator.process_inputs( - indir, infiles, mode == 'hashtable', read_only) + dictfiles = process_inputs(indir, infiles, mode == 'hashtable', read_only) result = mode_fn( outdir, indir, dictfiles, read_only, cmd, relative_cwd, resultfile) @@ -164,8 +257,7 @@ def MODEremap( if len(os.listdir(outdir)): print 'Can\'t remap in a non-empty directory' return 1 - tree_creator.recreate_tree( - outdir, indir, dictfiles.keys(), tree_creator.HARDLINK) + recreate_tree(outdir, indir, dictfiles.keys(), tree_creator.HARDLINK) if read_only: tree_creator.make_writable(outdir, True) return 0 @@ -176,8 +268,7 @@ def MODErun( """Always uses a temporary directory.""" try: outdir = tempfile.mkdtemp(prefix='isolate') - tree_creator.recreate_tree( - outdir, indir, dictfiles.keys(), tree_creator.HARDLINK) + recreate_tree(outdir, indir, dictfiles.keys(), tree_creator.HARDLINK) cwd = os.path.join(outdir, relative_cwd) if not os.path.isdir(cwd): os.makedirs(cwd) @@ -187,8 +278,6 @@ def MODErun( logging.info('Running %s, cwd=%s' % (cmd, cwd)) return subprocess.call(cmd, cwd=cwd) finally: - if read_only: - tree_creator.make_writable(outdir, False) tree_creator.rmtree(outdir) diff --git a/tools/isolate/tree_creator.py b/tools/isolate/tree_creator.py index ad0990c..074f721 100644..100755 --- a/tools/isolate/tree_creator.py +++ b/tools/isolate/tree_creator.py @@ -1,21 +1,25 @@ +#!/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. -"""File related utility functions. +"""Reads a manifest, creates a tree of hardlinks and runs the test. -Creates a tree of hardlinks, symlinks or copy the inputs files. Calculate files -hash. +Keeps a local cache. """ import ctypes -import hashlib +import json import logging +import optparse import os +import re import shutil -import stat +import subprocess import sys +import tempfile import time +import urllib # Types of action accepted by recreate_tree(). @@ -37,77 +41,20 @@ def os_link(source, link_name): os.link(source, link_name) -def expand_directories(indir, infiles, blacklist): - """Expands the directories, applies the blacklist and verifies files exist.""" - logging.debug('expand_directories(%s, %s, %s)' % (indir, infiles, blacklist)) - outfiles = [] - for relfile in infiles: - if os.path.isabs(relfile): - raise MappingError('Can\'t map absolute path %s' % relfile) - infile = os.path.normpath(os.path.join(indir, relfile)) - if not infile.startswith(indir): - raise MappingError('Can\'t map file %s outside %s' % (infile, indir)) - - if relfile.endswith('/'): - if not os.path.isdir(infile): - raise MappingError( - 'Input directory %s must have a trailing slash' % infile) - for dirpath, dirnames, filenames in os.walk(infile): - # Convert the absolute path to subdir + relative subdirectory. - relpath = dirpath[len(indir)+1:] - outfiles.extend(os.path.join(relpath, f) for f in filenames) - for index, dirname in enumerate(dirnames): - # Do not process blacklisted directories. - if blacklist(os.path.join(relpath, dirname)): - del dirnames[index] - else: - if not os.path.isfile(infile): - raise MappingError('Input file %s doesn\'t exist' % infile) - outfiles.append(relfile) - return outfiles - - -def process_inputs(indir, infiles, need_hash, read_only): - """Returns a dictionary of input files, populated with the files' mode and - hash. - - The file mode is manipulated if read_only is True. In practice, we only save - one of 4 modes: 0755 (rwx), 0644 (rw), 0555 (rx), 0444 (r). - """ - outdict = {} - for infile in infiles: - filepath = os.path.join(indir, infile) - filemode = stat.S_IMODE(os.stat(filepath).st_mode) - # Remove write access for non-owner. - filemode &= ~(stat.S_IWGRP | stat.S_IWOTH) - if read_only: - filemode &= ~stat.S_IWUSR - if filemode & stat.S_IXUSR: - filemode |= (stat.S_IXGRP | stat.S_IXOTH) - else: - filemode &= ~(stat.S_IXGRP | stat.S_IXOTH) - outdict[infile] = { - 'mode': filemode, - } - if need_hash: - h = hashlib.sha1() - with open(filepath, 'rb') as f: - h.update(f.read()) - outdict[infile]['sha-1'] = h.hexdigest() - return outdict - - 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 os.path.isfile(outfile): raise MappingError('%s already exist' % outfile) if action == COPY: shutil.copy(infile, outfile) elif action == SYMLINK and sys.platform != 'win32': + # On windows, symlink are converted to hardlink and fails over to copy. os.symlink(infile, outfile) - elif action == HARDLINK: + else: try: os_link(infile, outfile) except OSError: @@ -116,38 +63,6 @@ def link_file(outfile, infile, action): 'Failed to hardlink, failing back to copy %s to %s' % ( infile, outfile)) shutil.copy(infile, outfile) - else: - raise ValueError('Unknown mapping action %s' % action) - - -def recreate_tree(outdir, indir, infiles, action): - """Creates a new tree with only the input files in it. - - Arguments: - outdir: Output directory to create the files in. - indir: Root directory the infiles are based in. - infiles: List of files to map from |indir| to |outdir|. - action: See assert below. - """ - logging.debug( - 'recreate_tree(%s, %s, %s, %s)' % (outdir, indir, infiles, action)) - logging.info('Mapping from %s to %s' % (indir, outdir)) - - assert action in (HARDLINK, SYMLINK, COPY) - outdir = os.path.normpath(outdir) - if not os.path.isdir(outdir): - logging.info ('Creating %s' % outdir) - os.makedirs(outdir) - # Do not call abspath until the directory exists. - outdir = os.path.abspath(outdir) - - for relfile in infiles: - infile = os.path.join(indir, relfile) - outfile = os.path.join(outdir, relfile) - outsubdir = os.path.dirname(outfile) - if not os.path.isdir(outsubdir): - os.makedirs(outsubdir) - link_file(outfile, infile, action) def _set_write_bit(path, read_only): @@ -177,6 +92,7 @@ def make_writable(root, 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: @@ -189,3 +105,206 @@ def rmtree(root): time.sleep(delay) else: shutil.rmtree(root) + + +def open_remote(file_or_url): + """Reads a file or url.""" + if re.match(r'^https?://.+$', file_or_url): + return urllib.urlopen(file_or_url) + return open(file_or_url, 'rb') + + +def download_or_copy(file_or_url, dest): + """Copies a file or download an url.""" + if re.match(r'^https?://.+$', file_or_url): + urllib.urlretrieve(file_or_url, dest) + else: + shutil.copy(file_or_url, dest) + + +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 + + +class Cache(object): + """Stateful LRU cache. + + Saves its state as json file. + """ + STATE_FILE = 'state.json' + + def __init__(self, cache_dir, remote, max_cache_size, min_free_space): + """ + Arguments: + - cache_dir: Directory where to place the cache. + - remote: Remote directory (NFS, SMB, etc) or HTTP url to fetch the objects + from + - 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. + """ + self.cache_dir = cache_dir + self.remote = remote + self.max_cache_size = max_cache_size + self.min_free_space = min_free_space + self.state_file = os.path.join(cache_dir, self.STATE_FILE) + # The files are kept as an array in a LRU style. E.g. self.state[0] is the + # oldest item. + self.state = [] + + 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, 'rb')) + except ValueError: + # Too bad. The file will be overwritten and the cache cleared. + pass + self.trim() + + def trim(self): + """Trims anything we don't know, make sure enough free space exists.""" + for f in os.listdir(self.cache_dir): + if f == self.STATE_FILE or f in self.state: + continue + logging.warn('Unknown file %s from cache' % f) + # Insert as the oldest file. It will be deleted eventually if not + # accessed. + self.state.insert(0, f) + + # Ensure enough free space. + while ( + self.min_free_space and + self.state and + get_free_space(self.cache_dir) < self.min_free_space): + os.remove(self.path(self.state.pop(0))) + + # Ensure maximum cache size. + if self.max_cache_size and self.state: + sizes = [os.stat(self.path(f)).st_size for f in self.state] + while sizes and sum(sizes) > self.max_cache_size: + # Delete the oldest item. + os.remove(self.path(self.state.pop(0))) + sizes.pop(0) + + self.save() + + def retrieve(self, item): + """Retrieves a file from the remote and add it to the cache.""" + assert not '/' in item + try: + index = self.state.index(item) + # Was already in cache. Update it's LRU value. + self.state.pop(index) + self.state.append(item) + return False + except ValueError: + out = self.path(item) + download_or_copy(os.path.join(self.remote, item), out) + self.state.append(item) + return True + finally: + self.save() + + 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')) + + +def run_tha_test(manifest, cache_dir, remote, max_cache_size, min_free_space): + """Downloads the dependencies in the cache, hardlinks them into a temporary + directory and runs the executable. + """ + cache = Cache(cache_dir, remote, max_cache_size, min_free_space) + outdir = tempfile.mkdtemp(prefix='run_tha_test') + try: + for filepath, properties in manifest['files'].iteritems(): + infile = properties['sha-1'] + outfile = os.path.join(outdir, filepath) + cache.retrieve(infile) + outfiledir = os.path.dirname(outfile) + if not os.path.isdir(outfiledir): + os.makedirs(outfiledir) + link_file(outfile, cache.path(infile), HARDLINK) + os.chmod(outfile, properties['mode']) + + cwd = os.path.join(outdir, manifest['relative_cwd']) + if not os.path.isdir(cwd): + os.makedirs(cwd) + if manifest.get('read_only'): + make_writable(outdir, True) + cmd = manifest['command'] + logging.info('Running %s, cwd=%s' % (cmd, cwd)) + return subprocess.call(cmd, cwd=cwd) + finally: + # Save first, in case an exception occur in the following lines, then clean + # up. + cache.save() + rmtree(outdir) + cache.trim() + + +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( + '-m', '--manifest', + metavar='FILE', + help='File/url describing what to map or run') + parser.add_option('--no-run', action='store_true', help='Skip the run part') + parser.add_option( + '--cache', + default='cache', + metavar='DIR', + help='Cache directory, default=%default') + parser.add_option( + '-r', '--remote', metavar='URL', help='Remote where to get the items') + parser.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') + parser.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') + + 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 not options.manifest: + parser.error('--manifest is required.') + if not options.remote: + parser.error('--remote is required.') + if args: + parser.error('Unsupported args %s' % ' '.join(args)) + + manifest = json.load(open_remote(options.manifest)) + return run_tha_test( + manifest, os.path.abspath(options.cache), options.remote, + options.max_cache_size, options.min_free_space) + + +if __name__ == '__main__': + sys.exit(main()) |