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 |