blob: bd60d6672aaed776423f45a3d871abaad1253d39 [file] [log] [blame]
lmr9ea7f8a2009-10-05 18:39:47 +00001#!/usr/bin/python
2"""
3Script to verify errors on autotest code contributions (patches).
4The 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
16Usage: 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@redhat.com>
21"""
22
Eric Li861b2d52011-02-04 14:50:35 -080023import os, stat, logging, sys, optparse, time
lmr9ea7f8a2009-10-05 18:39:47 +000024import common
25from autotest_lib.client.common_lib import utils, error, logging_config
26from autotest_lib.client.common_lib import logging_manager
27
28
29class 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
35class 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
100class 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.")
Eric Li861b2d52011-02-04 14:50:35 -0800107 self.ignored_extension_list = ['.orig', '.bak']
lmr9ea7f8a2009-10-05 18:39:47 +0000108
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 == "?":
Eric Li861b2d52011-02-04 14:50:35 -0800116 for extension in self.ignored_extension_list:
117 if not line.endswith(extension):
118 unknown_files.append(line[1:].strip())
lmr9ea7f8a2009-10-05 18:39:47 +0000119 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
182class FileChecker(object):
183 """
184 Picks up a given file and performs various checks, looking after problems
185 and eventually suggesting solutions.
186 """
Eric Li861b2d52011-02-04 14:50:35 -0800187 def __init__(self, path, confirm=False):
lmr9ea7f8a2009-10-05 18:39:47 +0000188 """
189 Class constructor, sets the path attribute.
190
191 @param path: Path to the file that will be checked.
Eric Li861b2d52011-02-04 14:50:35 -0800192 @param confirm: Whether to answer yes to all questions asked without
193 prompting the user.
lmr9ea7f8a2009-10-05 18:39:47 +0000194 """
195 self.path = path
Eric Li861b2d52011-02-04 14:50:35 -0800196 self.confirm = confirm
lmr9ea7f8a2009-10-05 18:39:47 +0000197 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 = []
Eric Li861b2d52011-02-04 14:50:35 -0800213 self.indentation_exceptions = ['job_unittest.py']
lmr9ea7f8a2009-10-05 18:39:47 +0000214
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:
lmr9ea7f8a2009-10-05 18:39:47 +0000235 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:
Eric Li861b2d52011-02-04 14:50:35 -0800249 logging.error("Syntax issues found during '%s'", c_cmd)
lmr9ea7f8a2009-10-05 18:39:47 +0000250
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:
Eric Li861b2d52011-02-04 14:50:35 -0800266 logging.error("Unittest issues found during '%s'",
267 unittest_cmd)
lmr9ea7f8a2009-10-05 18:39:47 +0000268
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:
Eric Li861b2d52011-02-04 14:50:35 -0800278 self.corrective_actions.append("svn propset svn:executable ON %s" % self.path)
lmr9ea7f8a2009-10-05 18:39:47 +0000279 else:
280 if self.is_executable:
Eric Li861b2d52011-02-04 14:50:35 -0800281 self.corrective_actions.append("svn propdel svn:executable %s" % self.path)
lmr9ea7f8a2009-10-05 18:39:47 +0000282
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:
lmr9ea7f8a2009-10-05 18:39:47 +0000295 for action in self.corrective_actions:
Eric Li0a993912011-05-17 12:56:25 -0700296 answer = utils.ask("Would you like to execute %s?" % action,
297 auto=self.confirm)
lmr9ea7f8a2009-10-05 18:39:47 +0000298 if answer == "y":
299 rc = utils.system(action, ignore_status=True)
300 if rc != 0:
301 logging.error("Error executing %s" % action)
302
303
304class PatchChecker(object):
Eric Li861b2d52011-02-04 14:50:35 -0800305 def __init__(self, patch=None, patchwork_id=None, confirm=False):
306 self.confirm = confirm
lmr9ea7f8a2009-10-05 18:39:47 +0000307 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. ")
Eric Li0a993912011-05-17 12:56:25 -0700323 answer = utils.ask("Would you like to revert them?", auto=self.confirm)
lmr9ea7f8a2009-10-05 18:39:47 +0000324 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")
Eric Li0a993912011-05-17 12:56:25 -0700371 answer = utils.ask("Would you like to add them to VCS ?")
lmr9ea7f8a2009-10-05 18:39:47 +0000372 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
lmr9ea7f8a2009-10-05 18:39:47 +0000378
379 for modified_file in modified_files_after:
Eric Li861b2d52011-02-04 14:50:35 -0800380 # 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()
lmr9ea7f8a2009-10-05 18:39:47 +0000385
386
387 def check(self):
388 self.vcs.apply_patch(self.patch)
389 self._check_files_modified_patch()
390
391
392if __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')
Eric Li861b2d52011-02-04 14:50:35 -0800400 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')
lmr9ea7f8a2009-10-05 18:39:47 +0000406
407 options, args = parser.parse_args()
408 local_patch = options.local_patch
409 id = options.id
410 debug = options.debug
Eric Li861b2d52011-02-04 14:50:35 -0800411 full_check = options.full_check
412 confirm = options.confirm
lmr9ea7f8a2009-10-05 18:39:47 +0000413
414 logging_manager.configure_logging(CheckPatchLoggingConfig(), verbose=debug)
415
Eric Li861b2d52011-02-04 14:50:35 -0800416 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()
lmr9ea7f8a2009-10-05 18:39:47 +0000425 else:
Eric Li861b2d52011-02-04 14:50:35 -0800426 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()