Chromium Code Reviews| OLD | NEW |
|---|---|
| 1 # Copyright 2013 The Chromium Authors. All rights reserved. | 1 # Copyright 2013 The Chromium Authors. All rights reserved. |
| 2 # Use of this source code is governed by a BSD-style license that can be | 2 # Use of this source code is governed by a BSD-style license that can be |
| 3 # found in the LICENSE file. | 3 # found in the LICENSE file. |
| 4 | 4 |
| 5 import os | 5 import os |
| 6 import functools | |
| 7 | |
| 6 from slave import recipe_api | 8 from slave import recipe_api |
| 9 from slave import recipe_config_types | |
| 7 | 10 |
| 8 | 11 |
| 9 def path_method(api, name, base): | 12 def PathHandler(api, test): |
| 10 """Returns a shortcut static method which functions like os.path.join but | 13 def PathHandler_inner(path): |
| 11 with a fixed first component |base|. | 14 assert isinstance(path, recipe_config_types.Path) |
| 12 """ | 15 suffix = '' |
| 13 def path_func_inner(*pieces, **kwargs): | 16 if path.wrapper and api.m.platform.is_win: |
| 14 """Return a path to a file in '%s'. | 17 suffix = '.bat' |
| 15 | 18 base_path = None |
| 16 It supports the following kwargs: | 19 if path.base in api.c.dynamic_paths: |
| 17 wrapper (bool): If true, the path should be considered to be a wrapper | 20 base_path = api.c.dynamic_paths[path.base] |
| 18 script, and will gain the appropriate '.bat' extension | 21 elif path.base in api.c.base_paths: |
| 19 on windows. | 22 if test.enabled: |
| 20 """ | 23 # TODO(iannucci): Remove special case in followup cl |
| 21 use_wrapper = kwargs.get('wrapper') and api.m.platform.is_win | 24 if path.base == 'root': |
| 22 WRAPPER_EXTENSION = '.bat' if use_wrapper else '' | 25 base_path = '[ROOT]' |
| 23 assert api.pardir not in pieces | 26 else: |
| 24 return api.join(base, *filter(bool, pieces)) + WRAPPER_EXTENSION | 27 base_path = '[%s_ROOT]' % path.base.upper() |
| 25 path_func_inner.__name__ = name | 28 else: # pragma: no cover |
| 26 path_func_inner.__doc__ = path_func_inner.__doc__ % base | 29 base_path = api.join(*api.c.base_paths[path.base]) |
| 27 return path_func_inner | 30 assert base_path, 'Could not get base %r for path' % path.base |
| 31 return api.join(base_path, *path.pieces) + suffix | |
| 32 return PathHandler_inner | |
| 28 | 33 |
| 29 | 34 |
| 30 class mock_path(object): | 35 def string_filter(func): |
| 36 @functools.wraps(func) | |
| 37 def inner(*args, **kwargs): | |
| 38 return func(*map(str, args), **kwargs) | |
| 39 return inner | |
| 40 | |
| 41 | |
| 42 class fake_path(object): | |
| 31 """Standin for os.path when we're in test mode. | 43 """Standin for os.path when we're in test mode. |
| 32 | 44 |
| 33 This class simulates the os.path interface exposed by PathApi, respecting the | 45 This class simulates the os.path interface exposed by PathApi, respecting the |
| 34 current platform according to the `platform` module. This allows us to | 46 current platform according to the `platform` module. This allows us to |
| 35 simulate path functions according to the platform being tested, rather than | 47 simulate path functions according to the platform being tested, rather than |
| 36 the platform which is currently running. | 48 the platform which is currently running. |
| 37 """ | 49 """ |
| 38 | 50 |
| 39 def __init__(self, api, _mock_path_exists): | 51 def __init__(self, api, _mock_path_exists): |
| 40 self._api = api | 52 self._api = api |
| 41 self._mock_path_exists = set(_mock_path_exists) | 53 self._mock_path_exists = set(_mock_path_exists) |
| 42 self._pth = None | 54 self._pth = None |
| 43 | 55 |
| 44 def __getattr__(self, name): | 56 def __getattr__(self, name): |
| 45 if not self._pth: | 57 if not self._pth: |
| 46 if self._api.platform.is_win: | 58 if self._api.m.platform.is_win: |
| 47 import ntpath as pth | 59 import ntpath as pth |
| 48 elif self._api.platform.is_mac or self._api.platform.is_linux: | 60 elif self._api.m.platform.is_mac or self._api.m.platform.is_linux: |
| 49 import posixpath as pth | 61 import posixpath as pth |
| 50 self._pth = pth | 62 self._pth = pth |
| 51 return getattr(self._pth, name) | 63 return getattr(self._pth, name) |
| 52 | 64 |
| 53 def _initialize_exists(self): # pylint: disable=E0202 | 65 def _initialize_exists(self): # pylint: disable=E0202 |
| 54 """ | 66 """ |
| 55 Calculates all the parent paths of the mock'd paths and makes exists() | 67 Calculates all the parent paths of the mock'd paths and makes exists() |
| 56 read from this new set(). | 68 read from this new set(). |
| 57 """ | 69 """ |
| 58 self._initialize_exists = lambda: None | 70 self._initialize_exists = lambda: None |
| 59 for path in list(self._mock_path_exists): | 71 for path in list(self._mock_path_exists): |
| 60 self.mock_add_paths(path) | 72 self.mock_add_paths(path) |
| 61 self.exists = lambda path: path in self._mock_path_exists | 73 self.exists = lambda path: path in self._mock_path_exists |
| 62 | 74 |
| 63 def mock_add_paths(self, path): | 75 def mock_add_paths(self, path): |
| 64 """ | 76 """ |
| 65 Adds a path and all of its parents to the set of existing paths. | 77 Adds a path and all of its parents to the set of existing paths. |
| 66 """ | 78 """ |
| 67 self._initialize_exists() | 79 self._initialize_exists() |
| 80 path = str(path) | |
| 68 while path: | 81 while path: |
| 69 self._mock_path_exists.add(path) | 82 self._mock_path_exists.add(path) |
| 70 path = self.dirname(path) | 83 path = self.dirname(path) |
| 71 | 84 |
| 72 def exists(self, path): # pylint: disable=E0202 | 85 def exists(self, path): # pylint: disable=E0202 |
| 73 """Return True if path refers to an existing path.""" | 86 """Return True if path refers to an existing path.""" |
| 74 self._initialize_exists() | 87 self._initialize_exists() |
| 75 return self.exists(path) | 88 return self.exists(path) |
| 76 | 89 |
| 77 def abspath(self, path): | 90 def abspath(self, path): |
| 78 """Returns the absolute version of path.""" | 91 """Returns the absolute version of path.""" |
| 79 path = self.normpath(path) | 92 path = self.normpath(path) |
| 80 if path[0] != '[': # pragma: no cover | 93 if path[0] != '[': # pragma: no cover |
| 81 # We should never really hit this, but simulate the effect. | 94 # We should never really hit this, but simulate the effect. |
| 82 return self.api.slave_build(path) | 95 return self.api.slave_build(path) |
| 83 else: | 96 else: |
| 84 return path | 97 return path |
| 85 | 98 |
| 86 | 99 |
| 87 class PathApi(recipe_api.RecipeApi): | 100 class PathApi(recipe_api.RecipeApi): |
| 88 """ | 101 """ |
| 89 PathApi provides common os.path functions as well as convenience functions | 102 PathApi provides common os.path functions as well as convenience functions |
| 90 for generating absolute paths to things in a testable way. | 103 for generating absolute paths to things in a testable way. |
| 91 | 104 |
| 92 Mocks: | 105 Mocks: |
| 93 exists (list): Paths which should exist in the test case. Thes must be paths | 106 exists (list): Paths which should exist in the test case. Thes must be paths |
| 94 using the [*_ROOT] placeholders. ex. '[BUILD_ROOT]/scripts'. | 107 using the [*_ROOT] placeholders. ex. '[BUILD_ROOT]/scripts'. |
| 95 """ | 108 """ |
| 96 | 109 |
| 97 OK_METHODS = ('abspath', 'basename', 'exists', 'join', 'pardir', | 110 OK_ATTRS = ('pardir', 'sep', 'pathsep') |
| 98 'pathsep', 'sep', 'split', 'splitext') | 111 |
| 112 # Because the native 'path' type in python is a str, we filter the *args | |
| 113 # of these methods to stringify them first (otherwise they would be getting | |
| 114 # recipe_util_types.Path instances). | |
| 115 FILTER_METHODS = ('abspath', 'basename', 'exists', 'join', 'split', | |
|
agable
2013/09/26 21:46:02
Could still call them "OK_METHODS", though, since
iannucci
2013/09/27 02:08:20
Yeah, but I wanted to be extra-clear, and we just
agable
2013/09/27 17:48:16
I would actually prefer that it be called OK_METHO
| |
| 116 'splitext') | |
| 117 | |
| 118 def get_config_defaults(self): | |
| 119 return { 'CURRENT_WORKING_DIR': self._startup_cwd } | |
| 99 | 120 |
| 100 def __init__(self, **kwargs): | 121 def __init__(self, **kwargs): |
| 101 super(PathApi, self).__init__(**kwargs) | 122 super(PathApi, self).__init__(**kwargs) |
| 123 recipe_config_types.Path.set_handler(PathHandler(self, self._test_data)) | |
| 102 | 124 |
| 103 if not self._test_data.enabled: # pragma: no cover | 125 if not self._test_data.enabled: # pragma: no cover |
| 104 self._path_mod = os.path | 126 self._path_mod = os.path |
| 105 # e.g. /b/build/slave/<slavename>/build | 127 # Capture the cwd on process start to avoid shenanigans. |
| 106 self.slave_build = path_method( | 128 startup_cwd = os.path.abspath(os.getcwd()).split(os.path.sep) |
| 107 self, 'slave_build', self.abspath(os.getcwd())) | 129 # Guarantee that the firt element is an absolute drive or the posix root. |
| 130 if ':' in startup_cwd[0]: | |
|
agable
2013/09/26 21:46:02
if startup_cwd[0].endswith(':'):
Also, are windows
iannucci
2013/09/27 02:08:20
Done
| |
| 131 startup_cwd[0] += '\\' | |
| 132 elif startup_cwd[0] == '': | |
| 133 startup_cwd[0] = '/' | |
| 134 else: | |
| 135 assert False, 'What is this, I don\'t even: %r' % startup_cwd | |
|
agable
2013/09/26 21:46:02
As much as I approve... error strings should proba
iannucci
2013/09/27 02:08:20
:(
Done.
:(
| |
| 136 self._startup_cwd = startup_cwd | |
| 137 else: | |
| 138 self._path_mod = fake_path(self, self._test_data.get('exists', [])) | |
| 139 self._startup_cwd = ['/', 'BAG OF CATS'] | |
|
agable
2013/09/26 21:46:02
'FAKE TESTING CWD'? '------------M-A-G-I-C---D-I-V
iannucci
2013/09/27 02:08:20
Done. Though I was very tempted by 'FUNGIBLE BUNNY
| |
| 108 | 140 |
| 109 # e.g. /b | 141 # For now everything works on buildbot, so set it 'automatically' here. |
| 110 r = self.abspath(self.join(self.slave_build(), *([self.pardir]*4))) | 142 self.set_config('buildbot', include_deps=False) |
| 111 for token in ('build_internal', 'build', 'depot_tools'): | |
| 112 # e.g. /b/{token} | |
| 113 setattr(self, token, path_method(self, token, self.join(r, token))) | |
| 114 self.root = path_method(self, 'root', r) | |
| 115 else: | |
| 116 self._path_mod = mock_path(self.m, self._test_data.get('exists', [])) | |
| 117 self.slave_build = path_method(self, 'slave_build', '[SLAVE_BUILD_ROOT]') | |
| 118 self.build_internal = path_method( | |
| 119 self, 'build_internal', '[BUILD_INTERNAL_ROOT]') | |
| 120 self.build = path_method(self, 'build', '[BUILD_ROOT]') | |
| 121 self.depot_tools = path_method(self, 'depot_tools', '[DEPOT_TOOLS_ROOT]') | |
| 122 self.root = path_method(self, 'root', '[ROOT]') | |
| 123 | |
| 124 # Because it only makes sense to call self.checkout() after | |
| 125 # a checkout has been defined, make calls to self.checkout() | |
| 126 # explode with a helpful message until that point. | |
| 127 def _boom(*_args, **_kwargs): # pragma: no cover | |
| 128 assert False, ('Cannot call path.checkout() without calling ' | |
| 129 'path.add_checkout()') | |
| 130 | |
| 131 self._checkouts = [] | |
| 132 self._checkout = _boom | |
| 133 | |
| 134 def checkout(self, *args, **kwargs): | |
| 135 """ | |
| 136 Build a path into the checked out source. | |
| 137 | |
| 138 The checked out source is often a forest of trees possibly inside other | |
| 139 trees. One of these trees' root is designated as special/primary and | |
| 140 this method builds paths inside of it. For Chrome, that would be 'src'. | |
| 141 This defaults to the special root of the first checkout. | |
| 142 """ | |
| 143 return self._checkout(*args, **kwargs) | |
| 144 | 143 |
| 145 def mock_add_paths(self, path): | 144 def mock_add_paths(self, path): |
| 146 """For testing purposes, assert that |path| exists.""" | 145 """For testing purposes, assert that |path| exists.""" |
| 147 if self._test_data.enabled: | 146 if self._test_data.enabled: |
| 148 self._path_mod.mock_add_paths(path) | 147 self._path_mod.mock_add_paths(path) |
| 149 | 148 |
| 150 def add_checkout(self, checkout, *pieces): | |
| 151 """Assert that we have a source directory with this name. """ | |
| 152 checkout = self.join(checkout, *pieces) | |
| 153 self.assert_absolute(checkout) | |
| 154 if not self._checkouts: | |
| 155 self._checkout = path_method(self, 'checkout', checkout) | |
| 156 self._checkouts.append(checkout) | |
| 157 | |
| 158 def choose_checkout(self, checkout, *pieces): # pragma: no cover | |
| 159 assert checkout in self._checkouts, 'No such checkout' | |
| 160 checkout = self.join(checkout, *pieces) | |
| 161 self.assert_absolute(checkout) | |
| 162 self._checkout = path_method(self, 'checkout', checkout) | |
| 163 | |
| 164 def assert_absolute(self, path): | 149 def assert_absolute(self, path): |
| 165 assert self.abspath(path) == path, '%s is not absolute' % path | 150 assert self.abspath(path) == str(path), '%s is not absolute' % path |
| 166 | 151 |
| 167 def makedirs(self, name, path, mode=0777): | 152 def makedirs(self, name, path, mode=0777): |
| 168 """ | 153 """ |
| 169 Like os.makedirs, except that if the directory exists, then there is no | 154 Like os.makedirs, except that if the directory exists, then there is no |
| 170 error. | 155 error. |
| 171 """ | 156 """ |
| 172 self.assert_absolute(path) | 157 self.assert_absolute(path) |
| 173 yield self.m.python.inline( | 158 yield self.m.python.inline( |
| 174 'makedirs ' + name, | 159 'makedirs ' + name, |
| 175 """ | 160 """ |
| 176 import sys, os | 161 import sys, os |
| 177 path = sys.argv[1] | 162 path = sys.argv[1] |
| 178 mode = int(sys.argv[2]) | 163 mode = int(sys.argv[2]) |
| 179 if not os.path.isdir(path): | 164 if not os.path.isdir(path): |
| 180 if os.path.exists(path): | 165 if os.path.exists(path): |
| 181 print "%s exists but is not a dir" % path | 166 print "%s exists but is not a dir" % path |
| 182 sys.exit(1) | 167 sys.exit(1) |
| 183 os.makedirs(path, mode) | 168 os.makedirs(path, mode) |
| 184 """, | 169 """, |
| 185 args=[path, str(mode)], | 170 args=[path, str(mode)], |
| 186 ) | 171 ) |
| 187 self.mock_add_paths(path) | 172 self.mock_add_paths(path) |
| 188 | 173 |
| 174 def set_dynamic_path(self, pathname, path, default=False): | |
| 175 assert isinstance(path, recipe_config_types.Path), ( | |
| 176 'Setting dynamic path to something other than a Path: %r' % path) | |
| 177 assert pathname in self.c.dynamic_paths, ( | |
| 178 'Must declare dynamic path (%r) in config before setting it.' % path) | |
| 179 assert path.base in self.c.base_paths, ( | |
| 180 'Dynamic path values must be based on a base_path.') | |
| 181 if default and self.c.dynamic_paths.get(pathname): | |
|
agable
2013/09/26 21:46:02
'default' is insufficiently descriptive. Maybe 'ov
iannucci
2013/09/27 02:08:20
Done.
| |
| 182 return | |
| 183 self.c.dynamic_paths[pathname] = path | |
| 184 | |
| 189 def __getattr__(self, name): | 185 def __getattr__(self, name): |
| 190 if name in self.OK_METHODS: | 186 if name in self.c.dynamic_paths: |
| 187 r = self.c.dynamic_paths[name] | |
| 188 if r is None: | |
| 189 # Pass back a Path referring to this dynamic path in order to late-bind | |
| 190 # it. Attempting to evaluate this path as a string before it's set is | |
| 191 # an error. | |
| 192 r = recipe_config_types.Path(name) | |
| 193 return r | |
| 194 if name in self.c.base_paths: | |
| 195 return recipe_config_types.Path(name) | |
| 196 if name in self.OK_ATTRS: | |
| 191 return getattr(self._path_mod, name) | 197 return getattr(self._path_mod, name) |
| 198 if name in self.FILTER_METHODS: | |
| 199 return string_filter(getattr(self._path_mod, name)) | |
| 192 raise AttributeError("'%s' object has no attribute '%s'" % | 200 raise AttributeError("'%s' object has no attribute '%s'" % |
| 193 (self._path_mod, name)) # pragma: no cover | 201 (self._path_mod, name)) # pragma: no cover |
| 194 | 202 |
| 195 def __dir__(self): # pragma: no cover | 203 def __dir__(self): # pragma: no cover |
| 196 # Used for helping out show_me_the_modules.py | 204 # Used for helping out show_me_the_modules.py |
| 197 return self.__dict__.keys() + list(self.OK_METHODS) | 205 return self.__dict__.keys() + list(self.OK_METHODS) |
| 198 | |
| 199 # TODO(iannucci): Custom paths? | |
|
agable
2013/09/26 21:46:02
Yayyy.
| |
| OLD | NEW |