1# Lint as: python2, python3 2# Copyright (c) 2012 The Chromium OS 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 6from __future__ import absolute_import 7from __future__ import division 8from __future__ import print_function 9 10import abc 11import logging 12import os 13import re 14 15import common 16from autotest_lib.client.common_lib import error, utils 17from autotest_lib.client.common_lib.cros import dev_server 18import six 19from six.moves import filter 20 21 22# Relevant CrosDynamicSuiteExceptions are defined in client/common_lib/error.py. 23 24 25class ControlFileGetter(six.with_metaclass(abc.ABCMeta, object)): 26 """ 27 Interface for classes that can list and fetch known control files. 28 """ 29 30 31 @abc.abstractmethod 32 def get_control_file_list(self, suite_name=''): 33 """ 34 Gather a list of paths to control files. 35 36 @param suite_name: The name of a suite we would like control files for. 37 @return A list of file paths. 38 @throws NoControlFileList if there is an error while listing. 39 """ 40 pass 41 42 43 @abc.abstractmethod 44 def get_control_file_contents(self, test_path): 45 """ 46 Given a path to a control file, return its contents. 47 48 @param test_path: the path to the control file. 49 @return the contents of the control file specified by the path. 50 @throws ControlFileNotFound if the file cannot be retrieved. 51 """ 52 pass 53 54 55 @abc.abstractmethod 56 def get_control_file_contents_by_name(self, test_name): 57 """ 58 Given the name of a control file, return its contents. 59 60 @param test_name: the name of the test whose control file is desired. 61 @return the contents of the control file specified by the name. 62 @throws ControlFileNotFound if the file cannot be retrieved. 63 """ 64 pass 65 66 67class SuiteControlFileGetter(ControlFileGetter): 68 """Interface that additionally supports getting by suite.""" 69 70 71 @abc.abstractmethod 72 def get_suite_info(self, suite_name=''): 73 """ 74 Gather the control paths and contents of all the control files. 75 76 @param suite_name: The name of a suite we would like control files for. 77 @return the control paths and contents of all the control files 78 specified by the name. 79 @throws SuiteControlFileException if the info cannot be retrieved. 80 """ 81 pass 82 83 84class CacheingAndFilteringControlFileGetter(ControlFileGetter): 85 """Wraps ControlFileGetter to cache the retrieved control file list and 86 filter out unwanted control files.""" 87 88 CONTROL_FILE_FILTERS = ['src/debian/control'] 89 90 def __init__(self): 91 super(CacheingAndFilteringControlFileGetter, self).__init__() 92 self._files = [] 93 94 95 def get_control_file_list(self, suite_name=''): 96 """ 97 Gather a list of paths to control files. 98 99 Gets a list of control files; populates |self._files| with that list 100 and then returns the paths to all useful and wanted files in the list. 101 102 @param suite_name: The name of a suite we would like control files for. 103 @return A list of file paths. 104 @throws NoControlFileList if there is an error while listing. 105 """ 106 files = self._get_control_file_list(suite_name=suite_name) 107 for cf_filter in self.CONTROL_FILE_FILTERS: 108 files = [path for path in files if not path.endswith(cf_filter)] 109 self._files = files 110 return self._files 111 112 113 @abc.abstractmethod 114 def _get_control_file_list(self, suite_name=''): 115 pass 116 117 118 def get_control_file_path(self, test_name): 119 """ 120 Given the name of a control file, return its path. 121 122 Searches through previously-compiled list in |self._files| for a 123 test named |test_name| and returns the contents of the control file 124 for that test if it is found. 125 126 @param test_name: the name of the test whose control file is desired. 127 @return control file path 128 @throws ControlFileNotFound if the file cannot be retrieved. 129 """ 130 if not self._files and not self.get_control_file_list(): 131 raise error.ControlFileNotFound('No control files found.') 132 133 if 'control' not in test_name: 134 regexp = re.compile(os.path.join(test_name, 'control$')) 135 else: 136 regexp = re.compile(test_name + '$') 137 candidates = list(filter(regexp.search, self._files)) 138 if not candidates: 139 logging.debug('Cannot find %s in %r', regexp.pattern, self._files) 140 raise error.ControlFileNotFound('No control file for ' + test_name) 141 if len(candidates) > 1: 142 raise error.ControlFileNotFound(test_name + ' is not unique.') 143 return candidates[0] 144 145 146 def get_control_file_contents_by_name(self, test_name): 147 """ 148 Given the name of a control file, return its contents. 149 150 Searches through previously-compiled list in |self._files| for a 151 test named |test_name| and returns the contents of the control file 152 for that test if it is found. 153 154 @param test_name: the name of the test whose control file is desired. 155 @return the contents of the control file specified by the name. 156 @throws ControlFileNotFound if the file cannot be retrieved. 157 """ 158 path = self.get_control_file_path(test_name) 159 return self.get_control_file_contents(path) 160 161 162class FileSystemGetter(CacheingAndFilteringControlFileGetter): 163 """ 164 Class that can list and fetch known control files from disk. 165 166 @var _CONTROL_PATTERN: control file name format to match. 167 """ 168 169 _CONTROL_PATTERN = '^control(?:\..+)?$' 170 171 def __init__(self, paths): 172 """ 173 @param paths: base directories to start search. 174 """ 175 super(FileSystemGetter, self).__init__() 176 self._paths = paths 177 178 179 def _is_useful_file(self, name): 180 return '__init__.py' not in name and '.svn' not in name 181 182 183 def _get_control_file_list(self, suite_name=''): 184 """ 185 Gather a list of paths to control files under |self._paths|. 186 187 Searches under |self._paths| for files that match 188 |self._CONTROL_PATTERN|. Populates |self._files| with that list 189 and then returns the paths to all useful files in the list. 190 191 @param suite_name: The name of a suite we would like control files for. 192 @return A list of files that match |self._CONTROL_PATTERN|. 193 @throws NoControlFileList if we find no files. 194 """ 195 if suite_name: 196 logging.debug('Getting control files for a specific suite has ' 197 'not been implemented for FileSystemGetter. ' 198 'Getting all control files instead.') 199 200 201 regexp = re.compile(self._CONTROL_PATTERN) 202 directories = self._paths 203 # Some of our callers are ill-considered and request that we 204 # search all of /usr/local/autotest (crbug.com/771823). 205 # Fixing the callers immediately is somewhere between a 206 # nuisance and hard. So, we have a blocklist, hoping two 207 # wrongs will somehow make it right. 208 blocklist = { 209 'site-packages', 210 'venv', 211 'results', 212 'logs', 213 'containers', 214 } 215 while len(directories) > 0: 216 directory = directories.pop() 217 if not os.path.exists(directory): 218 continue 219 try: 220 for name in os.listdir(directory): 221 if name in blocklist: 222 continue 223 fullpath = os.path.join(directory, name) 224 if os.path.isfile(fullpath): 225 if regexp.search(name): 226 # if we are a control file 227 self._files.append(fullpath) 228 elif (not os.path.islink(fullpath) 229 and os.path.isdir(fullpath)): 230 directories.append(fullpath) 231 except OSError: 232 # Some directories under results/ like the Chrome Crash 233 # Reports will cause issues when attempted to be searched. 234 logging.error('Unable to search directory %s for control ' 235 'files.', directory) 236 pass 237 if not self._files: 238 msg = 'No control files under ' + ','.join(self._paths) 239 raise error.NoControlFileList(msg) 240 return [f for f in self._files if self._is_useful_file(f)] 241 242 243 def get_control_file_contents(self, test_path): 244 """ 245 Get the contents of the control file at |test_path|. 246 247 @return The contents of the aforementioned file. 248 @throws ControlFileNotFound if the file cannot be retrieved. 249 """ 250 try: 251 return utils.read_file(test_path) 252 except EnvironmentError as errs: 253 (errno, strerror) = errs.args 254 msg = "Can't retrieve {0}: {1} ({2})".format(test_path, 255 strerror, 256 errno) 257 raise error.ControlFileNotFound(msg) 258 259 260class DevServerGetter(CacheingAndFilteringControlFileGetter, 261 SuiteControlFileGetter): 262 """Class that can list and fetch known control files from DevServer. 263 264 @var _CONTROL_PATTERN: control file name format to match. 265 """ 266 def __init__(self, build, ds): 267 """ 268 @param build: The build from which to get control files. 269 @param ds: An existing dev_server.DevServer object to use. 270 """ 271 super(DevServerGetter, self).__init__() 272 self._dev_server = ds 273 self._build = build 274 275 276 @staticmethod 277 def create(build, ds=None): 278 """Wraps constructor. Can be mocked for testing purposes. 279 @param build: The build from which to get control files. 280 @param ds: An existing dev_server.DevServer object to use 281 (default=None) 282 @returns: New DevServerGetter. 283 """ 284 return DevServerGetter(build, ds) 285 286 287 def _get_control_file_list(self, suite_name=''): 288 """ 289 Gather a list of paths to control files from |self._dev_server|. 290 291 Get a listing of all the control files for |self._build| on 292 |self._dev_server|. Populates |self._files| with that list 293 and then returns paths (under the autotest dir) to them. If suite_name 294 is specified, this method populates |self._files| with the control 295 files from just the specified suite. 296 297 @param suite_name: The name of a suite we would like control files for. 298 @return A list of control file paths. 299 @throws NoControlFileList if there is an error while listing. 300 """ 301 try: 302 return self._dev_server.list_control_files(self._build, 303 suite_name=suite_name) 304 except dev_server.DevServerException as e: 305 raise error.NoControlFileList(e) 306 307 308 def get_control_file_contents(self, test_path): 309 """ 310 Return the contents of |test_path| from |self._dev_server|. 311 312 Get the contents of the control file at |test_path| for |self._build| on 313 |self._dev_server|. 314 315 @return The contents of |test_path|. None on failure. 316 @throws ControlFileNotFound if the file cannot be retrieved. 317 """ 318 try: 319 return self._dev_server.get_control_file(self._build, test_path) 320 except dev_server.DevServerException as e: 321 raise error.ControlFileNotFound(e) 322 323 324 def _list_suite_controls(self, suite_name=''): 325 """ 326 Gather a dict {path:content} of all control files from 327 |self._dev_server|. 328 329 Get a dict of contents of all the control files for |self._build| on 330 |self._dev_server|: path is the key, and the control file content is 331 the value. 332 333 @param suite_name: The name of a suite we would like control files for. 334 @return A dict of paths and contents of all control files. 335 @throws NoControlFileList if there is an error while listing. 336 """ 337 try: 338 return self._dev_server.list_suite_controls(self._build, 339 suite_name=suite_name) 340 except dev_server.DevServerException as e: 341 raise error.SuiteControlFileException(e) 342 343 344 def get_suite_info(self, suite_name=''): 345 """ 346 Gather info of a list of control files from |self._dev_server|. 347 348 The info is a dict: {control_path: control_file_content} for 349 |self._build| on |self._dev_server|. 350 351 @param suite_name: The name of a suite we would like control files for. 352 @return A dict of paths and contents of all control files: 353 {path1: content1, path2: content2, ..., pathX: contentX} 354 """ 355 file_contents = self._list_suite_controls(suite_name=suite_name) 356 files = list(file_contents.keys()) 357 for cf_filter in self.CONTROL_FILE_FILTERS: 358 files = [path for path in files if not path.endswith(cf_filter)] 359 self._files = files 360 return {f: file_contents[f] for f in files} 361