diff options
-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 | ||||
-rwxr-xr-x | scripts/patchtest | 17 |
8 files changed, 134 insertions, 168 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 | |||
diff --git a/scripts/patchtest b/scripts/patchtest index 0be7062dc2..3ca8c6e48f 100755 --- a/scripts/patchtest +++ b/scripts/patchtest | |||
@@ -9,12 +9,12 @@ | |||
9 | # SPDX-License-Identifier: GPL-2.0-only | 9 | # SPDX-License-Identifier: GPL-2.0-only |
10 | # | 10 | # |
11 | 11 | ||
12 | import sys | 12 | import json |
13 | import os | ||
14 | import unittest | ||
15 | import logging | 13 | import logging |
14 | import os | ||
15 | import sys | ||
16 | import traceback | 16 | import traceback |
17 | import json | 17 | import unittest |
18 | 18 | ||
19 | # Include current path so test cases can see it | 19 | # Include current path so test cases can see it |
20 | sys.path.insert(0, os.path.dirname(os.path.realpath(__file__))) | 20 | sys.path.insert(0, os.path.dirname(os.path.realpath(__file__))) |
@@ -25,13 +25,14 @@ sys.path.insert(0, os.path.join(os.path.dirname(os.path.realpath(__file__)), '.. | |||
25 | from data import PatchTestInput | 25 | from data import PatchTestInput |
26 | from repo import PatchTestRepo | 26 | from repo import PatchTestRepo |
27 | 27 | ||
28 | import utils | 28 | logger = logging.getLogger("patchtest") |
29 | logger = utils.logger_create('patchtest') | 29 | loggerhandler = logging.StreamHandler() |
30 | loggerhandler.setFormatter(logging.Formatter("%(message)s")) | ||
31 | logger.addHandler(loggerhandler) | ||
32 | logger.setLevel(logging.INFO) | ||
30 | info = logger.info | 33 | info = logger.info |
31 | error = logger.error | 34 | error = logger.error |
32 | 35 | ||
33 | import repo | ||
34 | |||
35 | def getResult(patch, mergepatch, logfile=None): | 36 | def getResult(patch, mergepatch, logfile=None): |
36 | 37 | ||
37 | class PatchTestResult(unittest.TextTestResult): | 38 | class PatchTestResult(unittest.TextTestResult): |