| 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 PathTostring(api, test): |
| 10 """Returns a shortcut static method which functions like os.path.join but | 13 def PathTostring_inner(path): |
| 11 with a fixed first component |base|. | 14 assert isinstance(path, recipe_config_types.Path) |
| 12 """ | 15 base_path = None |
| 13 def path_func_inner(*pieces, **kwargs): | 16 suffix = path.platform_ext.get(api.m.platform.name, '') |
| 14 """Return a path to a file in '%s'. | 17 if path.base in api.c.dynamic_paths: |
| 15 | 18 base_path = api.c.dynamic_paths[path.base] |
| 16 It supports the following kwargs: | 19 elif path.base in api.c.base_paths: |
| 17 wrapper (bool): If true, the path should be considered to be a wrapper | 20 if test.enabled: |
| 18 script, and will gain the appropriate '.bat' extension | 21 # TODO(iannucci): Remove special case in followup cl |
| 19 on windows. | 22 if path.base == 'root': |
| 20 """ | 23 base_path = '[ROOT]' |
| 21 use_wrapper = kwargs.get('wrapper') and api.m.platform.is_win | 24 else: |
| 22 WRAPPER_EXTENSION = '.bat' if use_wrapper else '' | 25 base_path = '[%s_ROOT]' % path.base.upper() |
| 23 assert api.pardir not in pieces | 26 else: # pragma: no cover |
| 24 return api.join(base, *filter(bool, pieces)) + WRAPPER_EXTENSION | 27 base_path = api.join(*api.c.base_paths[path.base]) |
| 25 path_func_inner.__name__ = name | 28 assert base_path, 'Could not get base %r for path' % path.base |
| 26 path_func_inner.__doc__ = path_func_inner.__doc__ % base | 29 return api.join(base_path, *path.pieces) + suffix |
| 27 return path_func_inner | 30 return PathTostring_inner |
| 28 | 31 |
| 29 | 32 |
| 30 class mock_path(object): | 33 def string_filter(func): |
| 34 @functools.wraps(func) |
| 35 def inner(*args, **kwargs): |
| 36 return func(*map(str, args), **kwargs) |
| 37 return inner |
| 38 |
| 39 |
| 40 class fake_path(object): |
| 31 """Standin for os.path when we're in test mode. | 41 """Standin for os.path when we're in test mode. |
| 32 | 42 |
| 33 This class simulates the os.path interface exposed by PathApi, respecting the | 43 This class simulates the os.path interface exposed by PathApi, respecting the |
| 34 current platform according to the `platform` module. This allows us to | 44 current platform according to the `platform` module. This allows us to |
| 35 simulate path functions according to the platform being tested, rather than | 45 simulate path functions according to the platform being tested, rather than |
| 36 the platform which is currently running. | 46 the platform which is currently running. |
| 37 """ | 47 """ |
| 38 | 48 |
| 39 def __init__(self, api, _mock_path_exists): | 49 def __init__(self, api, _mock_path_exists): |
| 40 self._api = api | 50 self._api = api |
| 41 self._mock_path_exists = set(_mock_path_exists) | 51 self._mock_path_exists = set(_mock_path_exists) |
| 42 self._pth = None | 52 self._pth = None |
| 43 | 53 |
| 44 def __getattr__(self, name): | 54 def __getattr__(self, name): |
| 45 if not self._pth: | 55 if not self._pth: |
| 46 if self._api.platform.is_win: | 56 if self._api.m.platform.is_win: |
| 47 import ntpath as pth | 57 import ntpath as pth |
| 48 elif self._api.platform.is_mac or self._api.platform.is_linux: | 58 elif self._api.m.platform.is_mac or self._api.m.platform.is_linux: |
| 49 import posixpath as pth | 59 import posixpath as pth |
| 50 self._pth = pth | 60 self._pth = pth |
| 51 return getattr(self._pth, name) | 61 return getattr(self._pth, name) |
| 52 | 62 |
| 53 def _initialize_exists(self): # pylint: disable=E0202 | 63 def _initialize_exists(self): # pylint: disable=E0202 |
| 54 """ | 64 """ |
| 55 Calculates all the parent paths of the mock'd paths and makes exists() | 65 Calculates all the parent paths of the mock'd paths and makes exists() |
| 56 read from this new set(). | 66 read from this new set(). |
| 57 """ | 67 """ |
| 58 self._initialize_exists = lambda: None | 68 self._initialize_exists = lambda: None |
| 59 for path in list(self._mock_path_exists): | 69 for path in list(self._mock_path_exists): |
| 60 self.mock_add_paths(path) | 70 self.mock_add_paths(path) |
| 61 self.exists = lambda path: path in self._mock_path_exists | 71 self.exists = lambda path: path in self._mock_path_exists |
| 62 | 72 |
| 63 def mock_add_paths(self, path): | 73 def mock_add_paths(self, path): |
| 64 """ | 74 """ |
| 65 Adds a path and all of its parents to the set of existing paths. | 75 Adds a path and all of its parents to the set of existing paths. |
| 66 """ | 76 """ |
| 67 self._initialize_exists() | 77 self._initialize_exists() |
| 78 path = str(path) |
| 68 while path: | 79 while path: |
| 69 self._mock_path_exists.add(path) | 80 self._mock_path_exists.add(path) |
| 70 path = self.dirname(path) | 81 path = self.dirname(path) |
| 71 | 82 |
| 72 def exists(self, path): # pylint: disable=E0202 | 83 def exists(self, path): # pylint: disable=E0202 |
| 73 """Return True if path refers to an existing path.""" | 84 """Return True if path refers to an existing path.""" |
| 74 self._initialize_exists() | 85 self._initialize_exists() |
| 75 return self.exists(path) | 86 return self.exists(path) |
| 76 | 87 |
| 77 def abspath(self, path): | 88 def abspath(self, path): |
| 78 """Returns the absolute version of path.""" | 89 """Returns the absolute version of path.""" |
| 79 path = self.normpath(path) | 90 path = self.normpath(path) |
| 80 if path[0] != '[': # pragma: no cover | 91 if path[0] != '[': # pragma: no cover |
| 81 # We should never really hit this, but simulate the effect. | 92 # We should never really hit this, but simulate the effect. |
| 82 return self.api.slave_build(path) | 93 return self.api.slave_build(path) |
| 83 else: | 94 else: |
| 84 return path | 95 return path |
| 85 | 96 |
| 86 | 97 |
| 87 class PathApi(recipe_api.RecipeApi): | 98 class PathApi(recipe_api.RecipeApi): |
| 88 """ | 99 """ |
| 89 PathApi provides common os.path functions as well as convenience functions | 100 PathApi provides common os.path functions as well as convenience functions |
| 90 for generating absolute paths to things in a testable way. | 101 for generating absolute paths to things in a testable way. |
| 91 | 102 |
| 92 Mocks: | 103 Mocks: |
| 93 exists (list): Paths which should exist in the test case. Thes must be paths | 104 exists (list): Paths which should exist in the test case. Thes must be paths |
| 94 using the [*_ROOT] placeholders. ex. '[BUILD_ROOT]/scripts'. | 105 using the [*_ROOT] placeholders. ex. '[BUILD_ROOT]/scripts'. |
| 95 """ | 106 """ |
| 96 | 107 |
| 97 OK_METHODS = ('abspath', 'basename', 'exists', 'join', 'pardir', | 108 OK_ATTRS = ('pardir', 'sep', 'pathsep') |
| 98 'pathsep', 'sep', 'split', 'splitext') | 109 |
| 110 # Because the native 'path' type in python is a str, we filter the *args |
| 111 # of these methods to stringify them first (otherwise they would be getting |
| 112 # recipe_util_types.Path instances). |
| 113 FILTER_METHODS = ('abspath', 'basename', 'exists', 'join', 'split', |
| 114 'splitext') |
| 115 |
| 116 def get_config_defaults(self): |
| 117 return { 'CURRENT_WORKING_DIR': self._startup_cwd } |
| 99 | 118 |
| 100 def __init__(self, **kwargs): | 119 def __init__(self, **kwargs): |
| 101 super(PathApi, self).__init__(**kwargs) | 120 super(PathApi, self).__init__(**kwargs) |
| 121 recipe_config_types.Path.set_tostring_fn( |
| 122 PathTostring(self, self._test_data)) |
| 102 | 123 |
| 103 if not self._test_data.enabled: # pragma: no cover | 124 if not self._test_data.enabled: # pragma: no cover |
| 104 self._path_mod = os.path | 125 self._path_mod = os.path |
| 105 # e.g. /b/build/slave/<slavename>/build | 126 # Capture the cwd on process start to avoid shenanigans. |
| 106 self.slave_build = path_method( | 127 startup_cwd = os.path.abspath(os.getcwd()).split(os.path.sep) |
| 107 self, 'slave_build', self.abspath(os.getcwd())) | 128 # Guarantee that the firt element is an absolute drive or the posix root. |
| 129 if startup_cwd[0].endswith(':'): |
| 130 startup_cwd[0] += '\\' |
| 131 elif startup_cwd[0] == '': |
| 132 startup_cwd[0] = '/' |
| 133 else: |
| 134 assert False, 'Got unexpected startup_cwd format: %r' % startup_cwd |
| 135 self._startup_cwd = startup_cwd |
| 136 else: |
| 137 self._path_mod = fake_path(self, self._test_data.get('exists', [])) |
| 138 self._startup_cwd = ['/', 'FakeTestingCWD'] |
| 108 | 139 |
| 109 # e.g. /b | 140 # For now everything works on buildbot, so set it 'automatically' here. |
| 110 r = self.abspath(self.join(self.slave_build(), *([self.pardir]*4))) | 141 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 | 142 |
| 145 def mock_add_paths(self, path): | 143 def mock_add_paths(self, path): |
| 146 """For testing purposes, assert that |path| exists.""" | 144 """For testing purposes, assert that |path| exists.""" |
| 147 if self._test_data.enabled: | 145 if self._test_data.enabled: |
| 148 self._path_mod.mock_add_paths(path) | 146 self._path_mod.mock_add_paths(path) |
| 149 | 147 |
| 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): | 148 def assert_absolute(self, path): |
| 165 assert self.abspath(path) == path, '%s is not absolute' % path | 149 assert self.abspath(path) == str(path), '%s is not absolute' % path |
| 166 | 150 |
| 167 def makedirs(self, name, path, mode=0777): | 151 def makedirs(self, name, path, mode=0777): |
| 168 """ | 152 """ |
| 169 Like os.makedirs, except that if the directory exists, then there is no | 153 Like os.makedirs, except that if the directory exists, then there is no |
| 170 error. | 154 error. |
| 171 """ | 155 """ |
| 172 self.assert_absolute(path) | 156 self.assert_absolute(path) |
| 173 yield self.m.python.inline( | 157 yield self.m.python.inline( |
| 174 'makedirs ' + name, | 158 'makedirs ' + name, |
| 175 """ | 159 """ |
| 176 import sys, os | 160 import sys, os |
| 177 path = sys.argv[1] | 161 path = sys.argv[1] |
| 178 mode = int(sys.argv[2]) | 162 mode = int(sys.argv[2]) |
| 179 if not os.path.isdir(path): | 163 if not os.path.isdir(path): |
| 180 if os.path.exists(path): | 164 if os.path.exists(path): |
| 181 print "%s exists but is not a dir" % path | 165 print "%s exists but is not a dir" % path |
| 182 sys.exit(1) | 166 sys.exit(1) |
| 183 os.makedirs(path, mode) | 167 os.makedirs(path, mode) |
| 184 """, | 168 """, |
| 185 args=[path, str(mode)], | 169 args=[path, str(mode)], |
| 186 ) | 170 ) |
| 187 self.mock_add_paths(path) | 171 self.mock_add_paths(path) |
| 188 | 172 |
| 173 def set_dynamic_path(self, pathname, path, overwrite=True): |
| 174 """Set a named dynamic path to a concrete value. |
| 175 * path must be based on a real base_path (not another dynamic path) |
| 176 * if overwrite is False and the path is already set, do nothing. |
| 177 """ |
| 178 assert isinstance(path, recipe_config_types.Path), ( |
| 179 'Setting dynamic path to something other than a Path: %r' % path) |
| 180 assert pathname in self.c.dynamic_paths, ( |
| 181 'Must declare dynamic path (%r) in config before setting it.' % path) |
| 182 assert path.base in self.c.base_paths, ( |
| 183 'Dynamic path values must be based on a base_path.') |
| 184 if not overwrite and self.c.dynamic_paths.get(pathname): |
| 185 return |
| 186 self.c.dynamic_paths[pathname] = path |
| 187 |
| 189 def __getattr__(self, name): | 188 def __getattr__(self, name): |
| 190 if name in self.OK_METHODS: | 189 if name in self.c.dynamic_paths: |
| 190 r = self.c.dynamic_paths[name] |
| 191 if r is None: |
| 192 # Pass back a Path referring to this dynamic path in order to late-bind |
| 193 # it. Attempting to evaluate this path as a string before it's set is |
| 194 # an error. |
| 195 r = recipe_config_types.Path(name) |
| 196 return r |
| 197 if name in self.c.base_paths: |
| 198 return recipe_config_types.Path(name) |
| 199 if name in self.OK_ATTRS: |
| 191 return getattr(self._path_mod, name) | 200 return getattr(self._path_mod, name) |
| 201 if name in self.FILTER_METHODS: |
| 202 return string_filter(getattr(self._path_mod, name)) |
| 192 raise AttributeError("'%s' object has no attribute '%s'" % | 203 raise AttributeError("'%s' object has no attribute '%s'" % |
| 193 (self._path_mod, name)) # pragma: no cover | 204 (self._path_mod, name)) # pragma: no cover |
| 194 | 205 |
| 195 def __dir__(self): # pragma: no cover | 206 def __dir__(self): # pragma: no cover |
| 196 # Used for helping out show_me_the_modules.py | 207 # Used for helping out show_me_the_modules.py |
| 197 return self.__dict__.keys() + list(self.OK_METHODS) | 208 return self.__dict__.keys() + list(self.OK_METHODS) |
| 198 | |
| 199 # TODO(iannucci): Custom paths? | |
| OLD | NEW |