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/ | ||
