1 #!/usr/bin/python 2 """ 3 Script to verify errors on autotest code contributions (patches). 4 The workflow is as follows: 5 6 * Patch will be applied and eventual problems will be notified. 7 * If there are new files created, remember user to add them to VCS. 8 * If any added file looks like a executable file, remember user to make them 9 executable. 10 * If any of the files added or modified introduces trailing whitespaces, tabs 11 or incorrect indentation, report problems. 12 * If any of the files have problems during pylint validation, report failures. 13 * If any of the files changed have a unittest suite, run the unittest suite 14 and report any failures. 15 16 Usage: check_patch.py -p [/path/to/patch] 17 check_patch.py -i [patchwork id] 18 19 @copyright: Red Hat Inc, 2009. 20 @author: Lucas Meneghel Rodrigues <lmr (at] redhat.com> 21 """ 22 23 import os, stat, logging, sys, optparse, time 24 import common 25 from autotest_lib.client.common_lib import utils, error, logging_config 26 from autotest_lib.client.common_lib import logging_manager 27 28 29 class CheckPatchLoggingConfig(logging_config.LoggingConfig): 30 def configure_logging(self, results_dir=None, verbose=False): 31 super(CheckPatchLoggingConfig, self).configure_logging(use_console=True, 32 verbose=verbose) 33 34 35 class VCS(object): 36 """ 37 Abstraction layer to the version control system. 38 """ 39 def __init__(self): 40 """ 41 Class constructor. Guesses the version control name and instantiates it 42 as a backend. 43 """ 44 backend_name = self.guess_vcs_name() 45 if backend_name == "SVN": 46 self.backend = SubVersionBackend() 47 48 49 def guess_vcs_name(self): 50 if os.path.isdir(".svn"): 51 return "SVN" 52 else: 53 logging.error("Could not figure version control system. Are you " 54 "on a working directory? Aborting.") 55 sys.exit(1) 56 57 58 def get_unknown_files(self): 59 """ 60 Return a list of files unknown to the VCS. 61 """ 62 return self.backend.get_unknown_files() 63 64 65 def get_modified_files(self): 66 """ 67 Return a list of files that were modified, according to the VCS. 68 """ 69 return self.backend.get_modified_files() 70 71 72 def add_untracked_file(self, file): 73 """ 74 Add an untracked file to version control. 75 """ 76 return self.backend.add_untracked_file(file) 77 78 79 def revert_file(self, file): 80 """ 81 Restore file according to the latest state on the reference repo. 82 """ 83 return self.backend.revert_file(file) 84 85 86 def apply_patch(self, patch): 87 """ 88 Applies a patch using the most appropriate method to the particular VCS. 89 """ 90 return self.backend.apply_patch(patch) 91 92 93 def update(self): 94 """ 95 Updates the tree according to the latest state of the public tree 96 """ 97 return self.backend.update() 98 99 100 class SubVersionBackend(object): 101 """ 102 Implementation of a subversion backend for use with the VCS abstraction 103 layer. 104 """ 105 def __init__(self): 106 logging.debug("Subversion VCS backend initialized.") 107 self.ignored_extension_list = ['.orig', '.bak'] 108 109 110 def get_unknown_files(self): 111 status = utils.system_output("svn status --ignore-externals") 112 unknown_files = [] 113 for line in status.split("\n"): 114 status_flag = line[0] 115 if line and status_flag == "?": 116 for extension in self.ignored_extension_list: 117 if not line.endswith(extension): 118 unknown_files.append(line[1:].strip()) 119 return unknown_files 120 121 122 def get_modified_files(self): 123 status = utils.system_output("svn status --ignore-externals") 124 modified_files = [] 125 for line in status.split("\n"): 126 status_flag = line[0] 127 if line and status_flag == "M" or status_flag == "A": 128 modified_files.append(line[1:].strip()) 129 return modified_files 130 131 132 def add_untracked_file(self, file): 133 """ 134 Add an untracked file under revision control. 135 136 @param file: Path to untracked file. 137 """ 138 try: 139 utils.run('svn add %s' % file) 140 except error.CmdError, e: 141 logging.error("Problem adding file %s to svn: %s", file, e) 142 sys.exit(1) 143 144 145 def revert_file(self, file): 146 """ 147 Revert file against last revision. 148 149 @param file: Path to file to be reverted. 150 """ 151 try: 152 utils.run('svn revert %s' % file) 153 except error.CmdError, e: 154 logging.error("Problem reverting file %s: %s", file, e) 155 sys.exit(1) 156 157 158 def apply_patch(self, patch): 159 """ 160 Apply a patch to the code base. Patches are expected to be made using 161 level -p1, and taken according to the code base top level. 162 163 @param patch: Path to the patch file. 164 """ 165 try: 166 utils.system_output("patch -p1 < %s" % patch) 167 except: 168 logging.error("Patch applied incorrectly. Possible causes: ") 169 logging.error("1 - Patch might not be -p1") 170 logging.error("2 - You are not at the top of the autotest tree") 171 logging.error("3 - Patch was made using an older tree") 172 logging.error("4 - Mailer might have messed the patch") 173 sys.exit(1) 174 175 def update(self): 176 try: 177 utils.system("svn update", ignore_status=True) 178 except error.CmdError, e: 179 logging.error("SVN tree update failed: %s" % e) 180 181 182 class FileChecker(object): 183 """ 184 Picks up a given file and performs various checks, looking after problems 185 and eventually suggesting solutions. 186 """ 187 def __init__(self, path, confirm=False): 188 """ 189 Class constructor, sets the path attribute. 190 191 @param path: Path to the file that will be checked. 192 @param confirm: Whether to answer yes to all questions asked without 193 prompting the user. 194 """ 195 self.path = path 196 self.confirm = confirm 197 self.basename = os.path.basename(self.path) 198 if self.basename.endswith('.py'): 199 self.is_python = True 200 else: 201 self.is_python = False 202 203 mode = os.stat(self.path)[stat.ST_MODE] 204 if mode & stat.S_IXUSR: 205 self.is_executable = True 206 else: 207 self.is_executable = False 208 209 checked_file = open(self.path, "r") 210 self.first_line = checked_file.readline() 211 checked_file.close() 212 self.corrective_actions = [] 213 self.indentation_exceptions = ['job_unittest.py'] 214 215 216 def _check_indent(self): 217 """ 218 Verifies the file with reindent.py. This tool performs the following 219 checks on python files: 220 221 * Trailing whitespaces 222 * Tabs 223 * End of line 224 * Incorrect indentation 225 226 For the purposes of checking, the dry run mode is used and no changes 227 are made. It is up to the user to decide if he wants to run reindent 228 to correct the issues. 229 """ 230 reindent_raw = utils.system_output('reindent.py -v -d %s | head -1' % 231 self.path) 232 reindent_results = reindent_raw.split(" ")[-1].strip(".") 233 if reindent_results == "changed": 234 if self.basename not in self.indentation_exceptions: 235 self.corrective_actions.append("reindent.py -v %s" % self.path) 236 237 238 def _check_code(self): 239 """ 240 Verifies the file with run_pylint.py. This tool will call the static 241 code checker pylint using the special autotest conventions and warn 242 only on problems. If problems are found, a report will be generated. 243 Some of the problems reported might be bogus, but it's allways good 244 to look at them. 245 """ 246 c_cmd = 'run_pylint.py %s' % self.path 247 rc = utils.system(c_cmd, ignore_status=True) 248 if rc != 0: 249 logging.error("Syntax issues found during '%s'", c_cmd) 250 251 252 def _check_unittest(self): 253 """ 254 Verifies if the file in question has a unittest suite, if so, run the 255 unittest and report on any failures. This is important to keep our 256 unit tests up to date. 257 """ 258 if "unittest" not in self.basename: 259 stripped_name = self.basename.strip(".py") 260 unittest_name = stripped_name + "_unittest.py" 261 unittest_path = self.path.replace(self.basename, unittest_name) 262 if os.path.isfile(unittest_path): 263 unittest_cmd = 'python %s' % unittest_path 264 rc = utils.system(unittest_cmd, ignore_status=True) 265 if rc != 0: 266 logging.error("Unittest issues found during '%s'", 267 unittest_cmd) 268 269 270 def _check_permissions(self): 271 """ 272 Verifies the execution permissions, specifically: 273 * Files with no shebang and execution permissions are reported. 274 * Files with shebang and no execution permissions are reported. 275 """ 276 if self.first_line.startswith("#!"): 277 if not self.is_executable: 278 self.corrective_actions.append("svn propset svn:executable ON %s" % self.path) 279 else: 280 if self.is_executable: 281 self.corrective_actions.append("svn propdel svn:executable %s" % self.path) 282 283 284 def report(self): 285 """ 286 Executes all required checks, if problems are found, the possible 287 corrective actions are listed. 288 """ 289 self._check_permissions() 290 if self.is_python: 291 self._check_indent() 292 self._check_code() 293 self._check_unittest() 294 if self.corrective_actions: 295 for action in self.corrective_actions: 296 answer = utils.ask("Would you like to execute %s?" % action, 297 auto=self.confirm) 298 if answer == "y": 299 rc = utils.system(action, ignore_status=True) 300 if rc != 0: 301 logging.error("Error executing %s" % action) 302 303 304 class PatchChecker(object): 305 def __init__(self, patch=None, patchwork_id=None, confirm=False): 306 self.confirm = confirm 307 self.base_dir = os.getcwd() 308 if patch: 309 self.patch = os.path.abspath(patch) 310 if patchwork_id: 311 self.patch = self._fetch_from_patchwork(patchwork_id) 312 313 if not os.path.isfile(self.patch): 314 logging.error("Invalid patch file %s provided. Aborting.", 315 self.patch) 316 sys.exit(1) 317 318 self.vcs = VCS() 319 changed_files_before = self.vcs.get_modified_files() 320 if changed_files_before: 321 logging.error("Repository has changed files prior to patch " 322 "application. ") 323 answer = utils.ask("Would you like to revert them?", auto=self.confirm) 324 if answer == "n": 325 logging.error("Not safe to proceed without reverting files.") 326 sys.exit(1) 327 else: 328 for changed_file in changed_files_before: 329 self.vcs.revert_file(changed_file) 330 331 self.untracked_files_before = self.vcs.get_unknown_files() 332 self.vcs.update() 333 334 335 def _fetch_from_patchwork(self, id): 336 """ 337 Gets a patch file from patchwork and puts it under the cwd so it can 338 be applied. 339 340 @param id: Patchwork patch id. 341 """ 342 patch_url = "http://patchwork.test.kernel.org/patch/%s/mbox/" % id 343 patch_dest = os.path.join(self.base_dir, 'patchwork-%s.patch' % id) 344 patch = utils.get_file(patch_url, patch_dest) 345 # Patchwork sometimes puts garbage on the path, such as long 346 # sequences of underscores (_______). Get rid of those. 347 patch_ro = open(patch, 'r') 348 patch_contents = patch_ro.readlines() 349 patch_ro.close() 350 patch_rw = open(patch, 'w') 351 for line in patch_contents: 352 if not line.startswith("___"): 353 patch_rw.write(line) 354 patch_rw.close() 355 return patch 356 357 358 def _check_files_modified_patch(self): 359 untracked_files_after = self.vcs.get_unknown_files() 360 modified_files_after = self.vcs.get_modified_files() 361 add_to_vcs = [] 362 for untracked_file in untracked_files_after: 363 if untracked_file not in self.untracked_files_before: 364 add_to_vcs.append(untracked_file) 365 366 if add_to_vcs: 367 logging.info("The files: ") 368 for untracked_file in add_to_vcs: 369 logging.info(untracked_file) 370 logging.info("Might need to be added to VCS") 371 answer = utils.ask("Would you like to add them to VCS ?") 372 if answer == "y": 373 for untracked_file in add_to_vcs: 374 self.vcs.add_untracked_file(untracked_file) 375 modified_files_after.append(untracked_file) 376 elif answer == "n": 377 pass 378 379 for modified_file in modified_files_after: 380 # Additional safety check, new commits might introduce 381 # new directories 382 if os.path.isfile(modified_file): 383 file_checker = FileChecker(modified_file) 384 file_checker.report() 385 386 387 def check(self): 388 self.vcs.apply_patch(self.patch) 389 self._check_files_modified_patch() 390 391 392 if __name__ == "__main__": 393 parser = optparse.OptionParser() 394 parser.add_option('-p', '--patch', dest="local_patch", action='store', 395 help='path to a patch file that will be checked') 396 parser.add_option('-i', '--patchwork-id', dest="id", action='store', 397 help='id of a given patchwork patch') 398 parser.add_option('--verbose', dest="debug", action='store_true', 399 help='include debug messages in console output') 400 parser.add_option('-f', '--full-check', dest="full_check", 401 action='store_true', 402 help='check the full tree for corrective actions') 403 parser.add_option('-y', '--yes', dest="confirm", 404 action='store_true', 405 help='Answer yes to all questions') 406 407 options, args = parser.parse_args() 408 local_patch = options.local_patch 409 id = options.id 410 debug = options.debug 411 full_check = options.full_check 412 confirm = options.confirm 413 414 logging_manager.configure_logging(CheckPatchLoggingConfig(), verbose=debug) 415 416 ignore_file_list = ['common.py'] 417 if full_check: 418 for root, dirs, files in os.walk('.'): 419 if not '.svn' in root: 420 for file in files: 421 if file not in ignore_file_list: 422 path = os.path.join(root, file) 423 file_checker = FileChecker(path, confirm=confirm) 424 file_checker.report() 425 else: 426 if local_patch: 427 patch_checker = PatchChecker(patch=local_patch, confirm=confirm) 428 elif id: 429 patch_checker = PatchChecker(patchwork_id=id, confirm=confirm) 430 else: 431 logging.error('No patch or patchwork id specified. Aborting.') 432 sys.exit(1) 433 patch_checker.check() 434