blob: 576b97d1a074a5e0b1b41bceefef44fed6941ef8 [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
23import os, stat, logging, sys, optparse
24import 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.")
107
108
109 def get_unknown_files(self):
110 status = utils.system_output("svn status --ignore-externals")
111 unknown_files = []
112 for line in status.split("\n"):
113 status_flag = line[0]
114 if line and status_flag == "?":
115 unknown_files.append(line[1:].strip())
116 return unknown_files
117
118
119 def get_modified_files(self):
120 status = utils.system_output("svn status --ignore-externals")
121 modified_files = []
122 for line in status.split("\n"):
123 status_flag = line[0]
124 if line and status_flag == "M" or status_flag == "A":
125 modified_files.append(line[1:].strip())
126 return modified_files
127
128
129 def add_untracked_file(self, file):
130 """
131 Add an untracked file under revision control.
132
133 @param file: Path to untracked file.
134 """
135 try:
136 utils.run('svn add %s' % file)
137 except error.CmdError, e:
138 logging.error("Problem adding file %s to svn: %s", file, e)
139 sys.exit(1)
140
141
142 def revert_file(self, file):
143 """
144 Revert file against last revision.
145
146 @param file: Path to file to be reverted.
147 """
148 try:
149 utils.run('svn revert %s' % file)
150 except error.CmdError, e:
151 logging.error("Problem reverting file %s: %s", file, e)
152 sys.exit(1)
153
154
155 def apply_patch(self, patch):
156 """
157 Apply a patch to the code base. Patches are expected to be made using
158 level -p1, and taken according to the code base top level.
159
160 @param patch: Path to the patch file.
161 """
162 try:
163 utils.system_output("patch -p1 < %s" % patch)
164 except:
165 logging.error("Patch applied incorrectly. Possible causes: ")
166 logging.error("1 - Patch might not be -p1")
167 logging.error("2 - You are not at the top of the autotest tree")
168 logging.error("3 - Patch was made using an older tree")
169 logging.error("4 - Mailer might have messed the patch")
170 sys.exit(1)
171
172 def update(self):
173 try:
174 utils.system("svn update", ignore_status=True)
175 except error.CmdError, e:
176 logging.error("SVN tree update failed: %s" % e)
177
178
179class FileChecker(object):
180 """
181 Picks up a given file and performs various checks, looking after problems
182 and eventually suggesting solutions.
183 """
184 def __init__(self, path):
185 """
186 Class constructor, sets the path attribute.
187
188 @param path: Path to the file that will be checked.
189 """
190 self.path = path
191 self.basename = os.path.basename(self.path)
192 if self.basename.endswith('.py'):
193 self.is_python = True
194 else:
195 self.is_python = False
196
197 mode = os.stat(self.path)[stat.ST_MODE]
198 if mode & stat.S_IXUSR:
199 self.is_executable = True
200 else:
201 self.is_executable = False
202
203 checked_file = open(self.path, "r")
204 self.first_line = checked_file.readline()
205 checked_file.close()
206 self.corrective_actions = []
207 self.indentation_exceptions = ['cli/job_unittest.py']
208
209
210 def _check_indent(self):
211 """
212 Verifies the file with reindent.py. This tool performs the following
213 checks on python files:
214
215 * Trailing whitespaces
216 * Tabs
217 * End of line
218 * Incorrect indentation
219
220 For the purposes of checking, the dry run mode is used and no changes
221 are made. It is up to the user to decide if he wants to run reindent
222 to correct the issues.
223 """
224 reindent_raw = utils.system_output('reindent.py -v -d %s | head -1' %
225 self.path)
226 reindent_results = reindent_raw.split(" ")[-1].strip(".")
227 if reindent_results == "changed":
228 if self.basename not in self.indentation_exceptions:
229 logging.error("Possible indentation and spacing issues on "
230 "file %s" % self.path)
231 self.corrective_actions.append("reindent.py -v %s" % self.path)
232
233
234 def _check_code(self):
235 """
236 Verifies the file with run_pylint.py. This tool will call the static
237 code checker pylint using the special autotest conventions and warn
238 only on problems. If problems are found, a report will be generated.
239 Some of the problems reported might be bogus, but it's allways good
240 to look at them.
241 """
242 c_cmd = 'run_pylint.py %s' % self.path
243 rc = utils.system(c_cmd, ignore_status=True)
244 if rc != 0:
245 logging.error("Possible syntax problems on file %s", self.path)
246 logging.error("You might want to rerun '%s'", c_cmd)
247
248
249 def _check_unittest(self):
250 """
251 Verifies if the file in question has a unittest suite, if so, run the
252 unittest and report on any failures. This is important to keep our
253 unit tests up to date.
254 """
255 if "unittest" not in self.basename:
256 stripped_name = self.basename.strip(".py")
257 unittest_name = stripped_name + "_unittest.py"
258 unittest_path = self.path.replace(self.basename, unittest_name)
259 if os.path.isfile(unittest_path):
260 unittest_cmd = 'python %s' % unittest_path
261 rc = utils.system(unittest_cmd, ignore_status=True)
262 if rc != 0:
263 logging.error("Problems during unit test execution "
264 "for file %s", self.path)
265 logging.error("You might want to rerun '%s'", unittest_cmd)
266
267
268 def _check_permissions(self):
269 """
270 Verifies the execution permissions, specifically:
271 * Files with no shebang and execution permissions are reported.
272 * Files with shebang and no execution permissions are reported.
273 """
274 if self.first_line.startswith("#!"):
275 if not self.is_executable:
276 logging.info("File %s seems to require execution "
277 "permissions. ", self.path)
278 self.corrective_actions.append("chmod +x %s" % self.path)
279 else:
280 if self.is_executable:
281 logging.info("File %s does not seem to require execution "
282 "permissions. ", self.path)
283 self.corrective_actions.append("chmod -x %s" % self.path)
284
285
286 def report(self):
287 """
288 Executes all required checks, if problems are found, the possible
289 corrective actions are listed.
290 """
291 self._check_permissions()
292 if self.is_python:
293 self._check_indent()
294 self._check_code()
295 self._check_unittest()
296 if self.corrective_actions:
297 logging.info("The following corrective actions are suggested:")
298 for action in self.corrective_actions:
299 logging.info(action)
300 answer = raw_input("Would you like to apply it? (y/n) ")
301 if answer == "y":
302 rc = utils.system(action, ignore_status=True)
303 if rc != 0:
304 logging.error("Error executing %s" % action)
305
306
307class PatchChecker(object):
308 def __init__(self, patch=None, patchwork_id=None):
309 self.base_dir = os.getcwd()
310 if patch:
311 self.patch = os.path.abspath(patch)
312 if patchwork_id:
313 self.patch = self._fetch_from_patchwork(patchwork_id)
314
315 if not os.path.isfile(self.patch):
316 logging.error("Invalid patch file %s provided. Aborting.",
317 self.patch)
318 sys.exit(1)
319
320 self.vcs = VCS()
321 changed_files_before = self.vcs.get_modified_files()
322 if changed_files_before:
323 logging.error("Repository has changed files prior to patch "
324 "application. ")
325 answer = raw_input("Would you like to revert them? (y/n) ")
326 if answer == "n":
327 logging.error("Not safe to proceed without reverting files.")
328 sys.exit(1)
329 else:
330 for changed_file in changed_files_before:
331 self.vcs.revert_file(changed_file)
332
333 self.untracked_files_before = self.vcs.get_unknown_files()
334 self.vcs.update()
335
336
337 def _fetch_from_patchwork(self, id):
338 """
339 Gets a patch file from patchwork and puts it under the cwd so it can
340 be applied.
341
342 @param id: Patchwork patch id.
343 """
344 patch_url = "http://patchwork.test.kernel.org/patch/%s/mbox/" % id
345 patch_dest = os.path.join(self.base_dir, 'patchwork-%s.patch' % id)
346 patch = utils.get_file(patch_url, patch_dest)
347 # Patchwork sometimes puts garbage on the path, such as long
348 # sequences of underscores (_______). Get rid of those.
349 patch_ro = open(patch, 'r')
350 patch_contents = patch_ro.readlines()
351 patch_ro.close()
352 patch_rw = open(patch, 'w')
353 for line in patch_contents:
354 if not line.startswith("___"):
355 patch_rw.write(line)
356 patch_rw.close()
357 return patch
358
359
360 def _check_files_modified_patch(self):
361 untracked_files_after = self.vcs.get_unknown_files()
362 modified_files_after = self.vcs.get_modified_files()
363 add_to_vcs = []
364 for untracked_file in untracked_files_after:
365 if untracked_file not in self.untracked_files_before:
366 add_to_vcs.append(untracked_file)
367
368 if add_to_vcs:
369 logging.info("The files: ")
370 for untracked_file in add_to_vcs:
371 logging.info(untracked_file)
372 logging.info("Might need to be added to VCS")
373 logging.info("Would you like to add them to VCS ? (y/n/abort) ")
374 answer = raw_input()
375 if answer == "y":
376 for untracked_file in add_to_vcs:
377 self.vcs.add_untracked_file(untracked_file)
378 modified_files_after.append(untracked_file)
379 elif answer == "n":
380 pass
381 elif answer == "abort":
382 sys.exit(1)
383
384 for modified_file in modified_files_after:
385 file_checker = FileChecker(modified_file)
386 file_checker.report()
387
388
389 def check(self):
390 self.vcs.apply_patch(self.patch)
391 self._check_files_modified_patch()
392
393
394if __name__ == "__main__":
395 parser = optparse.OptionParser()
396 parser.add_option('-p', '--patch', dest="local_patch", action='store',
397 help='path to a patch file that will be checked')
398 parser.add_option('-i', '--patchwork-id', dest="id", action='store',
399 help='id of a given patchwork patch')
400 parser.add_option('--verbose', dest="debug", action='store_true',
401 help='include debug messages in console output')
402
403 options, args = parser.parse_args()
404 local_patch = options.local_patch
405 id = options.id
406 debug = options.debug
407
408 logging_manager.configure_logging(CheckPatchLoggingConfig(), verbose=debug)
409
410 if local_patch:
411 patch_checker = PatchChecker(patch=local_patch)
412 elif id:
413 patch_checker = PatchChecker(patchwork_id=id)
414 else:
415 logging.error('No patch or patchwork id specified. Aborting.')
416 sys.exit(1)
417
418 patch_checker.check()