@@ -13,11 +13,12 @@
import email
import re
+
# From: https://stackoverflow.com/questions/59681461/read-a-big-mbox-file-with-python
class MboxReader:
def __init__(self, filepath):
- self.handle = open(filepath, 'rb')
- assert self.handle.readline().startswith(b'From ')
+ self.handle = open(filepath, "rb")
+ assert self.handle.readline().startswith(b"From ")
def __enter__(self):
return self
@@ -32,24 +33,26 @@ class MboxReader:
lines = []
while True:
line = self.handle.readline()
- if line == b'' or line.startswith(b'From '):
- yield email.message_from_bytes(b''.join(lines))
- if line == b'':
+ if line == b"" or line.startswith(b"From "):
+ yield email.message_from_bytes(b"".join(lines))
+ if line == b"":
break
lines = []
continue
lines.append(line)
+
class Patch:
def __init__(self, data):
- self.author = data['From']
- self.to = data['To']
- self.cc = data['Cc']
- self.subject = data['Subject']
- self.split_body = re.split('---', data.get_payload(), maxsplit=1)
+ self.author = data["From"]
+ self.to = data["To"]
+ self.cc = data["Cc"]
+ self.subject = data["Subject"]
+ self.split_body = re.split("---", data.get_payload(), maxsplit=1)
self.commit_message = self.split_body[0]
self.diff = self.split_body[1]
+
class PatchSeries:
def __init__(self, filepath):
with MboxReader(filepath) as mbox:
@@ -78,8 +81,8 @@ class PatchSeries:
branch, branches, valid_branches = None, [], []
if fullprefix:
- prefix = fullprefix.strip('[]')
- branches = [ b.strip() for b in prefix.split(',')]
+ prefix = fullprefix.strip("[]")
+ branches = [b.strip() for b in prefix.split(",")]
valid_branches = [b for b in branches if PatchSeries.valid_branch(b)]
if len(valid_branches):
@@ -89,20 +92,21 @@ class PatchSeries:
# found, then assume there was no branch tag in the subject line
# and that the patch targets master
if branch is not None:
- return branch.split(']')[0]
+ return branch.split("]")[0]
else:
return "master"
@staticmethod
def valid_branch(branch):
- """ Check if branch is valid name """
+ """Check if branch is valid name"""
lbranch = branch.lower()
- invalid = lbranch.startswith('patch') or \
- lbranch.startswith('rfc') or \
- lbranch.startswith('resend') or \
- re.search(r'^v\d+', lbranch) or \
- re.search(r'^\d+/\d+', lbranch)
+ invalid = (
+ lbranch.startswith("patch")
+ or lbranch.startswith("rfc")
+ or lbranch.startswith("resend")
+ or re.search(r"^v\d+", lbranch)
+ or re.search(r"^\d+/\d+", lbranch)
+ )
return not invalid
-
@@ -19,6 +19,7 @@ import argparse
default_testdir = os.path.abspath(os.path.dirname(__file__) + "/tests")
default_repodir = os.path.abspath(os.path.dirname(__file__) + "/../../..")
+
class PatchtestParser(object):
"""Abstract the patchtest argument parser"""
@@ -33,46 +34,69 @@ class PatchtestParser(object):
target_patch_group = parser.add_mutually_exclusive_group(required=True)
- target_patch_group.add_argument('--patch', metavar='PATCH', dest='patch_path',
- help='The patch to be tested')
-
- target_patch_group.add_argument('--directory', metavar='DIRECTORY', dest='patch_path',
- help='The directory containing patches to be tested')
-
- parser.add_argument('--repodir', metavar='REPO',
- default=default_repodir,
- help="Name of the repository where patch is merged")
-
- parser.add_argument('--testdir', metavar='TESTDIR',
- default=default_testdir,
- help="Directory where test cases are located")
-
- parser.add_argument('--top-level-directory', '-t',
- dest='topdir',
- default=None,
- help="Top level directory of project (defaults to start directory)")
-
- parser.add_argument('--pattern', '-p',
- dest='pattern',
- default='test*.py',
- help="Pattern to match test files")
-
- parser.add_argument('--base-branch', '-b',
- dest='basebranch',
- help="Branch name used by patchtest to branch from. By default, it uses the current one.")
-
- parser.add_argument('--base-commit', '-c',
- dest='basecommit',
- help="Commit ID used by patchtest to branch from. By default, it uses HEAD.")
-
- parser.add_argument('--debug', '-d',
- action='store_true',
- help='Enable debug output')
-
- parser.add_argument('--log-results',
- action='store_true',
- help='Enable logging to a file matching the target patch name with ".testresult" appended')
-
+ target_patch_group.add_argument(
+ "--patch", metavar="PATCH", dest="patch_path", help="The patch to be tested"
+ )
+
+ target_patch_group.add_argument(
+ "--directory",
+ metavar="DIRECTORY",
+ dest="patch_path",
+ help="The directory containing patches to be tested",
+ )
+
+ parser.add_argument(
+ "--repodir",
+ metavar="REPO",
+ default=default_repodir,
+ help="Name of the repository where patch is merged",
+ )
+
+ parser.add_argument(
+ "--testdir",
+ metavar="TESTDIR",
+ default=default_testdir,
+ help="Directory where test cases are located",
+ )
+
+ parser.add_argument(
+ "--top-level-directory",
+ "-t",
+ dest="topdir",
+ default=None,
+ help="Top level directory of project (defaults to start directory)",
+ )
+
+ parser.add_argument(
+ "--pattern",
+ "-p",
+ dest="pattern",
+ default="test*.py",
+ help="Pattern to match test files",
+ )
+
+ parser.add_argument(
+ "--base-branch",
+ "-b",
+ dest="basebranch",
+ help="Branch name used by patchtest to branch from. By default, it uses the current one.",
+ )
+
+ parser.add_argument(
+ "--base-commit",
+ "-c",
+ dest="basecommit",
+ help="Commit ID used by patchtest to branch from. By default, it uses HEAD.",
+ )
+
+ parser.add_argument(
+ "--debug", "-d", action="store_true", help="Enable debug output"
+ )
+
+ parser.add_argument(
+ "--log-results",
+ action="store_true",
+ help='Enable logging to a file matching the target patch name with ".testresult" appended',
+ )
return parser
-
@@ -17,23 +17,23 @@ inappropriate = pyparsing.CaselessLiteral("Inappropriate")
submitted = pyparsing.CaselessLiteral("Submitted")
# word related
-nestexpr = pyparsing.nestedExpr(opener='[', closer=']')
+nestexpr = pyparsing.nestedExpr(opener="[", closer="]")
inappropriateinfo = pyparsing.Literal("Inappropriate") + nestexpr
submittedinfo = pyparsing.Literal("Submitted") + nestexpr
word = pyparsing.Word(pyparsing.alphas)
-worddot = pyparsing.Word(pyparsing.alphas+".")
+worddot = pyparsing.Word(pyparsing.alphas + ".")
# metadata
-metadata_lic = 'LICENSE'
-invalid_license = 'PATCHTESTINVALID'
-metadata_chksum = 'LIC_FILES_CHKSUM'
-license_var = 'LICENSE'
-closed = 'CLOSED'
-lictag_re = pyparsing.AtLineStart("License-Update:")
+metadata_lic = "LICENSE"
+invalid_license = "PATCHTESTINVALID"
+metadata_chksum = "LIC_FILES_CHKSUM"
+license_var = "LICENSE"
+closed = "CLOSED"
+lictag_re = pyparsing.AtLineStart("License-Update:")
lic_chksum_added = pyparsing.AtLineStart("+" + metadata_chksum)
lic_chksum_removed = pyparsing.AtLineStart("-" + metadata_chksum)
-add_mark = pyparsing.Regex('\\+ ')
+add_mark = pyparsing.Regex("\\+ ")
patch_max_line_length = 200
metadata_src_uri = "SRC_URI"
metadata_summary = "SUMMARY"
@@ -42,20 +42,20 @@ cve_status_var = "CVE_STATUS"
endcommit_messages_regex = re.compile(
r"\(From \w+-\w+ rev:|(?<!\S)Signed-off-by|(?<!\S)---\n"
)
-patchmetadata_regex = re.compile(
- r"-{3} \S+|\+{3} \S+|@{2} -\d+,\d+ \+\d+,\d+ @{2} \S+"
-)
+patchmetadata_regex = re.compile(r"-{3} \S+|\+{3} \S+|@{2} -\d+,\d+ \+\d+,\d+ @{2} \S+")
# mbox
-auh_email = 'auh@yoctoproject.org'
+auh_email = "auh@yoctoproject.org"
-invalid_submitters = [pyparsing.Regex("^Upgrade Helper.+"),
- pyparsing.Regex(auh_email),
- pyparsing.Regex("uh@not\.set"),
- pyparsing.Regex("\S+@example\.com")]
+invalid_submitters = [
+ pyparsing.Regex("^Upgrade Helper.+"),
+ pyparsing.Regex(auh_email),
+ pyparsing.Regex("uh@not\.set"),
+ pyparsing.Regex("\S+@example\.com"),
+]
-mbox_bugzilla = pyparsing.Regex('\[\s?YOCTO.*\]')
-mbox_bugzilla_validation = pyparsing.Regex('\[(\s?YOCTO\s?#\s?(\d+)\s?,?)+\]')
+mbox_bugzilla = pyparsing.Regex("\[\s?YOCTO.*\]")
+mbox_bugzilla_validation = pyparsing.Regex("\[(\s?YOCTO\s?#\s?(\d+)\s?,?)+\]")
mbox_revert_shortlog_regex = pyparsing.Regex('Revert\s+".*"')
mbox_shortlog_maxlength = 90
@@ -67,30 +67,56 @@ upstream_status_regex = pyparsing.AtLineStart("+" + "Upstream-Status")
# shortlog
-shortlog_target = pyparsing.OneOrMore(pyparsing.Word(pyparsing.printables.replace(':','')))
+shortlog_target = pyparsing.OneOrMore(
+ pyparsing.Word(pyparsing.printables.replace(":", ""))
+)
shortlog_summary = pyparsing.OneOrMore(pyparsing.Word(pyparsing.printables))
shortlog = line_start + shortlog_target + colon + shortlog_summary + line_end
# signed-off-bys
-email_pattern = pyparsing.Regex(r"(?P<user>[A-Za-z0-9._%+-]+)@(?P<hostname>[A-Za-z0-9.-]+)\.(?P<domain>[A-Za-z]{2,})")
+email_pattern = pyparsing.Regex(
+ r"(?P<user>[A-Za-z0-9._%+-]+)@(?P<hostname>[A-Za-z0-9.-]+)\.(?P<domain>[A-Za-z]{2,})"
+)
signed_off_by_prefix = pyparsing.Literal("Signed-off-by:")
-signed_off_by_name = pyparsing.Regex('\S+.*(?= <)')
+signed_off_by_name = pyparsing.Regex("\S+.*(?= <)")
signed_off_by_email = lessthan + email_pattern + greaterthan
-signed_off_by = pyparsing.AtLineStart(signed_off_by_prefix + signed_off_by_name + signed_off_by_email)
-patch_signed_off_by = pyparsing.AtLineStart("+" + signed_off_by_prefix + signed_off_by_name + signed_off_by_email)
+signed_off_by = pyparsing.AtLineStart(
+ signed_off_by_prefix + signed_off_by_name + signed_off_by_email
+)
+patch_signed_off_by = pyparsing.AtLineStart(
+ "+" + signed_off_by_prefix + signed_off_by_name + signed_off_by_email
+)
# upstream-status
-upstream_status_literal_valid_status = ["Pending", "Backport", "Denied", "Inappropriate", "Submitted"]
-upstream_status_nonliteral_valid_status = ["Pending", "Backport", "Denied", "Inappropriate [reason]", "Submitted [where]"]
+upstream_status_literal_valid_status = [
+ "Pending",
+ "Backport",
+ "Denied",
+ "Inappropriate",
+ "Submitted",
+]
+upstream_status_nonliteral_valid_status = [
+ "Pending",
+ "Backport",
+ "Denied",
+ "Inappropriate [reason]",
+ "Submitted [where]",
+]
upstream_status_valid_status = pyparsing.Or(
[pyparsing.Literal(status) for status in upstream_status_literal_valid_status]
)
upstream_status_prefix = pyparsing.Literal("Upstream-Status")
-upstream_status = line_start + upstream_status_prefix + colon + upstream_status_valid_status
-upstream_status_inappropriate_info = line_start + upstream_status_prefix + colon + inappropriateinfo
-upstream_status_submitted_info = line_start + upstream_status_prefix + colon + submittedinfo
+upstream_status = (
+ line_start + upstream_status_prefix + colon + upstream_status_valid_status
+)
+upstream_status_inappropriate_info = (
+ line_start + upstream_status_prefix + colon + inappropriateinfo
+)
+upstream_status_submitted_info = (
+ line_start + upstream_status_prefix + colon + submittedinfo
+)
@@ -12,10 +12,11 @@ import git
import os
import mbox
+
class PatchTestRepo(object):
# prefixes used for temporal branches/stashes
- prefix = 'patchtest'
+ prefix = "patchtest"
def __init__(self, patch, repodir, commit=None, branch=None):
self.repodir = repodir
@@ -28,23 +29,27 @@ class PatchTestRepo(object):
valid_patch_branch = None
if self.patch.branch in self.repo.branches:
valid_patch_branch = self.patch.branch
-
+
# Target Commit
# Priority (top has highest priority):
# 1. commit given at cmd line
# 2. branch given at cmd line
# 3. branch given at the patch
# 3. current HEAD
- self._commit = self._get_commitid(commit) or \
- self._get_commitid(branch) or \
- self._get_commitid(valid_patch_branch) or \
- self._get_commitid('HEAD')
+ self._commit = (
+ self._get_commitid(commit)
+ or self._get_commitid(branch)
+ or self._get_commitid(valid_patch_branch)
+ or self._get_commitid("HEAD")
+ )
self._workingbranch = "%s_%s" % (PatchTestRepo.prefix, os.getpid())
# create working branch. Use the '-B' flag so that we just
# check out the existing one if it's there
- self.repo.git.execute(['git', 'checkout', '-B', self._workingbranch, self._commit])
+ self.repo.git.execute(
+ ["git", "checkout", "-B", self._workingbranch, self._commit]
+ )
self._patchmerged = False
@@ -52,7 +57,10 @@ class PatchTestRepo(object):
self._patchcanbemerged = True
try:
# Make sure to get the absolute path of the file
- self.repo.git.execute(['git', 'apply', '--check', os.path.abspath(self.patch.path)], with_exceptions=True)
+ self.repo.git.execute(
+ ["git", "apply", "--check", os.path.abspath(self.patch.path)],
+ with_exceptions=True,
+ )
except git.exc.GitCommandError as ce:
self._patchcanbemerged = False
@@ -76,10 +84,12 @@ class PatchTestRepo(object):
def merge(self):
if self._patchcanbemerged:
- self.repo.git.execute(['git', 'am', '--keep-cr', os.path.abspath(self.patch.path)])
+ self.repo.git.execute(
+ ["git", "am", "--keep-cr", os.path.abspath(self.patch.path)]
+ )
self._patchmerged = True
def clean(self):
- self.repo.git.execute(['git', 'checkout', self.current_branch])
- self.repo.git.execute(['git', 'branch', '-D', self._workingbranch])
+ self.repo.git.execute(["git", "checkout", self.current_branch])
+ self.repo.git.execute(["git", "branch", "-D", self._workingbranch])
self._patchmerged = False
@@ -11,18 +11,33 @@ import subprocess
import sys
currentdir = os.path.dirname(os.path.abspath(__file__))
-patchesdir = os.path.join(currentdir, 'files')
-topdir = os.path.dirname(currentdir)
-parentdir = os.path.dirname(topdir)
+patchesdir = os.path.join(currentdir, "files")
+topdir = os.path.dirname(currentdir)
+parentdir = os.path.dirname(topdir)
# path to the repo root
repodir = os.path.dirname(os.path.dirname(parentdir))
-def print_results(passcount, failcount, skipcount, xpasscount, xfailcount, xskipcount, errorcount):
- total = passcount + skipcount + failcount + xpasscount + xfailcount + xskipcount + errorcount
- print("============================================================================")
+
+def print_results(
+ passcount, failcount, skipcount, xpasscount, xfailcount, xskipcount, errorcount
+):
+ total = (
+ passcount
+ + skipcount
+ + failcount
+ + xpasscount
+ + xfailcount
+ + xskipcount
+ + errorcount
+ )
+ print(
+ "============================================================================"
+ )
print("Testsuite summary for %s" % os.path.basename(topdir))
- print("============================================================================")
+ print(
+ "============================================================================"
+ )
print("# TOTAL: %s" % str(total))
print("# XPASS: %s" % str(xpasscount))
print("# XFAIL: %s" % str(xfailcount))
@@ -31,19 +46,29 @@ def print_results(passcount, failcount, skipcount, xpasscount, xfailcount, xskip
print("# FAIL: %s" % str(failcount))
print("# SKIP: %s" % str(skipcount))
print("# ERROR: %s" % str(errorcount))
- print("============================================================================")
+ print(
+ "============================================================================"
+ )
+
# Once the tests are in oe-core, we can remove the testdir param and use os.path.dirname to get relative paths
def test(root, patch):
res = True
patchpath = os.path.abspath(os.path.join(root, patch))
-
- cmd = 'patchtest --repodir %s --testdir %s/tests --patch %s' % (repodir, topdir, patchpath)
- results = subprocess.check_output(cmd, stderr=subprocess.STDOUT, universal_newlines=True, shell=True)
+
+ cmd = "patchtest --repodir %s --testdir %s/tests --patch %s" % (
+ repodir,
+ topdir,
+ patchpath,
+ )
+ results = subprocess.check_output(
+ cmd, stderr=subprocess.STDOUT, universal_newlines=True, shell=True
+ )
return results
-if __name__ == '__main__':
+
+if __name__ == "__main__":
passcount = 0
failcount = 0
skipcount = 0
@@ -53,31 +78,47 @@ if __name__ == '__main__':
errorcount = 0
results = None
-
+
for root, dirs, patches in os.walk(patchesdir):
for patch in patches:
results = test(root, patch)
- a = patch.split('.')
+ a = patch.split(".")
klass, testname = a[0], a[1]
expected_result = a[-1]
- testid = ".%s.%s" % (klass,testname)
+ testid = ".%s.%s" % (klass, testname)
for resultline in results.splitlines():
if testid in resultline:
- result, _ = resultline.split(':', 1)
+ result, _ = resultline.split(":", 1)
if expected_result.upper() == "FAIL" and result.upper() == "FAIL":
xfailcount = xfailcount + 1
- print("XFAIL: %s (file: %s)" % (testid.strip("."), os.path.basename(patch)))
+ print(
+ "XFAIL: %s (file: %s)"
+ % (testid.strip("."), os.path.basename(patch))
+ )
elif expected_result.upper() == "PASS" and result.upper() == "PASS":
xpasscount = xpasscount + 1
- print("XPASS: %s (file: %s)" % (testid.strip("."), os.path.basename(patch)))
+ print(
+ "XPASS: %s (file: %s)"
+ % (testid.strip("."), os.path.basename(patch))
+ )
elif expected_result.upper() == "SKIP" and result.upper() == "SKIP":
xskipcount = xskipcount + 1
- print("XSKIP: %s (file: %s)" % (testid.strip("."), os.path.basename(patch)))
+ print(
+ "XSKIP: %s (file: %s)"
+ % (testid.strip("."), os.path.basename(patch))
+ )
else:
- print("%s: %s (%s)" % (result.upper(), testid.strip("."), os.path.basename(patch)))
+ print(
+ "%s: %s (%s)"
+ % (
+ result.upper(),
+ testid.strip("."),
+ os.path.basename(patch),
+ )
+ )
if result.upper() == "PASS":
passcount = passcount + 1
elif result.upper() == "FAIL":
@@ -85,10 +126,15 @@ if __name__ == '__main__':
elif result.upper() == "SKIP":
skipcount = skipcount + 1
else:
- print("Bad result on test %s against %s" % (testid.strip("."), os.path.basename(patch)))
+ print(
+ "Bad result on test %s against %s"
+ % (testid.strip("."), os.path.basename(patch))
+ )
errorcount = errorcount + 1
break
else:
- print ("No test for=%s" % patch)
+ print("No test for=%s" % patch)
- print_results(passcount, failcount, skipcount, xpasscount, xfailcount, xskipcount, errorcount)
+ print_results(
+ passcount, failcount, skipcount, xpasscount, xfailcount, xskipcount, errorcount
+ )
@@ -26,40 +26,43 @@ Commit = collections.namedtuple(
"Commit", ["author", "subject", "commit_message", "shortlog", "payload"]
)
-Commit = collections.namedtuple('Commit', ['author', 'subject', 'commit_message', 'shortlog', 'payload'])
class PatchtestOEError(Exception):
"""Exception for handling patchtest-oe errors"""
+
def __init__(self, message, exitcode=1):
super().__init__(message)
self.exitcode = exitcode
+
class Base(unittest.TestCase):
# if unit test fails, fail message will throw at least the following JSON: {"id": <testid>}
@staticmethod
def msg_to_commit(msg):
payload = msg.get_payload()
- return Commit(subject=msg['subject'].replace('\n', ' ').replace(' ', ' '),
- author=msg.get('From'),
- shortlog=Base.shortlog(msg['subject']),
- commit_message=Base.commit_message(payload),
- payload=payload)
+ return Commit(
+ subject=msg["subject"].replace("\n", " ").replace(" ", " "),
+ author=msg.get("From"),
+ shortlog=Base.shortlog(msg["subject"]),
+ commit_message=Base.commit_message(payload),
+ payload=payload,
+ )
@staticmethod
def commit_message(payload):
commit_message = payload.__str__()
match = patchtest_patterns.endcommit_messages_regex.search(payload)
if match:
- commit_message = payload[:match.start()]
+ commit_message = payload[: match.start()]
return commit_message
@staticmethod
def shortlog(shlog):
# remove possible prefix (between brackets) before colon
- start = shlog.find(']', 0, shlog.find(':'))
+ start = shlog.find("]", 0, shlog.find(":"))
# remove also newlines and spaces at both sides
- return shlog[start + 1:].replace('\n', '').strip()
+ return shlog[start + 1 :].replace("\n", "").strip()
@classmethod
def setUpClass(cls):
@@ -68,7 +71,7 @@ class Base(unittest.TestCase):
cls.mbox = mailbox.mbox(PatchtestParser.repo.patch.path)
# Patch may be malformed, so try parsing it
- cls.unidiff_parse_error = ''
+ cls.unidiff_parse_error = ""
cls.patchset = None
try:
cls.patchset = unidiff.PatchSet.from_filename(
@@ -81,7 +84,7 @@ class Base(unittest.TestCase):
# Easy to iterate list of commits
cls.commits = []
for msg in cls.mbox:
- if msg['subject'] and msg.get_payload():
+ if msg["subject"] and msg.get_payload():
cls.commits.append(Base.msg_to_commit(msg))
cls.setUpClassLocal()
@@ -99,38 +102,36 @@ class Base(unittest.TestCase):
pass
def fail(self, issue, fix=None, commit=None, data=None):
- """ Convert to a JSON string failure data"""
- value = {'id': self.id(),
- 'issue': issue}
+ """Convert to a JSON string failure data"""
+ value = {"id": self.id(), "issue": issue}
if fix:
- value['fix'] = fix
+ value["fix"] = fix
if commit:
- value['commit'] = {'subject': commit.subject,
- 'shortlog': commit.shortlog}
+ value["commit"] = {"subject": commit.subject, "shortlog": commit.shortlog}
# extend return value with other useful info
if data:
- value['data'] = data
+ value["data"] = data
return super(Base, self).fail(json.dumps(value))
def skip(self, issue, data=None):
- """ Convert the skip string to JSON"""
- value = {'id': self.id(),
- 'issue': issue}
+ """Convert the skip string to JSON"""
+ value = {"id": self.id(), "issue": issue}
# extend return value with other useful info
if data:
- value['data'] = data
+ value["data"] = data
return super(Base, self).skipTest(json.dumps(value))
def shortid(self):
- return self.id().split('.')[-1]
+ return self.id().split(".")[-1]
def __str__(self):
- return json.dumps({'id': self.id()})
+ return json.dumps({"id": self.id()})
+
class Metadata(Base):
@classmethod
@@ -154,19 +155,20 @@ class Metadata(Base):
if scripts_path not in sys.path:
sys.path.insert(0, scripts_path)
import scriptpath
+
scriptpath.add_bitbake_lib_path()
import bb.tinfoil
except ImportError:
- raise PatchtestOEError('Could not import tinfoil module')
+ raise PatchtestOEError("Could not import tinfoil module")
orig_cwd = os.path.abspath(os.curdir)
# Load tinfoil
tinfoil = None
try:
- builddir = os.environ.get('BUILDDIR')
+ builddir = os.environ.get("BUILDDIR")
if not builddir:
- logger.warn('Bitbake environment not loaded?')
+ logger.warn("Bitbake environment not loaded?")
return tinfoil
os.chdir(builddir)
tinfoil = bb.tinfoil.Tinfoil()
@@ -174,7 +176,9 @@ class Metadata(Base):
except bb.tinfoil.TinfoilUIException as te:
if tinfoil:
tinfoil.shutdown()
- raise PatchtestOEError('Could not prepare properly tinfoil (TinfoilUIException)')
+ raise PatchtestOEError(
+ "Could not prepare properly tinfoil (TinfoilUIException)"
+ )
except Exception as e:
if tinfoil:
tinfoil.shutdown()
@@ -194,7 +198,7 @@ class Metadata(Base):
pn_native = None
for _path, _pn in data:
if path in _path:
- if 'native' in _pn:
+ if "native" in _pn:
# store the native PN but look for the non-native one first
pn_native = _pn
else:
@@ -208,9 +212,9 @@ class Metadata(Base):
# on renames (usually upgrades), we need to check (FILE) base names
# because the unidiff library does not provided the new filename, just the modified one
# and tinfoil datastore, once the patch is merged, will contain the new filename
- path_basename = path.split('_')[0]
+ path_basename = path.split("_")[0]
for _path, _pn in data:
- _path_basename = _path.split('_')[0]
+ _path_basename = _path.split("_")[0]
if path_basename == _path_basename:
pn = _pn
return pn
@@ -223,7 +227,11 @@ class Metadata(Base):
# get metadata filename additions, modification and removals
for patch in patchset:
- if patch.path.endswith('.bb') or patch.path.endswith('.bbappend') or patch.path.endswith('.inc'):
+ if (
+ patch.path.endswith(".bb")
+ or patch.path.endswith(".bbappend")
+ or patch.path.endswith(".inc")
+ ):
if patch.is_added_file:
added_paths.append(
os.path.join(
@@ -243,10 +251,14 @@ class Metadata(Base):
)
)
- data = cls.tinfoil.cooker.recipecaches[''].pkg_fn.items()
+ data = cls.tinfoil.cooker.recipecaches[""].pkg_fn.items()
- added = [find_pn(data,path) for path in added_paths]
- modified = [find_pn(data,path) for path in modified_paths]
- removed = [find_pn(data,path) for path in removed_paths]
+ added = [find_pn(data, path) for path in added_paths]
+ modified = [find_pn(data, path) for path in modified_paths]
+ removed = [find_pn(data, path) for path in removed_paths]
- return [a for a in added if a], [m for m in modified if m], [r for r in removed if r]
+ return (
+ [a for a in added if a],
+ [m for m in modified if m],
+ [r for r in removed if r],
+ )
@@ -12,40 +12,73 @@ import re
import subprocess
from patchtest_parser import PatchtestParser
+
def headlog():
output = subprocess.check_output(
"cd %s; git log --pretty='%%h#%%aN#%%cD:#%%s' -1" % PatchtestParser.repodir,
universal_newlines=True,
- shell=True
- )
- return output.split('#')
+ shell=True,
+ )
+ return output.split("#")
+
class TestMbox(base.Base):
# base paths of main yocto project sub-projects
paths = {
- 'oe-core': ['meta-selftest', 'meta-skeleton', 'meta', 'scripts'],
- 'bitbake': ['bitbake'],
- 'documentation': ['documentation'],
- 'poky': ['meta-poky','meta-yocto-bsp'],
- 'oe': ['meta-gpe', 'meta-gnome', 'meta-efl', 'meta-networking', 'meta-multimedia','meta-initramfs', 'meta-ruby', 'contrib', 'meta-xfce', 'meta-filesystems', 'meta-perl', 'meta-webserver', 'meta-systemd', 'meta-oe', 'meta-python']
- }
-
- # scripts folder is a mix of oe-core and poky, most is oe-core code except:
- poky_scripts = ['scripts/yocto-bsp', 'scripts/yocto-kernel', 'scripts/yocto-layer', 'scripts/lib/bsp']
-
- Project = collections.namedtuple('Project', ['name', 'listemail', 'gitrepo', 'paths'])
-
- bitbake = Project(name='Bitbake', listemail='bitbake-devel@lists.openembedded.org', gitrepo='http://git.openembedded.org/bitbake/', paths=paths['bitbake'])
- doc = Project(name='Documentantion', listemail='yocto@yoctoproject.org', gitrepo='http://git.yoctoproject.org/cgit/cgit.cgi/yocto-docs/', paths=paths['documentation'])
- poky = Project(name='Poky', listemail='poky@yoctoproject.org', gitrepo='http://git.yoctoproject.org/cgit/cgit.cgi/poky/', paths=paths['poky'])
- oe = Project(name='oe', listemail='openembedded-devel@lists.openembedded.org', gitrepo='http://git.openembedded.org/meta-openembedded/', paths=paths['oe'])
-
+ "oe-core": ["meta-selftest", "meta-skeleton", "meta", "scripts"],
+ "bitbake": ["bitbake"],
+ "documentation": ["documentation"],
+ "poky": ["meta-poky", "meta-yocto-bsp"],
+ "oe": [
+ "meta-filesystems",
+ "meta-gnome",
+ "meta-initramfs",
+ "meta-multimedia",
+ "meta-networking",
+ "meta-oe",
+ "meta-perl",
+ "meta-python",
+ "meta-webserver",
+ "meta-xfce",
+ ],
+ }
+
+ Project = collections.namedtuple(
+ "Project", ["name", "listemail", "gitrepo", "paths"]
+ )
+
+ bitbake = Project(
+ name="Bitbake",
+ listemail="bitbake-devel@lists.openembedded.org",
+ gitrepo="http://git.openembedded.org/bitbake/",
+ paths=paths["bitbake"],
+ )
+ doc = Project(
+ name="Documentantion",
+ listemail="yocto@yoctoproject.org",
+ gitrepo="http://git.yoctoproject.org/cgit/cgit.cgi/yocto-docs/",
+ paths=paths["documentation"],
+ )
+ poky = Project(
+ name="Poky",
+ listemail="poky@yoctoproject.org",
+ gitrepo="http://git.yoctoproject.org/cgit/cgit.cgi/poky/",
+ paths=paths["poky"],
+ )
+ oe = Project(
+ name="oe",
+ listemail="openembedded-devel@lists.openembedded.org",
+ gitrepo="http://git.openembedded.org/meta-openembedded/",
+ paths=paths["oe"],
+ )
def test_signed_off_by_presence(self):
for commit in self.commits:
# skip those patches that revert older commits, these do not required the tag presence
- if patchtest_patterns.mbox_revert_shortlog_regex.search_string(commit.shortlog):
+ if patchtest_patterns.mbox_revert_shortlog_regex.search_string(
+ commit.shortlog
+ ):
continue
if not patchtest_patterns.signed_off_by.search_string(commit.payload):
self.fail(
@@ -57,7 +90,7 @@ class TestMbox(base.Base):
for commit in self.commits:
shortlog = commit.shortlog
if not shortlog.strip():
- self.skip('Empty shortlog, no reason to execute shortlog format test')
+ self.skip("Empty shortlog, no reason to execute shortlog format test")
else:
# no reason to re-check on revert shortlogs
if shortlog.startswith('Revert "'):
@@ -65,13 +98,15 @@ class TestMbox(base.Base):
try:
patchtest_patterns.shortlog.parseString(shortlog)
except pyparsing.ParseException as pe:
- self.fail('Commit shortlog (first line of commit message) should follow the format "<target>: <summary>"',
- commit=commit)
+ self.fail(
+ 'Commit shortlog (first line of commit message) should follow the format "<target>: <summary>"',
+ commit=commit,
+ )
def test_shortlog_length(self):
for commit in self.commits:
# no reason to re-check on revert shortlogs
- shortlog = re.sub('^(\[.*?\])+ ', '', commit.shortlog)
+ shortlog = re.sub("^(\[.*?\])+ ", "", commit.shortlog)
if shortlog.startswith('Revert "'):
continue
l = len(shortlog)
@@ -113,38 +148,63 @@ class TestMbox(base.Base):
for commit in self.commits:
match = project_regex.search_string(commit.subject)
if match:
- self.fail('Series sent to the wrong mailing list or some patches from the series correspond to different mailing lists',
- commit=commit)
+ self.fail(
+ "Series sent to the wrong mailing list or some patches from the series correspond to different mailing lists",
+ commit=commit,
+ )
for patch in self.patchset:
- folders = patch.path.split('/')
+ folders = patch.path.split("/")
base_path = folders[0]
for project in [self.bitbake, self.doc, self.oe, self.poky]:
- if base_path in project.paths:
- self.fail('Series sent to the wrong mailing list or some patches from the series correspond to different mailing lists',
- data=[('Suggested ML', '%s [%s]' % (project.listemail, project.gitrepo)),
- ('Patch\'s path:', patch.path)])
+ if base_path in project.paths:
+ self.fail(
+ "Series sent to the wrong mailing list or some patches from the series correspond to different mailing lists",
+ data=[
+ (
+ "Suggested ML",
+ "%s [%s]" % (project.listemail, project.gitrepo),
+ ),
+ ("Patch's path:", patch.path),
+ ],
+ )
# check for poky's scripts code
- if base_path.startswith('scripts'):
+ if base_path.startswith("scripts"):
for poky_file in self.poky_scripts:
if patch.path.startswith(poky_file):
- self.fail('Series sent to the wrong mailing list or some patches from the series correspond to different mailing lists',
- data=[('Suggested ML', '%s [%s]' % (self.poky.listemail, self.poky.gitrepo)),('Patch\'s path:', patch.path)])
+ self.fail(
+ "Series sent to the wrong mailing list or some patches from the series correspond to different mailing lists",
+ data=[
+ (
+ "Suggested ML",
+ "%s [%s]"
+ % (self.poky.listemail, self.poky.gitrepo),
+ ),
+ ("Patch's path:", patch.path),
+ ],
+ )
def test_mbox_format(self):
if self.unidiff_parse_error:
- self.fail('Series has malformed diff lines. Create the series again using git-format-patch and ensure it applies using git am',
- data=[('Diff line',self.unidiff_parse_error)])
+ self.fail(
+ "Series has malformed diff lines. Create the series again using git-format-patch and ensure it applies using git am",
+ data=[("Diff line", self.unidiff_parse_error)],
+ )
def test_commit_message_presence(self):
for commit in self.commits:
if not commit.commit_message.strip():
- self.fail('Please include a commit message on your patch explaining the change', commit=commit)
+ self.fail(
+ "Please include a commit message on your patch explaining the change",
+ commit=commit,
+ )
def test_bugzilla_entry_format(self):
for commit in self.commits:
- if not patchtest_patterns.mbox_bugzilla.search_string(commit.commit_message):
+ if not patchtest_patterns.mbox_bugzilla.search_string(
+ commit.commit_message
+ ):
self.skip("No bug ID found")
elif not patchtest_patterns.mbox_bugzilla_validation.search_string(
commit.commit_message
@@ -158,7 +218,11 @@ class TestMbox(base.Base):
for commit in self.commits:
for invalid in patchtest_patterns.invalid_submitters:
if invalid.search_string(commit.author):
- self.fail('Invalid author %s. Resend the series with a valid patch author' % commit.author, commit=commit)
+ self.fail(
+ "Invalid author %s. Resend the series with a valid patch author"
+ % commit.author,
+ commit=commit,
+ )
def test_non_auh_upgrade(self):
for commit in self.commits:
@@ -14,19 +14,20 @@ from patchtest_parser import PatchtestParser
# Data store commonly used to share values between pre and post-merge tests
PatchTestDataStore = collections.defaultdict(str)
+
class TestMetadata(base.Metadata):
def test_license_presence(self):
if not self.added:
- self.skip('No added recipes, skipping test')
+ self.skip("No added recipes, skipping test")
# TODO: this is a workaround so we can parse the recipe not
# containing the LICENSE var: add some default license instead
# of INVALID into auto.conf, then remove this line at the end
- auto_conf = os.path.join(os.environ.get('BUILDDIR'), 'conf', 'auto.conf')
- open_flag = 'w'
+ auto_conf = os.path.join(os.environ.get("BUILDDIR"), "conf", "auto.conf")
+ open_flag = "w"
if os.path.exists(auto_conf):
- open_flag = 'a'
+ open_flag = "a"
with open(auto_conf, open_flag) as fd:
for pn in self.added:
fd.write('LICENSE ??= "%s"\n' % patchtest_patterns.invalid_license)
@@ -40,43 +41,44 @@ class TestMetadata(base.Metadata):
break
# remove auto.conf line or the file itself
- if open_flag == 'w':
+ if open_flag == "w":
os.remove(auto_conf)
else:
- fd = open(auto_conf, 'r')
+ fd = open(auto_conf, "r")
lines = fd.readlines()
fd.close()
- with open(auto_conf, 'w') as fd:
- fd.write(''.join(lines[:-1]))
+ with open(auto_conf, "w") as fd:
+ fd.write("".join(lines[:-1]))
if no_license:
- self.fail('Recipe does not have the LICENSE field set.')
+ self.fail("Recipe does not have the LICENSE field set.")
def test_lic_files_chksum_presence(self):
if not self.added:
- self.skip('No added recipes, skipping test')
+ self.skip("No added recipes, skipping test")
for pn in self.added:
rd = self.tinfoil.parse_recipe(pn)
- pathname = rd.getVar('FILE')
+ pathname = rd.getVar("FILE")
# we are not interested in images
- if '/images/' in pathname:
+ if "/images/" in pathname:
continue
lic_files_chksum = rd.getVar(patchtest_patterns.metadata_chksum)
if rd.getVar(patchtest_patterns.license_var) == patchtest_patterns.closed:
continue
if not lic_files_chksum:
self.fail(
- "%s is missing in newly added recipe" % patchtest_patterns.metadata_chksum
+ "%s is missing in newly added recipe"
+ % patchtest_patterns.metadata_chksum
)
def test_lic_files_chksum_modified_not_mentioned(self):
if not self.modified:
- self.skip('No modified recipes, skipping test')
+ self.skip("No modified recipes, skipping test")
for patch in self.patchset:
# for the moment, we are just interested in metadata
- if patch.path.endswith('.patch'):
+ if patch.path.endswith(".patch"):
continue
payload = str(patch)
if patchtest_patterns.lic_chksum_added.search_string(
@@ -84,15 +86,19 @@ class TestMetadata(base.Metadata):
) or patchtest_patterns.lic_chksum_removed.search_string(payload):
# if any patch on the series contain reference on the metadata, fail
for commit in self.commits:
- if patchtest_patterns.lictag_re.search_string(commit.commit_message):
+ if patchtest_patterns.lictag_re.search_string(
+ commit.commit_message
+ ):
break
else:
- self.fail('LIC_FILES_CHKSUM changed without "License-Update:" tag and description in commit message')
+ self.fail(
+ 'LIC_FILES_CHKSUM changed without "License-Update:" tag and description in commit message'
+ )
def test_max_line_length(self):
for patch in self.patchset:
# for the moment, we are just interested in metadata
- if patch.path.endswith('.patch'):
+ if patch.path.endswith(".patch"):
continue
payload = str(patch)
for line in payload.splitlines():
@@ -101,7 +107,10 @@ class TestMetadata(base.Metadata):
if current_line_length > patchtest_patterns.patch_max_line_length:
self.fail(
"Patch line too long (current length %s, maximum is %s)"
- % (current_line_length, patchtest_patterns.patch_max_line_length),
+ % (
+ current_line_length,
+ patchtest_patterns.patch_max_line_length,
+ ),
data=[
("Patch", patch.path),
("Line", "%s ..." % line[0:80]),
@@ -113,12 +122,12 @@ class TestMetadata(base.Metadata):
if not PatchtestParser.repo.canbemerged:
self.skip("Patch cannot be merged")
if not self.modified:
- self.skip('No modified recipes, skipping pretest')
+ self.skip("No modified recipes, skipping pretest")
# get the proper metadata values
for pn in self.modified:
# we are not interested in images
- if 'core-image' in pn:
+ if "core-image" in pn:
continue
rd = self.tinfoil.parse_recipe(pn)
PatchTestDataStore[
@@ -130,12 +139,12 @@ class TestMetadata(base.Metadata):
if not PatchtestParser.repo.canbemerged:
self.skip("Patch cannot be merged")
if not self.modified:
- self.skip('No modified recipes, skipping pretest')
+ self.skip("No modified recipes, skipping pretest")
# get the proper metadata values
for pn in self.modified:
# we are not interested in images
- if 'core-image' in pn:
+ if "core-image" in pn:
continue
rd = self.tinfoil.parse_recipe(pn)
PatchTestDataStore[
@@ -144,14 +153,27 @@ class TestMetadata(base.Metadata):
for pn in self.modified:
pretest_src_uri = PatchTestDataStore[
- "pre%s-%s-%s" % (self.shortid(), patchtest_patterns.metadata_src_uri, pn)
+ "pre%s-%s-%s"
+ % (self.shortid(), patchtest_patterns.metadata_src_uri, pn)
].split()
test_src_uri = PatchTestDataStore[
"%s-%s-%s" % (self.shortid(), patchtest_patterns.metadata_src_uri, pn)
].split()
- pretest_files = set([os.path.basename(patch) for patch in pretest_src_uri if patch.startswith('file://')])
- test_files = set([os.path.basename(patch) for patch in test_src_uri if patch.startswith('file://')])
+ pretest_files = set(
+ [
+ os.path.basename(patch)
+ for patch in pretest_src_uri
+ if patch.startswith("file://")
+ ]
+ )
+ test_files = set(
+ [
+ os.path.basename(patch)
+ for patch in test_src_uri
+ if patch.startswith("file://")
+ ]
+ )
# check if files were removed
if len(test_files) < len(pretest_files):
@@ -169,16 +191,18 @@ class TestMetadata(base.Metadata):
# TODO: we are not taking into account renames, so test may raise false positives
not_removed = filesremoved_from_usr_uri - filesremoved_from_patchset
if not_removed:
- self.fail('Patches not removed from tree. Remove them and amend the submitted mbox',
- data=[('Patch', f) for f in not_removed])
+ self.fail(
+ "Patches not removed from tree. Remove them and amend the submitted mbox",
+ data=[("Patch", f) for f in not_removed],
+ )
def test_summary_presence(self):
if not self.added:
- self.skip('No added recipes, skipping test')
+ self.skip("No added recipes, skipping test")
for pn in self.added:
# we are not interested in images
- if 'core-image' in pn:
+ if "core-image" in pn:
continue
rd = self.tinfoil.parse_recipe(pn)
summary = rd.getVar(patchtest_patterns.metadata_summary)
@@ -186,7 +210,8 @@ class TestMetadata(base.Metadata):
# "${PN} version ${PN}-${PR}" is the default, so fail if default
if summary.startswith("%s version" % pn):
self.fail(
- "%s is missing in newly added recipe" % patchtest_patterns.metadata_summary
+ "%s is missing in newly added recipe"
+ % patchtest_patterns.metadata_summary
)
def test_cve_check_ignore(self):
@@ -200,7 +225,7 @@ class TestMetadata(base.Metadata):
self.skip("No modified recipes or older target branch, skipping test")
for pn in self.modified:
# we are not interested in images
- if 'core-image' in pn:
+ if "core-image" in pn:
continue
rd = self.tinfoil.parse_recipe(pn)
cve_check_ignore = rd.getVar(patchtest_patterns.cve_check_ignore_var)
@@ -208,5 +233,8 @@ class TestMetadata(base.Metadata):
if cve_check_ignore is not None:
self.fail(
"%s is deprecated and should be replaced by %s"
- % (patchtest_patterns.cve_check_ignore_var, patchtest_patterns.cve_status_var)
+ % (
+ patchtest_patterns.cve_check_ignore_var,
+ patchtest_patterns.cve_status_var,
+ )
)
@@ -10,6 +10,7 @@ import os
import patchtest_patterns
import pyparsing
+
class TestPatch(base.Base):
@classmethod
@@ -17,7 +18,7 @@ class TestPatch(base.Base):
cls.newpatches = []
# get just those relevant patches: new software patches
for patch in cls.patchset:
- if patch.path.endswith('.patch') and patch.is_added_file:
+ if patch.path.endswith(".patch") and patch.is_added_file:
cls.newpatches.append(patch)
cls.mark = str(patchtest_patterns.signed_off_by_prefix).strip('"')
@@ -27,21 +28,27 @@ class TestPatch(base.Base):
def setUp(self):
if self.unidiff_parse_error:
- self.skip('Parse error %s' % self.unidiff_parse_error)
+ self.skip("Parse error %s" % self.unidiff_parse_error)
- self.valid_status = ", ".join(patchtest_patterns.upstream_status_nonliteral_valid_status)
+ self.valid_status = ", ".join(
+ patchtest_patterns.upstream_status_nonliteral_valid_status
+ )
self.standard_format = "Upstream-Status: <Valid status>"
# we are just interested in series that introduce CVE patches, thus discard other
# possibilities: modification to current CVEs, patch directly introduced into the
# recipe, upgrades already including the CVE, etc.
- new_cves = [p for p in self.patchset if p.path.endswith('.patch') and p.is_added_file]
+ new_cves = [
+ p for p in self.patchset if p.path.endswith(".patch") and p.is_added_file
+ ]
if not new_cves:
- self.skip('No new CVE patches introduced')
+ self.skip("No new CVE patches introduced")
def test_upstream_status_presence_format(self):
if not TestPatch.newpatches:
- self.skip("There are no new software patches, no reason to test Upstream-Status presence/format")
+ self.skip(
+ "There are no new software patches, no reason to test Upstream-Status presence/format"
+ )
for newpatch in TestPatch.newpatches:
payload = newpatch.__str__()
@@ -91,7 +98,9 @@ class TestPatch(base.Base):
)
else:
try:
- patchtest_patterns.upstream_status.parseString(line.lstrip("+"))
+ patchtest_patterns.upstream_status.parseString(
+ line.lstrip("+")
+ )
except pyparsing.ParseException as pe:
self.fail(
"Upstream-Status is in incorrect format",
@@ -104,7 +113,10 @@ class TestPatch(base.Base):
def test_signed_off_by_presence(self):
if not TestPatch.newpatches:
- self.skip("There are no new software patches, no reason to test %s presence" % PatchSignedOffBy.mark)
+ self.skip(
+ "There are no new software patches, no reason to test %s presence"
+ % PatchSignedOffBy.mark
+ )
for newpatch in TestPatch.newpatches:
payload = newpatch.__str__()
@@ -114,7 +126,10 @@ class TestPatch(base.Base):
if TestPatch.prog.search_string(payload):
break
else:
- self.fail('A patch file has been added without a Signed-off-by tag: \'%s\'' % os.path.basename(newpatch.path))
+ self.fail(
+ "A patch file has been added without a Signed-off-by tag: '%s'"
+ % os.path.basename(newpatch.path)
+ )
def test_cve_tag_format(self):
for commit in TestPatch.commits:
@@ -127,5 +142,7 @@ class TestPatch(base.Base):
tag_found = True
break
if not tag_found:
- self.fail('Missing or incorrectly formatted CVE tag in patch file. Correct or include the CVE tag in the patch with format: "CVE: CVE-YYYY-XXXX"',
- commit=commit)
+ self.fail(
+ 'Missing or incorrectly formatted CVE tag in patch file. Correct or include the CVE tag in the patch with format: "CVE: CVE-YYYY-XXXX"',
+ commit=commit,
+ )
@@ -12,9 +12,9 @@ import pylint.lint as lint
class PyLint(base.Base):
- pythonpatches = []
+ pythonpatches = []
pylint_pretest = {}
- pylint_test = {}
+ pylint_test = {}
pylint_options = " -E --disable='E0611, E1101, F0401, E0602' --msg-template='L:{line} F:{module} I:{msg}'"
@classmethod
@@ -22,26 +22,32 @@ class PyLint(base.Base):
# get just those patches touching python files
cls.pythonpatches = []
for patch in cls.patchset:
- if patch.path.endswith('.py'):
+ if patch.path.endswith(".py"):
if not patch.is_removed_file:
cls.pythonpatches.append(patch)
def setUp(self):
if self.unidiff_parse_error:
- self.skip('Python-unidiff parse error')
+ self.skip("Python-unidiff parse error")
if not PyLint.pythonpatches:
- self.skip('No python related patches, skipping test')
+ self.skip("No python related patches, skipping test")
def pretest_pylint(self):
for pythonpatch in self.pythonpatches:
if pythonpatch.is_modified_file:
pylint_output = StringIO()
reporter = TextReporter(pylint_output)
- lint.Run([self.pylint_options, pythonpatch.path], reporter=reporter, exit=False)
+ lint.Run(
+ [self.pylint_options, pythonpatch.path],
+ reporter=reporter,
+ exit=False,
+ )
for line in pylint_output.readlines():
- if not '*' in line:
+ if not "*" in line:
if line.strip():
- self.pylint_pretest[line.strip().split(' ',1)[0]] = line.strip().split(' ',1)[1]
+ self.pylint_pretest[line.strip().split(" ", 1)[0]] = (
+ line.strip().split(" ", 1)[1]
+ )
def test_pylint(self):
for pythonpatch in self.pythonpatches:
@@ -53,13 +59,22 @@ class PyLint(base.Base):
path = pythonpatch.path
pylint_output = StringIO()
reporter = TextReporter(pylint_output)
- lint.Run([self.pylint_options, pythonpatch.path], reporter=reporter, exit=False)
+ lint.Run(
+ [self.pylint_options, pythonpatch.path], reporter=reporter, exit=False
+ )
for line in pylint_output.readlines():
- if not '*' in line:
- if line.strip():
- self.pylint_test[line.strip().split(' ',1)[0]] = line.strip().split(' ',1)[1]
+ if not "*" in line:
+ if line.strip():
+ self.pylint_test[line.strip().split(" ", 1)[0]] = (
+ line.strip().split(" ", 1)[1]
+ )
for issue in self.pylint_test:
- if self.pylint_test[issue] not in self.pylint_pretest.values():
- self.fail('Errors in your Python code were encountered. Please check your code with a linter and resubmit',
- data=[('Output', 'Please, fix the listed issues:'), ('', issue + ' ' + self.pylint_test[issue])])
+ if self.pylint_test[issue] not in self.pylint_pretest.values():
+ self.fail(
+ "Errors in your Python code were encountered. Please check your code with a linter and resubmit",
+ data=[
+ ("Output", "Please, fix the listed issues:"),
+ ("", issue + " " + self.pylint_test[issue]),
+ ],
+ )
@@ -20,7 +20,10 @@ import unittest
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'))
+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
@@ -33,16 +36,18 @@ logger.setLevel(logging.INFO)
info = logger.info
error = logger.error
+
def getResult(patch, mergepatch, logfile=None):
class PatchTestResult(unittest.TextTestResult):
- """ Patchtest TextTestResult """
- shouldStop = True
+ """Patchtest TextTestResult"""
+
+ shouldStop = True
longMessage = False
- success = 'PASS'
- fail = 'FAIL'
- skip = 'SKIP'
+ success = "PASS"
+ fail = "FAIL"
+ skip = "SKIP"
def startTestRun(self):
# let's create the repo already, it can be used later on
@@ -53,9 +58,9 @@ def getResult(patch, mergepatch, logfile=None):
"patch": patch,
}
- self.repo_error = False
- self.test_error = False
- self.test_failure = False
+ self.repo_error = False
+ self.test_error = False
+ self.test_failure = False
try:
self.repo = PatchtestParser.repo = PatchTestRepo(**repoargs)
@@ -74,39 +79,58 @@ def getResult(patch, mergepatch, logfile=None):
logger.error(traceback.print_exc())
def addFailure(self, test, err):
- test_description = 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")
+ test_description = (
+ 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")
+ )
self.test_failure = True
- fail_str = '{}: {}: {} ({})'.format(self.fail,
- test_description, json.loads(str(err[1]))["issue"],
- test.id())
+ fail_str = "{}: {}: {} ({})".format(
+ self.fail, test_description, json.loads(str(err[1]))["issue"], test.id()
+ )
print(fail_str)
if logfile:
with open(logfile, "a") as f:
f.write(fail_str + "\n")
def addSuccess(self, test):
- test_description = 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")
- success_str = '{}: {} ({})'.format(self.success,
- test_description, test.id())
+ test_description = (
+ 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")
+ )
+ success_str = "{}: {} ({})".format(
+ self.success, test_description, test.id()
+ )
print(success_str)
if logfile:
with open(logfile, "a") as f:
f.write(success_str + "\n")
def addSkip(self, test, reason):
- test_description = 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")
- skip_str = '{}: {}: {} ({})'.format(self.skip,
- test_description, json.loads(str(reason))["issue"],
- test.id())
+ test_description = (
+ 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")
+ )
+ skip_str = "{}: {}: {} ({})".format(
+ self.skip, test_description, json.loads(str(reason))["issue"], test.id()
+ )
print(skip_str)
if logfile:
with open(logfile, "a") as f:
@@ -122,6 +146,7 @@ def getResult(patch, mergepatch, logfile=None):
return PatchTestResult
+
def _runner(resultklass, prefix=None):
# load test with the corresponding prefix
loader = unittest.TestLoader()
@@ -145,29 +170,31 @@ def _runner(resultklass, prefix=None):
result = runner.run(suite)
except:
logger.error(traceback.print_exc())
- logger.error('patchtest: something went wrong')
+ logger.error("patchtest: something went wrong")
return 1
if result.test_failure or result.test_error:
- return 1
+ return 1
return 0
+
def run(patch, logfile=None):
- """ Load, setup and run pre and post-merge tests """
+ """Load, setup and run pre and post-merge tests"""
# Get the result class and install the control-c handler
unittest.installHandler()
# run pre-merge tests, meaning those methods with 'pretest' as prefix
premerge_resultklass = getResult(patch, False, logfile)
- premerge_result = _runner(premerge_resultklass, 'pretest')
+ premerge_result = _runner(premerge_resultklass, "pretest")
# run post-merge tests, meaning those methods with 'test' as prefix
postmerge_resultklass = getResult(patch, True, logfile)
- postmerge_result = _runner(postmerge_resultklass, 'test')
+ postmerge_result = _runner(postmerge_resultklass, "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:
@@ -182,6 +209,7 @@ def print_result_message(preresult, postresult):
logger.info("OK: patchtest: All patchtests passed")
print("----------------------------------------------------------------------\n")
+
def main():
tmp_patch = False
patch_path = PatchtestParser.patch_path
@@ -192,20 +220,25 @@ def main():
git_status = os.popen("(cd %s && git status)" % PatchtestParser.repodir).read()
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")
+ 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
if os.path.isdir(patch_path):
- patch_list = [os.path.join(patch_path, filename) for filename in sorted(os.listdir(patch_path))]
+ patch_list = [
+ os.path.join(patch_path, filename)
+ for filename in sorted(os.listdir(patch_path))
+ ]
else:
patch_list = [patch_path]
for patch in patch_list:
if os.path.getsize(patch) == 0:
- logger.error('patchtest: patch is empty')
+ logger.error("patchtest: patch is empty")
return 1
- logger.info('Testing patch %s' % patch)
+ logger.info("Testing patch %s" % patch)
if log_results:
log_path = patch + ".testresult"
@@ -221,7 +254,8 @@ def main():
if tmp_patch:
os.remove(patch)
-if __name__ == '__main__':
+
+if __name__ == "__main__":
ret = 1
# Parse the command line arguments and store it on the PatchtestParser namespace
@@ -239,6 +273,7 @@ if __name__ == '__main__':
ret = main()
except Exception:
import traceback
+
traceback.print_exc(5)
sys.exit(ret)
@@ -18,16 +18,17 @@ import git
re_prefix = re.compile(r"(\[.*\])", re.DOTALL)
+
def get_branch(filepath_repo, filepath_mbox, default_branch):
branch = None
# get all remotes branches
- gitbranches = git.Git(filepath_repo).branch('-a').splitlines()
+ gitbranches = git.Git(filepath_repo).branch("-a").splitlines()
# from gitbranches, just get the names
- branches = [b.split('/')[-1] for b in gitbranches]
+ branches = [b.split("/")[-1] for b in gitbranches]
- subject = ' '.join(mailbox.mbox(filepath_mbox)[0]['subject'].splitlines())
+ subject = " ".join(mailbox.mbox(filepath_mbox)[0]["subject"].splitlines())
# we expect that patches will have somewhere between one and three
# consecutive sets of square brackets with tokens inside, e.g.:
@@ -42,23 +43,23 @@ def get_branch(filepath_repo, filepath_mbox, default_branch):
# Or they may contain both:
# [PATCH v2 3/4]
# In any case, we want mprefix to contain all of these tokens so
- # that we can search for branch names within them.
- mprefix = re.findall(r'\[.*?\]', subject)
+ # that we can search for branch names within them.
+ mprefix = re.findall(r"\[.*?\]", subject)
found_branch = None
if mprefix:
# Iterate over the tokens and compare against the branch list to
# figure out which one the patch is targeting
for token in mprefix:
- stripped = token.lower().strip('[]')
- if default_branch in stripped:
- found_branch = default_branch
- break
- else:
- for branch in branches:
- # ignore branches named "core"
- if branch != "core" and stripped.rfind(branch) != -1:
- found_branch = token.split(' ')[0].strip('[]')
- break
+ stripped = token.lower().strip("[]")
+ if default_branch in stripped:
+ found_branch = default_branch
+ break
+ else:
+ for branch in branches:
+ # ignore branches named "core"
+ if branch != "core" and stripped.rfind(branch) != -1:
+ found_branch = token.split(" ")[0].strip("[]")
+ break
# if there's no mprefix content or no known branches were found in
# the tokens, assume the target is master
@@ -67,15 +68,26 @@ def get_branch(filepath_repo, filepath_mbox, default_branch):
return (subject, found_branch)
-if __name__ == '__main__':
+
+if __name__ == "__main__":
parser = argparse.ArgumentParser()
- parser.add_argument('repo', metavar='REPO', help='Main repository')
- parser.add_argument('mbox', metavar='MBOX', help='mbox filename')
- parser.add_argument('--default-branch', metavar='DEFAULT_BRANCH', default='master', help='Use this branch if no one is found')
- parser.add_argument('--separator', '-s', metavar='SEPARATOR', default=' ', help='Char separator for output data')
+ parser.add_argument("repo", metavar="REPO", help="Main repository")
+ parser.add_argument("mbox", metavar="MBOX", help="mbox filename")
+ parser.add_argument(
+ "--default-branch",
+ metavar="DEFAULT_BRANCH",
+ default="master",
+ help="Use this branch if no one is found",
+ )
+ parser.add_argument(
+ "--separator",
+ "-s",
+ metavar="SEPARATOR",
+ default=" ",
+ help="Char separator for output data",
+ )
args = parser.parse_args()
subject, branch = get_branch(args.repo, args.mbox, args.default_branch)
print("branch: %s" % branch)
-
@@ -10,7 +10,7 @@
# Copyright (C) 2023 BayLibre Inc.
#
# SPDX-License-Identifier: GPL-2.0-only
-#
+#
import argparse
import boto3
@@ -33,19 +33,36 @@ under 'Yocto Project Subprojects'). For more information on specific
failures, see: https://wiki.yoctoproject.org/wiki/Patchtest. Thank
you!"""
-def has_a_failed_test(raw_results):
- return any(raw_result.split(':')[0] == "FAIL" for raw_result in raw_results.splitlines())
-parser = argparse.ArgumentParser(description="Send patchtest results to a submitter for a given patch")
-parser.add_argument("-p", "--patch", dest="patch", required=True, help="The patch file to summarize")
-parser.add_argument("-d", "--debug", dest="debug", required=False, action='store_true', help="Print raw email headers and content, but don't actually send it")
+def has_a_failed_test(raw_results):
+ return any(
+ raw_result.split(":")[0] == "FAIL" for raw_result in raw_results.splitlines()
+ )
+
+
+parser = argparse.ArgumentParser(
+ description="Send patchtest results to a submitter for a given patch"
+)
+parser.add_argument(
+ "-p", "--patch", dest="patch", required=True, help="The patch file to summarize"
+)
+parser.add_argument(
+ "-d",
+ "--debug",
+ dest="debug",
+ required=False,
+ action="store_true",
+ help="Print raw email headers and content, but don't actually send it",
+)
args = parser.parse_args()
if not os.path.exists(args.patch):
print(f"Patch '{args.patch}' not found - did you provide the right path?")
sys.exit(1)
elif not os.path.exists(args.patch + ".testresult"):
- print(f"Found patch '{args.patch}' but '{args.patch}.testresult' was not present. Have you run patchtest on the patch?")
+ print(
+ f"Found patch '{args.patch}' but '{args.patch}.testresult' was not present. Have you run patchtest on the patch?"
+ )
sys.exit(1)
result_file = args.patch + ".testresult"
@@ -57,17 +74,17 @@ with open(result_file, "r") as f:
# we know these patch files will only contain a single patch, so only
# worry about the first element for getting the subject
mbox = mailbox.mbox(args.patch)
-mbox_subject = mbox[0]['subject']
+mbox_subject = mbox[0]["subject"]
subject_line = f"Patchtest results for {mbox_subject}"
# extract the submitter email address and use it as the reply address
# for the results
-reply_address = mbox[0]['from']
+reply_address = mbox[0]["from"]
# extract the message ID and use that as the in-reply-to address
# TODO: This will need to change again when patchtest can handle a whole
# series at once
-in_reply_to = mbox[0]['Message-ID']
+in_reply_to = mbox[0]["Message-ID"]
# the address the results email is sent from
from_address = "patchtest@automation.yoctoproject.org"
@@ -77,24 +94,35 @@ cc_address = "openembedded-core@lists.openembedded.org"
if has_a_failed_test(testresult):
reply_contents = None
- if len(max(open(result_file, 'r'), key=len)) > 220:
+ if len(max(open(result_file, "r"), key=len)) > 220:
warning = "Tests failed for the patch, but the results log could not be processed due to excessive result line length."
reply_contents = greeting + warning + suggestions
else:
reply_contents = greeting + testresult + suggestions
- ses_client = boto3.client('ses', region_name='us-west-2')
+ ses_client = boto3.client("ses", region_name="us-west-2")
# Construct the headers for the email. We only want to reply
# directly to the tested patch, so make In-Reply-To and References
# the same value.
- raw_data = 'From: ' + from_address + '\nTo: ' + reply_address + \
- '\nCC: ' + cc_address + '\nSubject:' + subject_line + \
- '\nIn-Reply-To:' + in_reply_to + \
- '\nReferences:' + in_reply_to + \
- '\nMIME-Version: 1.0" + \
- "\nContent-type: Multipart/Mixed;boundary="NextPart"\n\n--NextPart\nContent-Type: text/plain\n\n' + \
- reply_contents + '\n\n--NextPart'
+ raw_data = (
+ "From: "
+ + from_address
+ + "\nTo: "
+ + reply_address
+ + "\nCC: "
+ + cc_address
+ + "\nSubject:"
+ + subject_line
+ + "\nIn-Reply-To:"
+ + in_reply_to
+ + "\nReferences:"
+ + in_reply_to
+ + '\nMIME-Version: 1.0" + \
+ "\nContent-type: Multipart/Mixed;boundary="NextPart"\n\n--NextPart\nContent-Type: text/plain\n\n'
+ + reply_contents
+ + "\n\n--NextPart"
+ )
if args.debug:
print(f"RawMessage: \n\n{raw_data}")
Black makes things easier to read. Signed-off-by: Trevor Gamblin <tgamblin@baylibre.com> --- meta/lib/patchtest/mbox.py | 44 +++--- meta/lib/patchtest/patchtest_parser.py | 106 ++++++++----- meta/lib/patchtest/patchtest_patterns.py | 84 ++++++---- meta/lib/patchtest/repo.py | 32 ++-- meta/lib/patchtest/selftest/selftest | 92 ++++++++--- meta/lib/patchtest/tests/base.py | 86 ++++++----- meta/lib/patchtest/tests/test_mbox.py | 144 +++++++++++++----- meta/lib/patchtest/tests/test_metadata.py | 94 ++++++++---- meta/lib/patchtest/tests/test_patch.py | 39 +++-- .../lib/patchtest/tests/test_python_pylint.py | 45 ++++-- scripts/patchtest | 113 +++++++++----- scripts/patchtest-get-branch | 54 ++++--- scripts/patchtest-send-results | 66 +++++--- 13 files changed, 660 insertions(+), 339 deletions(-)