diff options
| author | Trevor Gamblin <tgamblin@baylibre.com> | 2024-09-24 07:54:59 -0400 |
|---|---|---|
| committer | Richard Purdie <richard.purdie@linuxfoundation.org> | 2024-09-30 17:00:50 +0100 |
| commit | d6ede9c73b44062d8831a08f522d519591bf29c2 (patch) | |
| tree | 17711c77f409da9d7ed38361c9551e6bb6f967dd /meta/lib/patchtest | |
| parent | bb0f1625d7655d04c6df3c144e488f676ff2f762 (diff) | |
| download | poky-d6ede9c73b44062d8831a08f522d519591bf29c2.tar.gz | |
patchtest: mbox.py: new data implementation
Consolidate and improve some objects:
- absorb utils.py functionality
- repo.py: use mbox.py
- repo.py: remove some cruft
- utils.py: replace with logs.py
- utils.py: delete
- patch.py: delete
- scripts/patchtest: use logging directly
- general cleanup
(From OE-Core rev: d4fbdb1d15f281b236137d63710c73bca8911a36)
Signed-off-by: Trevor Gamblin <tgamblin@baylibre.com>
Signed-off-by: Richard Purdie <richard.purdie@linuxfoundation.org>
Diffstat (limited to 'meta/lib/patchtest')
| -rw-r--r-- | meta/lib/patchtest/mbox.py | 108 | ||||
| -rw-r--r-- | meta/lib/patchtest/patch.py | 43 | ||||
| -rw-r--r-- | meta/lib/patchtest/patterns.py | 3 | ||||
| -rw-r--r-- | meta/lib/patchtest/repo.py | 63 | ||||
| -rw-r--r-- | meta/lib/patchtest/tests/base.py | 5 | ||||
| -rw-r--r-- | meta/lib/patchtest/tests/test_metadata.py | 2 | ||||
| -rw-r--r-- | meta/lib/patchtest/utils.py | 61 |
7 files changed, 125 insertions, 160 deletions
diff --git a/meta/lib/patchtest/mbox.py b/meta/lib/patchtest/mbox.py new file mode 100644 index 0000000000..1d95819b7a --- /dev/null +++ b/meta/lib/patchtest/mbox.py | |||
| @@ -0,0 +1,108 @@ | |||
| 1 | #! /usr/bin/env python3 | ||
| 2 | |||
| 3 | # series.py | ||
| 4 | # | ||
| 5 | # Read a series' mbox file and get information about the patches | ||
| 6 | # contained | ||
| 7 | # | ||
| 8 | # Copyright (C) 2024 BayLibre SAS | ||
| 9 | # | ||
| 10 | # SPDX-License-Identifier: GPL-2.0-only | ||
| 11 | # | ||
| 12 | |||
| 13 | import email | ||
| 14 | import re | ||
| 15 | |||
| 16 | # From: https://stackoverflow.com/questions/59681461/read-a-big-mbox-file-with-python | ||
| 17 | class MboxReader: | ||
| 18 | def __init__(self, filepath): | ||
| 19 | self.handle = open(filepath, 'rb') | ||
| 20 | assert self.handle.readline().startswith(b'From ') | ||
| 21 | |||
| 22 | def __enter__(self): | ||
| 23 | return self | ||
| 24 | |||
| 25 | def __exit__(self, exc_type, exc_value, exc_traceback): | ||
| 26 | self.handle.close() | ||
| 27 | |||
| 28 | def __iter__(self): | ||
| 29 | return iter(self.__next__()) | ||
| 30 | |||
| 31 | def __next__(self): | ||
| 32 | lines = [] | ||
| 33 | while True: | ||
| 34 | line = self.handle.readline() | ||
| 35 | if line == b'' or line.startswith(b'From '): | ||
| 36 | yield email.message_from_bytes(b''.join(lines)) | ||
| 37 | if line == b'': | ||
| 38 | break | ||
| 39 | lines = [] | ||
| 40 | continue | ||
| 41 | lines.append(line) | ||
| 42 | |||
| 43 | class Patch: | ||
| 44 | def __init__(self, data): | ||
| 45 | self.author = data['From'] | ||
| 46 | self.to = data['To'] | ||
| 47 | self.cc = data['Cc'] | ||
| 48 | self.subject = data['Subject'] | ||
| 49 | self.split_body = re.split('---', data.get_payload(), maxsplit=1) | ||
| 50 | self.commit_message = self.split_body[0] | ||
| 51 | self.diff = self.split_body[1] | ||
| 52 | |||
| 53 | class PatchSeries: | ||
| 54 | def __init__(self, filepath): | ||
| 55 | with MboxReader(filepath) as mbox: | ||
| 56 | self.patches = [Patch(message) for message in mbox] | ||
| 57 | |||
| 58 | assert self.patches | ||
| 59 | self.patch_count = len(self.patches) | ||
| 60 | self.path = filepath | ||
| 61 | |||
| 62 | @property | ||
| 63 | def path(self): | ||
| 64 | return self.path | ||
| 65 | |||
| 66 | self.branch = self.get_branch() | ||
| 67 | |||
| 68 | def get_branch(self): | ||
| 69 | fullprefix = "" | ||
| 70 | pattern = re.compile(r"(\[.*\])", re.DOTALL) | ||
| 71 | |||
| 72 | # There should be at least one patch in the series and it should | ||
| 73 | # include the branch name in the subject, so parse that | ||
| 74 | match = pattern.search(self.patches[0].subject) | ||
| 75 | if match: | ||
| 76 | fullprefix = match.group(1) | ||
| 77 | |||
| 78 | branch, branches, valid_branches = None, [], [] | ||
| 79 | |||
| 80 | if fullprefix: | ||
| 81 | prefix = fullprefix.strip('[]') | ||
| 82 | branches = [ b.strip() for b in prefix.split(',')] | ||
| 83 | valid_branches = [b for b in branches if PatchSeries.valid_branch(b)] | ||
| 84 | |||
| 85 | if len(valid_branches): | ||
| 86 | branch = valid_branches[0] | ||
| 87 | |||
| 88 | # Get the branch name excluding any brackets. If nothing was | ||
| 89 | # found, then assume there was no branch tag in the subject line | ||
| 90 | # and that the patch targets master | ||
| 91 | if branch is not None: | ||
| 92 | return branch.split(']')[0] | ||
| 93 | else: | ||
| 94 | return "master" | ||
| 95 | |||
| 96 | @staticmethod | ||
| 97 | def valid_branch(branch): | ||
| 98 | """ Check if branch is valid name """ | ||
| 99 | lbranch = branch.lower() | ||
| 100 | |||
| 101 | invalid = lbranch.startswith('patch') or \ | ||
| 102 | lbranch.startswith('rfc') or \ | ||
| 103 | lbranch.startswith('resend') or \ | ||
| 104 | re.search(r'^v\d+', lbranch) or \ | ||
| 105 | re.search(r'^\d+/\d+', lbranch) | ||
| 106 | |||
| 107 | return not invalid | ||
| 108 | |||
diff --git a/meta/lib/patchtest/patch.py b/meta/lib/patchtest/patch.py deleted file mode 100644 index 90faf3eeb4..0000000000 --- a/meta/lib/patchtest/patch.py +++ /dev/null | |||
| @@ -1,43 +0,0 @@ | |||
| 1 | # ex:ts=4:sw=4:sts=4:et | ||
| 2 | # -*- tab-width: 4; c-basic-offset: 4; indent-tabs-mode: nil -*- | ||
| 3 | # | ||
| 4 | # patchtestpatch: PatchTestPatch class which abstracts a patch file | ||
| 5 | # | ||
| 6 | # Copyright (C) 2016 Intel Corporation | ||
| 7 | # | ||
| 8 | # SPDX-License-Identifier: GPL-2.0-only | ||
| 9 | # | ||
| 10 | |||
| 11 | import logging | ||
| 12 | import utils | ||
| 13 | |||
| 14 | logger = logging.getLogger('patchtest') | ||
| 15 | |||
| 16 | class PatchTestPatch(object): | ||
| 17 | def __init__(self, path, forcereload=False): | ||
| 18 | self._path = path | ||
| 19 | self._forcereload = forcereload | ||
| 20 | |||
| 21 | self._contents = None | ||
| 22 | self._branch = None | ||
| 23 | |||
| 24 | @property | ||
| 25 | def contents(self): | ||
| 26 | if self._forcereload or (not self._contents): | ||
| 27 | logger.debug('Reading %s contents' % self._path) | ||
| 28 | try: | ||
| 29 | with open(self._path, newline='') as _f: | ||
| 30 | self._contents = _f.read() | ||
| 31 | except IOError: | ||
| 32 | logger.warn("Reading the mbox %s failed" % self.resource) | ||
| 33 | return self._contents | ||
| 34 | |||
| 35 | @property | ||
| 36 | def path(self): | ||
| 37 | return self._path | ||
| 38 | |||
| 39 | @property | ||
| 40 | def branch(self): | ||
| 41 | if not self._branch: | ||
| 42 | self._branch = utils.get_branch(self._path) | ||
| 43 | return self._branch | ||
diff --git a/meta/lib/patchtest/patterns.py b/meta/lib/patchtest/patterns.py index ba97a4ffe9..b703b0c8b9 100644 --- a/meta/lib/patchtest/patterns.py +++ b/meta/lib/patchtest/patterns.py | |||
| @@ -10,11 +10,8 @@ import pyparsing | |||
| 10 | colon = pyparsing.Literal(":") | 10 | colon = pyparsing.Literal(":") |
| 11 | line_start = pyparsing.LineStart() | 11 | line_start = pyparsing.LineStart() |
| 12 | line_end = pyparsing.LineEnd() | 12 | line_end = pyparsing.LineEnd() |
| 13 | at = pyparsing.Literal("@") | ||
| 14 | lessthan = pyparsing.Literal("<") | 13 | lessthan = pyparsing.Literal("<") |
| 15 | greaterthan = pyparsing.Literal(">") | 14 | greaterthan = pyparsing.Literal(">") |
| 16 | opensquare = pyparsing.Literal("[") | ||
| 17 | closesquare = pyparsing.Literal("]") | ||
| 18 | inappropriate = pyparsing.CaselessLiteral("Inappropriate") | 15 | inappropriate = pyparsing.CaselessLiteral("Inappropriate") |
| 19 | submitted = pyparsing.CaselessLiteral("Submitted") | 16 | submitted = pyparsing.CaselessLiteral("Submitted") |
| 20 | 17 | ||
diff --git a/meta/lib/patchtest/repo.py b/meta/lib/patchtest/repo.py index 5f361ac500..8ec8f68a0b 100644 --- a/meta/lib/patchtest/repo.py +++ b/meta/lib/patchtest/repo.py | |||
| @@ -8,40 +8,27 @@ | |||
| 8 | # SPDX-License-Identifier: GPL-2.0-only | 8 | # SPDX-License-Identifier: GPL-2.0-only |
| 9 | # | 9 | # |
| 10 | 10 | ||
| 11 | import os | ||
| 12 | import utils | ||
| 13 | import logging | ||
| 14 | import git | 11 | import git |
| 15 | from patch import PatchTestPatch | 12 | import os |
| 16 | 13 | import mbox | |
| 17 | logger = logging.getLogger('patchtest') | ||
| 18 | info=logger.info | ||
| 19 | 14 | ||
| 20 | class PatchTestRepo(object): | 15 | class PatchTestRepo(object): |
| 21 | 16 | ||
| 22 | # prefixes used for temporal branches/stashes | 17 | # prefixes used for temporal branches/stashes |
| 23 | prefix = 'patchtest' | 18 | prefix = 'patchtest' |
| 24 | 19 | ||
| 25 | |||
| 26 | def __init__(self, patch, repodir, commit=None, branch=None): | 20 | def __init__(self, patch, repodir, commit=None, branch=None): |
| 27 | self._repodir = repodir | 21 | self.repodir = repodir |
| 28 | self._repo = git.Repo.init(repodir) | 22 | self.repo = git.Repo.init(repodir) |
| 29 | self._patch = PatchTestPatch(patch) | 23 | self.patch = mbox.PatchSeries(patch) |
| 30 | self._current_branch = self._repo.active_branch.name | 24 | self.current_branch = self.repo.active_branch.name |
| 31 | 25 | ||
| 32 | # targeted branch defined on the patch may be invalid, so make sure there | 26 | # targeted branch defined on the patch may be invalid, so make sure there |
| 33 | # is a corresponding remote branch | 27 | # is a corresponding remote branch |
| 34 | valid_patch_branch = None | 28 | valid_patch_branch = None |
| 35 | if self._patch.branch in self._repo.branches: | 29 | if self.patch.branch in self.repo.branches: |
| 36 | valid_patch_branch = self._patch.branch | 30 | valid_patch_branch = self.patch.branch |
| 37 | 31 | ||
| 38 | # Target Branch | ||
| 39 | # Priority (top has highest priority): | ||
| 40 | # 1. branch given at cmd line | ||
| 41 | # 2. branch given at the patch | ||
| 42 | # 3. current branch | ||
| 43 | self._branch = branch or valid_patch_branch or self._current_branch | ||
| 44 | |||
| 45 | # Target Commit | 32 | # Target Commit |
| 46 | # Priority (top has highest priority): | 33 | # Priority (top has highest priority): |
| 47 | # 1. commit given at cmd line | 34 | # 1. commit given at cmd line |
| @@ -57,7 +44,7 @@ class PatchTestRepo(object): | |||
| 57 | 44 | ||
| 58 | # create working branch. Use the '-B' flag so that we just | 45 | # create working branch. Use the '-B' flag so that we just |
| 59 | # check out the existing one if it's there | 46 | # check out the existing one if it's there |
| 60 | self._repo.git.execute(['git', 'checkout', '-B', self._workingbranch, self._commit]) | 47 | self.repo.git.execute(['git', 'checkout', '-B', self._workingbranch, self._commit]) |
| 61 | 48 | ||
| 62 | self._patchmerged = False | 49 | self._patchmerged = False |
| 63 | 50 | ||
| @@ -65,35 +52,13 @@ class PatchTestRepo(object): | |||
| 65 | self._patchcanbemerged = True | 52 | self._patchcanbemerged = True |
| 66 | try: | 53 | try: |
| 67 | # Make sure to get the absolute path of the file | 54 | # Make sure to get the absolute path of the file |
| 68 | self._repo.git.execute(['git', 'apply', '--check', os.path.abspath(self._patch.path)], with_exceptions=True) | 55 | self.repo.git.execute(['git', 'apply', '--check', os.path.abspath(self.patch.path)], with_exceptions=True) |
| 69 | except git.exc.GitCommandError as ce: | 56 | except git.exc.GitCommandError as ce: |
| 70 | self._patchcanbemerged = False | 57 | self._patchcanbemerged = False |
| 71 | 58 | ||
| 72 | # for debugging purposes, print all repo parameters | ||
| 73 | logger.debug("Parameters") | ||
| 74 | logger.debug("\tRepository : %s" % self._repodir) | ||
| 75 | logger.debug("\tTarget Commit : %s" % self._commit) | ||
| 76 | logger.debug("\tTarget Branch : %s" % self._branch) | ||
| 77 | logger.debug("\tWorking branch : %s" % self._workingbranch) | ||
| 78 | logger.debug("\tPatch : %s" % self._patch) | ||
| 79 | |||
| 80 | @property | ||
| 81 | def patch(self): | ||
| 82 | return self._patch.path | ||
| 83 | |||
| 84 | @property | ||
| 85 | def branch(self): | ||
| 86 | return self._branch | ||
| 87 | |||
| 88 | @property | ||
| 89 | def commit(self): | ||
| 90 | return self._commit | ||
| 91 | |||
| 92 | @property | ||
| 93 | def ismerged(self): | 59 | def ismerged(self): |
| 94 | return self._patchmerged | 60 | return self._patchmerged |
| 95 | 61 | ||
| 96 | @property | ||
| 97 | def canbemerged(self): | 62 | def canbemerged(self): |
| 98 | return self._patchcanbemerged | 63 | return self._patchcanbemerged |
| 99 | 64 | ||
| @@ -103,7 +68,7 @@ class PatchTestRepo(object): | |||
| 103 | return None | 68 | return None |
| 104 | 69 | ||
| 105 | try: | 70 | try: |
| 106 | return self._repo.rev_parse(commit).hexsha | 71 | return self.repo.rev_parse(commit).hexsha |
| 107 | except Exception as e: | 72 | except Exception as e: |
| 108 | print(f"Couldn't find commit {commit} in repo") | 73 | print(f"Couldn't find commit {commit} in repo") |
| 109 | 74 | ||
| @@ -111,10 +76,10 @@ class PatchTestRepo(object): | |||
| 111 | 76 | ||
| 112 | def merge(self): | 77 | def merge(self): |
| 113 | if self._patchcanbemerged: | 78 | if self._patchcanbemerged: |
| 114 | self._repo.git.execute(['git', 'am', '--keep-cr', os.path.abspath(self._patch.path)]) | 79 | self.repo.git.execute(['git', 'am', '--keep-cr', os.path.abspath(self.patch.path)]) |
| 115 | self._patchmerged = True | 80 | self._patchmerged = True |
| 116 | 81 | ||
| 117 | def clean(self): | 82 | def clean(self): |
| 118 | self._repo.git.execute(['git', 'checkout', self._current_branch]) | 83 | self.repo.git.execute(['git', 'checkout', self.current_branch]) |
| 119 | self._repo.git.execute(['git', 'branch', '-D', self._workingbranch]) | 84 | self.repo.git.execute(['git', 'branch', '-D', self._workingbranch]) |
| 120 | self._patchmerged = False | 85 | self._patchmerged = False |
diff --git a/meta/lib/patchtest/tests/base.py b/meta/lib/patchtest/tests/base.py index 424e61b5be..911addb199 100644 --- a/meta/lib/patchtest/tests/base.py +++ b/meta/lib/patchtest/tests/base.py | |||
| @@ -37,7 +37,6 @@ class Base(unittest.TestCase): | |||
| 37 | endcommit_messages_regex = re.compile(r'\(From \w+-\w+ rev:|(?<!\S)Signed-off-by|(?<!\S)---\n') | 37 | endcommit_messages_regex = re.compile(r'\(From \w+-\w+ rev:|(?<!\S)Signed-off-by|(?<!\S)---\n') |
| 38 | patchmetadata_regex = re.compile(r'-{3} \S+|\+{3} \S+|@{2} -\d+,\d+ \+\d+,\d+ @{2} \S+') | 38 | patchmetadata_regex = re.compile(r'-{3} \S+|\+{3} \S+|@{2} -\d+,\d+ \+\d+,\d+ @{2} \S+') |
| 39 | 39 | ||
| 40 | |||
| 41 | @staticmethod | 40 | @staticmethod |
| 42 | def msg_to_commit(msg): | 41 | def msg_to_commit(msg): |
| 43 | payload = msg.get_payload() | 42 | payload = msg.get_payload() |
| @@ -66,13 +65,13 @@ class Base(unittest.TestCase): | |||
| 66 | def setUpClass(cls): | 65 | def setUpClass(cls): |
| 67 | 66 | ||
| 68 | # General objects: mailbox.mbox and patchset | 67 | # General objects: mailbox.mbox and patchset |
| 69 | cls.mbox = mailbox.mbox(PatchTestInput.repo.patch) | 68 | cls.mbox = mailbox.mbox(PatchTestInput.repo.patch.path) |
| 70 | 69 | ||
| 71 | # Patch may be malformed, so try parsing it | 70 | # Patch may be malformed, so try parsing it |
| 72 | cls.unidiff_parse_error = '' | 71 | cls.unidiff_parse_error = '' |
| 73 | cls.patchset = None | 72 | cls.patchset = None |
| 74 | try: | 73 | try: |
| 75 | cls.patchset = unidiff.PatchSet.from_filename(PatchTestInput.repo.patch, encoding=u'UTF-8') | 74 | cls.patchset = unidiff.PatchSet.from_filename(PatchTestInput.repo.patch.path, encoding=u'UTF-8') |
| 76 | except unidiff.UnidiffParseError as upe: | 75 | except unidiff.UnidiffParseError as upe: |
| 77 | cls.patchset = [] | 76 | cls.patchset = [] |
| 78 | cls.unidiff_parse_error = str(upe) | 77 | cls.unidiff_parse_error = str(upe) |
diff --git a/meta/lib/patchtest/tests/test_metadata.py b/meta/lib/patchtest/tests/test_metadata.py index 8c2305a184..d7e5e187f6 100644 --- a/meta/lib/patchtest/tests/test_metadata.py +++ b/meta/lib/patchtest/tests/test_metadata.py | |||
| @@ -168,7 +168,7 @@ class TestMetadata(base.Metadata): | |||
| 168 | def test_cve_check_ignore(self): | 168 | def test_cve_check_ignore(self): |
| 169 | # Skip if we neither modified a recipe or target branches are not | 169 | # Skip if we neither modified a recipe or target branches are not |
| 170 | # Nanbield and newer. CVE_CHECK_IGNORE was first deprecated in Nanbield. | 170 | # Nanbield and newer. CVE_CHECK_IGNORE was first deprecated in Nanbield. |
| 171 | if not self.modified or PatchTestInput.repo.branch == "kirkstone" or PatchTestInput.repo.branch == "dunfell": | 171 | if not self.modified or PatchTestInput.repo.patch.branch == "kirkstone" or PatchTestInput.repo.patch.branch == "dunfell": |
| 172 | self.skip('No modified recipes or older target branch, skipping test') | 172 | self.skip('No modified recipes or older target branch, skipping test') |
| 173 | for pn in self.modified: | 173 | for pn in self.modified: |
| 174 | # we are not interested in images | 174 | # we are not interested in images |
diff --git a/meta/lib/patchtest/utils.py b/meta/lib/patchtest/utils.py deleted file mode 100644 index 8eddf3e85f..0000000000 --- a/meta/lib/patchtest/utils.py +++ /dev/null | |||
| @@ -1,61 +0,0 @@ | |||
| 1 | # ex:ts=4:sw=4:sts=4:et | ||
| 2 | # -*- tab-width: 4; c-basic-offset: 4; indent-tabs-mode: nil -*- | ||
| 3 | # | ||
| 4 | # utils: common methods used by the patchtest framework | ||
| 5 | # | ||
| 6 | # Copyright (C) 2016 Intel Corporation | ||
| 7 | # | ||
| 8 | # SPDX-License-Identifier: GPL-2.0-only | ||
| 9 | # | ||
| 10 | |||
| 11 | import os | ||
| 12 | import subprocess | ||
| 13 | import logging | ||
| 14 | import re | ||
| 15 | import mailbox | ||
| 16 | |||
| 17 | def logger_create(name): | ||
| 18 | logger = logging.getLogger(name) | ||
| 19 | loggerhandler = logging.StreamHandler() | ||
| 20 | loggerhandler.setFormatter(logging.Formatter("%(message)s")) | ||
| 21 | logger.addHandler(loggerhandler) | ||
| 22 | logger.setLevel(logging.INFO) | ||
| 23 | return logger | ||
| 24 | |||
| 25 | def valid_branch(branch): | ||
| 26 | """ Check if branch is valid name """ | ||
| 27 | lbranch = branch.lower() | ||
| 28 | |||
| 29 | invalid = lbranch.startswith('patch') or \ | ||
| 30 | lbranch.startswith('rfc') or \ | ||
| 31 | lbranch.startswith('resend') or \ | ||
| 32 | re.search(r'^v\d+', lbranch) or \ | ||
| 33 | re.search(r'^\d+/\d+', lbranch) | ||
| 34 | |||
| 35 | return not invalid | ||
| 36 | |||
| 37 | def get_branch(path): | ||
| 38 | """ Get the branch name from mbox """ | ||
| 39 | fullprefix = "" | ||
| 40 | mbox = mailbox.mbox(path) | ||
| 41 | |||
| 42 | if len(mbox): | ||
| 43 | subject = mbox[0]['subject'] | ||
| 44 | if subject: | ||
| 45 | pattern = re.compile(r"(\[.*\])", re.DOTALL) | ||
| 46 | match = pattern.search(subject) | ||
| 47 | if match: | ||
| 48 | fullprefix = match.group(1) | ||
| 49 | |||
| 50 | branch, branches, valid_branches = None, [], [] | ||
| 51 | |||
| 52 | if fullprefix: | ||
| 53 | prefix = fullprefix.strip('[]') | ||
| 54 | branches = [ b.strip() for b in prefix.split(',')] | ||
| 55 | valid_branches = [b for b in branches if valid_branch(b)] | ||
| 56 | |||
| 57 | if len(valid_branches): | ||
| 58 | branch = valid_branches[0] | ||
| 59 | |||
| 60 | return branch | ||
| 61 | |||
