From 4a6f38c5327b40a45c340af49fee9a0d5cc890bd Mon Sep 17 00:00:00 2001 From: Trevor Gamblin Date: Wed, 13 Sep 2023 13:00:46 -0400 Subject: patchtest: Add tests from patchtest oe repo Copy the core components of the patchtest-oe repo into meta/lib/patchtest in oe-core. (From OE-Core rev: 257f64f4e4414b78981104aec132b067beb5a92a) Signed-off-by: Trevor Gamblin Signed-off-by: Richard Purdie --- meta/lib/patchtest/tests/__init__.py | 0 meta/lib/patchtest/tests/base.py | 239 +++++++++++++++++++++ meta/lib/patchtest/tests/pyparsing/common.py | 26 +++ .../patchtest/tests/pyparsing/parse_cve_tags.py | 18 ++ .../patchtest/tests/pyparsing/parse_shortlog.py | 14 ++ .../tests/pyparsing/parse_signed_off_by.py | 22 ++ .../tests/pyparsing/parse_upstream_status.py | 24 +++ meta/lib/patchtest/tests/test_mbox_author.py | 29 +++ meta/lib/patchtest/tests/test_mbox_bugzilla.py | 22 ++ meta/lib/patchtest/tests/test_mbox_cve.py | 49 +++++ meta/lib/patchtest/tests/test_mbox_description.py | 17 ++ meta/lib/patchtest/tests/test_mbox_format.py | 16 ++ meta/lib/patchtest/tests/test_mbox_mailinglist.py | 64 ++++++ meta/lib/patchtest/tests/test_mbox_merge.py | 25 +++ meta/lib/patchtest/tests/test_mbox_shortlog.py | 41 ++++ .../lib/patchtest/tests/test_mbox_signed_off_by.py | 28 +++ .../tests/test_metadata_lic_files_chksum.py | 82 +++++++ meta/lib/patchtest/tests/test_metadata_license.py | 55 +++++ .../patchtest/tests/test_metadata_max_length.py | 26 +++ meta/lib/patchtest/tests/test_metadata_src_uri.py | 75 +++++++ meta/lib/patchtest/tests/test_metadata_summary.py | 32 +++ meta/lib/patchtest/tests/test_patch_cve.py | 51 +++++ .../patchtest/tests/test_patch_signed_off_by.py | 43 ++++ .../patchtest/tests/test_patch_upstream_status.py | 64 ++++++ meta/lib/patchtest/tests/test_python_pylint.py | 61 ++++++ 25 files changed, 1123 insertions(+) create mode 100644 meta/lib/patchtest/tests/__init__.py create mode 100644 meta/lib/patchtest/tests/base.py create mode 100644 meta/lib/patchtest/tests/pyparsing/common.py create mode 100644 meta/lib/patchtest/tests/pyparsing/parse_cve_tags.py create mode 100644 meta/lib/patchtest/tests/pyparsing/parse_shortlog.py create mode 100644 meta/lib/patchtest/tests/pyparsing/parse_signed_off_by.py create mode 100644 meta/lib/patchtest/tests/pyparsing/parse_upstream_status.py create mode 100644 meta/lib/patchtest/tests/test_mbox_author.py create mode 100644 meta/lib/patchtest/tests/test_mbox_bugzilla.py create mode 100644 meta/lib/patchtest/tests/test_mbox_cve.py create mode 100644 meta/lib/patchtest/tests/test_mbox_description.py create mode 100644 meta/lib/patchtest/tests/test_mbox_format.py create mode 100644 meta/lib/patchtest/tests/test_mbox_mailinglist.py create mode 100644 meta/lib/patchtest/tests/test_mbox_merge.py create mode 100644 meta/lib/patchtest/tests/test_mbox_shortlog.py create mode 100644 meta/lib/patchtest/tests/test_mbox_signed_off_by.py create mode 100644 meta/lib/patchtest/tests/test_metadata_lic_files_chksum.py create mode 100644 meta/lib/patchtest/tests/test_metadata_license.py create mode 100644 meta/lib/patchtest/tests/test_metadata_max_length.py create mode 100644 meta/lib/patchtest/tests/test_metadata_src_uri.py create mode 100644 meta/lib/patchtest/tests/test_metadata_summary.py create mode 100644 meta/lib/patchtest/tests/test_patch_cve.py create mode 100644 meta/lib/patchtest/tests/test_patch_signed_off_by.py create mode 100644 meta/lib/patchtest/tests/test_patch_upstream_status.py create mode 100644 meta/lib/patchtest/tests/test_python_pylint.py (limited to 'meta/lib/patchtest/tests') diff --git a/meta/lib/patchtest/tests/__init__.py b/meta/lib/patchtest/tests/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/meta/lib/patchtest/tests/base.py b/meta/lib/patchtest/tests/base.py new file mode 100644 index 0000000000..27db380353 --- /dev/null +++ b/meta/lib/patchtest/tests/base.py @@ -0,0 +1,239 @@ +# Base class to be used by all test cases defined in the suite +# +# Copyright (C) 2016 Intel Corporation +# +# SPDX-License-Identifier: GPL-2.0 + +import unittest +import logging +import json +import unidiff +from data import PatchTestInput +import mailbox +import collections +import sys +import os +import re + +sys.path.insert(0, os.path.join(os.path.dirname(__file__), 'pyparsing')) + +logger = logging.getLogger('patchtest') +debug=logger.debug +info=logger.info +warn=logger.warn +error=logger.error + +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": } + + endcommit_messages_regex = re.compile('\(From \w+-\w+ rev:|(?") +opensquare = pyparsing.Literal("[") +closesquare = pyparsing.Literal("]") +inappropriate = pyparsing.CaselessLiteral("Inappropriate") +submitted = pyparsing.CaselessLiteral("Submitted") + +# word related +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+".") diff --git a/meta/lib/patchtest/tests/pyparsing/parse_cve_tags.py b/meta/lib/patchtest/tests/pyparsing/parse_cve_tags.py new file mode 100644 index 0000000000..dd7131a650 --- /dev/null +++ b/meta/lib/patchtest/tests/pyparsing/parse_cve_tags.py @@ -0,0 +1,18 @@ +# signed-off-by pyparsing definition +# +# Copyright (C) 2016 Intel Corporation +# +# SPDX-License-Identifier: GPL-2.0 + + +import pyparsing +import common + +name = pyparsing.Regex('\S+.*(?= <)') +username = pyparsing.OneOrMore(common.worddot) +domain = pyparsing.OneOrMore(common.worddot) +cve = pyparsing.Regex('CVE\-\d{4}\-\d+') +cve_mark = pyparsing.Literal("CVE:") + +cve_tag = pyparsing.AtLineStart(cve_mark + cve) +patch_cve_tag = pyparsing.AtLineStart("+" + cve_mark + cve) diff --git a/meta/lib/patchtest/tests/pyparsing/parse_shortlog.py b/meta/lib/patchtest/tests/pyparsing/parse_shortlog.py new file mode 100644 index 0000000000..26e9612c4a --- /dev/null +++ b/meta/lib/patchtest/tests/pyparsing/parse_shortlog.py @@ -0,0 +1,14 @@ +# subject pyparsing definition +# +# Copyright (C) 2016 Intel Corporation +# +# SPDX-License-Identifier: GPL-2.0 + +# NOTE:This is an oversimplified syntax of the mbox's summary + +import pyparsing +import common + +target = pyparsing.OneOrMore(pyparsing.Word(pyparsing.printables.replace(':',''))) +summary = pyparsing.OneOrMore(pyparsing.Word(pyparsing.printables)) +shortlog = common.start + target + common.colon + summary + common.end diff --git a/meta/lib/patchtest/tests/pyparsing/parse_signed_off_by.py b/meta/lib/patchtest/tests/pyparsing/parse_signed_off_by.py new file mode 100644 index 0000000000..c8a4351551 --- /dev/null +++ b/meta/lib/patchtest/tests/pyparsing/parse_signed_off_by.py @@ -0,0 +1,22 @@ +# signed-off-by pyparsing definition +# +# Copyright (C) 2016 Intel Corporation +# +# SPDX-License-Identifier: GPL-2.0 + + +import pyparsing +import common + +name = pyparsing.Regex('\S+.*(?= <)') +username = pyparsing.OneOrMore(common.worddot) +domain = pyparsing.OneOrMore(common.worddot) + +# taken from https://pyparsing-public.wikispaces.com/Helpful+Expressions +email = pyparsing.Regex(r"(?P[A-Za-z0-9._%+-]+)@(?P[A-Za-z0-9.-]+)\.(?P[A-Za-z]{2,})") + +email_enclosed = common.lessthan + email + common.greaterthan + +signed_off_by_mark = pyparsing.Literal("Signed-off-by:") +signed_off_by = pyparsing.AtLineStart(signed_off_by_mark + name + email_enclosed) +patch_signed_off_by = pyparsing.AtLineStart("+" + signed_off_by_mark + name + email_enclosed) diff --git a/meta/lib/patchtest/tests/pyparsing/parse_upstream_status.py b/meta/lib/patchtest/tests/pyparsing/parse_upstream_status.py new file mode 100644 index 0000000000..511b36d033 --- /dev/null +++ b/meta/lib/patchtest/tests/pyparsing/parse_upstream_status.py @@ -0,0 +1,24 @@ +# upstream-status pyparsing definition +# +# Copyright (C) 2016 Intel Corporation +# +# SPDX-License-Identifier: GPL-2.0 + + +import common +import pyparsing + +upstream_status_literal_valid_status = ["Pending", "Accepted", "Backport", "Denied", "Inappropriate", "Submitted"] +upstream_status_nonliteral_valid_status = ["Pending", "Accepted", "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_mark = pyparsing.Literal("Upstream-Status") +inappropriate_status_mark = common.inappropriate +submitted_status_mark = common.submitted + +upstream_status = common.start + upstream_status_mark + common.colon + upstream_status_valid_status +upstream_status_inappropriate_info = common.start + upstream_status_mark + common.colon + common.inappropriateinfo +upstream_status_submitted_info = common.start + upstream_status_mark + common.colon + common.submittedinfo diff --git a/meta/lib/patchtest/tests/test_mbox_author.py b/meta/lib/patchtest/tests/test_mbox_author.py new file mode 100644 index 0000000000..6c79f164d4 --- /dev/null +++ b/meta/lib/patchtest/tests/test_mbox_author.py @@ -0,0 +1,29 @@ +# Checks related to the patch's author +# +# Copyright (C) 2016 Intel Corporation +# +# SPDX-License-Identifier: GPL-2.0 + +import base +import re + +class Author(base.Base): + + auh_email = '' + + invalids = [re.compile("^Upgrade Helper.+"), + re.compile(re.escape(auh_email)), + re.compile("uh@not\.set"), + re.compile("\S+@example\.com")] + + + def test_author_valid(self): + for commit in self.commits: + for invalid in self.invalids: + if invalid.search(commit.author): + self.fail('Invalid author %s' % commit.author, 'Resend the series with a valid patch\'s author', commit) + + def test_non_auh_upgrade(self): + for commit in self.commits: + if self.auh_email in commit.payload: + self.fail('Invalid author %s in commit message' % self.auh_email, 'Resend the series with a valid patch\'s author', commit) diff --git a/meta/lib/patchtest/tests/test_mbox_bugzilla.py b/meta/lib/patchtest/tests/test_mbox_bugzilla.py new file mode 100644 index 0000000000..e8de48bb8d --- /dev/null +++ b/meta/lib/patchtest/tests/test_mbox_bugzilla.py @@ -0,0 +1,22 @@ +# Checks related to the patch's bugzilla tag +# +# Copyright (C) 2016 Intel Corporation +# +# SPDX-License-Identifier: GPL-2.0 + +import re +import base + +class Bugzilla(base.Base): + rexp_detect = re.compile("\[\s?YOCTO.*\]", re.IGNORECASE) + rexp_validation = re.compile("\[(\s?YOCTO\s?#\s?(\d+)\s?,?)+\]", re.IGNORECASE) + + def test_bugzilla_entry_format(self): + for commit in Bugzilla.commits: + for line in commit.commit_message.splitlines(): + if self.rexp_detect.match(line): + if not self.rexp_validation.match(line): + self.fail('Yocto Project bugzilla tag is not correctly formatted', + 'Specify bugzilla ID in commit description with format: "[YOCTO #]"', + commit) + diff --git a/meta/lib/patchtest/tests/test_mbox_cve.py b/meta/lib/patchtest/tests/test_mbox_cve.py new file mode 100644 index 0000000000..f99194c094 --- /dev/null +++ b/meta/lib/patchtest/tests/test_mbox_cve.py @@ -0,0 +1,49 @@ +# Checks related to the patch's CVE lines +# +# Copyright (C) 2016 Intel Corporation +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License along +# with this program; if not, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +# SPDX-License-Identifier: GPL-2.0-or-later + +import base +import os +import parse_cve_tags +import re + +class CVE(base.Base): + + revert_shortlog_regex = re.compile('Revert\s+".*"') + prog = parse_cve_tags.cve_tag + + def setUp(self): + if self.unidiff_parse_error: + self.skip('Parse error %s' % self.unidiff_parse_error) + + # 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] + if not new_cves: + self.skip('No new CVE patches introduced') + + def test_cve_presence_in_commit_message(self): + for commit in CVE.commits: + # skip those patches that revert older commits, these do not required the tag presence + if self.revert_shortlog_regex.match(commit.shortlog): + continue + if not self.prog.search_string(commit.payload): + self.fail('Missing or incorrectly formatted CVE tag in mbox', + 'Correct or include the CVE tag in the mbox with format: "CVE: CVE-YYYY-XXXX"', + commit) diff --git a/meta/lib/patchtest/tests/test_mbox_description.py b/meta/lib/patchtest/tests/test_mbox_description.py new file mode 100644 index 0000000000..7addc6b5f7 --- /dev/null +++ b/meta/lib/patchtest/tests/test_mbox_description.py @@ -0,0 +1,17 @@ +# Checks related to the patch's commit_message +# +# Copyright (C) 2016 Intel Corporation +# +# SPDX-License-Identifier: GPL-2.0 + +import base + +class CommitMessage(base.Base): + + def test_commit_message_presence(self): + for commit in CommitMessage.commits: + if not commit.commit_message.strip(): + self.fail('Patch is missing a descriptive commit message', + 'Please include a commit message on your patch explaining the change (most importantly why the change is being made)', + commit) + diff --git a/meta/lib/patchtest/tests/test_mbox_format.py b/meta/lib/patchtest/tests/test_mbox_format.py new file mode 100644 index 0000000000..85c452ca0d --- /dev/null +++ b/meta/lib/patchtest/tests/test_mbox_format.py @@ -0,0 +1,16 @@ +# Checks correct parsing of mboxes +# +# Copyright (C) 2016 Intel Corporation +# +# SPDX-License-Identifier: GPL-2.0 + +import base +import re + +class MboxFormat(base.Base): + + def test_mbox_format(self): + if self.unidiff_parse_error: + self.fail('Series cannot be parsed correctly due to malformed diff lines', + 'Create the series again using git-format-patch and ensure it can be applied using git am', + data=[('Diff line', re.sub('^.+:\s(?meta-.+)\]") + for commit in MailingList.commits: + match = project_regex.match(commit.subject) + if match: + self.fail('Series sent to the wrong mailing list', + 'Check the project\'s README (%s) and send the patch to the indicated list' % match.group('project'), + commit) + + for patch in self.patchset: + 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', 'Send the series again to the correct mailing list (ML)', + 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'): + 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', 'Send the series again to the correct mailing list (ML)', + data=[('Suggested ML', '%s [%s]' % (self.poky.listemail, self.poky.gitrepo)),('Patch\'s path:', patch.path)]) diff --git a/meta/lib/patchtest/tests/test_mbox_merge.py b/meta/lib/patchtest/tests/test_mbox_merge.py new file mode 100644 index 0000000000..c8b6718d15 --- /dev/null +++ b/meta/lib/patchtest/tests/test_mbox_merge.py @@ -0,0 +1,25 @@ +# Check if mbox was merged by patchtest +# +# Copyright (C) 2016 Intel Corporation +# +# SPDX-License-Identifier: GPL-2.0 + +import subprocess +import base +from data import PatchTestInput + +def headlog(): + output = subprocess.check_output( + "cd %s; git log --pretty='%%h#%%aN#%%cD:#%%s' -1" % PatchTestInput.repodir, + universal_newlines=True, + shell=True + ) + return output.split('#') + +class Merge(base.Base): + def test_series_merge_on_head(self): + if not PatchTestInput.repo.ismerged: + commithash, author, date, shortlog = headlog() + self.fail('Series does not apply on top of target branch', + 'Rebase your series on top of targeted branch', + data=[('Targeted branch', '%s (currently at %s)' % (PatchTestInput.repo.branch, commithash))]) diff --git a/meta/lib/patchtest/tests/test_mbox_shortlog.py b/meta/lib/patchtest/tests/test_mbox_shortlog.py new file mode 100644 index 0000000000..b6c2a209ff --- /dev/null +++ b/meta/lib/patchtest/tests/test_mbox_shortlog.py @@ -0,0 +1,41 @@ +# Checks related to the patch's summary +# +# Copyright (C) 2016 Intel Corporation +# +# SPDX-License-Identifier: GPL-2.0 + +import base +import parse_shortlog +import pyparsing + +maxlength = 90 + +class Shortlog(base.Base): + + def test_shortlog_format(self): + for commit in Shortlog.commits: + shortlog = commit.shortlog + if not shortlog.strip(): + self.skip('Empty shortlog, no reason to execute shortlog format test') + else: + # no reason to re-check on revert shortlogs + if shortlog.startswith('Revert "'): + continue + try: + parse_shortlog.shortlog.parseString(shortlog) + except pyparsing.ParseException as pe: + self.fail('Shortlog does not follow expected format', + 'Commit shortlog (first line of commit message) should follow the format ": "', + commit) + + def test_shortlog_length(self): + for commit in Shortlog.commits: + # no reason to re-check on revert shortlogs + shortlog = commit.shortlog + if shortlog.startswith('Revert "'): + continue + l = len(shortlog) + if l > maxlength: + self.fail('Commit shortlog is too long', + 'Edit shortlog so that it is %d characters or less (currently %d characters)' % (maxlength, l), + commit) diff --git a/meta/lib/patchtest/tests/test_mbox_signed_off_by.py b/meta/lib/patchtest/tests/test_mbox_signed_off_by.py new file mode 100644 index 0000000000..6458951f1c --- /dev/null +++ b/meta/lib/patchtest/tests/test_mbox_signed_off_by.py @@ -0,0 +1,28 @@ +# Checks related to the patch's signed-off-by lines +# +# Copyright (C) 2016 Intel Corporation +# +# SPDX-License-Identifier: GPL-2.0 + +import base +import parse_signed_off_by +import re + +class SignedOffBy(base.Base): + + revert_shortlog_regex = re.compile('Revert\s+".*"') + + @classmethod + def setUpClassLocal(cls): + # match self.mark with no '+' preceding it + cls.prog = parse_signed_off_by.signed_off_by + + def test_signed_off_by_presence(self): + for commit in SignedOffBy.commits: + # skip those patches that revert older commits, these do not required the tag presence + if self.revert_shortlog_regex.match(commit.shortlog): + continue + if not SignedOffBy.prog.search_string(commit.payload): + self.fail('Patch is missing Signed-off-by', + 'Sign off the patch (either manually or with "git commit --amend -s")', + commit) diff --git a/meta/lib/patchtest/tests/test_metadata_lic_files_chksum.py b/meta/lib/patchtest/tests/test_metadata_lic_files_chksum.py new file mode 100644 index 0000000000..e9a5b6bb4e --- /dev/null +++ b/meta/lib/patchtest/tests/test_metadata_lic_files_chksum.py @@ -0,0 +1,82 @@ +# Checks related to the patch's LIC_FILES_CHKSUM metadata variable +# +# Copyright (C) 2016 Intel Corporation +# +# SPDX-License-Identifier: GPL-2.0 + +import base +import re +from data import PatchTestInput, PatchTestDataStore + +class LicFilesChkSum(base.Metadata): + metadata = 'LIC_FILES_CHKSUM' + license = 'LICENSE' + closed = 'CLOSED' + lictag = 'License-Update' + lictag_re = re.compile("^%s:" % lictag, re.MULTILINE) + + def setUp(self): + # these tests just make sense on patches that can be merged + if not PatchTestInput.repo.canbemerged: + self.skip('Patch cannot be merged') + + def test_lic_files_chksum_presence(self): + if not self.added: + self.skip('No added recipes, skipping test') + + for pn in self.added: + rd = self.tinfoil.parse_recipe(pn) + pathname = rd.getVar('FILE') + # we are not interested in images + if '/images/' in pathname: + continue + lic_files_chksum = rd.getVar(self.metadata) + if rd.getVar(self.license) == self.closed: + continue + if not lic_files_chksum: + self.fail('%s is missing in newly added recipe' % self.metadata, + 'Specify the variable %s in %s' % (self.metadata, pn)) + + def pretest_lic_files_chksum_modified_not_mentioned(self): + if not self.modified: + self.skip('No modified recipes, skipping pretest') + # get the proper metadata values + for pn in self.modified: + rd = self.tinfoil.parse_recipe(pn) + pathname = rd.getVar('FILE') + # we are not interested in images + if '/images/' in pathname: + continue + PatchTestDataStore['%s-%s-%s' % (self.shortid(),self.metadata,pn)] = rd.getVar(self.metadata) + + def test_lic_files_chksum_modified_not_mentioned(self): + if not self.modified: + self.skip('No modified recipes, skipping test') + + # get the proper metadata values + for pn in self.modified: + rd = self.tinfoil.parse_recipe(pn) + pathname = rd.getVar('FILE') + # we are not interested in images + if '/images/' in pathname: + continue + PatchTestDataStore['%s-%s-%s' % (self.shortid(),self.metadata,pn)] = rd.getVar(self.metadata) + # compare if there were changes between pre-merge and merge + for pn in self.modified: + pretest = PatchTestDataStore['pre%s-%s-%s' % (self.shortid(),self.metadata, pn)] + test = PatchTestDataStore['%s-%s-%s' % (self.shortid(),self.metadata, pn)] + + # TODO: this is workaround to avoid false-positives when pretest metadata is empty (not reason found yet) + # For more info, check bug 12284 + if not pretest: + return + + if pretest != test: + # if any patch on the series contain reference on the metadata, fail + for commit in self.commits: + if self.lictag_re.search(commit.commit_message): + break + else: + self.fail('LIC_FILES_CHKSUM changed on target %s but there is no "%s" tag in commit message' % (pn, self.lictag), + 'Include "%s: " into the commit message with a brief description' % self.lictag, + data=[('Current checksum', pretest), ('New checksum', test)]) diff --git a/meta/lib/patchtest/tests/test_metadata_license.py b/meta/lib/patchtest/tests/test_metadata_license.py new file mode 100644 index 0000000000..16604dbfb1 --- /dev/null +++ b/meta/lib/patchtest/tests/test_metadata_license.py @@ -0,0 +1,55 @@ +# Checks related to the patch's LIC_FILES_CHKSUM metadata variable +# +# Copyright (C) 2016 Intel Corporation +# +# SPDX-License-Identifier: GPL-2.0 + +import base +import os +from data import PatchTestInput + +class License(base.Metadata): + metadata = 'LICENSE' + invalid_license = 'PATCHTESTINVALID' + + def setUp(self): + # these tests just make sense on patches that can be merged + if not PatchTestInput.repo.canbemerged: + self.skip('Patch cannot be merged') + + def test_license_presence(self): + if not self.added: + 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' + if os.path.exists(auto_conf): + open_flag = 'a' + with open(auto_conf, open_flag) as fd: + for pn in self.added: + fd.write('LICENSE ??= "%s"\n' % self.invalid_license) + + no_license = False + for pn in self.added: + rd = self.tinfoil.parse_recipe(pn) + license = rd.getVar(self.metadata) + if license == self.invalid_license: + no_license = True + break + + # remove auto.conf line or the file itself + if open_flag == 'w': + os.remove(auto_conf) + else: + fd = open(auto_conf, 'r') + lines = fd.readlines() + fd.close() + 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', 'Include a LICENSE into the new recipe') + diff --git a/meta/lib/patchtest/tests/test_metadata_max_length.py b/meta/lib/patchtest/tests/test_metadata_max_length.py new file mode 100644 index 0000000000..04a5e23469 --- /dev/null +++ b/meta/lib/patchtest/tests/test_metadata_max_length.py @@ -0,0 +1,26 @@ +# Checks related to patch line lengths +# +# Copyright (C) 2016 Intel Corporation +# +# SPDX-License-Identifier: GPL-2.0 + +import base +import re + +class MaxLength(base.Base): + add_mark = re.compile('\+ ') + max_length = 200 + + 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'): + continue + payload = str(patch) + for line in payload.splitlines(): + if self.add_mark.match(line): + current_line_length = len(line[1:]) + if current_line_length > self.max_length: + self.fail('Patch line too long (current length %s)' % current_line_length, + 'Shorten the corresponding patch line (max length supported %s)' % self.max_length, + data=[('Patch', patch.path), ('Line', '%s ...' % line[0:80])]) diff --git a/meta/lib/patchtest/tests/test_metadata_src_uri.py b/meta/lib/patchtest/tests/test_metadata_src_uri.py new file mode 100644 index 0000000000..718229d7ad --- /dev/null +++ b/meta/lib/patchtest/tests/test_metadata_src_uri.py @@ -0,0 +1,75 @@ +# Checks related to the patch's SRC_URI metadata variable +# +# Copyright (C) 2016 Intel Corporation +# +# SPDX-License-Identifier: GPL-2.0 + +import subprocess +import base +import re +import os +from data import PatchTestInput, PatchTestDataStore + +class SrcUri(base.Metadata): + + metadata = 'SRC_URI' + md5sum = 'md5sum' + sha256sum = 'sha256sum' + git_regex = re.compile('^git\:\/\/.*') + + def setUp(self): + # these tests just make sense on patches that can be merged + if not PatchTestInput.repo.canbemerged: + self.skip('Patch cannot be merged') + + def pretest_src_uri_left_files(self): + if not self.modified: + 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: + continue + rd = self.tinfoil.parse_recipe(pn) + PatchTestDataStore['%s-%s-%s' % (self.shortid(), self.metadata, pn)] = rd.getVar(self.metadata) + + def test_src_uri_left_files(self): + if not self.modified: + 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: + continue + rd = self.tinfoil.parse_recipe(pn) + PatchTestDataStore['%s-%s-%s' % (self.shortid(), self.metadata, pn)] = rd.getVar(self.metadata) + + for pn in self.modified: + pretest_src_uri = PatchTestDataStore['pre%s-%s-%s' % (self.shortid(), self.metadata, pn)].split() + test_src_uri = PatchTestDataStore['%s-%s-%s' % (self.shortid(), self.metadata, 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://')]) + + # check if files were removed + if len(test_files) < len(pretest_files): + + # get removals from patchset + filesremoved_from_patchset = set() + for patch in self.patchset: + if patch.is_removed_file: + filesremoved_from_patchset.add(os.path.basename(patch.path)) + + # get the deleted files from the SRC_URI + filesremoved_from_usr_uri = pretest_files - test_files + + # finally, get those patches removed at SRC_URI and not removed from the patchset + # 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', + 'Amend the patch containing the software patch file removal', + data=[('Patch', f) for f in not_removed]) + diff --git a/meta/lib/patchtest/tests/test_metadata_summary.py b/meta/lib/patchtest/tests/test_metadata_summary.py new file mode 100644 index 0000000000..931b26768e --- /dev/null +++ b/meta/lib/patchtest/tests/test_metadata_summary.py @@ -0,0 +1,32 @@ +# Checks related to the patch's summary metadata variable +# +# Copyright (C) 2016 Intel Corporation +# +# SPDX-License-Identifier: GPL-2.0 + +import base +from data import PatchTestInput + +class Summary(base.Metadata): + metadata = 'SUMMARY' + + def setUp(self): + # these tests just make sense on patches that can be merged + if not PatchTestInput.repo.canbemerged: + self.skip('Patch cannot be merged') + + def test_summary_presence(self): + if not self.added: + self.skip('No added recipes, skipping test') + + for pn in self.added: + # we are not interested in images + if 'core-image' in pn: + continue + rd = self.tinfoil.parse_recipe(pn) + summary = rd.getVar(self.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' % self.metadata, + 'Specify the variable %s in %s' % (self.metadata, pn)) diff --git a/meta/lib/patchtest/tests/test_patch_cve.py b/meta/lib/patchtest/tests/test_patch_cve.py new file mode 100644 index 0000000000..46ed9ef791 --- /dev/null +++ b/meta/lib/patchtest/tests/test_patch_cve.py @@ -0,0 +1,51 @@ +# Checks related to the patch's CVE lines +# +# Copyright (C) 2016 Intel Corporation +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License along +# with this program; if not, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +# SPDX-License-Identifier: GPL-2.0-or-later + +import base +import os +import re + +class CVE(base.Base): + + re_cve_pattern = re.compile("CVE\-\d{4}\-\d+", re.IGNORECASE) + re_cve_payload_tag = re.compile("\+CVE:(\s+CVE\-\d{4}\-\d+)+") + + def setUp(self): + if self.unidiff_parse_error: + self.skip('Parse error %s' % self.unidiff_parse_error) + + # 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] + if not new_cves: + self.skip('No new CVE patches introduced') + + def test_cve_tag_format(self): + for commit in CVE.commits: + if self.re_cve_pattern.search(commit.shortlog) or self.re_cve_pattern.search(commit.commit_message): + tag_found = False + for line in commit.payload.splitlines(): + if self.re_cve_payload_tag.match(line): + tag_found = True + break + if not tag_found: + self.fail('Missing or incorrectly formatted CVE tag in included patch file', + 'Correct or include the CVE tag on cve patch with format: "CVE: CVE-YYYY-XXXX"', + commit) diff --git a/meta/lib/patchtest/tests/test_patch_signed_off_by.py b/meta/lib/patchtest/tests/test_patch_signed_off_by.py new file mode 100644 index 0000000000..4855d6daf7 --- /dev/null +++ b/meta/lib/patchtest/tests/test_patch_signed_off_by.py @@ -0,0 +1,43 @@ +# Checks related to the patch's signed-off-by lines +# +# Copyright (C) 2016 Intel Corporation +# +# SPDX-License-Identifier: GPL-2.0 + +import base +import parse_signed_off_by +import re + +class PatchSignedOffBy(base.Base): + + @classmethod + def setUpClassLocal(cls): + 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: + cls.newpatches.append(patch) + + cls.mark = str(parse_signed_off_by.signed_off_by_mark).strip('"') + + # match PatchSignedOffBy.mark with '+' preceding it + cls.prog = parse_signed_off_by.patch_signed_off_by + + def setUp(self): + if self.unidiff_parse_error: + self.skip('Parse error %s' % self.unidiff_parse_error) + + def test_signed_off_by_presence(self): + if not PatchSignedOffBy.newpatches: + self.skip("There are no new software patches, no reason to test %s presence" % PatchSignedOffBy.mark) + + for newpatch in PatchSignedOffBy.newpatches: + payload = newpatch.__str__() + for line in payload.splitlines(): + if self.patchmetadata_regex.match(line): + continue + if PatchSignedOffBy.prog.search_string(payload): + break + else: + self.fail('A patch file has been added, but does not have a Signed-off-by tag', + 'Sign off the added patch file (%s)' % newpatch.path) diff --git a/meta/lib/patchtest/tests/test_patch_upstream_status.py b/meta/lib/patchtest/tests/test_patch_upstream_status.py new file mode 100644 index 0000000000..eda5353c66 --- /dev/null +++ b/meta/lib/patchtest/tests/test_patch_upstream_status.py @@ -0,0 +1,64 @@ +# Checks related to the patch's upstream-status lines +# +# Copyright (C) 2016 Intel Corporation +# +# SPDX-License-Identifier: GPL-2.0 + +import base +import parse_upstream_status +import pyparsing +import os + +class PatchUpstreamStatus(base.Base): + + upstream_status_regex = pyparsing.AtLineStart("+" + "Upstream-Status") + + @classmethod + def setUpClassLocal(cls): + 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: + cls.newpatches.append(patch) + + def setUp(self): + if self.unidiff_parse_error: + self.skip('Python-unidiff parse error') + self.valid_status = ', '.join(parse_upstream_status.upstream_status_nonliteral_valid_status) + self.standard_format = 'Upstream-Status: ' + + def test_upstream_status_presence_format(self): + if not PatchUpstreamStatus.newpatches: + self.skip("There are no new software patches, no reason to test Upstream-Status presence/format") + + for newpatch in PatchUpstreamStatus.newpatches: + payload = newpatch.__str__() + if not self.upstream_status_regex.search_string(payload): + self.fail('Added patch file is missing Upstream-Status in the header', + 'Add Upstream-Status: to the header of %s' % newpatch.path, + data=[('Standard format', self.standard_format), ('Valid status', self.valid_status)]) + for line in payload.splitlines(): + if self.patchmetadata_regex.match(line): + continue + if self.upstream_status_regex.search_string(line): + if parse_upstream_status.inappropriate_status_mark.searchString(line): + try: + parse_upstream_status.upstream_status_inappropriate_info.parseString(line.lstrip('+')) + except pyparsing.ParseException as pe: + self.fail('Upstream-Status is Inappropriate, but no reason was provided', + 'Include a brief reason why %s is inappropriate' % os.path.basename(newpatch.path), + data=[('Current', pe.pstr), ('Standard format', 'Upstream-Status: Inappropriate [reason]')]) + elif parse_upstream_status.submitted_status_mark.searchString(line): + try: + parse_upstream_status.upstream_status_submitted_info.parseString(line.lstrip('+')) + except pyparsing.ParseException as pe: + self.fail('Upstream-Status is Submitted, but it is not mentioned where', + 'Include where %s was submitted' % os.path.basename(newpatch.path), + data=[('Current', pe.pstr), ('Standard format', 'Upstream-Status: Submitted [where]')]) + else: + try: + parse_upstream_status.upstream_status.parseString(line.lstrip('+')) + except pyparsing.ParseException as pe: + self.fail('Upstream-Status is in incorrect format', + 'Fix Upstream-Status format in %s' % os.path.basename(newpatch.path), + data=[('Current', pe.pstr), ('Standard format', self.standard_format), ('Valid status', self.valid_status)]) diff --git a/meta/lib/patchtest/tests/test_python_pylint.py b/meta/lib/patchtest/tests/test_python_pylint.py new file mode 100644 index 0000000000..ea8efb7c2a --- /dev/null +++ b/meta/lib/patchtest/tests/test_python_pylint.py @@ -0,0 +1,61 @@ +# Checks related to the python code done with pylint +# +# Copyright (C) 2016 Intel Corporation +# +# SPDX-License-Identifier: GPL-2.0 + +import base +from data import PatchTestInput +import pylint.epylint as lint + +class PyLint(base.Base): + pythonpatches = [] + pylint_pretest = {} + pylint_test = {} + pylint_options = " -E --disable='E0611, E1101, F0401, E0602' --msg-template='L:{line} F:{module} I:{msg}'" + + @classmethod + def setUpClassLocal(cls): + # get just those patches touching python files + cls.pythonpatches = [] + for patch in cls.patchset: + 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') + if not PatchTestInput.repo.canbemerged: + self.skip('Patch cannot be merged, no reason to execute the test method') + if not PyLint.pythonpatches: + self.skip('No python related patches, skipping test') + + def pretest_pylint(self): + for pythonpatch in self.pythonpatches: + if pythonpatch.is_modified_file: + (pylint_stdout, pylint_stderr) = lint.py_run(command_options = pythonpatch.path + self.pylint_options, return_std=True) + for line in pylint_stdout.readlines(): + if not '*' in line: + if line.strip(): + self.pylint_pretest[line.strip().split(' ',1)[0]] = line.strip().split(' ',1)[1] + + def test_pylint(self): + for pythonpatch in self.pythonpatches: + # a condition checking whether a file is renamed or not + # unidiff doesn't support this yet + if pythonpatch.target_file is not pythonpatch.path: + path = pythonpatch.target_file[2:] + else: + path = pythonpatch.path + (pylint_stdout, pylint_stderr) = lint.py_run(command_options = path + self.pylint_options, return_std=True) + for line in pylint_stdout.readlines(): + 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', + 'Correct the lines introduced by your patch', + data=[('Output', 'Please, fix the listed issues:'), ('', issue + ' ' + self.pylint_test[issue])]) -- cgit v1.2.3-54-g00ecf