summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorTrevor Gamblin <tgamblin@baylibre.com>2023-10-16 15:44:56 -0400
committerRichard Purdie <richard.purdie@linuxfoundation.org>2023-10-17 11:41:34 +0100
commit9d137188ad03c111ff8df7396b6b3dfd59307ac0 (patch)
tree6dd7227dec950282973d21e9cfd20b5e505f6bb1
parent790aa2096f7c6be92f994a1f8ec22d3bef91f337 (diff)
downloadpoky-9d137188ad03c111ff8df7396b6b3dfd59307ac0.tar.gz
patchtest: add supporting modules
Add modules that support core patchtest functionality to meta/lib/patchtest. These include classes and functions for handling repository and patch objects, parsing the patchtest CLI arguments, and other utilities. (From OE-Core rev: 499cdad7a16f6cc256837069c7add294132127a4) Signed-off-by: Trevor Gamblin <tgamblin@baylibre.com> Signed-off-by: Richard Purdie <richard.purdie@linuxfoundation.org>
-rw-r--r--meta/lib/patchtest/data.py95
-rw-r--r--meta/lib/patchtest/patch.py73
-rw-r--r--meta/lib/patchtest/repo.py185
-rw-r--r--meta/lib/patchtest/utils.py179
4 files changed, 532 insertions, 0 deletions
diff --git a/meta/lib/patchtest/data.py b/meta/lib/patchtest/data.py
new file mode 100644
index 0000000000..b661dd6479
--- /dev/null
+++ b/meta/lib/patchtest/data.py
@@ -0,0 +1,95 @@
1# ex:ts=4:sw=4:sts=4:et
2# -*- tab-width: 4; c-basic-offset: 4; indent-tabs-mode: nil -*-
3#
4# patchtestdata: module used to share command line arguments between
5# patchtest & test suite and a data store between test cases
6#
7# Copyright (C) 2016 Intel Corporation
8#
9# This program is free software; you can redistribute it and/or modify
10# it under the terms of the GNU General Public License version 2 as
11# published by the Free Software Foundation.
12#
13# This program is distributed in the hope that it will be useful,
14# but WITHOUT ANY WARRANTY; without even the implied warranty of
15# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16# GNU General Public License for more details.
17#
18# You should have received a copy of the GNU General Public License along
19# with this program; if not, write to the Free Software Foundation, Inc.,
20# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
21#
22# Author: Leo Sandoval <leonardo.sandoval.gonzalez@linux.intel.com>
23#
24# NOTE: Strictly speaking, unit test should be isolated from outside,
25# but patchtest test suites uses command line input data and
26# pretest and test test cases may use the datastore defined
27# on this module
28
29import os
30import argparse
31import collections
32import tempfile
33import logging
34
35logger=logging.getLogger('patchtest')
36info=logger.info
37
38# Data store commonly used to share values between pre and post-merge tests
39PatchTestDataStore = collections.defaultdict(str)
40
41class PatchTestInput(object):
42 """Abstract the patchtest argument parser"""
43
44 @classmethod
45 def set_namespace(cls):
46 parser = cls.get_parser()
47 parser.parse_args(namespace=cls)
48
49 @classmethod
50 def get_parser(cls):
51 parser = argparse.ArgumentParser()
52
53 target_patch_group = parser.add_mutually_exclusive_group(required=True)
54
55 target_patch_group.add_argument('--patch', metavar='PATCH', dest='patch_path',
56 help='The patch to be tested')
57
58 target_patch_group.add_argument('--directory', metavar='DIRECTORY', dest='patch_path',
59 help='The directory containing patches to be tested')
60
61 parser.add_argument('repodir', metavar='REPO',
62 help="Name of the repository where patch is merged")
63
64 parser.add_argument('startdir', metavar='TESTDIR',
65 help="Directory where test cases are located")
66
67 parser.add_argument('--top-level-directory', '-t',
68 dest='topdir',
69 default=None,
70 help="Top level directory of project (defaults to start directory)")
71
72 parser.add_argument('--pattern', '-p',
73 dest='pattern',
74 default='test*.py',
75 help="Pattern to match test files")
76
77 parser.add_argument('--base-branch', '-b',
78 dest='basebranch',
79 help="Branch name used by patchtest to branch from. By default, it uses the current one.")
80
81 parser.add_argument('--base-commit', '-c',
82 dest='basecommit',
83 help="Commit ID used by patchtest to branch from. By default, it uses HEAD.")
84
85 parser.add_argument('--debug', '-d',
86 action='store_true',
87 help='Enable debug output')
88
89 parser.add_argument('--log-results',
90 action='store_true',
91 help='Enable logging to a file matching the target patch name with ".testresult" appended')
92
93
94 return parser
95
diff --git a/meta/lib/patchtest/patch.py b/meta/lib/patchtest/patch.py
new file mode 100644
index 0000000000..c0e7d579eb
--- /dev/null
+++ b/meta/lib/patchtest/patch.py
@@ -0,0 +1,73 @@
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# This program is free software; you can redistribute it and/or modify
9# it under the terms of the GNU General Public License version 2 as
10# published by the Free Software Foundation.
11#
12# This program is distributed in the hope that it will be useful,
13# but WITHOUT ANY WARRANTY; without even the implied warranty of
14# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15# GNU General Public License for more details.
16#
17# You should have received a copy of the GNU General Public License along
18# with this program; if not, write to the Free Software Foundation, Inc.,
19# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
20#
21
22import logging
23import utils
24
25logger = logging.getLogger('patchtest')
26
27class PatchTestPatch(object):
28 MERGE_STATUS_INVALID = 'INVALID'
29 MERGE_STATUS_NOT_MERGED = 'NOTMERGED'
30 MERGE_STATUS_MERGED_SUCCESSFULL = 'PASS'
31 MERGE_STATUS_MERGED_FAIL = 'FAIL'
32 MERGE_STATUS = (MERGE_STATUS_INVALID,
33 MERGE_STATUS_NOT_MERGED,
34 MERGE_STATUS_MERGED_SUCCESSFULL,
35 MERGE_STATUS_MERGED_FAIL)
36
37 def __init__(self, path, forcereload=False):
38 self._path = path
39 self._forcereload = forcereload
40
41 self._contents = None
42 self._branch = None
43 self._merge_status = PatchTestPatch.MERGE_STATUS_NOT_MERGED
44
45 @property
46 def contents(self):
47 if self._forcereload or (not self._contents):
48 logger.debug('Reading %s contents' % self._path)
49 try:
50 with open(self._path, newline='') as _f:
51 self._contents = _f.read()
52 except IOError:
53 logger.warn("Reading the mbox %s failed" % self.resource)
54 return self._contents
55
56 @property
57 def path(self):
58 return self._path
59
60 @property
61 def branch(self):
62 if not self._branch:
63 self._branch = utils.get_branch(self._path)
64 return self._branch
65
66 def setmergestatus(self, status):
67 self._merge_status = status
68
69 def getmergestatus(self):
70 return self._merge_status
71
72 merge_status = property(getmergestatus, setmergestatus)
73
diff --git a/meta/lib/patchtest/repo.py b/meta/lib/patchtest/repo.py
new file mode 100644
index 0000000000..5c85c65ffb
--- /dev/null
+++ b/meta/lib/patchtest/repo.py
@@ -0,0 +1,185 @@
1# ex:ts=4:sw=4:sts=4:et
2# -*- tab-width: 4; c-basic-offset: 4; indent-tabs-mode: nil -*-
3#
4# patchtestrepo: PatchTestRepo class used mainly to control a git repo from patchtest
5#
6# Copyright (C) 2016 Intel Corporation
7#
8# This program is free software; you can redistribute it and/or modify
9# it under the terms of the GNU General Public License version 2 as
10# published by the Free Software Foundation.
11#
12# This program is distributed in the hope that it will be useful,
13# but WITHOUT ANY WARRANTY; without even the implied warranty of
14# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15# GNU General Public License for more details.
16#
17# You should have received a copy of the GNU General Public License along
18# with this program; if not, write to the Free Software Foundation, Inc.,
19# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
20
21import os
22import utils
23import logging
24import json
25from patch import PatchTestPatch
26
27logger = logging.getLogger('patchtest')
28info=logger.info
29
30class PatchTestRepo(object):
31
32 # prefixes used for temporal branches/stashes
33 prefix = 'patchtest'
34
35 def __init__(self, patch, repodir, commit=None, branch=None):
36 self._repodir = repodir
37 self._patch = PatchTestPatch(patch)
38 self._current_branch = self._get_current_branch()
39
40 # targeted branch defined on the patch may be invalid, so make sure there
41 # is a corresponding remote branch
42 valid_patch_branch = None
43 if self._patch.branch in self.upstream_branches():
44 valid_patch_branch = self._patch.branch
45
46 # Target Branch
47 # Priority (top has highest priority):
48 # 1. branch given at cmd line
49 # 2. branch given at the patch
50 # 3. current branch
51 self._branch = branch or valid_patch_branch or self._current_branch
52
53 # Target Commit
54 # Priority (top has highest priority):
55 # 1. commit given at cmd line
56 # 2. branch given at cmd line
57 # 3. branch given at the patch
58 # 3. current HEAD
59 self._commit = self._get_commitid(commit) or \
60 self._get_commitid(branch) or \
61 self._get_commitid(valid_patch_branch) or \
62 self._get_commitid('HEAD')
63
64 self._workingbranch = "%s_%s" % (PatchTestRepo.prefix, os.getpid())
65
66 # create working branch
67 self._exec({'cmd': ['git', 'checkout', '-b', self._workingbranch, self._commit]})
68
69 self._patchmerged = False
70
71 # Check if patch can be merged using git-am
72 self._patchcanbemerged = True
73 try:
74 self._exec({'cmd': ['git', 'am', '--keep-cr'], 'input': self._patch.contents})
75 except utils.CmdException as ce:
76 self._exec({'cmd': ['git', 'am', '--abort']})
77 self._patchcanbemerged = False
78 finally:
79 # if patch was applied, remove it
80 if self._patchcanbemerged:
81 self._exec({'cmd':['git', 'reset', '--hard', self._commit]})
82
83 # for debugging purposes, print all repo parameters
84 logger.debug("Parameters")
85 logger.debug("\tRepository : %s" % self._repodir)
86 logger.debug("\tTarget Commit : %s" % self._commit)
87 logger.debug("\tTarget Branch : %s" % self._branch)
88 logger.debug("\tWorking branch : %s" % self._workingbranch)
89 logger.debug("\tPatch : %s" % self._patch)
90
91 @property
92 def patch(self):
93 return self._patch.path
94
95 @property
96 def branch(self):
97 return self._branch
98
99 @property
100 def commit(self):
101 return self._commit
102
103 @property
104 def ismerged(self):
105 return self._patchmerged
106
107 @property
108 def canbemerged(self):
109 return self._patchcanbemerged
110
111 def _exec(self, cmds):
112 _cmds = []
113 if isinstance(cmds, dict):
114 _cmds.append(cmds)
115 elif isinstance(cmds, list):
116 _cmds = cmds
117 else:
118 raise utils.CmdException({'cmd':str(cmds)})
119
120 results = []
121 cmdfailure = False
122 try:
123 results = utils.exec_cmds(_cmds, self._repodir)
124 except utils.CmdException as ce:
125 cmdfailure = True
126 raise ce
127 finally:
128 if cmdfailure:
129 for cmd in _cmds:
130 logger.debug("CMD: %s" % ' '.join(cmd['cmd']))
131 else:
132 for result in results:
133 cmd, rc, stdout, stderr = ' '.join(result['cmd']), result['returncode'], result['stdout'], result['stderr']
134 logger.debug("CMD: %s RCODE: %s STDOUT: %s STDERR: %s" % (cmd, rc, stdout, stderr))
135
136 return results
137
138 def _get_current_branch(self, commit='HEAD'):
139 cmd = {'cmd':['git', 'rev-parse', '--abbrev-ref', commit]}
140 cb = self._exec(cmd)[0]['stdout']
141 if cb == commit:
142 logger.warning('You may be detached so patchtest will checkout to master after execution')
143 cb = 'master'
144 return cb
145
146 def _get_commitid(self, commit):
147
148 if not commit:
149 return None
150
151 try:
152 cmd = {'cmd':['git', 'rev-parse', '--short', commit]}
153 return self._exec(cmd)[0]['stdout']
154 except utils.CmdException as ce:
155 # try getting the commit under any remotes
156 cmd = {'cmd':['git', 'remote']}
157 remotes = self._exec(cmd)[0]['stdout']
158 for remote in remotes.splitlines():
159 cmd = {'cmd':['git', 'rev-parse', '--short', '%s/%s' % (remote, commit)]}
160 try:
161 return self._exec(cmd)[0]['stdout']
162 except utils.CmdException:
163 pass
164
165 return None
166
167 def upstream_branches(self):
168 cmd = {'cmd':['git', 'branch', '--remotes']}
169 remote_branches = self._exec(cmd)[0]['stdout']
170
171 # just get the names, without the remote name
172 branches = set(branch.split('/')[-1] for branch in remote_branches.splitlines())
173 return branches
174
175 def merge(self):
176 if self._patchcanbemerged:
177 self._exec({'cmd': ['git', 'am', '--keep-cr'],
178 'input': self._patch.contents,
179 'updateenv': {'PTRESOURCE':self._patch.path}})
180 self._patchmerged = True
181
182 def clean(self):
183 self._exec({'cmd':['git', 'checkout', '%s' % self._current_branch]})
184 self._exec({'cmd':['git', 'branch', '-D', self._workingbranch]})
185 self._patchmerged = False
diff --git a/meta/lib/patchtest/utils.py b/meta/lib/patchtest/utils.py
new file mode 100644
index 0000000000..23428ae1c5
--- /dev/null
+++ b/meta/lib/patchtest/utils.py
@@ -0,0 +1,179 @@
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# This program is free software; you can redistribute it and/or modify
9# it under the terms of the GNU General Public License version 2 as
10# published by the Free Software Foundation.
11#
12# This program is distributed in the hope that it will be useful,
13# but WITHOUT ANY WARRANTY; without even the implied warranty of
14# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15# GNU General Public License for more details.
16#
17# You should have received a copy of the GNU General Public License along
18# with this program; if not, write to the Free Software Foundation, Inc.,
19# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
20
21import os
22import subprocess
23import logging
24import sys
25import re
26import mailbox
27
28class CmdException(Exception):
29 """ Simple exception class where its attributes are the ones passed when instantiated """
30 def __init__(self, cmd):
31 self._cmd = cmd
32 def __getattr__(self, name):
33 value = None
34 if self._cmd.has_key(name):
35 value = self._cmd[name]
36 return value
37
38def exec_cmd(cmd, cwd, ignore_error=False, input=None, strip=True, updateenv={}):
39 """
40 Input:
41
42 cmd: dict containing the following keys:
43
44 cmd : the command itself as an array of strings
45 ignore_error: if False, no exception is raised
46 strip: indicates if strip is done on the output (stdout and stderr)
47 input: input data to the command (stdin)
48 updateenv: environment variables to be appended to the current
49 process environment variables
50
51 NOTE: keys 'ignore_error' and 'input' are optional; if not included,
52 the defaults are the ones specify in the arguments
53 cwd: directory where commands are executed
54 ignore_error: raise CmdException if command fails to execute and
55 this value is False
56 input: input data (stdin) for the command
57
58 Output: dict containing the following keys:
59
60 cmd: the same as input
61 ignore_error: the same as input
62 strip: the same as input
63 input: the same as input
64 stdout: Standard output after command's execution
65 stderr: Standard error after command's execution
66 returncode: Return code after command's execution
67
68 """
69 cmddefaults = {
70 'cmd':'',
71 'ignore_error':ignore_error,
72 'strip':strip,
73 'input':input,
74 'updateenv':updateenv,
75 }
76
77 # update input values if necessary
78 cmddefaults.update(cmd)
79
80 _cmd = cmddefaults
81
82 if not _cmd['cmd']:
83 raise CmdException({'cmd':None, 'stderr':'no command given'})
84
85 # update the environment
86 env = os.environ
87 env.update(_cmd['updateenv'])
88
89 _command = [e for e in _cmd['cmd']]
90 p = subprocess.Popen(_command,
91 stdin=subprocess.PIPE,
92 stdout=subprocess.PIPE,
93 stderr=subprocess.PIPE,
94 universal_newlines=True,
95 cwd=cwd,
96 env=env)
97
98 # execute the command and strip output
99 (_stdout, _stderr) = p.communicate(_cmd['input'])
100 if _cmd['strip']:
101 _stdout, _stderr = map(str.strip, [_stdout, _stderr])
102
103 # generate the result
104 result = _cmd
105 result.update({'cmd':_command,'stdout':_stdout,'stderr':_stderr,'returncode':p.returncode})
106
107 # launch exception if necessary
108 if not _cmd['ignore_error'] and p.returncode:
109 raise CmdException(result)
110
111 return result
112
113def exec_cmds(cmds, cwd):
114 """ Executes commands
115
116 Input:
117 cmds: Array of commands
118 cwd: directory where commands are executed
119
120 Output: Array of output commands
121 """
122 results = []
123 _cmds = cmds
124
125 for cmd in _cmds:
126 result = exec_cmd(cmd, cwd)
127 results.append(result)
128
129 return results
130
131def logger_create(name):
132 logger = logging.getLogger(name)
133 loggerhandler = logging.StreamHandler()
134 loggerhandler.setFormatter(logging.Formatter("%(message)s"))
135 logger.addHandler(loggerhandler)
136 logger.setLevel(logging.INFO)
137 return logger
138
139def get_subject_prefix(path):
140 prefix = ""
141 mbox = mailbox.mbox(path)
142
143 if len(mbox):
144 subject = mbox[0]['subject']
145 if subject:
146 pattern = re.compile("(\[.*\])", re.DOTALL)
147 match = pattern.search(subject)
148 if match:
149 prefix = match.group(1)
150
151 return prefix
152
153def valid_branch(branch):
154 """ Check if branch is valid name """
155 lbranch = branch.lower()
156
157 invalid = lbranch.startswith('patch') or \
158 lbranch.startswith('rfc') or \
159 lbranch.startswith('resend') or \
160 re.search('^v\d+', lbranch) or \
161 re.search('^\d+/\d+', lbranch)
162
163 return not invalid
164
165def get_branch(path):
166 """ Get the branch name from mbox """
167 fullprefix = get_subject_prefix(path)
168 branch, branches, valid_branches = None, [], []
169
170 if fullprefix:
171 prefix = fullprefix.strip('[]')
172 branches = [ b.strip() for b in prefix.split(',')]
173 valid_branches = [b for b in branches if valid_branch(b)]
174
175 if len(valid_branches):
176 branch = valid_branches[0]
177
178 return branch
179