Chromium Code Reviews| OLD | NEW |
|---|---|
| 1 #!/usr/bin/env python | 1 #!/usr/bin/env python |
| 2 # Copyright (c) 2012 The Chromium Authors. All rights reserved. | 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 | 3 # Use of this source code is governed by a BSD-style license that can be |
| 4 # found in the LICENSE file. | 4 # found in the LICENSE file. |
| 5 | 5 |
| 6 """Reads a manifest, creates a tree of hardlinks and runs the test. | 6 """Reads a manifest, creates a tree of hardlinks and runs the test. |
| 7 | 7 |
| 8 Keeps a local cache. | 8 Keeps a local cache. |
| 9 """ | 9 """ |
| 10 | 10 |
| 11 import ctypes | 11 import ctypes |
| 12 import json | 12 import json |
| 13 import logging | 13 import logging |
| 14 import optparse | 14 import optparse |
| 15 import os | 15 import os |
| 16 import re | 16 import re |
| 17 import shutil | 17 import shutil |
| 18 import stat | 18 import stat |
| 19 import subprocess | 19 import subprocess |
| 20 import sys | 20 import sys |
| 21 import tempfile | 21 import tempfile |
| 22 import time | 22 import time |
| 23 import urllib | 23 import urllib |
| 24 | 24 |
| 25 | 25 |
| 26 # Types of action accepted by recreate_tree(). | 26 # Types of action accepted by recreate_tree(). |
| 27 HARDLINK, SYMLINK, COPY = range(1, 4) | 27 HARDLINK, SYMLINK, COPY = range(1, 4) |
| 28 | 28 |
| 29 RE_IS_SHA1 = re.compile(r'^[a-fA-F0-9]{40}$') | |
| 30 | |
| 31 | |
| 32 class ConfigError(ValueError): | |
| 33 """Generic failure to load a manifest.""" | |
| 34 pass | |
| 35 | |
| 29 | 36 |
| 30 class MappingError(OSError): | 37 class MappingError(OSError): |
| 31 """Failed to recreate the tree.""" | 38 """Failed to recreate the tree.""" |
| 32 pass | 39 pass |
| 33 | 40 |
| 34 | 41 |
| 35 def os_link(source, link_name): | 42 def os_link(source, link_name): |
| 36 """Add support for os.link() on Windows.""" | 43 """Add support for os.link() on Windows.""" |
| 37 if sys.platform == 'win32': | 44 if sys.platform == 'win32': |
| 38 if not ctypes.windll.kernel32.CreateHardLinkW( | 45 if not ctypes.windll.kernel32.CreateHardLinkW( |
| (...skipping 126 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 165 | 172 |
| 166 | 173 |
| 167 def make_temp_dir(prefix, root_dir): | 174 def make_temp_dir(prefix, root_dir): |
| 168 """Returns a temporary directory on the same file system than root_dir.""" | 175 """Returns a temporary directory on the same file system than root_dir.""" |
| 169 base_temp_dir = None | 176 base_temp_dir = None |
| 170 if not is_same_filesystem(root_dir, tempfile.gettempdir()): | 177 if not is_same_filesystem(root_dir, tempfile.gettempdir()): |
| 171 base_temp_dir = os.path.dirname(root_dir) | 178 base_temp_dir = os.path.dirname(root_dir) |
| 172 return tempfile.mkdtemp(prefix=prefix, dir=base_temp_dir) | 179 return tempfile.mkdtemp(prefix=prefix, dir=base_temp_dir) |
| 173 | 180 |
| 174 | 181 |
| 182 def assert_is_str(value): | |
| 183 if not isinstance(value, basestring): | |
| 184 raise ConfigError('Expected string, got %r' % value) | |
| 185 | |
| 186 | |
| 187 def assert_is_int(value): | |
| 188 if not isinstance(value, int): | |
| 189 raise ConfigError('Expected int, got %r' % value) | |
| 190 | |
| 191 | |
| 192 def assert_is_bool(value): | |
| 193 if value not in (None, True, False): | |
| 194 raise ConfigError('Expected bool, got %r' % value) | |
| 195 | |
| 196 | |
| 197 def assert_is_sha1(value): | |
| 198 assert_is_str(value) | |
| 199 if not RE_IS_SHA1.match(value): | |
| 200 raise ConfigError('Expected sha-1, got %r' % value) | |
| 201 | |
| 202 | |
| 203 def get_assert_is_list(check): | |
| 204 """Returns a function that asserts the value is a list.""" | |
| 205 def is_list_item(value): | |
| 206 if not isinstance(value, list): | |
| 207 raise ConfigError('Expected list, got %r' % value) | |
| 208 any(check(item) for item in value) | |
| 209 return is_list_item | |
| 210 | |
| 211 | |
| 212 def get_assert_is_dict(key_check, values_check): | |
| 213 """Returns a function that ensure the value is a dict. | |
| 214 | |
| 215 It checks all the keys passes key_check and values_check is a dict of each | |
| 216 checks for each key. | |
| 217 | |
| 218 If values_check contains a None entry, keys not in values_check will be | |
| 219 checked with this key. If None is not present in values_check, unknown keys | |
| 220 will be refused. | |
| 221 """ | |
| 222 def is_dict_item(value): | |
| 223 if not isinstance(value, dict): | |
| 224 raise ConfigError('Expected dict, got %r' % value) | |
| 225 | |
| 226 for key in value: | |
| 227 key_check(key) | |
| 228 | |
| 229 # values_check is the set of valid keys. | |
| 230 unknown = set(value) - set(values_check) | |
| 231 if unknown and not None in values_check: | |
| 232 raise ConfigError( | |
| 233 'Got unknown key(s) %s in %r; expected only %s' % | |
| 234 (', '.join(unknown), value, ', '.join(values_check))) | |
| 235 for k, v in value.iteritems(): | |
| 236 if k in values_check: | |
| 237 values_check[k](v) | |
| 238 else: | |
| 239 values_check[None](v) | |
| 240 return is_dict_item | |
| 241 | |
| 242 | |
| 243 def load_manifest(content): | |
| 244 """Verifies the manifest is valid and loads this object with the json data. | |
| 245 """ | |
| 246 data = json.loads(content) | |
| 247 | |
| 248 file_keys = { | |
| 249 'link': assert_is_str, | |
| 250 'mode': assert_is_int, | |
| 251 'sha-1': assert_is_sha1, | |
| 252 'timestamp': assert_is_int, | |
| 253 } | |
| 254 | |
| 255 files_key = { | |
| 256 # Any file has to fit files_keys specification. | |
| 257 None: get_assert_is_dict(assert_is_str, file_keys), | |
| 258 } | |
| 259 | |
| 260 keys = { | |
| 261 'command': get_assert_is_list(assert_is_str), | |
| 262 # Could use assert_is_valid_filename instead of assert_is_str. | |
| 263 'files': get_assert_is_dict(assert_is_str, files_key), | |
| 264 'read_only': assert_is_bool, | |
| 265 'relative_cwd': assert_is_str, | |
| 266 } | |
| 267 | |
| 268 get_assert_is_dict(assert_is_str, keys)(data) | |
|
cmp
2012/08/28 17:38:29
this code requires a lot of state to be in-mind to
| |
| 269 # Add a special verification. | |
| 270 for value in data.get('files', {}).itervalues(): | |
| 271 if bool('sha-1' in value) and bool('link' in value): | |
| 272 raise ConfigError( | |
| 273 'Did not expect both \'sha-1\' and \'link\', got: %r' % value) | |
| 274 return data | |
| 275 | |
| 276 | |
| 175 def fix_python_path(cmd): | 277 def fix_python_path(cmd): |
| 176 """Returns the fixed command line to call the right python executable.""" | 278 """Returns the fixed command line to call the right python executable.""" |
| 177 out = cmd[:] | 279 out = cmd[:] |
| 178 if out[0] == 'python': | 280 if out[0] == 'python': |
| 179 out[0] = sys.executable | 281 out[0] = sys.executable |
| 180 elif out[0].endswith('.py'): | 282 elif out[0].endswith('.py'): |
| 181 out.insert(0, sys.executable) | 283 out.insert(0, sys.executable) |
| 182 return out | 284 return out |
| 183 | 285 |
| 184 | 286 |
| (...skipping 201 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 386 os.makedirs(outfiledir) | 488 os.makedirs(outfiledir) |
| 387 if 'sha-1' in properties: | 489 if 'sha-1' in properties: |
| 388 # A normal file. | 490 # A normal file. |
| 389 infile = properties['sha-1'] | 491 infile = properties['sha-1'] |
| 390 cache.retrieve(infile) | 492 cache.retrieve(infile) |
| 391 link_file(outfile, cache.path(infile), HARDLINK) | 493 link_file(outfile, cache.path(infile), HARDLINK) |
| 392 elif 'link' in properties: | 494 elif 'link' in properties: |
| 393 # A symlink. | 495 # A symlink. |
| 394 os.symlink(properties['link'], outfile) | 496 os.symlink(properties['link'], outfile) |
| 395 else: | 497 else: |
| 396 raise ValueError('Unexpected entry: %s' % properties) | 498 raise ConfigError('Unexpected entry: %s' % properties) |
| 397 if 'mode' in properties: | 499 if 'mode' in properties: |
| 398 # It's not set on Windows. | 500 # It's not set on Windows. |
| 399 os.chmod(outfile, properties['mode']) | 501 os.chmod(outfile, properties['mode']) |
| 400 | 502 |
| 401 cwd = os.path.join(outdir, manifest.get('relative_cwd', '')) | 503 cwd = os.path.join(outdir, manifest.get('relative_cwd', '')) |
| 402 if not os.path.isdir(cwd): | 504 if not os.path.isdir(cwd): |
| 403 os.makedirs(cwd) | 505 os.makedirs(cwd) |
| 404 if manifest.get('read_only'): | 506 if manifest.get('read_only'): |
| 405 make_writable(outdir, True) | 507 make_writable(outdir, True) |
| 406 cmd = manifest['command'] | 508 cmd = manifest['command'] |
| (...skipping 68 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 475 parser.error('One and only one of --manifest or --hash is required.') | 577 parser.error('One and only one of --manifest or --hash is required.') |
| 476 if not options.remote: | 578 if not options.remote: |
| 477 parser.error('--remote is required.') | 579 parser.error('--remote is required.') |
| 478 if args: | 580 if args: |
| 479 parser.error('Unsupported args %s' % ' '.join(args)) | 581 parser.error('Unsupported args %s' % ' '.join(args)) |
| 480 | 582 |
| 481 if options.hash: | 583 if options.hash: |
| 482 # First calculate the reference to it. | 584 # First calculate the reference to it. |
| 483 options.manifest = '%s/%s' % (options.remote.rstrip('/'), options.hash) | 585 options.manifest = '%s/%s' % (options.remote.rstrip('/'), options.hash) |
| 484 try: | 586 try: |
| 485 manifest = json.load(open_remote(options.manifest)) | 587 manifest = load_manifest(open_remote(options.manifest).read()) |
| 486 except IOError as e: | 588 except IOError as e: |
| 487 parser.error( | 589 parser.error( |
| 488 'Failed to read manifest %s; remote:%s; hash:%s; %s' % | 590 'Failed to read manifest %s; remote:%s; hash:%s; %s' % |
| 489 (options.manifest, options.remote, options.hash, str(e))) | 591 (options.manifest, options.remote, options.hash, str(e))) |
| 490 | 592 |
| 491 policies = CachePolicies( | 593 policies = CachePolicies( |
| 492 options.max_cache_size, options.min_free_space, options.max_items) | 594 options.max_cache_size, options.min_free_space, options.max_items) |
| 493 try: | 595 try: |
| 494 return run_tha_test( | 596 return run_tha_test( |
| 495 manifest, | 597 manifest, |
| 496 os.path.abspath(options.cache), | 598 os.path.abspath(options.cache), |
| 497 options.remote, | 599 options.remote, |
| 498 policies) | 600 policies) |
| 499 except MappingError, e: | 601 except (ConfigError, MappingError), e: |
| 500 print >> sys.stderr, str(e) | 602 print >> sys.stderr, str(e) |
| 501 return 1 | 603 return 1 |
| 502 | 604 |
| 503 | 605 |
| 504 if __name__ == '__main__': | 606 if __name__ == '__main__': |
| 505 sys.exit(main()) | 607 sys.exit(main()) |
| OLD | NEW |