blob: 17a3cebb487f21d1d29a9229b3eef16a3821ed43 [file]
#!/usr/bin/env python3
# ex:ts=4:sw=4:sts=4:et
# -*- tab-width: 4; c-basic-offset: 4; indent-tabs-mode: nil -*-
#
# patchtest: execute all unittest test cases discovered for a single patch
#
# Copyright (C) 2016 Intel Corporation
#
# SPDX-License-Identifier: GPL-2.0-only
#
import json
import logging
import os
import signal
import subprocess
import sys
import traceback
import unittest
# Include current path so test cases can see it
sys.path.insert(0, os.path.dirname(os.path.realpath(__file__)))
# Include patchtest library
sys.path.insert(0, os.path.join(os.path.dirname(os.path.realpath(__file__)), '../meta/lib/patchtest'))
from patchtest_parser import PatchtestParser
from repo import PatchTestRepo
logger = logging.getLogger("patchtest")
loggerhandler = logging.StreamHandler()
loggerhandler.setFormatter(logging.Formatter("%(message)s"))
logger.addHandler(loggerhandler)
logger.setLevel(logging.INFO)
def _format_test_description(test):
return (test.id().split('.')[-1]
.replace('_', ' ')
.replace("cve", "CVE")
.replace("signed off by", "Signed-off-by")
.replace("upstream status", "Upstream-Status")
.replace("non auh", "non-AUH")
.replace("presence format", "presence"))
def _write_patchtest_result(line, logfile=None):
print(line)
if logfile:
with open(logfile, "a") as f:
f.write(line + "\n")
def getResult(patch, mergepatch, logfile=None):
class PatchTestResult(unittest.TextTestResult):
""" Patchtest TextTestResult """
shouldStop = True
longMessage = False
success = 'PASS'
fail = 'FAIL'
skip = 'SKIP'
def startTestRun(self):
self.repo_error = False
self.test_error = False
self.test_failure = False
try:
self.repo = PatchtestParser.repo = PatchTestRepo(
patch=patch,
repodir=PatchtestParser.repodir,
commit=PatchtestParser.basecommit,
branch=PatchtestParser.basebranch,
)
except:
logger.error(traceback.print_exc())
self.repo_error = True
self.stop()
return
if mergepatch:
self.repo.merge()
def addError(self, test, err):
self.test_error = True
(ty, va, trace) = err
traceback.print_exc()
def addFailure(self, test, err):
self.test_failure = True
desc = _format_test_description(test)
issue = json.loads(str(err[1]))["issue"]
_write_patchtest_result('{}: {}: {} ({})'.format(self.fail, desc, issue, test.id()), logfile)
def addSuccess(self, test):
desc = _format_test_description(test)
_write_patchtest_result('{}: {} ({})'.format(self.success, desc, test.id()), logfile)
def addSkip(self, test, reason):
desc = _format_test_description(test)
issue = json.loads(str(reason))["issue"]
_write_patchtest_result('{}: {}: {} ({})'.format(self.skip, desc, issue, test.id()), logfile)
def stopTestRun(self):
# in case there was an error on repo object creation, just return
if self.repo_error:
return
self.repo.clean()
return PatchTestResult
def _runner(resultklass, prefix=None):
# load test with the corresponding prefix
loader = unittest.TestLoader()
if prefix:
loader.testMethodPrefix = prefix
# create the suite with discovered tests and the corresponding runner
suite = loader.discover(
start_dir=PatchtestParser.testdir,
pattern=PatchtestParser.pattern,
top_level_dir=PatchtestParser.topdir,
)
# if there are no test cases, just quit
if not suite.countTestCases():
return 2
runner = unittest.TextTestRunner(resultclass=resultklass, verbosity=0)
try:
result = runner.run(suite)
except Exception:
traceback.print_exc()
logger.error('patchtest: something went wrong')
return 1 if (result.test_failure or result.test_error) else 0
def run(patch, logfile=None):
""" Load, setup and run pre and post-merge tests """
premerge_result = _runner(getResult(patch, False, logfile), 'pretest')
postmerge_result = _runner(getResult(patch, True, logfile), 'test')
print_result_message(premerge_result, postmerge_result)
return premerge_result or postmerge_result
def print_result_message(preresult, postresult):
print("----------------------------------------------------------------------\n")
if preresult == 2 and postresult == 2:
logger.error(
"patchtest: No test cases found - did you specify the correct suite directory?"
)
if preresult == 1 or postresult == 1:
logger.error(
"WARNING: patchtest: At least one patchtest caused a failure or an error - please check https://wiki.yoctoproject.org/wiki/Patchtest for further guidance"
)
else:
logger.info("OK: patchtest: All patchtests passed")
print("----------------------------------------------------------------------\n")
def main():
patch_path = PatchtestParser.patch_path
git_status = subprocess.run(
['git', '-C', PatchtestParser.repodir, 'status'],
capture_output=True, text=True,
).stdout
status_matches = ["Changes not staged for commit", "Changes to be committed"]
if any(match in git_status for match in status_matches):
logger.error("patchtest: there are uncommitted changes in the target repo that would be overwritten. Please commit or restore them before running patchtest")
return 1
builddir = os.environ.get('BUILDDIR')
if builddir:
bblayers_conf = os.path.join(builddir, 'conf', 'bblayers.conf')
if os.path.exists(bblayers_conf):
with open(bblayers_conf) as f:
if 'meta-selftest' not in f.read():
logger.error(
"patchtest: meta-selftest layer not found in %s - add it to BBLAYERS before running patchtest" % bblayers_conf
)
return 1
if os.path.isdir(patch_path):
patch_list = [os.path.join(patch_path, filename) for filename in sorted(os.listdir(patch_path))]
else:
patch_list = [patch_path]
interrupted = False
previous_sigint = signal.getsignal(signal.SIGINT)
def _sigint_handler(signum, frame):
nonlocal interrupted
interrupted = True
# Restore previous handler so a second CTRL+C exits immediately
signal.signal(signal.SIGINT, previous_sigint)
signal.default_int_handler(signum, frame)
signal.signal(signal.SIGINT, _sigint_handler)
ret = 0
for patch in patch_list:
if interrupted:
break
if os.path.getsize(patch) == 0:
logger.error('patchtest: patch is empty')
signal.signal(signal.SIGINT, previous_sigint)
return 1
logger.info('Testing patch %s' % patch)
log_path = None
if PatchtestParser.log_results:
log_path = patch + ".testresult"
with open(log_path, "a") as f:
f.write("Patchtest results for patch '%s':\n\n" % patch)
try:
result = run(patch, log_path)
ret = ret or result
except KeyboardInterrupt:
interrupted = True
if interrupted:
logger.error('\npatchtest: interrupted')
signal.signal(signal.SIGINT, previous_sigint)
return 1
signal.signal(signal.SIGINT, previous_sigint)
return ret
if __name__ == '__main__':
ret = 1
# Parse the command line arguments and store it on the PatchtestParser namespace
PatchtestParser.set_namespace()
# set debugging level
if PatchtestParser.debug:
logger.setLevel(logging.DEBUG)
# if topdir not define, default it to testdir
if not PatchtestParser.topdir:
PatchtestParser.topdir = PatchtestParser.testdir
try:
ret = main()
except Exception:
traceback.print_exc(5)
sys.exit(ret)