diff options
author | Trevor Gamblin <tgamblin@baylibre.com> | 2023-10-16 15:44:57 -0400 |
---|---|---|
committer | Richard Purdie <richard.purdie@linuxfoundation.org> | 2023-10-17 11:41:34 +0100 |
commit | 6e53a778f10c77eab3c0172a0cbc4d63efc663e9 (patch) | |
tree | 2790a50300d53a809ede675f0d163a999149331d | |
parent | 9d137188ad03c111ff8df7396b6b3dfd59307ac0 (diff) | |
download | poky-6e53a778f10c77eab3c0172a0cbc4d63efc663e9.tar.gz |
patchtest: add scripts to oe-core
Add the following from the patchtest repo:
- patchtest: core patch testing tool
- patchtest-get-branch: determine the target branch of a patch
- patchtest-get-series: pull patch series from Patchwork
- patchtest-send-results: send test results to selected mailing list
- patchtest-setup-sharedir: create sharedir for use with patchtest guest
mode
- patchtest.README: instructions for using patchtest based on the README
in the original repository
Note that the patchtest script was modified slightly from the repo
version to retain compatibility with the oe-core changes.
patchtest-send-results and patchtest-setup-sharedir are also primarily
intended for automated testing in guest mode, but are added for
consistency.
(From OE-Core rev: cf318c3c05fc050b8c838c04f28797325c569c5c)
Signed-off-by: Trevor Gamblin <tgamblin@baylibre.com>
Signed-off-by: Richard Purdie <richard.purdie@linuxfoundation.org>
-rwxr-xr-x | scripts/patchtest | 233 | ||||
-rwxr-xr-x | scripts/patchtest-get-branch | 92 | ||||
-rwxr-xr-x | scripts/patchtest-get-series | 125 | ||||
-rwxr-xr-x | scripts/patchtest-send-results | 93 | ||||
-rwxr-xr-x | scripts/patchtest-setup-sharedir | 95 | ||||
-rw-r--r-- | scripts/patchtest.README | 152 |
6 files changed, 790 insertions, 0 deletions
diff --git a/scripts/patchtest b/scripts/patchtest new file mode 100755 index 0000000000..9525a2be17 --- /dev/null +++ b/scripts/patchtest | |||
@@ -0,0 +1,233 @@ | |||
1 | #!/usr/bin/env python3 | ||
2 | # ex:ts=4:sw=4:sts=4:et | ||
3 | # -*- tab-width: 4; c-basic-offset: 4; indent-tabs-mode: nil -*- | ||
4 | # | ||
5 | # patchtest: execute all unittest test cases discovered for a single patch | ||
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 | |||
25 | import sys | ||
26 | import os | ||
27 | import unittest | ||
28 | import fileinput | ||
29 | import logging | ||
30 | import traceback | ||
31 | import json | ||
32 | |||
33 | # Include current path so test cases can see it | ||
34 | sys.path.insert(0, os.path.dirname(os.path.realpath(__file__))) | ||
35 | |||
36 | # Include patchtest library | ||
37 | sys.path.insert(0, os.path.join(os.path.dirname(os.path.realpath(__file__)), '../meta/lib/patchtest')) | ||
38 | |||
39 | from data import PatchTestInput | ||
40 | from repo import PatchTestRepo | ||
41 | |||
42 | import utils | ||
43 | logger = utils.logger_create('patchtest') | ||
44 | info = logger.info | ||
45 | error = logger.error | ||
46 | |||
47 | import repo | ||
48 | |||
49 | def getResult(patch, mergepatch, logfile=None): | ||
50 | |||
51 | class PatchTestResult(unittest.TextTestResult): | ||
52 | """ Patchtest TextTestResult """ | ||
53 | shouldStop = True | ||
54 | longMessage = False | ||
55 | |||
56 | success = 'PASS' | ||
57 | fail = 'FAIL' | ||
58 | skip = 'SKIP' | ||
59 | |||
60 | def startTestRun(self): | ||
61 | # let's create the repo already, it can be used later on | ||
62 | repoargs = { | ||
63 | 'repodir': PatchTestInput.repodir, | ||
64 | 'commit' : PatchTestInput.basecommit, | ||
65 | 'branch' : PatchTestInput.basebranch, | ||
66 | 'patch' : patch, | ||
67 | } | ||
68 | |||
69 | self.repo_error = False | ||
70 | self.test_error = False | ||
71 | self.test_failure = False | ||
72 | |||
73 | try: | ||
74 | self.repo = PatchTestInput.repo = PatchTestRepo(**repoargs) | ||
75 | except: | ||
76 | logger.error(traceback.print_exc()) | ||
77 | self.repo_error = True | ||
78 | self.stop() | ||
79 | return | ||
80 | |||
81 | if mergepatch: | ||
82 | self.repo.merge() | ||
83 | |||
84 | def addError(self, test, err): | ||
85 | self.test_error = True | ||
86 | (ty, va, trace) = err | ||
87 | logger.error(traceback.print_exc()) | ||
88 | |||
89 | def addFailure(self, test, err): | ||
90 | test_description = test.id().split('.')[-1].replace('_', ' ').replace("cve", "CVE").replace("signed off by", | ||
91 | "Signed-off-by").replace("upstream status", | ||
92 | "Upstream-Status").replace("non auh", | ||
93 | "non-AUH").replace("presence format", "presence") | ||
94 | self.test_failure = True | ||
95 | fail_str = '{}: {}: {} ({})'.format(self.fail, | ||
96 | test_description, json.loads(str(err[1]))["issue"], | ||
97 | test.id()) | ||
98 | print(fail_str) | ||
99 | if logfile: | ||
100 | with open(logfile, "a") as f: | ||
101 | f.write(fail_str + "\n") | ||
102 | |||
103 | def addSuccess(self, test): | ||
104 | test_description = test.id().split('.')[-1].replace('_', ' ').replace("cve", "CVE").replace("signed off by", | ||
105 | "Signed-off-by").replace("upstream status", | ||
106 | "Upstream-Status").replace("non auh", | ||
107 | "non-AUH").replace("presence format", "presence") | ||
108 | success_str = '{}: {} ({})'.format(self.success, | ||
109 | test_description, test.id()) | ||
110 | print(success_str) | ||
111 | if logfile: | ||
112 | with open(logfile, "a") as f: | ||
113 | f.write(success_str + "\n") | ||
114 | |||
115 | def addSkip(self, test, reason): | ||
116 | test_description = test.id().split('.')[-1].replace('_', ' ').replace("cve", "CVE").replace("signed off by", | ||
117 | "Signed-off-by").replace("upstream status", | ||
118 | "Upstream-Status").replace("non auh", | ||
119 | "non-AUH").replace("presence format", "presence") | ||
120 | skip_str = '{}: {}: {} ({})'.format(self.skip, | ||
121 | test_description, json.loads(str(reason))["issue"], | ||
122 | test.id()) | ||
123 | print(skip_str) | ||
124 | if logfile: | ||
125 | with open(logfile, "a") as f: | ||
126 | f.write(skip_str + "\n") | ||
127 | |||
128 | def stopTestRun(self): | ||
129 | |||
130 | # in case there was an error on repo object creation, just return | ||
131 | if self.repo_error: | ||
132 | return | ||
133 | |||
134 | self.repo.clean() | ||
135 | |||
136 | return PatchTestResult | ||
137 | |||
138 | def _runner(resultklass, prefix=None): | ||
139 | # load test with the corresponding prefix | ||
140 | loader = unittest.TestLoader() | ||
141 | if prefix: | ||
142 | loader.testMethodPrefix = prefix | ||
143 | |||
144 | # create the suite with discovered tests and the corresponding runner | ||
145 | suite = loader.discover(start_dir=PatchTestInput.startdir, pattern=PatchTestInput.pattern, top_level_dir=PatchTestInput.topdir) | ||
146 | ntc = suite.countTestCases() | ||
147 | |||
148 | # if there are no test cases, just quit | ||
149 | if not ntc: | ||
150 | return 2 | ||
151 | runner = unittest.TextTestRunner(resultclass=resultklass, verbosity=0) | ||
152 | |||
153 | try: | ||
154 | result = runner.run(suite) | ||
155 | except: | ||
156 | logger.error(traceback.print_exc()) | ||
157 | logger.error('patchtest: something went wrong') | ||
158 | return 1 | ||
159 | |||
160 | return 0 | ||
161 | |||
162 | def run(patch, logfile=None): | ||
163 | """ Load, setup and run pre and post-merge tests """ | ||
164 | # Get the result class and install the control-c handler | ||
165 | unittest.installHandler() | ||
166 | |||
167 | # run pre-merge tests, meaning those methods with 'pretest' as prefix | ||
168 | premerge_resultklass = getResult(patch, False, logfile) | ||
169 | premerge_result = _runner(premerge_resultklass, 'pretest') | ||
170 | |||
171 | # run post-merge tests, meaning those methods with 'test' as prefix | ||
172 | postmerge_resultklass = getResult(patch, True, logfile) | ||
173 | postmerge_result = _runner(postmerge_resultklass, 'test') | ||
174 | |||
175 | if premerge_result == 2 and postmerge_result == 2: | ||
176 | logger.error('patchtest: any test cases found - did you specify the correct suite directory?') | ||
177 | |||
178 | return premerge_result or postmerge_result | ||
179 | |||
180 | def main(): | ||
181 | tmp_patch = False | ||
182 | patch_path = PatchTestInput.patch_path | ||
183 | log_results = PatchTestInput.log_results | ||
184 | log_path = None | ||
185 | patch_list = None | ||
186 | |||
187 | if os.path.isdir(patch_path): | ||
188 | patch_list = [os.path.join(patch_path, filename) for filename in os.listdir(patch_path)] | ||
189 | else: | ||
190 | patch_list = [patch_path] | ||
191 | |||
192 | for patch in patch_list: | ||
193 | if os.path.getsize(patch) == 0: | ||
194 | logger.error('patchtest: patch is empty') | ||
195 | return 1 | ||
196 | |||
197 | logger.info('Testing patch %s' % patch) | ||
198 | |||
199 | if log_results: | ||
200 | log_path = patch + ".testresult" | ||
201 | with open(log_path, "a") as f: | ||
202 | f.write("Patchtest results for patch '%s':\n\n" % patch) | ||
203 | |||
204 | try: | ||
205 | if log_path: | ||
206 | run(patch, log_path) | ||
207 | else: | ||
208 | run(patch) | ||
209 | finally: | ||
210 | if tmp_patch: | ||
211 | os.remove(patch) | ||
212 | |||
213 | if __name__ == '__main__': | ||
214 | ret = 1 | ||
215 | |||
216 | # Parse the command line arguments and store it on the PatchTestInput namespace | ||
217 | PatchTestInput.set_namespace() | ||
218 | |||
219 | # set debugging level | ||
220 | if PatchTestInput.debug: | ||
221 | logger.setLevel(logging.DEBUG) | ||
222 | |||
223 | # if topdir not define, default it to startdir | ||
224 | if not PatchTestInput.topdir: | ||
225 | PatchTestInput.topdir = PatchTestInput.startdir | ||
226 | |||
227 | try: | ||
228 | ret = main() | ||
229 | except Exception: | ||
230 | import traceback | ||
231 | traceback.print_exc(5) | ||
232 | |||
233 | sys.exit(ret) | ||
diff --git a/scripts/patchtest-get-branch b/scripts/patchtest-get-branch new file mode 100755 index 0000000000..9415de98ef --- /dev/null +++ b/scripts/patchtest-get-branch | |||
@@ -0,0 +1,92 @@ | |||
1 | #!/usr/bin/env python3 | ||
2 | |||
3 | # Get target branch from the corresponding mbox | ||
4 | # | ||
5 | # NOTE: this script was based on patches coming to the openembedded-core | ||
6 | # where target branch is defined inside brackets as subject prefix | ||
7 | # i.e. [master], [rocko], etc. | ||
8 | # | ||
9 | # Copyright (C) 2016 Intel Corporation | ||
10 | # | ||
11 | # This program is free software; you can redistribute it and/or modify | ||
12 | # it under the terms of the GNU General Public License version 2 as | ||
13 | # published by the Free Software Foundation. | ||
14 | # | ||
15 | # This program is distributed in the hope that it will be useful, | ||
16 | # but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
17 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
18 | # GNU General Public License for more details. | ||
19 | # | ||
20 | # You should have received a copy of the GNU General Public License along | ||
21 | # with this program; if not, write to the Free Software Foundation, Inc., | ||
22 | # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. | ||
23 | |||
24 | import mailbox | ||
25 | import argparse | ||
26 | import re | ||
27 | import git | ||
28 | import sys | ||
29 | |||
30 | re_prefix = re.compile("(\[.*\])", re.DOTALL) | ||
31 | |||
32 | def get_branch(filepath_repo, filepath_mbox, default_branch): | ||
33 | branch = None | ||
34 | |||
35 | # get all remotes branches | ||
36 | gitbranches = git.Git(filepath_repo).branch('-a').splitlines() | ||
37 | |||
38 | # from gitbranches, just get the names | ||
39 | branches = [b.split('/')[-1] for b in gitbranches] | ||
40 | |||
41 | subject = ' '.join(mailbox.mbox(filepath_mbox)[0]['subject'].splitlines()) | ||
42 | |||
43 | # we expect that patches will have somewhere between one and three | ||
44 | # consecutive sets of square brackets with tokens inside, e.g.: | ||
45 | # 1. [PATCH] | ||
46 | # 2. [OE-core][PATCH] | ||
47 | # 3. [OE-core][kirkstone][PATCH] | ||
48 | # Some of them may also be part of a series, in which case the PATCH | ||
49 | # token will be formatted like: | ||
50 | # [PATCH 1/4] | ||
51 | # or they will be revisions to previous patches, where it will be: | ||
52 | # [PATCH v2] | ||
53 | # Or they may contain both: | ||
54 | # [PATCH v2 3/4] | ||
55 | # In any case, we want mprefix to contain all of these tokens so | ||
56 | # that we can search for branch names within them. | ||
57 | mprefix = re.findall(r'\[.*?\]', subject) | ||
58 | found_branch = None | ||
59 | if mprefix: | ||
60 | # Iterate over the tokens and compare against the branch list to | ||
61 | # figure out which one the patch is targeting | ||
62 | for token in mprefix: | ||
63 | stripped = token.lower().strip('[]') | ||
64 | if default_branch in stripped: | ||
65 | found_branch = default_branch | ||
66 | break | ||
67 | else: | ||
68 | for branch in branches: | ||
69 | # ignore branches named "core" | ||
70 | if branch != "core" and stripped.rfind(branch) != -1: | ||
71 | found_branch = token.split(' ')[0].strip('[]') | ||
72 | break | ||
73 | |||
74 | # if there's no mprefix content or no known branches were found in | ||
75 | # the tokens, assume the target is master | ||
76 | if found_branch is None: | ||
77 | found_branch = "master" | ||
78 | |||
79 | return (subject, found_branch) | ||
80 | |||
81 | if __name__ == '__main__': | ||
82 | |||
83 | parser = argparse.ArgumentParser() | ||
84 | parser.add_argument('repo', metavar='REPO', help='Main repository') | ||
85 | parser.add_argument('mbox', metavar='MBOX', help='mbox filename') | ||
86 | parser.add_argument('--default-branch', metavar='DEFAULT_BRANCH', default='master', help='Use this branch if no one is found') | ||
87 | parser.add_argument('--separator', '-s', metavar='SEPARATOR', default=' ', help='Char separator for output data') | ||
88 | args = parser.parse_args() | ||
89 | |||
90 | subject, branch = get_branch(args.repo, args.mbox, args.default_branch) | ||
91 | print("branch: %s" % branch) | ||
92 | |||
diff --git a/scripts/patchtest-get-series b/scripts/patchtest-get-series new file mode 100755 index 0000000000..773701f80b --- /dev/null +++ b/scripts/patchtest-get-series | |||
@@ -0,0 +1,125 @@ | |||
1 | #!/bin/bash -e | ||
2 | # | ||
3 | # get-latest-series: Download latest patch series from Patchwork | ||
4 | # | ||
5 | # Copyright (C) 2023 BayLibre Inc. | ||
6 | # | ||
7 | # This program is free software; you can redistribute it and/or modify | ||
8 | # it under the terms of the GNU General Public License version 2 as | ||
9 | # published by the Free Software Foundation. | ||
10 | # | ||
11 | # This program is distributed in the hope that it will be useful, | ||
12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
14 | # GNU General Public License for more details. | ||
15 | # | ||
16 | # You should have received a copy of the GNU General Public License along | ||
17 | # with this program; if not, write to the Free Software Foundation, Inc., | ||
18 | # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. | ||
19 | |||
20 | # the interval into the past which we want to check for new series, in minutes | ||
21 | INTERVAL_MINUTES=30 | ||
22 | |||
23 | # Maximum number of series to retrieve. the Patchwork API can support up to 250 | ||
24 | # at once | ||
25 | SERIES_LIMIT=250 | ||
26 | |||
27 | # Location to save patches | ||
28 | DOWNLOAD_PATH="." | ||
29 | |||
30 | # Name of the file to use/check as a log of previously-tested series IDs | ||
31 | SERIES_TEST_LOG=".series_test.log" | ||
32 | |||
33 | # Patchwork project to pull series patches from | ||
34 | PROJECT="oe-core" | ||
35 | |||
36 | # The Patchwork server to pull from | ||
37 | SERVER="https://patchwork.yoctoproject.org/api/1.2/" | ||
38 | |||
39 | help() | ||
40 | { | ||
41 | echo "Usage: get-latest-series [ -i | --interval MINUTES ] | ||
42 | [ -d | --directory DIRECTORY ] | ||
43 | [ -l | --limit COUNT ] | ||
44 | [ -h | --help ] | ||
45 | [ -t | --tested-series LOGFILE] | ||
46 | [ -p | --project PROJECT ] | ||
47 | [ -s | --server SERVER ]" | ||
48 | exit 2 | ||
49 | } | ||
50 | |||
51 | while [ "$1" != "" ]; do | ||
52 | case $1 in | ||
53 | -i|--interval) | ||
54 | INTERVAL_MINUTES=$2 | ||
55 | shift 2 | ||
56 | ;; | ||
57 | -l|--limit) | ||
58 | SERIES_LIMIT=$2 | ||
59 | shift 2 | ||
60 | ;; | ||
61 | -d|--directory) | ||
62 | DOWNLOAD_PATH=$2 | ||
63 | shift 2 | ||
64 | ;; | ||
65 | -p|--project) | ||
66 | PROJECT=$2 | ||
67 | shift 2 | ||
68 | ;; | ||
69 | -s|--server) | ||
70 | SERVER=$2 | ||
71 | shift 2 | ||
72 | ;; | ||
73 | -t|--tested-series) | ||
74 | SERIES_TEST_LOG=$2 | ||
75 | shift 2 | ||
76 | ;; | ||
77 | -h|--help) | ||
78 | help | ||
79 | ;; | ||
80 | *) | ||
81 | echo "Unknown option $1" | ||
82 | help | ||
83 | ;; | ||
84 | esac | ||
85 | done | ||
86 | |||
87 | # The time this script is running at | ||
88 | START_TIME=$(date --date "now" +"%Y-%m-%dT%H:%M:%S") | ||
89 | |||
90 | # the corresponding timestamp we want to check against for new patch series | ||
91 | SERIES_CHECK_LIMIT=$(date --date "now - ${INTERVAL_MINUTES} minutes" +"%Y-%m-%dT%H:%M:%S") | ||
92 | |||
93 | echo "Start time is $START_TIME" | ||
94 | echo "Series check limit is $SERIES_CHECK_LIMIT" | ||
95 | |||
96 | # Create DOWNLOAD_PATH if it doesn't exist | ||
97 | if [ ! -d "$DOWNLOAD_PATH" ]; then | ||
98 | mkdir "${DOWNLOAD_PATH}" | ||
99 | fi | ||
100 | |||
101 | # Create SERIES_TEST_LOG if it doesn't exist | ||
102 | if [ ! -f "$SERIES_TEST_LOG" ]; then | ||
103 | touch "${SERIES_TEST_LOG}" | ||
104 | fi | ||
105 | |||
106 | # Retrieve a list of series IDs from the 'git-pw series list' output. The API | ||
107 | # supports a maximum of 250 results, so make sure we allow that when required | ||
108 | SERIES_LIST=$(git-pw --project "${PROJECT}" --server "${SERVER}" series list --since "${SERIES_CHECK_LIMIT}" --limit "${SERIES_LIMIT}" | awk '{print $2}' | xargs | sed -e 's/[^0-9 ]//g') | ||
109 | |||
110 | if [ -z "$SERIES_LIST" ]; then | ||
111 | echo "No new series for project ${PROJECT} since ${SERIES_CHECK_LIMIT}" | ||
112 | exit 0 | ||
113 | fi | ||
114 | |||
115 | # Check each series ID | ||
116 | for SERIES in $SERIES_LIST; do | ||
117 | # Download the series only if it's not found in the SERIES_TEST_LOG | ||
118 | if ! grep -w --quiet "${SERIES}" "${SERIES_TEST_LOG}"; then | ||
119 | echo "Downloading $SERIES..." | ||
120 | git-pw series download --separate "${SERIES}" "${DOWNLOAD_PATH}" | ||
121 | echo "${SERIES}" >> "${SERIES_TEST_LOG}" | ||
122 | else | ||
123 | echo "Already tested ${SERIES}. Skipping..." | ||
124 | fi | ||
125 | done | ||
diff --git a/scripts/patchtest-send-results b/scripts/patchtest-send-results new file mode 100755 index 0000000000..2a2c57a10e --- /dev/null +++ b/scripts/patchtest-send-results | |||
@@ -0,0 +1,93 @@ | |||
1 | #!/usr/bin/env python3 | ||
2 | # ex:ts=4:sw=4:sts=4:et | ||
3 | # -*- tab-width: 4; c-basic-offset: 4; indent-tabs-mode: nil -*- | ||
4 | # | ||
5 | # patchtest: execute all unittest test cases discovered for a single patch | ||
6 | # Note that this script is currently under development and has been | ||
7 | # hard-coded with default values for testing purposes. This script | ||
8 | # should not be used without changing the default recipient, at minimum. | ||
9 | # | ||
10 | # Copyright (C) 2023 BayLibre Inc. | ||
11 | # | ||
12 | # This program is free software; you can redistribute it and/or modify | ||
13 | # it under the terms of the GNU General Public License version 2 as | ||
14 | # published by the Free Software Foundation. | ||
15 | # | ||
16 | # This program is distributed in the hope that it will be useful, | ||
17 | # but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
18 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
19 | # GNU General Public License for more details. | ||
20 | # | ||
21 | # You should have received a copy of the GNU General Public License along | ||
22 | # with this program; if not, write to the Free Software Foundation, Inc., | ||
23 | # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. | ||
24 | # | ||
25 | # Author: Trevor Gamblin <tgamblin@baylibre.com> | ||
26 | # | ||
27 | |||
28 | import argparse | ||
29 | import boto3 | ||
30 | import configparser | ||
31 | import mailbox | ||
32 | import os | ||
33 | import sys | ||
34 | |||
35 | greeting = """Thank you for your submission. Patchtest identified one | ||
36 | or more issues with the patch. Please see the log below for | ||
37 | more information:\n\n---\n""" | ||
38 | |||
39 | suggestions = """\n---\n\nPlease address the issues identified and | ||
40 | submit a new revision of the patch, or alternatively, reply to this | ||
41 | email with an explanation of why the patch format should be accepted. | ||
42 | Note that patchtest may report failures in the merge-on-head test for | ||
43 | patches that are part of a series if they rely on changes from | ||
44 | preceeding entries. | ||
45 | |||
46 | If you believe these results are due to an error in patchtest, please | ||
47 | submit a bug at https://bugzilla.yoctoproject.org/ (use the 'Patchtest' | ||
48 | category under 'Yocto Project Subprojects'). Thank you!""" | ||
49 | |||
50 | parser = argparse.ArgumentParser(description="Send patchtest results to a submitter for a given patch") | ||
51 | parser.add_argument("-p", "--patch", dest="patch", required=True, help="The patch file to summarize") | ||
52 | args = parser.parse_args() | ||
53 | |||
54 | if not os.path.exists(args.patch): | ||
55 | print(f"Patch '{args.patch}' not found - did you provide the right path?") | ||
56 | sys.exit(1) | ||
57 | elif not os.path.exists(args.patch + ".testresult"): | ||
58 | print(f"Found patch '{args.patch}' but '{args.patch}.testresult' was not present. Have you run patchtest on the patch?") | ||
59 | sys.exit(1) | ||
60 | |||
61 | result_file = args.patch + ".testresult" | ||
62 | result_basename = os.path.basename(args.patch) | ||
63 | testresult = None | ||
64 | |||
65 | with open(result_file, "r") as f: | ||
66 | testresult = f.read() | ||
67 | |||
68 | reply_contents = greeting + testresult + suggestions | ||
69 | subject_line = f"Patchtest results for {result_basename}" | ||
70 | |||
71 | if "FAIL" in testresult: | ||
72 | ses_client = boto3.client('ses', region_name='us-west-2') | ||
73 | response = ses_client.send_email( | ||
74 | Source='patchtest@automation.yoctoproject.org', | ||
75 | Destination={ | ||
76 | 'ToAddresses': ['test-list@lists.yoctoproject.org'], | ||
77 | }, | ||
78 | ReplyToAddresses=['test-list@lists.yoctoproject.org'], | ||
79 | Message={ | ||
80 | 'Subject': { | ||
81 | 'Data': subject_line, | ||
82 | 'Charset': 'utf-8' | ||
83 | }, | ||
84 | 'Body': { | ||
85 | 'Text': { | ||
86 | 'Data': reply_contents, | ||
87 | 'Charset': 'utf-8' | ||
88 | } | ||
89 | } | ||
90 | } | ||
91 | ) | ||
92 | else: | ||
93 | print(f"No failures identified for {args.patch}.") | ||
diff --git a/scripts/patchtest-setup-sharedir b/scripts/patchtest-setup-sharedir new file mode 100755 index 0000000000..a1497987cb --- /dev/null +++ b/scripts/patchtest-setup-sharedir | |||
@@ -0,0 +1,95 @@ | |||
1 | #!/bin/bash -e | ||
2 | # | ||
3 | # patchtest-setup-sharedir: Setup a directory for storing mboxes and | ||
4 | # repositories to be shared with the guest machine, including updates to | ||
5 | # the repos if the directory already exists | ||
6 | # | ||
7 | # Copyright (C) 2023 BayLibre Inc. | ||
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: Trevor Gamblin <tgamblin@baylibre.com> | ||
23 | |||
24 | # poky repository | ||
25 | POKY_REPO="https://git.yoctoproject.org/poky" | ||
26 | |||
27 | # patchtest repository | ||
28 | PATCHTEST_REPO="https://git.yoctoproject.org/patchtest" | ||
29 | |||
30 | # the name of the directory | ||
31 | SHAREDIR="patchtest_share" | ||
32 | |||
33 | help() | ||
34 | { | ||
35 | echo "Usage: patchtest-setup-sharedir [ -d | --directory SHAREDIR ] | ||
36 | [ -p | --patchtest PATCHTEST_REPO ] | ||
37 | [ -y | --poky POKY_REPO ]" | ||
38 | exit 2 | ||
39 | } | ||
40 | |||
41 | while [ "$1" != "" ]; do | ||
42 | case $1 in | ||
43 | -d|--directory) | ||
44 | SHAREDIR=$2 | ||
45 | shift 2 | ||
46 | ;; | ||
47 | -p|--patchtest) | ||
48 | PATCHTEST_REPO=$2 | ||
49 | shift 2 | ||
50 | ;; | ||
51 | -y|--poky) | ||
52 | POKY_REPO=$2 | ||
53 | shift 2 | ||
54 | ;; | ||
55 | -h|--help) | ||
56 | help | ||
57 | ;; | ||
58 | *) | ||
59 | echo "Unknown option $1" | ||
60 | help | ||
61 | ;; | ||
62 | esac | ||
63 | done | ||
64 | |||
65 | # define MBOX_DIR where the patch series will be stored by | ||
66 | # get-latest-series | ||
67 | MBOX_DIR="${SHAREDIR}/mboxes" | ||
68 | |||
69 | # Create SHAREDIR if it doesn't exist | ||
70 | if [ ! -d "$SHAREDIR" ]; then | ||
71 | mkdir -p "${SHAREDIR}" | ||
72 | echo "Created ${SHAREDIR}" | ||
73 | fi | ||
74 | |||
75 | # Create the mboxes directory if it doesn't exist | ||
76 | if [ ! -d "$MBOX_DIR" ]; then | ||
77 | mkdir -p "${MBOX_DIR}" | ||
78 | echo "Created ${MBOX_DIR}" | ||
79 | fi | ||
80 | |||
81 | # clone poky if it's not already present; otherwise, update it | ||
82 | if [ ! -d "$POKY_REPO" ]; then | ||
83 | BASENAME=$(basename ${POKY_REPO}) | ||
84 | git clone "${POKY_REPO}" "${SHAREDIR}/${BASENAME}" | ||
85 | else | ||
86 | (cd "${SHAREDIR}/$BASENAME" && git pull) | ||
87 | fi | ||
88 | |||
89 | # clone patchtest if it's not already present; otherwise, update it | ||
90 | if [ ! -d "$PATCHTEST_REPO" ]; then | ||
91 | BASENAME=$(basename ${PATCHTEST_REPO}) | ||
92 | git clone "${PATCHTEST_REPO}" "${SHAREDIR}/${BASENAME}" | ||
93 | else | ||
94 | (cd "${SHAREDIR}/$BASENAME" && git pull) | ||
95 | fi | ||
diff --git a/scripts/patchtest.README b/scripts/patchtest.README new file mode 100644 index 0000000000..689d513df5 --- /dev/null +++ b/scripts/patchtest.README | |||
@@ -0,0 +1,152 @@ | |||
1 | # Patchtest | ||
2 | |||
3 | ## Introduction | ||
4 | |||
5 | Patchtest is a test framework for community patches based on the standard | ||
6 | unittest python module. As input, it needs tree elements to work properly: | ||
7 | a patch in mbox format (either created with `git format-patch` or fetched | ||
8 | from 'patchwork'), a test suite and a target repository. | ||
9 | |||
10 | The first test suite intended to be used with patchtest is found in the | ||
11 | openembedded-core repository [1] targeted for patches that get into the | ||
12 | openembedded-core mailing list [2]. This suite is also intended as a | ||
13 | baseline for development of similar suites for other layers as needed. | ||
14 | |||
15 | Patchtest can either run on a host or a guest machine, depending on which | ||
16 | environment the execution needs to be done. If you plan to test your own patches | ||
17 | (a good practice before these are sent to the mailing list), the easiest way is | ||
18 | to install and execute on your local host; in the other hand, if automatic | ||
19 | testing is intended, the guest method is strongly recommended. The guest | ||
20 | method requires the use of the patchtest layer, in addition to the tools | ||
21 | available in oe-core: https://git.yoctoproject.org/patchtest/ | ||
22 | |||
23 | ## Installation | ||
24 | |||
25 | As a tool for use with the Yocto Project, the [quick start guide](https://docs.yoctoproject.org/brief-yoctoprojectqs/index.html) | ||
26 | contains the necessary prerequisites for a basic project. In addition, | ||
27 | patchtest relies on the following Python modules: | ||
28 | |||
29 | - boto3 (for sending automated results emails only) | ||
30 | - git-pw>=2.5.0 | ||
31 | - jinja2 | ||
32 | - pylint | ||
33 | - pyparsing>=3.0.9 | ||
34 | - unidiff | ||
35 | |||
36 | These can be installed by running `pip install -r | ||
37 | meta/lib/patchtest/requirements.txt`. Note that git-pw is not | ||
38 | automatically added to the user's PATH; by default, it is installed at | ||
39 | ~/.local/bin/git-pw. | ||
40 | |||
41 | For git-pw (and therefore scripts such as patchtest-get--series) to work, you need | ||
42 | to provide a Patchwork instance in your user's .gitconfig, like so (the project | ||
43 | can be specified using the --project argument): | ||
44 | |||
45 | git config --global pw.server "https://patchwork.yoctoproject.org/api/1.2/" | ||
46 | |||
47 | To work with patchtest, you should have the following repositories cloned: | ||
48 | |||
49 | 1. https://git.openembedded.org/openembedded-core/ (or https://git.yoctoproject.org/poky/) | ||
50 | 2. https://git.openembedded.org/bitbake/ (if not using poky) | ||
51 | 3. https://git.yoctoproject.org/patchtest (if using guest mode) | ||
52 | |||
53 | ## Usage | ||
54 | |||
55 | ### Obtaining Patches | ||
56 | |||
57 | Patch files can be obtained directly from cloned repositories using `git | ||
58 | format-patch -N` (where N is the number of patches starting from HEAD to | ||
59 | generate). git-pw can also be used with filters for users, patch/series IDs, | ||
60 | and timeboxes if specific patches are desired. For more information, see the | ||
61 | git-pw [documentation](https://patchwork.readthedocs.io/projects/git-pw/en/latest/). | ||
62 | |||
63 | Alternatively, `scripts/patchtest-get-series` can be used to pull mbox files from | ||
64 | the Patchwork instance configured previously in .gitconfig. It uses a log file | ||
65 | called ".series_test.log" to store and compare series IDs so that the same | ||
66 | versions of a patch are not tested multiple times unintentionally. By default, | ||
67 | it will pull up to five patch series from the last 30 minutes using oe-core as | ||
68 | the target project, but these parameters can be configured using the `--limit`, | ||
69 | `--interval`, and `--project` arguments respectively. For more information, run | ||
70 | `patchtest-get-series -h`. | ||
71 | |||
72 | ### Host Mode | ||
73 | |||
74 | To run patchtest on the host, do the following: | ||
75 | |||
76 | 1. In openembedded-core/poky, do `source oe-init-build-env` | ||
77 | 2. Generate patch files from the target repository by doing `git-format patch -N`, | ||
78 | where N is the number of patches starting at HEAD, or by using git-pw | ||
79 | or patchtest-get-series | ||
80 | 3. Run patchtest on a patch file by doing the following: | ||
81 | |||
82 | patchtest --patch /path/to/patch/file /path/to/target/repo /path/to/tests/directory | ||
83 | |||
84 | or, if you have stored the patch files in a directory, do: | ||
85 | |||
86 | patchtest --directory /path/to/patch/directory /path/to/target/repo /path/to/tests/directory | ||
87 | |||
88 | For example, to test `master-gcc-Fix--fstack-protector-issue-on-aarch64.patch` against the oe-core test suite: | ||
89 | |||
90 | patchtest --patch master-gcc-Fix--fstack-protector-issue-on-aarch64.patch /path/to/openembedded-core /path/to/openembedded-core/meta/lib/patchtest/tests | ||
91 | |||
92 | ### Guest Mode | ||
93 | |||
94 | Patchtest's guest mode has been refactored to more closely mirror the | ||
95 | typical Yocto Project image build workflow, but there are still some key | ||
96 | differences to keep in mind. The primary objective is to provide a level | ||
97 | of isolation from the host when testing patches pulled automatically | ||
98 | from the mailing lists. When executed this way, the test process is | ||
99 | essentially running random code from the internet and could be | ||
100 | catastrophic if malicious bits or even poorly-handled edge cases aren't | ||
101 | protected against. In order to use this mode, the | ||
102 | https://git.yoctoproject.org/patchtest/ repository must be cloned and | ||
103 | the meta-patchtest layer added to bblayers.conf. | ||
104 | |||
105 | The general flow of guest mode is: | ||
106 | |||
107 | 1. Run patchtest-setup-sharedir --directory <dirname> to create a | ||
108 | directory for mounting | ||
109 | 2. Collect patches via patchtest-get-series (or other manual step) into the | ||
110 | <dirname>/mboxes path | ||
111 | 3. Ensure that a user with ID 1200 has appropriate read/write | ||
112 | permissions to <dirname> and <dirname>/mboxes, so that the | ||
113 | "patchtest" user in the core-image-patchtest image can function | ||
114 | 4. Build the core-image-patchtest image | ||
115 | 5. Run the core-image-patchtest image with the mounted sharedir, like | ||
116 | so: | ||
117 | `runqemu kvm nographic qemuparams="-snapshot -fsdev | ||
118 | local,id=test_mount,path=/workspace/yocto/poky/build/patchtestdir,security_model=mapped | ||
119 | -device virtio-9p-pci,fsdev=test_mount,mount_tag=test_mount -smp 4 -m | ||
120 | 2048"` | ||
121 | |||
122 | Patchtest runs as an initscript for the core-image-patchtest image and | ||
123 | shuts down after completion, so there is no input required from a user | ||
124 | during operation. Unlike in host mode, the guest is designed to | ||
125 | automatically generate test result files, in the same directory as the | ||
126 | targeted patch files but with .testresult as an extension. These contain | ||
127 | the entire output of the patchtest run for each respective pass, | ||
128 | including the PASS, FAIL, and SKIP indicators for each test run. | ||
129 | |||
130 | ## Contributing | ||
131 | |||
132 | The yocto mailing list (yocto@lists.yoctoproject.org) is used for questions, | ||
133 | comments and patch review. It is subscriber only, so please register before | ||
134 | posting. | ||
135 | |||
136 | Send pull requests to yocto@lists.yoctoproject.org with '[patchtest]' in the | ||
137 | subject. | ||
138 | |||
139 | When sending single patches, please use something like: | ||
140 | |||
141 | git send-email -M -1 --to=yocto@lists.yoctoproject.org --subject-prefix=patchtest][PATCH | ||
142 | |||
143 | ## Maintenance | ||
144 | ----------- | ||
145 | |||
146 | Maintainers: | ||
147 | Trevor Gamblin <tgamblin@baylibre.com> | ||
148 | |||
149 | ## Links | ||
150 | ----- | ||
151 | [1] https://git.openembedded.org/openembedded-core/ | ||
152 | [2] https://www.yoctoproject.org/community/mailing-lists/ | ||