blob: 9fe350d0a51bccebdb0626eff15d1e03d58ad5e3 [file] [log] [blame]
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001#!/usr/bin/env python
2# Copyright (c) 2012 The Chromium Authors. All rights reserved.
3# Use of this source code is governed by a BSD-style license that can be
4# found in the LICENSE file.
5
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00006"""Reads a .isolated, creates a tree of hardlinks and runs the test.
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00007
8Keeps a local cache.
9"""
10
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +000011import ctypes
12import hashlib
13import json
14import logging
csharp@chromium.orgff2a4662012-11-21 20:49:32 +000015import logging.handlers
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +000016import optparse
17import os
18import Queue
19import re
20import shutil
21import stat
22import subprocess
23import sys
24import tempfile
25import threading
26import time
27import urllib
csharp@chromium.orga92403f2012-11-20 15:13:59 +000028import urllib2
29import zlib
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +000030
31
maruel@chromium.org6b365dc2012-10-18 19:17:56 +000032# Types of action accepted by link_file().
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +000033HARDLINK, SYMLINK, COPY = range(1, 4)
34
35RE_IS_SHA1 = re.compile(r'^[a-fA-F0-9]{40}$')
36
csharp@chromium.org8dc52542012-11-08 20:29:55 +000037# The file size to be used when we don't know the correct file size,
38# generally used for .isolated files.
39UNKNOWN_FILE_SIZE = None
40
csharp@chromium.orga92403f2012-11-20 15:13:59 +000041# The size of each chunk to read when downloading and unzipping files.
42ZIPPED_FILE_CHUNK = 16 * 1024
43
csharp@chromium.orgff2a4662012-11-21 20:49:32 +000044# The name of the log file to use.
45RUN_ISOLATED_LOG_FILE = 'run_isolated.log'
46
csharp@chromium.orge217f302012-11-22 16:51:53 +000047# The base directory containing this file.
48BASE_DIR = os.path.dirname(os.path.abspath(__file__))
49
50# The name of the log to use for the run_test_cases.py command
51RUN_TEST_CASES_LOG = os.path.join(BASE_DIR, 'run_test_cases.log')
52
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +000053
54class ConfigError(ValueError):
maruel@chromium.org0cd0b182012-10-22 13:34:15 +000055 """Generic failure to load a .isolated file."""
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +000056 pass
57
58
59class MappingError(OSError):
60 """Failed to recreate the tree."""
61 pass
62
63
csharp@chromium.orga92403f2012-11-20 15:13:59 +000064class DownloadFileOpener(urllib.FancyURLopener):
65 """This class is needed to get urlretrive to raise an exception on
66 404 errors, instead of still writing to the file with the error code.
67 """
68 def http_error_default(self, url, fp, errcode, errmsg, headers):
69 raise urllib2.HTTPError(url, errcode, errmsg, headers, fp)
70
71
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +000072def get_flavor():
73 """Returns the system default flavor. Copied from gyp/pylib/gyp/common.py."""
74 flavors = {
75 'cygwin': 'win',
76 'win32': 'win',
77 'darwin': 'mac',
78 'sunos5': 'solaris',
79 'freebsd7': 'freebsd',
80 'freebsd8': 'freebsd',
81 }
82 return flavors.get(sys.platform, 'linux')
83
84
85def os_link(source, link_name):
86 """Add support for os.link() on Windows."""
87 if sys.platform == 'win32':
88 if not ctypes.windll.kernel32.CreateHardLinkW(
89 unicode(link_name), unicode(source), 0):
90 raise OSError()
91 else:
92 os.link(source, link_name)
93
94
95def readable_copy(outfile, infile):
96 """Makes a copy of the file that is readable by everyone."""
97 shutil.copy(infile, outfile)
98 read_enabled_mode = (os.stat(outfile).st_mode | stat.S_IRUSR |
99 stat.S_IRGRP | stat.S_IROTH)
100 os.chmod(outfile, read_enabled_mode)
101
102
103def link_file(outfile, infile, action):
104 """Links a file. The type of link depends on |action|."""
105 logging.debug('Mapping %s to %s' % (infile, outfile))
106 if action not in (HARDLINK, SYMLINK, COPY):
107 raise ValueError('Unknown mapping action %s' % action)
108 if not os.path.isfile(infile):
109 raise MappingError('%s is missing' % infile)
110 if os.path.isfile(outfile):
111 raise MappingError(
112 '%s already exist; insize:%d; outsize:%d' %
113 (outfile, os.stat(infile).st_size, os.stat(outfile).st_size))
114
115 if action == COPY:
116 readable_copy(outfile, infile)
117 elif action == SYMLINK and sys.platform != 'win32':
118 # On windows, symlink are converted to hardlink and fails over to copy.
maruel@chromium.orgf43e68b2012-10-15 20:23:10 +0000119 os.symlink(infile, outfile) # pylint: disable=E1101
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000120 else:
121 try:
122 os_link(infile, outfile)
123 except OSError:
124 # Probably a different file system.
125 logging.warn(
126 'Failed to hardlink, failing back to copy %s to %s' % (
127 infile, outfile))
128 readable_copy(outfile, infile)
129
130
131def _set_write_bit(path, read_only):
132 """Sets or resets the executable bit on a file or directory."""
133 mode = os.lstat(path).st_mode
134 if read_only:
135 mode = mode & 0500
136 else:
137 mode = mode | 0200
138 if hasattr(os, 'lchmod'):
139 os.lchmod(path, mode) # pylint: disable=E1101
140 else:
141 if stat.S_ISLNK(mode):
142 # Skip symlink without lchmod() support.
143 logging.debug('Can\'t change +w bit on symlink %s' % path)
144 return
145
146 # TODO(maruel): Implement proper DACL modification on Windows.
147 os.chmod(path, mode)
148
149
150def make_writable(root, read_only):
151 """Toggle the writable bit on a directory tree."""
152 root = os.path.abspath(root)
153 for dirpath, dirnames, filenames in os.walk(root, topdown=True):
154 for filename in filenames:
155 _set_write_bit(os.path.join(dirpath, filename), read_only)
156
157 for dirname in dirnames:
158 _set_write_bit(os.path.join(dirpath, dirname), read_only)
159
160
161def rmtree(root):
162 """Wrapper around shutil.rmtree() to retry automatically on Windows."""
163 make_writable(root, False)
164 if sys.platform == 'win32':
165 for i in range(3):
166 try:
167 shutil.rmtree(root)
168 break
169 except WindowsError: # pylint: disable=E0602
170 delay = (i+1)*2
171 print >> sys.stderr, (
172 'The test has subprocess outliving it. Sleep %d seconds.' % delay)
173 time.sleep(delay)
174 else:
175 shutil.rmtree(root)
176
177
178def is_same_filesystem(path1, path2):
179 """Returns True if both paths are on the same filesystem.
180
181 This is required to enable the use of hardlinks.
182 """
183 assert os.path.isabs(path1), path1
184 assert os.path.isabs(path2), path2
185 if sys.platform == 'win32':
186 # If the drive letter mismatches, assume it's a separate partition.
187 # TODO(maruel): It should look at the underlying drive, a drive letter could
188 # be a mount point to a directory on another drive.
189 assert re.match(r'^[a-zA-Z]\:\\.*', path1), path1
190 assert re.match(r'^[a-zA-Z]\:\\.*', path2), path2
191 if path1[0].lower() != path2[0].lower():
192 return False
193 return os.stat(path1).st_dev == os.stat(path2).st_dev
194
195
196def get_free_space(path):
197 """Returns the number of free bytes."""
198 if sys.platform == 'win32':
199 free_bytes = ctypes.c_ulonglong(0)
200 ctypes.windll.kernel32.GetDiskFreeSpaceExW(
201 ctypes.c_wchar_p(path), None, None, ctypes.pointer(free_bytes))
202 return free_bytes.value
maruel@chromium.orgf43e68b2012-10-15 20:23:10 +0000203 # For OSes other than Windows.
204 f = os.statvfs(path) # pylint: disable=E1101
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000205 return f.f_bfree * f.f_frsize
206
207
208def make_temp_dir(prefix, root_dir):
209 """Returns a temporary directory on the same file system as root_dir."""
210 base_temp_dir = None
211 if not is_same_filesystem(root_dir, tempfile.gettempdir()):
212 base_temp_dir = os.path.dirname(root_dir)
213 return tempfile.mkdtemp(prefix=prefix, dir=base_temp_dir)
214
215
maruel@chromium.org0cd0b182012-10-22 13:34:15 +0000216def load_isolated(content):
217 """Verifies the .isolated file is valid and loads this object with the json
218 data.
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000219 """
220 try:
221 data = json.loads(content)
222 except ValueError:
223 raise ConfigError('Failed to parse: %s...' % content[:100])
224
225 if not isinstance(data, dict):
226 raise ConfigError('Expected dict, got %r' % data)
227
228 for key, value in data.iteritems():
229 if key == 'command':
230 if not isinstance(value, list):
231 raise ConfigError('Expected list, got %r' % value)
232 for subvalue in value:
233 if not isinstance(subvalue, basestring):
234 raise ConfigError('Expected string, got %r' % subvalue)
235
236 elif key == 'files':
237 if not isinstance(value, dict):
238 raise ConfigError('Expected dict, got %r' % value)
239 for subkey, subvalue in value.iteritems():
240 if not isinstance(subkey, basestring):
241 raise ConfigError('Expected string, got %r' % subkey)
242 if not isinstance(subvalue, dict):
243 raise ConfigError('Expected dict, got %r' % subvalue)
244 for subsubkey, subsubvalue in subvalue.iteritems():
maruel@chromium.orge5c17132012-11-21 18:18:46 +0000245 if subsubkey == 'l':
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000246 if not isinstance(subsubvalue, basestring):
247 raise ConfigError('Expected string, got %r' % subsubvalue)
maruel@chromium.orge5c17132012-11-21 18:18:46 +0000248 elif subsubkey == 'm':
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000249 if not isinstance(subsubvalue, int):
250 raise ConfigError('Expected int, got %r' % subsubvalue)
maruel@chromium.orge5c17132012-11-21 18:18:46 +0000251 elif subsubkey == 'h':
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000252 if not RE_IS_SHA1.match(subsubvalue):
253 raise ConfigError('Expected sha-1, got %r' % subsubvalue)
maruel@chromium.orge5c17132012-11-21 18:18:46 +0000254 elif subsubkey == 's':
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000255 if not isinstance(subsubvalue, int):
256 raise ConfigError('Expected int, got %r' % subsubvalue)
maruel@chromium.orge5c17132012-11-21 18:18:46 +0000257 elif subsubkey == 't':
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000258 if not isinstance(subsubvalue, int):
259 raise ConfigError('Expected int, got %r' % subsubvalue)
maruel@chromium.orge5c17132012-11-21 18:18:46 +0000260 elif subsubkey == 'T':
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000261 if not isinstance(subsubvalue, bool):
262 raise ConfigError('Expected bool, got %r' % subsubvalue)
263 else:
264 raise ConfigError('Unknown subsubkey %s' % subsubkey)
maruel@chromium.orge5c17132012-11-21 18:18:46 +0000265 if bool('h' in subvalue) and bool('l' in subvalue):
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000266 raise ConfigError(
maruel@chromium.orge5c17132012-11-21 18:18:46 +0000267 'Did not expect both \'h\' (sha-1) and \'l\' (link), got: %r' %
268 subvalue)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000269
270 elif key == 'includes':
271 if not isinstance(value, list):
272 raise ConfigError('Expected list, got %r' % value)
273 for subvalue in value:
274 if not RE_IS_SHA1.match(subvalue):
275 raise ConfigError('Expected sha-1, got %r' % subvalue)
276
277 elif key == 'read_only':
278 if not isinstance(value, bool):
279 raise ConfigError('Expected bool, got %r' % value)
280
281 elif key == 'relative_cwd':
282 if not isinstance(value, basestring):
283 raise ConfigError('Expected string, got %r' % value)
284
285 elif key == 'os':
286 if value != get_flavor():
287 raise ConfigError(
288 'Expected \'os\' to be \'%s\' but got \'%s\'' %
289 (get_flavor(), value))
290
291 else:
292 raise ConfigError('Unknown key %s' % key)
293
294 return data
295
296
297def fix_python_path(cmd):
298 """Returns the fixed command line to call the right python executable."""
299 out = cmd[:]
300 if out[0] == 'python':
301 out[0] = sys.executable
302 elif out[0].endswith('.py'):
303 out.insert(0, sys.executable)
304 return out
305
306
maruel@chromium.org8df128b2012-11-08 19:05:04 +0000307class WorkerThread(threading.Thread):
308 """Keeps the results of each task in a thread-local outputs variable."""
309 def __init__(self, tasks, *args, **kwargs):
310 super(WorkerThread, self).__init__(*args, **kwargs)
311 self._tasks = tasks
312 self.outputs = []
313 self.exceptions = []
314
315 self.daemon = True
316 self.start()
317
318 def run(self):
319 """Runs until a None task is queued."""
320 while True:
321 task = self._tasks.get()
322 if task is None:
maruel@chromium.org8df128b2012-11-08 19:05:04 +0000323 # We're done.
324 return
325 try:
326 func, args, kwargs = task
maruel@chromium.org8df128b2012-11-08 19:05:04 +0000327 self.outputs.append(func(*args, **kwargs))
328 except Exception, e:
329 logging.error('Caught exception! %s' % e)
330 self.exceptions.append(sys.exc_info())
331 finally:
332 self._tasks.task_done()
333
334
335class ThreadPool(object):
336 """Implements a multithreaded worker pool oriented for mapping jobs with
337 thread-local result storage.
338 """
339 QUEUE_CLASS = Queue.Queue
340
maruel@chromium.orgeb281652012-11-08 21:10:23 +0000341 def __init__(self, num_threads, queue_size=0):
maruel@chromium.org8df128b2012-11-08 19:05:04 +0000342 logging.debug('Creating ThreadPool')
maruel@chromium.orgeb281652012-11-08 21:10:23 +0000343 self.tasks = self.QUEUE_CLASS(queue_size)
maruel@chromium.org8df128b2012-11-08 19:05:04 +0000344 self._workers = [
maruel@chromium.orgeb281652012-11-08 21:10:23 +0000345 WorkerThread(self.tasks, name='worker-%d' % i)
maruel@chromium.org8df128b2012-11-08 19:05:04 +0000346 for i in range(num_threads)
347 ]
348
349 def add_task(self, func, *args, **kwargs):
350 """Adds a task, a function to be executed by a worker.
351
352 The function's return value will be stored in the the worker's thread local
353 outputs list.
354 """
maruel@chromium.orgeb281652012-11-08 21:10:23 +0000355 self.tasks.put((func, args, kwargs))
maruel@chromium.org8df128b2012-11-08 19:05:04 +0000356
maruel@chromium.orgeb281652012-11-08 21:10:23 +0000357 def join(self):
maruel@chromium.org8df128b2012-11-08 19:05:04 +0000358 """Extracts all the results from each threads unordered."""
maruel@chromium.orgeb281652012-11-08 21:10:23 +0000359 self.tasks.join()
maruel@chromium.org8df128b2012-11-08 19:05:04 +0000360 out = []
361 # Look for exceptions.
362 for w in self._workers:
363 if w.exceptions:
364 raise w.exceptions[0][0], w.exceptions[0][1], w.exceptions[0][2]
365 out.extend(w.outputs)
366 w.outputs = []
367 return out
368
369 def close(self):
370 """Closes all the threads."""
371 for _ in range(len(self._workers)):
372 # Enqueueing None causes the worker to stop.
maruel@chromium.orgeb281652012-11-08 21:10:23 +0000373 self.tasks.put(None)
maruel@chromium.org8df128b2012-11-08 19:05:04 +0000374 for t in self._workers:
375 t.join()
376
377 def __enter__(self):
378 """Enables 'with' statement."""
379 return self
380
381 def __exit__(self, exc_type, exc_value, traceback):
382 """Enables 'with' statement."""
383 self.close()
384
385
csharp@chromium.org8dc52542012-11-08 20:29:55 +0000386def valid_file(filepath, size):
387 """Determines if the given files appears valid (currently it just checks
388 the file's size)."""
389 return (size == UNKNOWN_FILE_SIZE or size == os.stat(filepath).st_size)
390
391
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000392class Profiler(object):
393 def __init__(self, name):
394 self.name = name
395 self.start_time = None
396
397 def __enter__(self):
398 self.start_time = time.time()
399 return self
400
401 def __exit__(self, _exc_type, _exec_value, _traceback):
402 time_taken = time.time() - self.start_time
403 logging.info('Profiling: Section %s took %3.3f seconds',
404 self.name, time_taken)
405
406
407class Remote(object):
maruel@chromium.orgfb155e92012-09-28 20:36:54 +0000408 """Priority based worker queue to fetch or upload files from a
409 content-address server. Any function may be given as the fetcher/upload,
410 as long as it takes two inputs (the item contents, and their relative
411 destination).
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000412
413 Supports local file system, CIFS or http remotes.
414
415 When the priority of items is equals, works in strict FIFO mode.
416 """
417 # Initial and maximum number of worker threads.
418 INITIAL_WORKERS = 2
419 MAX_WORKERS = 16
420 # Priorities.
421 LOW, MED, HIGH = (1<<8, 2<<8, 3<<8)
422 INTERNAL_PRIORITY_BITS = (1<<8) - 1
423 RETRIES = 5
424
maruel@chromium.orgfb155e92012-09-28 20:36:54 +0000425 def __init__(self, destination_root):
426 # Function to fetch a remote object or upload to a remote location..
427 self._do_item = self.get_file_handler(destination_root)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000428 # Contains tuple(priority, index, obj, destination).
429 self._queue = Queue.PriorityQueue()
430 # Contains tuple(priority, index, obj).
431 self._done = Queue.PriorityQueue()
432
maruel@chromium.orgfb155e92012-09-28 20:36:54 +0000433 # Contains generated exceptions that haven't been handled yet.
434 self._exceptions = Queue.Queue()
435
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000436 # To keep FIFO ordering in self._queue. It is assumed xrange's iterator is
437 # thread-safe.
438 self._next_index = xrange(0, 1<<30).__iter__().next
439
440 # Control access to the following member.
441 self._ready_lock = threading.Lock()
442 # Number of threads in wait state.
443 self._ready = 0
444
445 # Control access to the following member.
446 self._workers_lock = threading.Lock()
447 self._workers = []
448 for _ in range(self.INITIAL_WORKERS):
449 self._add_worker()
450
maruel@chromium.orgfb155e92012-09-28 20:36:54 +0000451 def join(self):
452 """Blocks until the queue is empty."""
453 self._queue.join()
454
455 def next_exception(self):
456 """Returns the next unhandled exception, or None if there is
457 no exception."""
458 try:
459 return self._exceptions.get_nowait()
460 except Queue.Empty:
461 return None
462
csharp@chromium.orgdf2968f2012-11-16 20:25:37 +0000463 def add_item(self, priority, obj, dest, size):
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000464 """Retrieves an object from the remote data store.
465
466 The smaller |priority| gets fetched first.
467
468 Thread-safe.
469 """
470 assert (priority & self.INTERNAL_PRIORITY_BITS) == 0
csharp@chromium.orgdf2968f2012-11-16 20:25:37 +0000471 self._add_to_queue(priority, obj, dest, size)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000472
473 def get_result(self):
474 """Returns the next file that was successfully fetched."""
475 r = self._done.get()
maruel@chromium.orgfb155e92012-09-28 20:36:54 +0000476 if r[0] == -1:
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000477 # It's an exception.
478 raise r[2][0], r[2][1], r[2][2]
479 return r[2]
480
csharp@chromium.orgdf2968f2012-11-16 20:25:37 +0000481 def _add_to_queue(self, priority, obj, dest, size):
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000482 with self._ready_lock:
483 start_new_worker = not self._ready
csharp@chromium.orgdf2968f2012-11-16 20:25:37 +0000484 self._queue.put((priority, self._next_index(), obj, dest, size))
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000485 if start_new_worker:
486 self._add_worker()
487
488 def _add_worker(self):
489 """Add one worker thread if there isn't too many. Thread-safe."""
490 with self._workers_lock:
491 if len(self._workers) >= self.MAX_WORKERS:
492 return False
493 worker = threading.Thread(target=self._run)
494 self._workers.append(worker)
495 worker.daemon = True
496 worker.start()
497
maruel@chromium.orgfb155e92012-09-28 20:36:54 +0000498 def _step_done(self, result):
499 """Worker helper function"""
500 self._done.put(result)
501 self._queue.task_done()
502 if result[0] == -1:
503 self._exceptions.put(sys.exc_info())
504
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000505 def _run(self):
506 """Worker thread loop."""
507 while True:
508 try:
509 with self._ready_lock:
510 self._ready += 1
511 item = self._queue.get()
512 finally:
513 with self._ready_lock:
514 self._ready -= 1
515 if not item:
516 return
csharp@chromium.orgdf2968f2012-11-16 20:25:37 +0000517 priority, index, obj, dest, size = item
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000518 try:
519 self._do_item(obj, dest)
csharp@chromium.orgdf2968f2012-11-16 20:25:37 +0000520 if size and not valid_file(dest, size):
521 download_size = os.stat(dest).st_size
522 os.remove(dest)
523 raise IOError('File incorrect size after download of %s. Got %s and '
maruel@chromium.org3f039182012-11-27 21:32:41 +0000524 'expected %s' % (obj, download_size, size))
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000525 except IOError:
526 # Retry a few times, lowering the priority.
527 if (priority & self.INTERNAL_PRIORITY_BITS) < self.RETRIES:
csharp@chromium.orgdf2968f2012-11-16 20:25:37 +0000528 self._add_to_queue(priority + 1, obj, dest, size)
maruel@chromium.orgfb155e92012-09-28 20:36:54 +0000529 self._queue.task_done()
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000530 continue
531 # Transfers the exception back. It has maximum priority.
maruel@chromium.orgfb155e92012-09-28 20:36:54 +0000532 self._step_done((-1, 0, sys.exc_info()))
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000533 except:
534 # Transfers the exception back. It has maximum priority.
maruel@chromium.orgfb155e92012-09-28 20:36:54 +0000535 self._step_done((-1, 0, sys.exc_info()))
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000536 else:
maruel@chromium.orgfb155e92012-09-28 20:36:54 +0000537 self._step_done((priority, index, obj))
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000538
csharp@chromium.org59c7bcf2012-11-21 21:13:18 +0000539 def get_file_handler(self, file_or_url): # pylint: disable=R0201
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000540 """Returns a object to retrieve objects from a remote."""
541 if re.match(r'^https?://.+$', file_or_url):
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000542 def download_file(item, dest):
543 # TODO(maruel): Reuse HTTP connections. The stdlib doesn't make this
544 # easy.
csharp@chromium.orga92403f2012-11-20 15:13:59 +0000545 try:
546 zipped_source = file_or_url.rstrip('/') + '-gzip/' + item
547 logging.debug('download_file(%s)', zipped_source)
548 connection = urllib2.urlopen(zipped_source)
549 decompressor = zlib.decompressobj()
maruel@chromium.org3f039182012-11-27 21:32:41 +0000550 size = 0
csharp@chromium.orga92403f2012-11-20 15:13:59 +0000551 with open(dest, 'wb') as f:
552 while True:
553 chunk = connection.read(ZIPPED_FILE_CHUNK)
554 if not chunk:
555 break
maruel@chromium.org3f039182012-11-27 21:32:41 +0000556 size += len(chunk)
csharp@chromium.orga92403f2012-11-20 15:13:59 +0000557 f.write(decompressor.decompress(chunk))
558 # Ensure that all the data was properly decompressed.
559 uncompressed_data = decompressor.flush()
560 assert not uncompressed_data
csharp@chromium.org186d6232012-11-26 14:36:12 +0000561 except zlib.error as e:
maruel@chromium.org5dd75dd2012-12-03 15:11:32 +0000562 # Log the first bytes to see if it's uncompressed data.
563 logging.warning('%r', e[:512])
maruel@chromium.org3f039182012-11-27 21:32:41 +0000564 raise IOError(
565 'Problem unzipping data for item %s. Got %d bytes.\n%s' %
566 (item, size, e))
csharp@chromium.orga92403f2012-11-20 15:13:59 +0000567
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000568 return download_file
569
570 def copy_file(item, dest):
571 source = os.path.join(file_or_url, item)
572 logging.debug('copy_file(%s, %s)', source, dest)
573 shutil.copy(source, dest)
574 return copy_file
575
576
577class CachePolicies(object):
578 def __init__(self, max_cache_size, min_free_space, max_items):
579 """
580 Arguments:
581 - max_cache_size: Trim if the cache gets larger than this value. If 0, the
582 cache is effectively a leak.
583 - min_free_space: Trim if disk free space becomes lower than this value. If
584 0, it unconditionally fill the disk.
585 - max_items: Maximum number of items to keep in the cache. If 0, do not
586 enforce a limit.
587 """
588 self.max_cache_size = max_cache_size
589 self.min_free_space = min_free_space
590 self.max_items = max_items
591
592
593class Cache(object):
594 """Stateful LRU cache.
595
596 Saves its state as json file.
597 """
598 STATE_FILE = 'state.json'
599
600 def __init__(self, cache_dir, remote, policies):
601 """
602 Arguments:
603 - cache_dir: Directory where to place the cache.
604 - remote: Remote where to fetch items from.
605 - policies: cache retention policies.
606 """
607 self.cache_dir = cache_dir
608 self.remote = remote
609 self.policies = policies
610 self.state_file = os.path.join(cache_dir, self.STATE_FILE)
611 # The tuple(file, size) are kept as an array in a LRU style. E.g.
612 # self.state[0] is the oldest item.
613 self.state = []
614 # A lookup map to speed up searching.
615 self._lookup = {}
616 self._dirty = False
617
618 # Items currently being fetched. Keep it local to reduce lock contention.
619 self._pending_queue = set()
620
621 # Profiling values.
622 self._added = []
623 self._removed = []
624 self._free_disk = 0
625
626 if not os.path.isdir(self.cache_dir):
627 os.makedirs(self.cache_dir)
628 if os.path.isfile(self.state_file):
629 try:
630 self.state = json.load(open(self.state_file, 'r'))
631 except (IOError, ValueError), e:
632 # Too bad. The file will be overwritten and the cache cleared.
633 logging.error(
634 'Broken state file %s, ignoring.\n%s' % (self.STATE_FILE, e))
635 if (not isinstance(self.state, list) or
636 not all(
637 isinstance(i, (list, tuple)) and len(i) == 2 for i in self.state)):
638 # Discard.
639 self.state = []
640 self._dirty = True
641
642 # Ensure that all files listed in the state still exist and add new ones.
643 previous = set(filename for filename, _ in self.state)
644 if len(previous) != len(self.state):
645 logging.warn('Cache state is corrupted')
646 self._dirty = True
647 self.state = []
648 else:
649 added = 0
650 for filename in os.listdir(self.cache_dir):
651 if filename == self.STATE_FILE:
652 continue
653 if filename in previous:
654 previous.remove(filename)
655 continue
656 # An untracked file.
657 self._dirty = True
658 if not RE_IS_SHA1.match(filename):
659 logging.warn('Removing unknown file %s from cache', filename)
660 os.remove(self.path(filename))
661 else:
662 # Insert as the oldest file. It will be deleted eventually if not
663 # accessed.
664 self._add(filename, False)
csharp@chromium.orge217f302012-11-22 16:51:53 +0000665 logging.warn('Add unknown file %s to cache', filename)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000666 added += 1
667 if added:
668 logging.warn('Added back %d unknown files', added)
669 self.state = [
670 (filename, size) for filename, size in self.state
671 if filename not in previous
672 ]
673 self._update_lookup()
674
675 with Profiler('SetupTrimming'):
676 self.trim()
677
678 def __enter__(self):
679 return self
680
681 def __exit__(self, _exc_type, _exec_value, _traceback):
682 with Profiler('CleanupTrimming'):
683 self.trim()
684
685 logging.info(
686 '%4d (%7dkb) added', len(self._added), sum(self._added) / 1024)
687 logging.info(
688 '%4d (%7dkb) current',
689 len(self.state),
690 sum(i[1] for i in self.state) / 1024)
691 logging.info(
692 '%4d (%7dkb) removed', len(self._removed), sum(self._removed) / 1024)
693 logging.info('%7dkb free', self._free_disk / 1024)
694
csharp@chromium.org8dc52542012-11-08 20:29:55 +0000695 def remove_file_at_index(self, index):
696 """Removes the file at the given index."""
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000697 try:
csharp@chromium.org8dc52542012-11-08 20:29:55 +0000698 filename, size = self.state.pop(index)
699 # TODO(csharp): _lookup should self-update.
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000700 del self._lookup[filename]
701 self._removed.append(size)
702 os.remove(self.path(filename))
703 self._dirty = True
704 except OSError as e:
705 logging.error('Error attempting to delete a file\n%s' % e)
706
csharp@chromium.org8dc52542012-11-08 20:29:55 +0000707 def remove_lru_file(self):
708 """Removes the last recently used file."""
709 self.remove_file_at_index(0)
710
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000711 def trim(self):
712 """Trims anything we don't know, make sure enough free space exists."""
713 # Ensure maximum cache size.
714 if self.policies.max_cache_size and self.state:
715 while sum(i[1] for i in self.state) > self.policies.max_cache_size:
716 self.remove_lru_file()
717
718 # Ensure maximum number of items in the cache.
719 if self.policies.max_items and self.state:
720 while len(self.state) > self.policies.max_items:
721 self.remove_lru_file()
722
723 # Ensure enough free space.
724 self._free_disk = get_free_space(self.cache_dir)
725 while (
726 self.policies.min_free_space and
727 self.state and
728 self._free_disk < self.policies.min_free_space):
729 self.remove_lru_file()
730 self._free_disk = get_free_space(self.cache_dir)
731
732 self.save()
733
csharp@chromium.org8dc52542012-11-08 20:29:55 +0000734 def retrieve(self, priority, item, size):
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000735 """Retrieves a file from the remote, if not already cached, and adds it to
736 the cache.
csharp@chromium.org8dc52542012-11-08 20:29:55 +0000737
738 If the file is in the cache, verifiy that the file is valid (i.e. it is
739 the correct size), retrieving it again if it isn't.
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000740 """
741 assert not '/' in item
742 path = self.path(item)
743 index = self._lookup.get(item)
csharp@chromium.org8dc52542012-11-08 20:29:55 +0000744
745 if index is not None:
746 if not valid_file(self.path(item), size):
747 self.remove_file_at_index(index)
748 self._update_lookup()
749 index = None
750 else:
751 assert index < len(self.state)
752 # Was already in cache. Update it's LRU value by putting it at the end.
753 self.state.append(self.state.pop(index))
754 self._dirty = True
755 self._update_lookup()
756
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000757 if index is None:
758 if item in self._pending_queue:
759 # Already pending. The same object could be referenced multiple times.
760 return
csharp@chromium.orgdf2968f2012-11-16 20:25:37 +0000761 self.remote.add_item(priority, item, path, size)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000762 self._pending_queue.add(item)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000763
764 def add(self, filepath, obj):
765 """Forcibly adds a file to the cache."""
766 if not obj in self._lookup:
767 link_file(self.path(obj), filepath, HARDLINK)
768 self._add(obj, True)
769
770 def path(self, item):
771 """Returns the path to one item."""
772 return os.path.join(self.cache_dir, item)
773
774 def save(self):
775 """Saves the LRU ordering."""
776 json.dump(self.state, open(self.state_file, 'wb'), separators=(',',':'))
777
778 def wait_for(self, items):
779 """Starts a loop that waits for at least one of |items| to be retrieved.
780
781 Returns the first item retrieved.
782 """
783 # Flush items already present.
784 for item in items:
785 if item in self._lookup:
786 return item
787
788 assert all(i in self._pending_queue for i in items), (
789 items, self._pending_queue)
790 # Note that:
791 # len(self._pending_queue) ==
792 # ( len(self.remote._workers) - self.remote._ready +
793 # len(self._remote._queue) + len(self._remote.done))
794 # There is no lock-free way to verify that.
795 while self._pending_queue:
796 item = self.remote.get_result()
797 self._pending_queue.remove(item)
798 self._add(item, True)
799 if item in items:
800 return item
801
802 def _add(self, item, at_end):
803 """Adds an item in the internal state.
804
805 If |at_end| is False, self._lookup becomes inconsistent and
806 self._update_lookup() must be called.
807 """
808 size = os.stat(self.path(item)).st_size
809 self._added.append(size)
810 if at_end:
811 self.state.append((item, size))
812 self._lookup[item] = len(self.state) - 1
813 else:
814 self.state.insert(0, (item, size))
815 self._dirty = True
816
817 def _update_lookup(self):
818 self._lookup = dict(
819 (filename, index) for index, (filename, _) in enumerate(self.state))
820
821
822
maruel@chromium.org0cd0b182012-10-22 13:34:15 +0000823class IsolatedFile(object):
824 """Represents a single parsed .isolated file."""
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000825 def __init__(self, obj_hash):
826 """|obj_hash| is really the sha-1 of the file."""
maruel@chromium.org0cd0b182012-10-22 13:34:15 +0000827 logging.debug('IsolatedFile(%s)' % obj_hash)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000828 self.obj_hash = obj_hash
829 # Set once all the left-side of the tree is parsed. 'Tree' here means the
maruel@chromium.org0cd0b182012-10-22 13:34:15 +0000830 # .isolate and all the .isolated files recursively included by it with
831 # 'includes' key. The order of each sha-1 in 'includes', each representing a
832 # .isolated file in the hash table, is important, as the later ones are not
833 # processed until the firsts are retrieved and read.
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000834 self.can_fetch = False
835
836 # Raw data.
837 self.data = {}
maruel@chromium.org0cd0b182012-10-22 13:34:15 +0000838 # A IsolatedFile instance, one per object in self.includes.
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000839 self.children = []
840
maruel@chromium.org0cd0b182012-10-22 13:34:15 +0000841 # Set once the .isolated file is loaded.
842 self._is_parsed = False
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000843 # Set once the files are fetched.
844 self.files_fetched = False
845
846 def load(self, content):
maruel@chromium.org0cd0b182012-10-22 13:34:15 +0000847 """Verifies the .isolated file is valid and loads this object with the json
848 data.
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000849 """
maruel@chromium.org0cd0b182012-10-22 13:34:15 +0000850 logging.debug('IsolatedFile.load(%s)' % self.obj_hash)
851 assert not self._is_parsed
852 self.data = load_isolated(content)
853 self.children = [IsolatedFile(i) for i in self.data.get('includes', [])]
854 self._is_parsed = True
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000855
856 def fetch_files(self, cache, files):
maruel@chromium.org0cd0b182012-10-22 13:34:15 +0000857 """Adds files in this .isolated file not present in |files| dictionary.
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000858
859 Preemptively request files.
860
861 Note that |files| is modified by this function.
862 """
863 assert self.can_fetch
maruel@chromium.org0cd0b182012-10-22 13:34:15 +0000864 if not self._is_parsed or self.files_fetched:
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000865 return
866 logging.debug('fetch_files(%s)' % self.obj_hash)
867 for filepath, properties in self.data.get('files', {}).iteritems():
maruel@chromium.org0cd0b182012-10-22 13:34:15 +0000868 # Root isolated has priority on the files being mapped. In particular,
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000869 # overriden files must not be fetched.
870 if filepath not in files:
871 files[filepath] = properties
maruel@chromium.orge5c17132012-11-21 18:18:46 +0000872 if 'h' in properties:
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000873 # Preemptively request files.
874 logging.debug('fetching %s' % filepath)
maruel@chromium.orge5c17132012-11-21 18:18:46 +0000875 cache.retrieve(Remote.MED, properties['h'], properties['s'])
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000876 self.files_fetched = True
877
878
879class Settings(object):
maruel@chromium.org0cd0b182012-10-22 13:34:15 +0000880 """Results of a completely parsed .isolated file."""
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000881 def __init__(self):
882 self.command = []
883 self.files = {}
884 self.read_only = None
885 self.relative_cwd = None
maruel@chromium.org0cd0b182012-10-22 13:34:15 +0000886 # The main .isolated file, a IsolatedFile instance.
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000887 self.root = None
888 logging.debug('Settings')
889
maruel@chromium.org0cd0b182012-10-22 13:34:15 +0000890 def load(self, cache, root_isolated_hash):
891 """Loads the .isolated and all the included .isolated asynchronously.
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000892
maruel@chromium.org0cd0b182012-10-22 13:34:15 +0000893 It enables support for "included" .isolated files. They are processed in
894 strict order but fetched asynchronously from the cache. This is important so
895 that a file in an included .isolated file that is overridden by an embedding
896 .isolated file is not fetched neededlessly. The includes are fetched in one
897 pass and the files are fetched as soon as all the ones on the left-side
898 of the tree were fetched.
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000899
maruel@chromium.org0cd0b182012-10-22 13:34:15 +0000900 The prioritization is very important here for nested .isolated files.
901 'includes' have the highest priority and the algorithm is optimized for both
902 deep and wide trees. A deep one is a long link of .isolated files referenced
903 one at a time by one item in 'includes'. A wide one has a large number of
904 'includes' in a single .isolated file. 'left' is defined as an included
905 .isolated file earlier in the 'includes' list. So the order of the elements
906 in 'includes' is important.
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000907 """
maruel@chromium.org0cd0b182012-10-22 13:34:15 +0000908 self.root = IsolatedFile(root_isolated_hash)
csharp@chromium.org8dc52542012-11-08 20:29:55 +0000909 cache.retrieve(Remote.HIGH, root_isolated_hash, UNKNOWN_FILE_SIZE)
maruel@chromium.org0cd0b182012-10-22 13:34:15 +0000910 pending = {root_isolated_hash: self.root}
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000911 # Keeps the list of retrieved items to refuse recursive includes.
maruel@chromium.org0cd0b182012-10-22 13:34:15 +0000912 retrieved = [root_isolated_hash]
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000913
914 def update_self(node):
915 node.fetch_files(cache, self.files)
916 # Grabs properties.
917 if not self.command and node.data.get('command'):
918 self.command = node.data['command']
919 if self.read_only is None and node.data.get('read_only') is not None:
920 self.read_only = node.data['read_only']
921 if (self.relative_cwd is None and
922 node.data.get('relative_cwd') is not None):
923 self.relative_cwd = node.data['relative_cwd']
924
925 def traverse_tree(node):
926 if node.can_fetch:
927 if not node.files_fetched:
928 update_self(node)
929 will_break = False
930 for i in node.children:
931 if not i.can_fetch:
932 if will_break:
933 break
934 # Automatically mark the first one as fetcheable.
935 i.can_fetch = True
936 will_break = True
937 traverse_tree(i)
938
939 while pending:
940 item_hash = cache.wait_for(pending)
941 item = pending.pop(item_hash)
942 item.load(open(cache.path(item_hash), 'r').read())
maruel@chromium.org0cd0b182012-10-22 13:34:15 +0000943 if item_hash == root_isolated_hash:
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000944 # It's the root item.
945 item.can_fetch = True
946
947 for new_child in item.children:
948 h = new_child.obj_hash
949 if h in retrieved:
maruel@chromium.org0cd0b182012-10-22 13:34:15 +0000950 raise ConfigError('IsolatedFile %s is retrieved recursively' % h)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000951 pending[h] = new_child
csharp@chromium.org8dc52542012-11-08 20:29:55 +0000952 cache.retrieve(Remote.HIGH, h, UNKNOWN_FILE_SIZE)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000953
954 # Traverse the whole tree to see if files can now be fetched.
955 traverse_tree(self.root)
956 def check(n):
957 return all(check(x) for x in n.children) and n.files_fetched
958 assert check(self.root)
959 self.relative_cwd = self.relative_cwd or ''
960 self.read_only = self.read_only or False
961
962
maruel@chromium.org0cd0b182012-10-22 13:34:15 +0000963def run_tha_test(isolated_hash, cache_dir, remote, policies):
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000964 """Downloads the dependencies in the cache, hardlinks them into a temporary
965 directory and runs the executable.
966 """
967 settings = Settings()
968 with Cache(cache_dir, Remote(remote), policies) as cache:
969 outdir = make_temp_dir('run_tha_test', cache_dir)
970 try:
971 # Initiate all the files download.
maruel@chromium.org0cd0b182012-10-22 13:34:15 +0000972 with Profiler('GetIsolateds') as _prof:
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000973 # Optionally support local files.
maruel@chromium.org0cd0b182012-10-22 13:34:15 +0000974 if not RE_IS_SHA1.match(isolated_hash):
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000975 # Adds it in the cache. While not strictly necessary, this simplifies
976 # the rest.
maruel@chromium.org0cd0b182012-10-22 13:34:15 +0000977 h = hashlib.sha1(open(isolated_hash, 'r').read()).hexdigest()
978 cache.add(isolated_hash, h)
979 isolated_hash = h
980 settings.load(cache, isolated_hash)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000981
982 if not settings.command:
983 print >> sys.stderr, 'No command to run'
984 return 1
985
986 with Profiler('GetRest') as _prof:
987 logging.debug('Creating directories')
988 # Creates the tree of directories to create.
989 directories = set(os.path.dirname(f) for f in settings.files)
990 for item in list(directories):
991 while item:
992 directories.add(item)
993 item = os.path.dirname(item)
994 for d in sorted(directories):
995 if d:
996 os.mkdir(os.path.join(outdir, d))
997
998 # Creates the links if necessary.
999 for filepath, properties in settings.files.iteritems():
1000 if 'link' not in properties:
1001 continue
1002 outfile = os.path.join(outdir, filepath)
maruel@chromium.orgf43e68b2012-10-15 20:23:10 +00001003 # symlink doesn't exist on Windows. So the 'link' property should
1004 # never be specified for windows .isolated file.
maruel@chromium.orge5c17132012-11-21 18:18:46 +00001005 os.symlink(properties['l'], outfile) # pylint: disable=E1101
1006 if 'm' in properties:
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001007 # It's not set on Windows.
maruel@chromium.org96768a42012-10-31 18:49:18 +00001008 lchmod = getattr(os, 'lchmod', None)
1009 if lchmod:
maruel@chromium.orge5c17132012-11-21 18:18:46 +00001010 lchmod(outfile, properties['m'])
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001011
1012 # Remaining files to be processed.
1013 # Note that files could still be not be downloaded yet here.
1014 remaining = dict()
1015 for filepath, props in settings.files.iteritems():
maruel@chromium.orge5c17132012-11-21 18:18:46 +00001016 if 'h' in props:
1017 remaining.setdefault(props['h'], []).append((filepath, props))
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001018
1019 # Do bookkeeping while files are being downloaded in the background.
1020 cwd = os.path.join(outdir, settings.relative_cwd)
1021 if not os.path.isdir(cwd):
1022 os.makedirs(cwd)
1023 cmd = settings.command[:]
1024 # Ensure paths are correctly separated on windows.
1025 cmd[0] = cmd[0].replace('/', os.path.sep)
1026 cmd = fix_python_path(cmd)
1027
1028 # Now block on the remaining files to be downloaded and mapped.
1029 while remaining:
1030 obj = cache.wait_for(remaining)
1031 for filepath, properties in remaining.pop(obj):
1032 outfile = os.path.join(outdir, filepath)
1033 link_file(outfile, cache.path(obj), HARDLINK)
maruel@chromium.orgd02e8ed2012-11-21 20:30:14 +00001034 if 'm' in properties:
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001035 # It's not set on Windows.
maruel@chromium.orgd02e8ed2012-11-21 20:30:14 +00001036 os.chmod(outfile, properties['m'])
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001037
1038 if settings.read_only:
1039 make_writable(outdir, True)
1040 logging.info('Running %s, cwd=%s' % (cmd, cwd))
csharp@chromium.orge217f302012-11-22 16:51:53 +00001041
1042 # TODO(csharp): This should be specified somewhere else.
1043 # Add a rotating log file if one doesn't already exist.
1044 env = os.environ.copy()
1045 env.setdefault('RUN_TEST_CASES_LOG_FILE', RUN_TEST_CASES_LOG)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001046 try:
1047 with Profiler('RunTest') as _prof:
csharp@chromium.orge217f302012-11-22 16:51:53 +00001048 return subprocess.call(cmd, cwd=cwd, env=env)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001049 except OSError:
1050 print >> sys.stderr, 'Failed to run %s; cwd=%s' % (cmd, cwd)
1051 raise
1052 finally:
1053 rmtree(outdir)
1054
1055
1056def main():
1057 parser = optparse.OptionParser(
1058 usage='%prog <options>', description=sys.modules[__name__].__doc__)
1059 parser.add_option(
1060 '-v', '--verbose', action='count', default=0, help='Use multiple times')
1061 parser.add_option('--no-run', action='store_true', help='Skip the run part')
1062
1063 group = optparse.OptionGroup(parser, 'Data source')
1064 group.add_option(
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001065 '-s', '--isolated',
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001066 metavar='FILE',
1067 help='File/url describing what to map or run')
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001068 # TODO(maruel): Remove once not used anymore.
1069 group.add_option(
1070 '-m', '--manifest', dest='isolated', help=optparse.SUPPRESS_HELP)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001071 group.add_option(
1072 '-H', '--hash',
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001073 help='Hash of the .isolated to grab from the hash table')
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001074 parser.add_option_group(group)
1075
1076 group.add_option(
1077 '-r', '--remote', metavar='URL', help='Remote where to get the items')
1078 group = optparse.OptionGroup(parser, 'Cache management')
1079 group.add_option(
1080 '--cache',
1081 default='cache',
1082 metavar='DIR',
1083 help='Cache directory, default=%default')
1084 group.add_option(
1085 '--max-cache-size',
1086 type='int',
1087 metavar='NNN',
1088 default=20*1024*1024*1024,
1089 help='Trim if the cache gets larger than this value, default=%default')
1090 group.add_option(
1091 '--min-free-space',
1092 type='int',
1093 metavar='NNN',
1094 default=1*1024*1024*1024,
1095 help='Trim if disk free space becomes lower than this value, '
1096 'default=%default')
1097 group.add_option(
1098 '--max-items',
1099 type='int',
1100 metavar='NNN',
1101 default=100000,
1102 help='Trim if more than this number of items are in the cache '
1103 'default=%default')
1104 parser.add_option_group(group)
1105
1106 options, args = parser.parse_args()
1107 level = [logging.ERROR, logging.INFO, logging.DEBUG][min(2, options.verbose)]
csharp@chromium.orgff2a4662012-11-21 20:49:32 +00001108
1109 logging_console = logging.StreamHandler()
1110 logging_console.setFormatter(logging.Formatter(
1111 '%(levelname)5s %(module)15s(%(lineno)3d): %(message)s'))
1112 logging_console.setLevel(level)
1113 logging.getLogger().addHandler(logging_console)
1114
1115 logging_rotating_file = logging.handlers.RotatingFileHandler(
1116 RUN_ISOLATED_LOG_FILE,
1117 maxBytes=10 * 1024 * 1024, backupCount=5)
1118 logging_rotating_file.setLevel(logging.DEBUG)
1119 logging_rotating_file.setFormatter(logging.Formatter(
1120 '%(asctime)s %(levelname)-8s %(module)15s(%(lineno)3d): %(message)s'))
1121 logging.getLogger().addHandler(logging_rotating_file)
1122
1123 logging.getLogger().setLevel(logging.DEBUG)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001124
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001125 if bool(options.isolated) == bool(options.hash):
maruel@chromium.org5dd75dd2012-12-03 15:11:32 +00001126 logging.debug('One and only one of --isolated or --hash is required.')
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001127 parser.error('One and only one of --isolated or --hash is required.')
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001128 if not options.remote:
maruel@chromium.org5dd75dd2012-12-03 15:11:32 +00001129 logging.debug('--remote is required.')
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001130 parser.error('--remote is required.')
1131 if args:
maruel@chromium.org5dd75dd2012-12-03 15:11:32 +00001132 logging.debug('Unsupported args %s' % ' '.join(args))
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001133 parser.error('Unsupported args %s' % ' '.join(args))
1134
1135 policies = CachePolicies(
1136 options.max_cache_size, options.min_free_space, options.max_items)
1137 try:
1138 return run_tha_test(
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001139 options.isolated or options.hash,
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001140 os.path.abspath(options.cache),
1141 options.remote,
1142 policies)
maruel@chromium.org5dd75dd2012-12-03 15:11:32 +00001143 except Exception, e:
1144 # Make sure any exception is logged.
1145 logging.exception(e)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001146 return 1
1147
1148
1149if __name__ == '__main__':
1150 sys.exit(main())