blob: 6ae28767473891a56c1edc1b57a5aa71d6328a45 [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.orgb7e79a22013-09-13 01:24:56 +000011__version__ = '0.2'
maruel@chromium.orgdedbf492013-09-12 20:42:11 +000012
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +000013import ctypes
14import hashlib
15import json
16import logging
17import optparse
18import os
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +000019import re
20import shutil
21import stat
22import subprocess
23import sys
24import tempfile
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +000025import time
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +000026
vadimsh@chromium.orga4326472013-08-24 02:05:41 +000027from third_party.depot_tools import fix_encoding
28
vadimsh@chromium.org3e97deb2013-08-24 00:56:44 +000029from utils import lru
vadimsh@chromium.orgb074b162013-08-22 17:55:46 +000030from utils import threading_utils
vadimsh@chromium.orga4326472013-08-24 02:05:41 +000031from utils import tools
vadimsh@chromium.org3e97deb2013-08-24 00:56:44 +000032from utils import zip_package
vadimsh@chromium.org8b9d56b2013-08-21 22:24:35 +000033
maruel@chromium.orgdedbf492013-09-12 20:42:11 +000034import isolateserver
35from isolateserver import ConfigError, MappingError
36
vadimsh@chromium.orga4326472013-08-24 02:05:41 +000037
vadimsh@chromium.org85071062013-08-21 23:37:45 +000038# Absolute path to this file (can be None if running from zip on Mac).
39THIS_FILE_PATH = os.path.abspath(__file__) if __file__ else None
vadimsh@chromium.org8b9d56b2013-08-21 22:24:35 +000040
41# Directory that contains this file (might be inside zip package).
vadimsh@chromium.org85071062013-08-21 23:37:45 +000042BASE_DIR = os.path.dirname(THIS_FILE_PATH) if __file__ else None
vadimsh@chromium.org8b9d56b2013-08-21 22:24:35 +000043
44# Directory that contains currently running script file.
45MAIN_DIR = os.path.dirname(os.path.abspath(zip_package.get_main_script_path()))
46
maruel@chromium.org6b365dc2012-10-18 19:17:56 +000047# Types of action accepted by link_file().
maruel@chromium.orgba6489b2013-07-11 20:23:33 +000048HARDLINK, HARDLINK_WITH_FALLBACK, SYMLINK, COPY = range(1, 5)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +000049
50RE_IS_SHA1 = re.compile(r'^[a-fA-F0-9]{40}$')
51
csharp@chromium.orgff2a4662012-11-21 20:49:32 +000052# The name of the log file to use.
53RUN_ISOLATED_LOG_FILE = 'run_isolated.log'
54
csharp@chromium.orge217f302012-11-22 16:51:53 +000055# The name of the log to use for the run_test_cases.py command
vadimsh@chromium.org8b9d56b2013-08-21 22:24:35 +000056RUN_TEST_CASES_LOG = 'run_test_cases.log'
csharp@chromium.orge217f302012-11-22 16:51:53 +000057
csharp@chromium.org9c59ff12012-12-12 02:32:29 +000058# The delay (in seconds) to wait between logging statements when retrieving
59# the required files. This is intended to let the user (or buildbot) know that
60# the program is still running.
61DELAY_BETWEEN_UPDATES_IN_SECS = 30
62
vadimsh@chromium.org5db0f4f2013-07-04 13:57:02 +000063# Maximum expected delay (in seconds) between successive file fetches
64# in run_tha_test. If it takes longer than that, a deadlock might be happening
65# and all stack frames for all threads are dumped to log.
66DEADLOCK_TIMEOUT = 5 * 60
67
vadimsh@chromium.org87d63262013-04-04 19:34:21 +000068
maruel@chromium.org9e9ceaa2013-04-05 15:42:42 +000069# Used by get_flavor().
70FLAVOR_MAPPING = {
71 'cygwin': 'win',
72 'win32': 'win',
73 'darwin': 'mac',
74 'sunos5': 'solaris',
75 'freebsd7': 'freebsd',
76 'freebsd8': 'freebsd',
77}
78
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +000079
vadimsh@chromium.org8b9d56b2013-08-21 22:24:35 +000080def get_as_zip_package(executable=True):
81 """Returns ZipPackage with this module and all its dependencies.
82
83 If |executable| is True will store run_isolated.py as __main__.py so that
84 zip package is directly executable be python.
85 """
86 # Building a zip package when running from another zip package is
87 # unsupported and probably unneeded.
88 assert not zip_package.is_zipped_module(sys.modules[__name__])
vadimsh@chromium.org85071062013-08-21 23:37:45 +000089 assert THIS_FILE_PATH
90 assert BASE_DIR
vadimsh@chromium.org8b9d56b2013-08-21 22:24:35 +000091 package = zip_package.ZipPackage(root=BASE_DIR)
92 package.add_python_file(THIS_FILE_PATH, '__main__.py' if executable else None)
maruel@chromium.orgdedbf492013-09-12 20:42:11 +000093 package.add_python_file(os.path.join(BASE_DIR, 'isolateserver.py'))
vadimsh@chromium.org8b9d56b2013-08-21 22:24:35 +000094 package.add_directory(os.path.join(BASE_DIR, 'third_party'))
95 package.add_directory(os.path.join(BASE_DIR, 'utils'))
96 return package
97
98
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +000099def get_flavor():
100 """Returns the system default flavor. Copied from gyp/pylib/gyp/common.py."""
maruel@chromium.org9e9ceaa2013-04-05 15:42:42 +0000101 return FLAVOR_MAPPING.get(sys.platform, 'linux')
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000102
103
104def os_link(source, link_name):
105 """Add support for os.link() on Windows."""
106 if sys.platform == 'win32':
107 if not ctypes.windll.kernel32.CreateHardLinkW(
108 unicode(link_name), unicode(source), 0):
109 raise OSError()
110 else:
111 os.link(source, link_name)
112
113
114def readable_copy(outfile, infile):
115 """Makes a copy of the file that is readable by everyone."""
csharp@chromium.org59d116d2013-07-05 18:04:08 +0000116 shutil.copy2(infile, outfile)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000117 read_enabled_mode = (os.stat(outfile).st_mode | stat.S_IRUSR |
118 stat.S_IRGRP | stat.S_IROTH)
119 os.chmod(outfile, read_enabled_mode)
120
121
122def link_file(outfile, infile, action):
123 """Links a file. The type of link depends on |action|."""
124 logging.debug('Mapping %s to %s' % (infile, outfile))
maruel@chromium.orgba6489b2013-07-11 20:23:33 +0000125 if action not in (HARDLINK, HARDLINK_WITH_FALLBACK, SYMLINK, COPY):
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000126 raise ValueError('Unknown mapping action %s' % action)
127 if not os.path.isfile(infile):
128 raise MappingError('%s is missing' % infile)
129 if os.path.isfile(outfile):
130 raise MappingError(
131 '%s already exist; insize:%d; outsize:%d' %
132 (outfile, os.stat(infile).st_size, os.stat(outfile).st_size))
133
134 if action == COPY:
135 readable_copy(outfile, infile)
136 elif action == SYMLINK and sys.platform != 'win32':
137 # On windows, symlink are converted to hardlink and fails over to copy.
maruel@chromium.orgf43e68b2012-10-15 20:23:10 +0000138 os.symlink(infile, outfile) # pylint: disable=E1101
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000139 else:
140 try:
141 os_link(infile, outfile)
maruel@chromium.orgba6489b2013-07-11 20:23:33 +0000142 except OSError as e:
143 if action == HARDLINK:
144 raise MappingError(
145 'Failed to hardlink %s to %s: %s' % (infile, outfile, e))
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000146 # Probably a different file system.
maruel@chromium.org9e98e432013-05-31 17:06:51 +0000147 logging.warning(
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000148 'Failed to hardlink, failing back to copy %s to %s' % (
149 infile, outfile))
150 readable_copy(outfile, infile)
151
152
153def _set_write_bit(path, read_only):
154 """Sets or resets the executable bit on a file or directory."""
155 mode = os.lstat(path).st_mode
156 if read_only:
157 mode = mode & 0500
158 else:
159 mode = mode | 0200
160 if hasattr(os, 'lchmod'):
161 os.lchmod(path, mode) # pylint: disable=E1101
162 else:
163 if stat.S_ISLNK(mode):
164 # Skip symlink without lchmod() support.
165 logging.debug('Can\'t change +w bit on symlink %s' % path)
166 return
167
168 # TODO(maruel): Implement proper DACL modification on Windows.
169 os.chmod(path, mode)
170
171
172def make_writable(root, read_only):
173 """Toggle the writable bit on a directory tree."""
csharp@chromium.org837352f2013-01-17 21:17:03 +0000174 assert os.path.isabs(root), root
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000175 for dirpath, dirnames, filenames in os.walk(root, topdown=True):
176 for filename in filenames:
177 _set_write_bit(os.path.join(dirpath, filename), read_only)
178
179 for dirname in dirnames:
180 _set_write_bit(os.path.join(dirpath, dirname), read_only)
181
182
183def rmtree(root):
184 """Wrapper around shutil.rmtree() to retry automatically on Windows."""
185 make_writable(root, False)
186 if sys.platform == 'win32':
187 for i in range(3):
188 try:
189 shutil.rmtree(root)
190 break
191 except WindowsError: # pylint: disable=E0602
192 delay = (i+1)*2
193 print >> sys.stderr, (
194 'The test has subprocess outliving it. Sleep %d seconds.' % delay)
195 time.sleep(delay)
196 else:
197 shutil.rmtree(root)
198
199
200def is_same_filesystem(path1, path2):
201 """Returns True if both paths are on the same filesystem.
202
203 This is required to enable the use of hardlinks.
204 """
205 assert os.path.isabs(path1), path1
206 assert os.path.isabs(path2), path2
207 if sys.platform == 'win32':
208 # If the drive letter mismatches, assume it's a separate partition.
209 # TODO(maruel): It should look at the underlying drive, a drive letter could
210 # be a mount point to a directory on another drive.
211 assert re.match(r'^[a-zA-Z]\:\\.*', path1), path1
212 assert re.match(r'^[a-zA-Z]\:\\.*', path2), path2
213 if path1[0].lower() != path2[0].lower():
214 return False
215 return os.stat(path1).st_dev == os.stat(path2).st_dev
216
217
218def get_free_space(path):
219 """Returns the number of free bytes."""
220 if sys.platform == 'win32':
221 free_bytes = ctypes.c_ulonglong(0)
222 ctypes.windll.kernel32.GetDiskFreeSpaceExW(
223 ctypes.c_wchar_p(path), None, None, ctypes.pointer(free_bytes))
224 return free_bytes.value
maruel@chromium.orgf43e68b2012-10-15 20:23:10 +0000225 # For OSes other than Windows.
226 f = os.statvfs(path) # pylint: disable=E1101
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000227 return f.f_bfree * f.f_frsize
228
229
230def make_temp_dir(prefix, root_dir):
231 """Returns a temporary directory on the same file system as root_dir."""
232 base_temp_dir = None
233 if not is_same_filesystem(root_dir, tempfile.gettempdir()):
234 base_temp_dir = os.path.dirname(root_dir)
235 return tempfile.mkdtemp(prefix=prefix, dir=base_temp_dir)
236
237
frankf@chromium.org3348ee02013-06-27 14:53:17 +0000238def load_isolated(content, os_flavor=None):
maruel@chromium.org0cd0b182012-10-22 13:34:15 +0000239 """Verifies the .isolated file is valid and loads this object with the json
240 data.
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000241 """
242 try:
243 data = json.loads(content)
244 except ValueError:
245 raise ConfigError('Failed to parse: %s...' % content[:100])
246
247 if not isinstance(data, dict):
248 raise ConfigError('Expected dict, got %r' % data)
249
250 for key, value in data.iteritems():
251 if key == 'command':
252 if not isinstance(value, list):
253 raise ConfigError('Expected list, got %r' % value)
maruel@chromium.org89ad2db2012-12-12 14:29:22 +0000254 if not value:
255 raise ConfigError('Expected non-empty command')
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000256 for subvalue in value:
257 if not isinstance(subvalue, basestring):
258 raise ConfigError('Expected string, got %r' % subvalue)
259
260 elif key == 'files':
261 if not isinstance(value, dict):
262 raise ConfigError('Expected dict, got %r' % value)
263 for subkey, subvalue in value.iteritems():
264 if not isinstance(subkey, basestring):
265 raise ConfigError('Expected string, got %r' % subkey)
266 if not isinstance(subvalue, dict):
267 raise ConfigError('Expected dict, got %r' % subvalue)
268 for subsubkey, subsubvalue in subvalue.iteritems():
maruel@chromium.orge5c17132012-11-21 18:18:46 +0000269 if subsubkey == 'l':
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000270 if not isinstance(subsubvalue, basestring):
271 raise ConfigError('Expected string, got %r' % subsubvalue)
maruel@chromium.orge5c17132012-11-21 18:18:46 +0000272 elif subsubkey == 'm':
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000273 if not isinstance(subsubvalue, int):
274 raise ConfigError('Expected int, got %r' % subsubvalue)
maruel@chromium.orge5c17132012-11-21 18:18:46 +0000275 elif subsubkey == 'h':
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000276 if not RE_IS_SHA1.match(subsubvalue):
277 raise ConfigError('Expected sha-1, got %r' % subsubvalue)
maruel@chromium.orge5c17132012-11-21 18:18:46 +0000278 elif subsubkey == 's':
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000279 if not isinstance(subsubvalue, int):
280 raise ConfigError('Expected int, got %r' % subsubvalue)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000281 else:
282 raise ConfigError('Unknown subsubkey %s' % subsubkey)
maruel@chromium.orge5c17132012-11-21 18:18:46 +0000283 if bool('h' in subvalue) and bool('l' in subvalue):
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000284 raise ConfigError(
maruel@chromium.orge5c17132012-11-21 18:18:46 +0000285 'Did not expect both \'h\' (sha-1) and \'l\' (link), got: %r' %
286 subvalue)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000287
288 elif key == 'includes':
289 if not isinstance(value, list):
290 raise ConfigError('Expected list, got %r' % value)
maruel@chromium.org89ad2db2012-12-12 14:29:22 +0000291 if not value:
292 raise ConfigError('Expected non-empty includes list')
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000293 for subvalue in value:
294 if not RE_IS_SHA1.match(subvalue):
295 raise ConfigError('Expected sha-1, got %r' % subvalue)
296
297 elif key == 'read_only':
298 if not isinstance(value, bool):
299 raise ConfigError('Expected bool, got %r' % value)
300
301 elif key == 'relative_cwd':
302 if not isinstance(value, basestring):
303 raise ConfigError('Expected string, got %r' % value)
304
305 elif key == 'os':
frankf@chromium.org3348ee02013-06-27 14:53:17 +0000306 expected_value = os_flavor or get_flavor()
307 if value != expected_value:
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000308 raise ConfigError(
309 'Expected \'os\' to be \'%s\' but got \'%s\'' %
frankf@chromium.org3348ee02013-06-27 14:53:17 +0000310 (expected_value, value))
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000311
312 else:
313 raise ConfigError('Unknown key %s' % key)
314
315 return data
316
317
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000318class CachePolicies(object):
319 def __init__(self, max_cache_size, min_free_space, max_items):
320 """
321 Arguments:
322 - max_cache_size: Trim if the cache gets larger than this value. If 0, the
323 cache is effectively a leak.
324 - min_free_space: Trim if disk free space becomes lower than this value. If
325 0, it unconditionally fill the disk.
326 - max_items: Maximum number of items to keep in the cache. If 0, do not
327 enforce a limit.
328 """
329 self.max_cache_size = max_cache_size
330 self.min_free_space = min_free_space
331 self.max_items = max_items
332
333
334class Cache(object):
335 """Stateful LRU cache.
336
337 Saves its state as json file.
338 """
339 STATE_FILE = 'state.json'
340
maruel@chromium.org3e42ce82013-09-12 18:36:59 +0000341 def __init__(self, cache_dir, remote_fetcher, policies):
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000342 """
343 Arguments:
344 - cache_dir: Directory where to place the cache.
maruel@chromium.orgdedbf492013-09-12 20:42:11 +0000345 - remote_fetcher: isolateserver.RemoteOperation where to fetch items from.
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000346 - policies: cache retention policies.
347 """
348 self.cache_dir = cache_dir
maruel@chromium.org3e42ce82013-09-12 18:36:59 +0000349 self.remote_fetcher = remote_fetcher
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000350 self.policies = policies
351 self.state_file = os.path.join(cache_dir, self.STATE_FILE)
vadimsh@chromium.org3e97deb2013-08-24 00:56:44 +0000352 self.lru = lru.LRUDict()
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000353
354 # Items currently being fetched. Keep it local to reduce lock contention.
355 self._pending_queue = set()
356
357 # Profiling values.
358 self._added = []
359 self._removed = []
360 self._free_disk = 0
361
vadimsh@chromium.orga4326472013-08-24 02:05:41 +0000362 with tools.Profiler('Setup'):
maruel@chromium.org770993b2012-12-11 17:16:48 +0000363 if not os.path.isdir(self.cache_dir):
364 os.makedirs(self.cache_dir)
vadimsh@chromium.org3e97deb2013-08-24 00:56:44 +0000365
366 # Load state of the cache.
vadimsh@chromium.orga40428e2013-07-04 15:43:14 +0000367 if os.path.isfile(self.state_file):
368 try:
vadimsh@chromium.org3e97deb2013-08-24 00:56:44 +0000369 self.lru = lru.LRUDict.load(self.state_file)
370 except ValueError as err:
371 logging.error('Failed to load cache state: %s' % (err,))
372 # Don't want to keep broken state file.
373 os.remove(self.state_file)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000374
maruel@chromium.org770993b2012-12-11 17:16:48 +0000375 # Ensure that all files listed in the state still exist and add new ones.
vadimsh@chromium.org3e97deb2013-08-24 00:56:44 +0000376 previous = self.lru.keys_set()
377 unknown = []
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000378 for filename in os.listdir(self.cache_dir):
379 if filename == self.STATE_FILE:
380 continue
381 if filename in previous:
382 previous.remove(filename)
383 continue
384 # An untracked file.
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000385 if not RE_IS_SHA1.match(filename):
maruel@chromium.org9e98e432013-05-31 17:06:51 +0000386 logging.warning('Removing unknown file %s from cache', filename)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000387 os.remove(self.path(filename))
maruel@chromium.org770993b2012-12-11 17:16:48 +0000388 continue
vadimsh@chromium.org3e97deb2013-08-24 00:56:44 +0000389 # File that's not referenced in 'state.json'.
390 # TODO(vadimsh): Verify its SHA1 matches file name.
391 logging.warning('Adding unknown file %s to cache', filename)
392 unknown.append(filename)
maruel@chromium.org770993b2012-12-11 17:16:48 +0000393
vadimsh@chromium.org3e97deb2013-08-24 00:56:44 +0000394 if unknown:
395 # Add as oldest files. They will be deleted eventually if not accessed.
396 self._add_oldest_list(unknown)
397 logging.warning('Added back %d unknown files', len(unknown))
398
maruel@chromium.org770993b2012-12-11 17:16:48 +0000399 if previous:
vadimsh@chromium.org3e97deb2013-08-24 00:56:44 +0000400 # Filter out entries that were not found.
maruel@chromium.org9e98e432013-05-31 17:06:51 +0000401 logging.warning('Removed %d lost files', len(previous))
vadimsh@chromium.org3e97deb2013-08-24 00:56:44 +0000402 for filename in previous:
403 self.lru.pop(filename)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000404 self.trim()
405
406 def __enter__(self):
407 return self
408
409 def __exit__(self, _exc_type, _exec_value, _traceback):
vadimsh@chromium.orga4326472013-08-24 02:05:41 +0000410 with tools.Profiler('CleanupTrimming'):
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000411 self.trim()
412
413 logging.info(
maruel@chromium.org5fd6f472012-12-11 00:26:08 +0000414 '%5d (%8dkb) added', len(self._added), sum(self._added) / 1024)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000415 logging.info(
maruel@chromium.org5fd6f472012-12-11 00:26:08 +0000416 '%5d (%8dkb) current',
vadimsh@chromium.org3e97deb2013-08-24 00:56:44 +0000417 len(self.lru),
418 sum(self.lru.itervalues()) / 1024)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000419 logging.info(
maruel@chromium.org5fd6f472012-12-11 00:26:08 +0000420 '%5d (%8dkb) removed', len(self._removed), sum(self._removed) / 1024)
421 logging.info(' %8dkb free', self._free_disk / 1024)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000422
csharp@chromium.org8dc52542012-11-08 20:29:55 +0000423 def remove_lru_file(self):
vadimsh@chromium.org3e97deb2013-08-24 00:56:44 +0000424 """Removes the last recently used file and returns its size."""
425 item, size = self.lru.pop_oldest()
426 self._delete_file(item, size)
427 return size
csharp@chromium.org8dc52542012-11-08 20:29:55 +0000428
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000429 def trim(self):
430 """Trims anything we don't know, make sure enough free space exists."""
431 # Ensure maximum cache size.
vadimsh@chromium.org3e97deb2013-08-24 00:56:44 +0000432 if self.policies.max_cache_size:
433 total_size = sum(self.lru.itervalues())
434 while total_size > self.policies.max_cache_size:
435 total_size -= self.remove_lru_file()
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000436
437 # Ensure maximum number of items in the cache.
vadimsh@chromium.org3e97deb2013-08-24 00:56:44 +0000438 if self.policies.max_items and len(self.lru) > self.policies.max_items:
439 for _ in xrange(len(self.lru) - self.policies.max_items):
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000440 self.remove_lru_file()
441
442 # Ensure enough free space.
443 self._free_disk = get_free_space(self.cache_dir)
maruel@chromium.org9e98e432013-05-31 17:06:51 +0000444 trimmed_due_to_space = False
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000445 while (
446 self.policies.min_free_space and
vadimsh@chromium.org3e97deb2013-08-24 00:56:44 +0000447 self.lru and
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000448 self._free_disk < self.policies.min_free_space):
maruel@chromium.org9e98e432013-05-31 17:06:51 +0000449 trimmed_due_to_space = True
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000450 self.remove_lru_file()
451 self._free_disk = get_free_space(self.cache_dir)
maruel@chromium.org9e98e432013-05-31 17:06:51 +0000452 if trimmed_due_to_space:
vadimsh@chromium.org3e97deb2013-08-24 00:56:44 +0000453 total = sum(self.lru.itervalues())
maruel@chromium.org9e98e432013-05-31 17:06:51 +0000454 logging.warning(
455 'Trimmed due to not enough free disk space: %.1fkb free, %.1fkb '
456 'cache (%.1f%% of its maximum capacity)',
457 self._free_disk / 1024.,
458 total / 1024.,
459 100. * self.policies.max_cache_size / float(total),
460 )
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000461 self.save()
462
csharp@chromium.org8dc52542012-11-08 20:29:55 +0000463 def retrieve(self, priority, item, size):
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000464 """Retrieves a file from the remote, if not already cached, and adds it to
465 the cache.
csharp@chromium.org8dc52542012-11-08 20:29:55 +0000466
vadimsh@chromium.org3e97deb2013-08-24 00:56:44 +0000467 If the file is in the cache, verify that the file is valid (i.e. it is
csharp@chromium.org8dc52542012-11-08 20:29:55 +0000468 the correct size), retrieving it again if it isn't.
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000469 """
470 assert not '/' in item
471 path = self.path(item)
vadimsh@chromium.org3e97deb2013-08-24 00:56:44 +0000472 found = False
csharp@chromium.org8dc52542012-11-08 20:29:55 +0000473
vadimsh@chromium.org3e97deb2013-08-24 00:56:44 +0000474 if item in self.lru:
maruel@chromium.orgdedbf492013-09-12 20:42:11 +0000475 if not isolateserver.valid_file(self.path(item), size):
vadimsh@chromium.org3e97deb2013-08-24 00:56:44 +0000476 self.lru.pop(item)
477 self._delete_file(item, size)
csharp@chromium.org8dc52542012-11-08 20:29:55 +0000478 else:
csharp@chromium.org8dc52542012-11-08 20:29:55 +0000479 # Was already in cache. Update it's LRU value by putting it at the end.
vadimsh@chromium.org3e97deb2013-08-24 00:56:44 +0000480 self.lru.touch(item)
481 found = True
csharp@chromium.org8dc52542012-11-08 20:29:55 +0000482
vadimsh@chromium.org3e97deb2013-08-24 00:56:44 +0000483 if not found:
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000484 if item in self._pending_queue:
485 # Already pending. The same object could be referenced multiple times.
486 return
maruel@chromium.org9e98e432013-05-31 17:06:51 +0000487 # TODO(maruel): It should look at the free disk space, the current cache
488 # size and the size of the new item on every new item:
489 # - Trim the cache as more entries are listed when free disk space is low,
490 # otherwise if the amount of data downloaded during the run > free disk
491 # space, it'll crash.
492 # - Make sure there's enough free disk space to fit all dependencies of
493 # this run! If not, abort early.
maruel@chromium.org3e42ce82013-09-12 18:36:59 +0000494 self.remote_fetcher.add_item(priority, item, path, size)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000495 self._pending_queue.add(item)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000496
497 def add(self, filepath, obj):
498 """Forcibly adds a file to the cache."""
vadimsh@chromium.org3e97deb2013-08-24 00:56:44 +0000499 if obj not in self.lru:
500 link_file(self.path(obj), filepath, HARDLINK)
501 self._add(obj)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000502
503 def path(self, item):
504 """Returns the path to one item."""
505 return os.path.join(self.cache_dir, item)
506
507 def save(self):
508 """Saves the LRU ordering."""
vadimsh@chromium.org3e97deb2013-08-24 00:56:44 +0000509 self.lru.save(self.state_file)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000510
511 def wait_for(self, items):
512 """Starts a loop that waits for at least one of |items| to be retrieved.
513
514 Returns the first item retrieved.
515 """
516 # Flush items already present.
517 for item in items:
vadimsh@chromium.org3e97deb2013-08-24 00:56:44 +0000518 if item in self.lru:
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000519 return item
520
521 assert all(i in self._pending_queue for i in items), (
522 items, self._pending_queue)
523 # Note that:
524 # len(self._pending_queue) ==
maruel@chromium.org3e42ce82013-09-12 18:36:59 +0000525 # ( len(self.remote_fetcher._workers) - self.remote_fetcher._ready +
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000526 # len(self._remote._queue) + len(self._remote.done))
527 # There is no lock-free way to verify that.
528 while self._pending_queue:
maruel@chromium.org3e42ce82013-09-12 18:36:59 +0000529 item = self.remote_fetcher.get_one_result()
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000530 self._pending_queue.remove(item)
vadimsh@chromium.org3e97deb2013-08-24 00:56:44 +0000531 self._add(item)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000532 if item in items:
533 return item
534
vadimsh@chromium.org3e97deb2013-08-24 00:56:44 +0000535 def _add(self, item):
536 """Adds an item into LRU cache marking it as a newest one."""
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000537 size = os.stat(self.path(item)).st_size
538 self._added.append(size)
vadimsh@chromium.org3e97deb2013-08-24 00:56:44 +0000539 self.lru.add(item, size)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000540
vadimsh@chromium.org3e97deb2013-08-24 00:56:44 +0000541 def _add_oldest_list(self, items):
542 """Adds a bunch of items into LRU cache marking them as oldest ones."""
543 pairs = []
544 for item in items:
545 size = os.stat(self.path(item)).st_size
546 self._added.append(size)
547 pairs.append((item, size))
548 self.lru.batch_insert_oldest(pairs)
549
550 def _delete_file(self, item, size):
551 """Deletes cache file from the file system."""
552 self._removed.append(size)
553 try:
554 os.remove(self.path(item))
555 except OSError as e:
556 logging.error('Error attempting to delete a file\n%s' % e)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000557
558
maruel@chromium.org0cd0b182012-10-22 13:34:15 +0000559class IsolatedFile(object):
560 """Represents a single parsed .isolated file."""
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000561 def __init__(self, obj_hash):
562 """|obj_hash| is really the sha-1 of the file."""
maruel@chromium.org0cd0b182012-10-22 13:34:15 +0000563 logging.debug('IsolatedFile(%s)' % obj_hash)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000564 self.obj_hash = obj_hash
565 # Set once all the left-side of the tree is parsed. 'Tree' here means the
maruel@chromium.org0cd0b182012-10-22 13:34:15 +0000566 # .isolate and all the .isolated files recursively included by it with
567 # 'includes' key. The order of each sha-1 in 'includes', each representing a
568 # .isolated file in the hash table, is important, as the later ones are not
569 # processed until the firsts are retrieved and read.
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000570 self.can_fetch = False
571
572 # Raw data.
573 self.data = {}
maruel@chromium.org0cd0b182012-10-22 13:34:15 +0000574 # A IsolatedFile instance, one per object in self.includes.
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000575 self.children = []
576
maruel@chromium.org0cd0b182012-10-22 13:34:15 +0000577 # Set once the .isolated file is loaded.
578 self._is_parsed = False
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000579 # Set once the files are fetched.
580 self.files_fetched = False
581
582 def load(self, content):
maruel@chromium.org0cd0b182012-10-22 13:34:15 +0000583 """Verifies the .isolated file is valid and loads this object with the json
584 data.
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000585 """
maruel@chromium.org0cd0b182012-10-22 13:34:15 +0000586 logging.debug('IsolatedFile.load(%s)' % self.obj_hash)
587 assert not self._is_parsed
588 self.data = load_isolated(content)
589 self.children = [IsolatedFile(i) for i in self.data.get('includes', [])]
590 self._is_parsed = True
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000591
592 def fetch_files(self, cache, files):
maruel@chromium.org0cd0b182012-10-22 13:34:15 +0000593 """Adds files in this .isolated file not present in |files| dictionary.
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000594
595 Preemptively request files.
596
597 Note that |files| is modified by this function.
598 """
599 assert self.can_fetch
maruel@chromium.org0cd0b182012-10-22 13:34:15 +0000600 if not self._is_parsed or self.files_fetched:
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000601 return
602 logging.debug('fetch_files(%s)' % self.obj_hash)
603 for filepath, properties in self.data.get('files', {}).iteritems():
maruel@chromium.org0cd0b182012-10-22 13:34:15 +0000604 # Root isolated has priority on the files being mapped. In particular,
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000605 # overriden files must not be fetched.
606 if filepath not in files:
607 files[filepath] = properties
maruel@chromium.orge5c17132012-11-21 18:18:46 +0000608 if 'h' in properties:
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000609 # Preemptively request files.
610 logging.debug('fetching %s' % filepath)
maruel@chromium.orgdedbf492013-09-12 20:42:11 +0000611 cache.retrieve(
612 isolateserver.RemoteOperation.MED,
613 properties['h'],
614 properties['s'])
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000615 self.files_fetched = True
616
617
618class Settings(object):
maruel@chromium.org0cd0b182012-10-22 13:34:15 +0000619 """Results of a completely parsed .isolated file."""
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000620 def __init__(self):
621 self.command = []
622 self.files = {}
623 self.read_only = None
624 self.relative_cwd = None
maruel@chromium.org0cd0b182012-10-22 13:34:15 +0000625 # The main .isolated file, a IsolatedFile instance.
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000626 self.root = None
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000627
maruel@chromium.org0cd0b182012-10-22 13:34:15 +0000628 def load(self, cache, root_isolated_hash):
629 """Loads the .isolated and all the included .isolated asynchronously.
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000630
maruel@chromium.org0cd0b182012-10-22 13:34:15 +0000631 It enables support for "included" .isolated files. They are processed in
632 strict order but fetched asynchronously from the cache. This is important so
633 that a file in an included .isolated file that is overridden by an embedding
vadimsh@chromium.orgf4c063e2013-07-04 14:23:31 +0000634 .isolated file is not fetched needlessly. The includes are fetched in one
maruel@chromium.org0cd0b182012-10-22 13:34:15 +0000635 pass and the files are fetched as soon as all the ones on the left-side
636 of the tree were fetched.
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000637
maruel@chromium.org0cd0b182012-10-22 13:34:15 +0000638 The prioritization is very important here for nested .isolated files.
639 'includes' have the highest priority and the algorithm is optimized for both
640 deep and wide trees. A deep one is a long link of .isolated files referenced
641 one at a time by one item in 'includes'. A wide one has a large number of
642 'includes' in a single .isolated file. 'left' is defined as an included
643 .isolated file earlier in the 'includes' list. So the order of the elements
644 in 'includes' is important.
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000645 """
maruel@chromium.org0cd0b182012-10-22 13:34:15 +0000646 self.root = IsolatedFile(root_isolated_hash)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000647
vadimsh@chromium.orgf4c063e2013-07-04 14:23:31 +0000648 # Isolated files being retrieved now: hash -> IsolatedFile instance.
649 pending = {}
650 # Set of hashes of already retrieved items to refuse recursive includes.
651 seen = set()
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000652
vadimsh@chromium.orgf4c063e2013-07-04 14:23:31 +0000653 def retrieve(isolated_file):
654 h = isolated_file.obj_hash
655 if h in seen:
656 raise ConfigError('IsolatedFile %s is retrieved recursively' % h)
657 assert h not in pending
658 seen.add(h)
659 pending[h] = isolated_file
maruel@chromium.orgdedbf492013-09-12 20:42:11 +0000660 cache.retrieve(
661 isolateserver.RemoteOperation.HIGH,
662 h,
663 isolateserver.UNKNOWN_FILE_SIZE)
vadimsh@chromium.orgf4c063e2013-07-04 14:23:31 +0000664
665 retrieve(self.root)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000666
667 while pending:
668 item_hash = cache.wait_for(pending)
669 item = pending.pop(item_hash)
670 item.load(open(cache.path(item_hash), 'r').read())
maruel@chromium.org0cd0b182012-10-22 13:34:15 +0000671 if item_hash == root_isolated_hash:
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000672 # It's the root item.
673 item.can_fetch = True
674
675 for new_child in item.children:
vadimsh@chromium.orgf4c063e2013-07-04 14:23:31 +0000676 retrieve(new_child)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000677
678 # Traverse the whole tree to see if files can now be fetched.
vadimsh@chromium.orgf4c063e2013-07-04 14:23:31 +0000679 self._traverse_tree(cache, self.root)
680
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000681 def check(n):
682 return all(check(x) for x in n.children) and n.files_fetched
683 assert check(self.root)
vadimsh@chromium.orgf4c063e2013-07-04 14:23:31 +0000684
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000685 self.relative_cwd = self.relative_cwd or ''
686 self.read_only = self.read_only or False
687
vadimsh@chromium.orgf4c063e2013-07-04 14:23:31 +0000688 def _traverse_tree(self, cache, node):
689 if node.can_fetch:
690 if not node.files_fetched:
691 self._update_self(cache, node)
692 will_break = False
693 for i in node.children:
694 if not i.can_fetch:
695 if will_break:
696 break
697 # Automatically mark the first one as fetcheable.
698 i.can_fetch = True
699 will_break = True
700 self._traverse_tree(cache, i)
701
702 def _update_self(self, cache, node):
703 node.fetch_files(cache, self.files)
704 # Grabs properties.
705 if not self.command and node.data.get('command'):
706 self.command = node.data['command']
707 if self.read_only is None and node.data.get('read_only') is not None:
708 self.read_only = node.data['read_only']
709 if (self.relative_cwd is None and
710 node.data.get('relative_cwd') is not None):
711 self.relative_cwd = node.data['relative_cwd']
712
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000713
csharp@chromium.orgffd8cf02013-01-09 21:57:38 +0000714def create_directories(base_directory, files):
715 """Creates the directory structure needed by the given list of files."""
716 logging.debug('create_directories(%s, %d)', base_directory, len(files))
717 # Creates the tree of directories to create.
718 directories = set(os.path.dirname(f) for f in files)
719 for item in list(directories):
720 while item:
721 directories.add(item)
722 item = os.path.dirname(item)
723 for d in sorted(directories):
724 if d:
725 os.mkdir(os.path.join(base_directory, d))
726
727
728def create_links(base_directory, files):
729 """Creates any links needed by the given set of files."""
730 for filepath, properties in files:
csharp@chromium.org89eaf082013-03-26 18:56:21 +0000731 if 'l' not in properties:
csharp@chromium.orgffd8cf02013-01-09 21:57:38 +0000732 continue
maruel@chromium.org3320ee12013-03-28 13:23:31 +0000733 if sys.platform == 'win32':
734 # TODO(maruel): Create junctions or empty text files similar to what
735 # cygwin do?
736 logging.warning('Ignoring symlink %s', filepath)
737 continue
csharp@chromium.orgffd8cf02013-01-09 21:57:38 +0000738 outfile = os.path.join(base_directory, filepath)
739 # symlink doesn't exist on Windows. So the 'link' property should
740 # never be specified for windows .isolated file.
741 os.symlink(properties['l'], outfile) # pylint: disable=E1101
742 if 'm' in properties:
743 lchmod = getattr(os, 'lchmod', None)
744 if lchmod:
745 lchmod(outfile, properties['m'])
746
747
748def setup_commands(base_directory, cwd, cmd):
749 """Correctly adjusts and then returns the required working directory
750 and command needed to run the test.
751 """
752 assert not os.path.isabs(cwd), 'The cwd must be a relative path, got %s' % cwd
753 cwd = os.path.join(base_directory, cwd)
754 if not os.path.isdir(cwd):
755 os.makedirs(cwd)
756
757 # Ensure paths are correctly separated on windows.
758 cmd[0] = cmd[0].replace('/', os.path.sep)
vadimsh@chromium.orga4326472013-08-24 02:05:41 +0000759 cmd = tools.fix_python_path(cmd)
csharp@chromium.orgffd8cf02013-01-09 21:57:38 +0000760
761 return cwd, cmd
762
763
764def generate_remaining_files(files):
765 """Generates a dictionary of all the remaining files to be downloaded."""
766 remaining = {}
767 for filepath, props in files:
768 if 'h' in props:
769 remaining.setdefault(props['h'], []).append((filepath, props))
770
771 return remaining
772
773
maruel@chromium.org3e42ce82013-09-12 18:36:59 +0000774def run_tha_test(isolated_hash, cache_dir, retriever, policies):
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000775 """Downloads the dependencies in the cache, hardlinks them into a temporary
776 directory and runs the executable.
777 """
778 settings = Settings()
maruel@chromium.orgdedbf492013-09-12 20:42:11 +0000779 with Cache(
780 cache_dir, isolateserver.RemoteOperation(retriever), policies) as cache:
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000781 outdir = make_temp_dir('run_tha_test', cache_dir)
782 try:
783 # Initiate all the files download.
vadimsh@chromium.orga4326472013-08-24 02:05:41 +0000784 with tools.Profiler('GetIsolateds'):
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000785 # Optionally support local files.
maruel@chromium.org0cd0b182012-10-22 13:34:15 +0000786 if not RE_IS_SHA1.match(isolated_hash):
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000787 # Adds it in the cache. While not strictly necessary, this simplifies
788 # the rest.
maruel@chromium.orgcb3c3d52013-03-14 18:55:30 +0000789 h = hashlib.sha1(open(isolated_hash, 'rb').read()).hexdigest()
maruel@chromium.org0cd0b182012-10-22 13:34:15 +0000790 cache.add(isolated_hash, h)
791 isolated_hash = h
792 settings.load(cache, isolated_hash)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000793
794 if not settings.command:
795 print >> sys.stderr, 'No command to run'
796 return 1
797
vadimsh@chromium.orga4326472013-08-24 02:05:41 +0000798 with tools.Profiler('GetRest'):
csharp@chromium.orgffd8cf02013-01-09 21:57:38 +0000799 create_directories(outdir, settings.files)
800 create_links(outdir, settings.files.iteritems())
801 remaining = generate_remaining_files(settings.files.iteritems())
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000802
803 # Do bookkeeping while files are being downloaded in the background.
csharp@chromium.orgffd8cf02013-01-09 21:57:38 +0000804 cwd, cmd = setup_commands(outdir, settings.relative_cwd,
805 settings.command[:])
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000806
807 # Now block on the remaining files to be downloaded and mapped.
csharp@chromium.org9c59ff12012-12-12 02:32:29 +0000808 logging.info('Retrieving remaining files')
809 last_update = time.time()
vadimsh@chromium.orgb074b162013-08-22 17:55:46 +0000810 with threading_utils.DeadlockDetector(DEADLOCK_TIMEOUT) as detector:
vadimsh@chromium.org5db0f4f2013-07-04 13:57:02 +0000811 while remaining:
812 detector.ping()
813 obj = cache.wait_for(remaining)
814 for filepath, properties in remaining.pop(obj):
815 outfile = os.path.join(outdir, filepath)
maruel@chromium.orgb7c003d2013-07-24 13:04:30 +0000816 link_file(outfile, cache.path(obj), HARDLINK)
vadimsh@chromium.org5db0f4f2013-07-04 13:57:02 +0000817 if 'm' in properties:
818 # It's not set on Windows.
819 os.chmod(outfile, properties['m'])
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000820
vadimsh@chromium.org5db0f4f2013-07-04 13:57:02 +0000821 if time.time() - last_update > DELAY_BETWEEN_UPDATES_IN_SECS:
822 msg = '%d files remaining...' % len(remaining)
823 print msg
824 logging.info(msg)
825 last_update = time.time()
csharp@chromium.org9c59ff12012-12-12 02:32:29 +0000826
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000827 if settings.read_only:
vadimsh@chromium.org5db0f4f2013-07-04 13:57:02 +0000828 logging.info('Making files read only')
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000829 make_writable(outdir, True)
830 logging.info('Running %s, cwd=%s' % (cmd, cwd))
csharp@chromium.orge217f302012-11-22 16:51:53 +0000831
832 # TODO(csharp): This should be specified somewhere else.
vadimsh@chromium.org8b9d56b2013-08-21 22:24:35 +0000833 # TODO(vadimsh): Pass it via 'env_vars' in manifest.
csharp@chromium.orge217f302012-11-22 16:51:53 +0000834 # Add a rotating log file if one doesn't already exist.
835 env = os.environ.copy()
vadimsh@chromium.org8b9d56b2013-08-21 22:24:35 +0000836 env.setdefault('RUN_TEST_CASES_LOG_FILE',
837 os.path.join(MAIN_DIR, RUN_TEST_CASES_LOG))
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000838 try:
vadimsh@chromium.orga4326472013-08-24 02:05:41 +0000839 with tools.Profiler('RunTest'):
csharp@chromium.orge217f302012-11-22 16:51:53 +0000840 return subprocess.call(cmd, cwd=cwd, env=env)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000841 except OSError:
842 print >> sys.stderr, 'Failed to run %s; cwd=%s' % (cmd, cwd)
843 raise
844 finally:
845 rmtree(outdir)
846
847
848def main():
vadimsh@chromium.orga4326472013-08-24 02:05:41 +0000849 tools.disable_buffering()
850 parser = tools.OptionParserWithLogging(
maruel@chromium.orgdedbf492013-09-12 20:42:11 +0000851 usage='%prog <options>',
852 version=__version__,
853 log_file=RUN_ISOLATED_LOG_FILE)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000854
855 group = optparse.OptionGroup(parser, 'Data source')
856 group.add_option(
maruel@chromium.org0cd0b182012-10-22 13:34:15 +0000857 '-s', '--isolated',
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000858 metavar='FILE',
859 help='File/url describing what to map or run')
860 group.add_option(
861 '-H', '--hash',
maruel@chromium.org0cd0b182012-10-22 13:34:15 +0000862 help='Hash of the .isolated to grab from the hash table')
maruel@chromium.orgb7e79a22013-09-13 01:24:56 +0000863 group.add_option(
864 '-I', '--isolate-server', metavar='URL',
865 default=
866 'https://isolateserver.appspot.com',
867 help='Remote where to get the items. Defaults to %default')
868 group.add_option(
869 '-n', '--namespace',
870 default='default-gzip',
871 help='namespace to use when using isolateserver, default: %default')
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000872 parser.add_option_group(group)
873
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000874 group = optparse.OptionGroup(parser, 'Cache management')
875 group.add_option(
876 '--cache',
877 default='cache',
878 metavar='DIR',
879 help='Cache directory, default=%default')
880 group.add_option(
881 '--max-cache-size',
882 type='int',
883 metavar='NNN',
884 default=20*1024*1024*1024,
885 help='Trim if the cache gets larger than this value, default=%default')
886 group.add_option(
887 '--min-free-space',
888 type='int',
889 metavar='NNN',
maruel@chromium.org9e98e432013-05-31 17:06:51 +0000890 default=2*1024*1024*1024,
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000891 help='Trim if disk free space becomes lower than this value, '
892 'default=%default')
893 group.add_option(
894 '--max-items',
895 type='int',
896 metavar='NNN',
897 default=100000,
898 help='Trim if more than this number of items are in the cache '
899 'default=%default')
900 parser.add_option_group(group)
901
902 options, args = parser.parse_args()
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000903
maruel@chromium.org0cd0b182012-10-22 13:34:15 +0000904 if bool(options.isolated) == bool(options.hash):
maruel@chromium.org5dd75dd2012-12-03 15:11:32 +0000905 logging.debug('One and only one of --isolated or --hash is required.')
maruel@chromium.org0cd0b182012-10-22 13:34:15 +0000906 parser.error('One and only one of --isolated or --hash is required.')
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000907 if args:
maruel@chromium.org5dd75dd2012-12-03 15:11:32 +0000908 logging.debug('Unsupported args %s' % ' '.join(args))
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000909 parser.error('Unsupported args %s' % ' '.join(args))
910
csharp@chromium.orgffd8cf02013-01-09 21:57:38 +0000911 options.cache = os.path.abspath(options.cache)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000912 policies = CachePolicies(
913 options.max_cache_size, options.min_free_space, options.max_items)
csharp@chromium.orgffd8cf02013-01-09 21:57:38 +0000914
maruel@chromium.orgb7e79a22013-09-13 01:24:56 +0000915 retriever = isolateserver.get_storage_api(
916 options.isolate_server, options.namespace)
maruel@chromium.org3e42ce82013-09-12 18:36:59 +0000917 try:
918 return run_tha_test(
919 options.isolated or options.hash,
920 options.cache,
921 retriever.retrieve,
922 policies)
923 except Exception, e:
924 # Make sure any exception is logged.
925 logging.exception(e)
926 return 1
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000927
928
929if __name__ == '__main__':
csharp@chromium.orgbfb98742013-03-26 20:28:36 +0000930 # Ensure that we are always running with the correct encoding.
vadimsh@chromium.orga4326472013-08-24 02:05:41 +0000931 fix_encoding.fix_encoding()
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000932 sys.exit(main())