You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
433 lines
15 KiB
433 lines
15 KiB
#!/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()
|
|
|