blob: bd60d6672aaed776423f45a3d871abaad1253d39 [file] [log] [blame]
#!/usr/bin/python
"""
Script to verify errors on autotest code contributions (patches).
The workflow is as follows:
* Patch will be applied and eventual problems will be notified.
* If there are new files created, remember user to add them to VCS.
* If any added file looks like a executable file, remember user to make them
executable.
* If any of the files added or modified introduces trailing whitespaces, tabs
or incorrect indentation, report problems.
* If any of the files have problems during pylint validation, report failures.
* If any of the files changed have a unittest suite, run the unittest suite
and report any failures.
Usage: check_patch.py -p [/path/to/patch]
check_patch.py -i [patchwork id]
@copyright: Red Hat Inc, 2009.
@author: Lucas Meneghel Rodrigues <lmr@redhat.com>
"""
import os, stat, logging, sys, optparse, time
import common
from autotest_lib.client.common_lib import utils, error, logging_config
from autotest_lib.client.common_lib import logging_manager
class CheckPatchLoggingConfig(logging_config.LoggingConfig):
def configure_logging(self, results_dir=None, verbose=False):
super(CheckPatchLoggingConfig, self).configure_logging(use_console=True,
verbose=verbose)
class VCS(object):
"""
Abstraction layer to the version control system.
"""
def __init__(self):
"""
Class constructor. Guesses the version control name and instantiates it
as a backend.
"""
backend_name = self.guess_vcs_name()
if backend_name == "SVN":
self.backend = SubVersionBackend()
def guess_vcs_name(self):
if os.path.isdir(".svn"):
return "SVN"
else:
logging.error("Could not figure version control system. Are you "
"on a working directory? Aborting.")
sys.exit(1)
def get_unknown_files(self):
"""
Return a list of files unknown to the VCS.
"""
return self.backend.get_unknown_files()
def get_modified_files(self):
"""
Return a list of files that were modified, according to the VCS.
"""
return self.backend.get_modified_files()
def add_untracked_file(self, file):
"""
Add an untracked file to version control.
"""
return self.backend.add_untracked_file(file)
def revert_file(self, file):
"""
Restore file according to the latest state on the reference repo.
"""
return self.backend.revert_file(file)
def apply_patch(self, patch):
"""
Applies a patch using the most appropriate method to the particular VCS.
"""
return self.backend.apply_patch(patch)
def update(self):
"""
Updates the tree according to the latest state of the public tree
"""
return self.backend.update()
class SubVersionBackend(object):
"""
Implementation of a subversion backend for use with the VCS abstraction
layer.
"""
def __init__(self):
logging.debug("Subversion VCS backend initialized.")
self.ignored_extension_list = ['.orig', '.bak']
def get_unknown_files(self):
status = utils.system_output("svn status --ignore-externals")
unknown_files = []
for line in status.split("\n"):
status_flag = line[0]
if line and status_flag == "?":
for extension in self.ignored_extension_list:
if not line.endswith(extension):
unknown_files.append(line[1:].strip())
return unknown_files
def get_modified_files(self):
status = utils.system_output("svn status --ignore-externals")
modified_files = []
for line in status.split("\n"):
status_flag = line[0]
if line and status_flag == "M" or status_flag == "A":
modified_files.append(line[1:].strip())
return modified_files
def add_untracked_file(self, file):
"""
Add an untracked file under revision control.
@param file: Path to untracked file.
"""
try:
utils.run('svn add %s' % file)
except error.CmdError, e:
logging.error("Problem adding file %s to svn: %s", file, e)
sys.exit(1)
def revert_file(self, file):
"""
Revert file against last revision.
@param file: Path to file to be reverted.
"""
try:
utils.run('svn revert %s' % file)
except error.CmdError, e:
logging.error("Problem reverting file %s: %s", file, e)
sys.exit(1)
def apply_patch(self, patch):
"""
Apply a patch to the code base. Patches are expected to be made using
level -p1, and taken according to the code base top level.
@param patch: Path to the patch file.
"""
try:
utils.system_output("patch -p1 < %s" % patch)
except:
logging.error("Patch applied incorrectly. Possible causes: ")
logging.error("1 - Patch might not be -p1")
logging.error("2 - You are not at the top of the autotest tree")
logging.error("3 - Patch was made using an older tree")
logging.error("4 - Mailer might have messed the patch")
sys.exit(1)
def update(self):
try:
utils.system("svn update", ignore_status=True)
except error.CmdError, e:
logging.error("SVN tree update failed: %s" % e)
class FileChecker(object):
"""
Picks up a given file and performs various checks, looking after problems
and eventually suggesting solutions.
"""
def __init__(self, path, confirm=False):
"""
Class constructor, sets the path attribute.
@param path: Path to the file that will be checked.
@param confirm: Whether to answer yes to all questions asked without
prompting the user.
"""
self.path = path
self.confirm = confirm
self.basename = os.path.basename(self.path)
if self.basename.endswith('.py'):
self.is_python = True
else:
self.is_python = False
mode = os.stat(self.path)[stat.ST_MODE]
if mode & stat.S_IXUSR:
self.is_executable = True
else:
self.is_executable = False
checked_file = open(self.path, "r")
self.first_line = checked_file.readline()
checked_file.close()
self.corrective_actions = []
self.indentation_exceptions = ['job_unittest.py']
def _check_indent(self):
"""
Verifies the file with reindent.py. This tool performs the following
checks on python files:
* Trailing whitespaces
* Tabs
* End of line
* Incorrect indentation
For the purposes of checking, the dry run mode is used and no changes
are made. It is up to the user to decide if he wants to run reindent
to correct the issues.
"""
reindent_raw = utils.system_output('reindent.py -v -d %s | head -1' %
self.path)
reindent_results = reindent_raw.split(" ")[-1].strip(".")
if reindent_results == "changed":
if self.basename not in self.indentation_exceptions:
self.corrective_actions.append("reindent.py -v %s" % self.path)
def _check_code(self):
"""
Verifies the file with run_pylint.py. This tool will call the static
code checker pylint using the special autotest conventions and warn
only on problems. If problems are found, a report will be generated.
Some of the problems reported might be bogus, but it's allways good
to look at them.
"""
c_cmd = 'run_pylint.py %s' % self.path
rc = utils.system(c_cmd, ignore_status=True)
if rc != 0:
logging.error("Syntax issues found during '%s'", c_cmd)
def _check_unittest(self):
"""
Verifies if the file in question has a unittest suite, if so, run the
unittest and report on any failures. This is important to keep our
unit tests up to date.
"""
if "unittest" not in self.basename:
stripped_name = self.basename.strip(".py")
unittest_name = stripped_name + "_unittest.py"
unittest_path = self.path.replace(self.basename, unittest_name)
if os.path.isfile(unittest_path):
unittest_cmd = 'python %s' % unittest_path
rc = utils.system(unittest_cmd, ignore_status=True)
if rc != 0:
logging.error("Unittest issues found during '%s'",
unittest_cmd)
def _check_permissions(self):
"""
Verifies the execution permissions, specifically:
* Files with no shebang and execution permissions are reported.
* Files with shebang and no execution permissions are reported.
"""
if self.first_line.startswith("#!"):
if not self.is_executable:
self.corrective_actions.append("svn propset svn:executable ON %s" % self.path)
else:
if self.is_executable:
self.corrective_actions.append("svn propdel svn:executable %s" % self.path)
def report(self):
"""
Executes all required checks, if problems are found, the possible
corrective actions are listed.
"""
self._check_permissions()
if self.is_python:
self._check_indent()
self._check_code()
self._check_unittest()
if self.corrective_actions:
for action in self.corrective_actions:
answer = utils.ask("Would you like to execute %s?" % action,
auto=self.confirm)
if answer == "y":
rc = utils.system(action, ignore_status=True)
if rc != 0:
logging.error("Error executing %s" % action)
class PatchChecker(object):
def __init__(self, patch=None, patchwork_id=None, confirm=False):
self.confirm = confirm
self.base_dir = os.getcwd()
if patch:
self.patch = os.path.abspath(patch)
if patchwork_id:
self.patch = self._fetch_from_patchwork(patchwork_id)
if not os.path.isfile(self.patch):
logging.error("Invalid patch file %s provided. Aborting.",
self.patch)
sys.exit(1)
self.vcs = VCS()
changed_files_before = self.vcs.get_modified_files()
if changed_files_before:
logging.error("Repository has changed files prior to patch "
"application. ")
answer = utils.ask("Would you like to revert them?", auto=self.confirm)
if answer == "n":
logging.error("Not safe to proceed without reverting files.")
sys.exit(1)
else:
for changed_file in changed_files_before:
self.vcs.revert_file(changed_file)
self.untracked_files_before = self.vcs.get_unknown_files()
self.vcs.update()
def _fetch_from_patchwork(self, id):
"""
Gets a patch file from patchwork and puts it under the cwd so it can
be applied.
@param id: Patchwork patch id.
"""
patch_url = "http://patchwork.test.kernel.org/patch/%s/mbox/" % id
patch_dest = os.path.join(self.base_dir, 'patchwork-%s.patch' % id)
patch = utils.get_file(patch_url, patch_dest)
# Patchwork sometimes puts garbage on the path, such as long
# sequences of underscores (_______). Get rid of those.
patch_ro = open(patch, 'r')
patch_contents = patch_ro.readlines()
patch_ro.close()
patch_rw = open(patch, 'w')
for line in patch_contents:
if not line.startswith("___"):
patch_rw.write(line)
patch_rw.close()
return patch
def _check_files_modified_patch(self):
untracked_files_after = self.vcs.get_unknown_files()
modified_files_after = self.vcs.get_modified_files()
add_to_vcs = []
for untracked_file in untracked_files_after:
if untracked_file not in self.untracked_files_before:
add_to_vcs.append(untracked_file)
if add_to_vcs:
logging.info("The files: ")
for untracked_file in add_to_vcs:
logging.info(untracked_file)
logging.info("Might need to be added to VCS")
answer = utils.ask("Would you like to add them to VCS ?")
if answer == "y":
for untracked_file in add_to_vcs:
self.vcs.add_untracked_file(untracked_file)
modified_files_after.append(untracked_file)
elif answer == "n":
pass
for modified_file in modified_files_after:
# Additional safety check, new commits might introduce
# new directories
if os.path.isfile(modified_file):
file_checker = FileChecker(modified_file)
file_checker.report()
def check(self):
self.vcs.apply_patch(self.patch)
self._check_files_modified_patch()
if __name__ == "__main__":
parser = optparse.OptionParser()
parser.add_option('-p', '--patch', dest="local_patch", action='store',
help='path to a patch file that will be checked')
parser.add_option('-i', '--patchwork-id', dest="id", action='store',
help='id of a given patchwork patch')
parser.add_option('--verbose', dest="debug", action='store_true',
help='include debug messages in console output')
parser.add_option('-f', '--full-check', dest="full_check",
action='store_true',
help='check the full tree for corrective actions')
parser.add_option('-y', '--yes', dest="confirm",
action='store_true',
help='Answer yes to all questions')
options, args = parser.parse_args()
local_patch = options.local_patch
id = options.id
debug = options.debug
full_check = options.full_check
confirm = options.confirm
logging_manager.configure_logging(CheckPatchLoggingConfig(), verbose=debug)
ignore_file_list = ['common.py']
if full_check:
for root, dirs, files in os.walk('.'):
if not '.svn' in root:
for file in files:
if file not in ignore_file_list:
path = os.path.join(root, file)
file_checker = FileChecker(path, confirm=confirm)
file_checker.report()
else:
if local_patch:
patch_checker = PatchChecker(patch=local_patch, confirm=confirm)
elif id:
patch_checker = PatchChecker(patchwork_id=id, confirm=confirm)
else:
logging.error('No patch or patchwork id specified. Aborting.')
sys.exit(1)
patch_checker.check()