summaryrefslogtreecommitdiffstats
path: root/meta/lib/patchtest
diff options
context:
space:
mode:
Diffstat (limited to 'meta/lib/patchtest')
-rw-r--r--meta/lib/patchtest/mbox.py108
-rw-r--r--meta/lib/patchtest/patch.py43
-rw-r--r--meta/lib/patchtest/patterns.py3
-rw-r--r--meta/lib/patchtest/repo.py63
-rw-r--r--meta/lib/patchtest/tests/base.py5
-rw-r--r--meta/lib/patchtest/tests/test_metadata.py2
-rw-r--r--meta/lib/patchtest/utils.py61
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
13import email
14import re
15
16# From: https://stackoverflow.com/questions/59681461/read-a-big-mbox-file-with-python
17class 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
43class 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
53class 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
11import logging
12import utils
13
14logger = logging.getLogger('patchtest')
15
16class 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
10colon = pyparsing.Literal(":") 10colon = pyparsing.Literal(":")
11line_start = pyparsing.LineStart() 11line_start = pyparsing.LineStart()
12line_end = pyparsing.LineEnd() 12line_end = pyparsing.LineEnd()
13at = pyparsing.Literal("@")
14lessthan = pyparsing.Literal("<") 13lessthan = pyparsing.Literal("<")
15greaterthan = pyparsing.Literal(">") 14greaterthan = pyparsing.Literal(">")
16opensquare = pyparsing.Literal("[")
17closesquare = pyparsing.Literal("]")
18inappropriate = pyparsing.CaselessLiteral("Inappropriate") 15inappropriate = pyparsing.CaselessLiteral("Inappropriate")
19submitted = pyparsing.CaselessLiteral("Submitted") 16submitted = 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
11import os
12import utils
13import logging
14import git 11import git
15from patch import PatchTestPatch 12import os
16 13import mbox
17logger = logging.getLogger('patchtest')
18info=logger.info
19 14
20class PatchTestRepo(object): 15class 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
11import os
12import subprocess
13import logging
14import re
15import mailbox
16
17def 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
25def 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
37def 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