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 blacklist, hoping two 207 # wrongs will somehow make it right. 208 blacklist = { 209 'site-packages', 'venv', 'results', 'logs', 'containers', 210 } 211 while len(directories) > 0: 212 directory = directories.pop() 213 if not os.path.exists(directory): 214 continue 215 try: 216 for name in os.listdir(directory): 217 if name in blacklist: 218 continue 219 fullpath = os.path.join(directory, name) 220 if os.path.isfile(fullpath): 221 if regexp.search(name): 222 # if we are a control file 223 self._files.append(fullpath) 224 elif (not os.path.islink(fullpath) 225 and os.path.isdir(fullpath)): 226 directories.append(fullpath) 227 except OSError: 228 # Some directories under results/ like the Chrome Crash 229 # Reports will cause issues when attempted to be searched. 230 logging.error('Unable to search directory %s for control ' 231 'files.', directory) 232 pass 233 if not self._files: 234 msg = 'No control files under ' + ','.join(self._paths) 235 raise error.NoControlFileList(msg) 236 return [f for f in self._files if self._is_useful_file(f)] 237 238 239 def get_control_file_contents(self, test_path): 240 """ 241 Get the contents of the control file at |test_path|. 242 243 @return The contents of the aforementioned file. 244 @throws ControlFileNotFound if the file cannot be retrieved. 245 """ 246 try: 247 return utils.read_file(test_path) 248 except EnvironmentError as errs: 249 (errno, strerror) = errs.args 250 msg = "Can't retrieve {0}: {1} ({2})".format(test_path, 251 strerror, 252 errno) 253 raise error.ControlFileNotFound(msg) 254 255 256class DevServerGetter(CacheingAndFilteringControlFileGetter, 257 SuiteControlFileGetter): 258 """Class that can list and fetch known control files from DevServer. 259 260 @var _CONTROL_PATTERN: control file name format to match. 261 """ 262 def __init__(self, build, ds): 263 """ 264 @param build: The build from which to get control files. 265 @param ds: An existing dev_server.DevServer object to use. 266 """ 267 super(DevServerGetter, self).__init__() 268 self._dev_server = ds 269 self._build = build 270 271 272 @staticmethod 273 def create(build, ds=None): 274 """Wraps constructor. Can be mocked for testing purposes. 275 @param build: The build from which to get control files. 276 @param ds: An existing dev_server.DevServer object to use 277 (default=None) 278 @returns: New DevServerGetter. 279 """ 280 return DevServerGetter(build, ds) 281 282 283 def _get_control_file_list(self, suite_name=''): 284 """ 285 Gather a list of paths to control files from |self._dev_server|. 286 287 Get a listing of all the control files for |self._build| on 288 |self._dev_server|. Populates |self._files| with that list 289 and then returns paths (under the autotest dir) to them. If suite_name 290 is specified, this method populates |self._files| with the control 291 files from just the specified suite. 292 293 @param suite_name: The name of a suite we would like control files for. 294 @return A list of control file paths. 295 @throws NoControlFileList if there is an error while listing. 296 """ 297 try: 298 return self._dev_server.list_control_files(self._build, 299 suite_name=suite_name) 300 except dev_server.DevServerException as e: 301 raise error.NoControlFileList(e) 302 303 304 def get_control_file_contents(self, test_path): 305 """ 306 Return the contents of |test_path| from |self._dev_server|. 307 308 Get the contents of the control file at |test_path| for |self._build| on 309 |self._dev_server|. 310 311 @return The contents of |test_path|. None on failure. 312 @throws ControlFileNotFound if the file cannot be retrieved. 313 """ 314 try: 315 return self._dev_server.get_control_file(self._build, test_path) 316 except dev_server.DevServerException as e: 317 raise error.ControlFileNotFound(e) 318 319 320 def _list_suite_controls(self, suite_name=''): 321 """ 322 Gather a dict {path:content} of all control files from 323 |self._dev_server|. 324 325 Get a dict of contents of all the control files for |self._build| on 326 |self._dev_server|: path is the key, and the control file content is 327 the value. 328 329 @param suite_name: The name of a suite we would like control files for. 330 @return A dict of paths and contents of all control files. 331 @throws NoControlFileList if there is an error while listing. 332 """ 333 try: 334 return self._dev_server.list_suite_controls(self._build, 335 suite_name=suite_name) 336 except dev_server.DevServerException as e: 337 raise error.SuiteControlFileException(e) 338 339 340 def get_suite_info(self, suite_name=''): 341 """ 342 Gather info of a list of control files from |self._dev_server|. 343 344 The info is a dict: {control_path: control_file_content} for 345 |self._build| on |self._dev_server|. 346 347 @param suite_name: The name of a suite we would like control files for. 348 @return A dict of paths and contents of all control files: 349 {path1: content1, path2: content2, ..., pathX: contentX} 350 """ 351 file_contents = self._list_suite_controls(suite_name=suite_name) 352 files = list(file_contents.keys()) 353 for cf_filter in self.CONTROL_FILE_FILTERS: 354 files = [path for path in files if not path.endswith(cf_filter)] 355 self._files = files 356 return {f: file_contents[f] for f in files} 357