diff options
-rw-r--r-- | tools/test-series/requirements.txt | 3 | ||||
-rwxr-xr-x | tools/test-series/test-series | 496 |
2 files changed, 499 insertions, 0 deletions
diff --git a/tools/test-series/requirements.txt b/tools/test-series/requirements.txt new file mode 100644 index 0000000..14d817a --- /dev/null +++ b/tools/test-series/requirements.txt @@ -0,0 +1,3 @@ +beautifulsoup4 +lxml +python-requests >= 2.4.2 diff --git a/tools/test-series/test-series b/tools/test-series/test-series new file mode 100755 index 0000000..e037fce --- /dev/null +++ b/tools/test-series/test-series @@ -0,0 +1,496 @@ +#!/usr/bin/env python3 +# +# Patch series test build automation script +# +# Copyright (C) 2015-2017 Intel Corporation +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License along +# with this program; if not, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +# +# +# Requirements: +# - Using pip: +# +# $ pip install beautifulsoup4 lxml +# +# Setup: +# +# - Path to a local git repository directory that can +# push to a remote repository must be provided. +# - if no local "git patchwork.default.url" is set, +# http://patchwork.openembedded.org will be used + +import argparse +import git +import os +import sys +import subprocess +from datetime import datetime +import getpass +import requests +import re + +# ***** Default values ***** # +sourceDir = os.path.dirname(os.path.realpath(__file__)) +default_user = getpass.getuser() +default_patchwork_dir = "/home/%s/patchwork/patchwork" % default_user +default_patchwork_url = "https://patchwork.openembedded.org" +default_builder = "nightly-oecore" +default_autobuilder_dir = "/home/%s/yocto-autobuilder" % default_user +default_autobuilder_url = "https://autobuilder.yocto.io" +default_password = "password" +default_repo_name = "origin" +default_repo_url = None +default_base_branch = "master" +# delete test-branch after starting build +default_delete_branch = False +# push branch to repoUrl +default_no_push = False +# skip starting the build +default_no_build = False +default_update_basebranch = False +default_test_success_only = False +test_name = "test-build" +initial_result = "pending" + + +class TestSeriesAPI(object): + + def __init__(self, cmd): + self.cmd = cmd + + def _checkout_to_branch(self, branch): + try: + msg = subprocess.check_output(['git', 'checkout', branch] + ).decode('utf-8').strip() + if msg != "": + print("I: %s" % msg) + return 0 + except subprocess.CalledProcessError: + return 1 + + def _get_current_branch(self): + try: + msg = subprocess.check_output(['git', 'symbolic-ref', 'HEAD'] + ).decode('utf-8').strip() + if 'refs/heads/' in msg: + return msg[11:] + except subprocess.CalledProcessError: + return None + + def _name_test_branch(self, series): + prefix = "test-" + postfix = datetime.now().strftime('-%Y%b%d-%H%M%S') + if not series: + name = prefix + "series" + postfix + print("I: No series (-s) and no test-branch name (-tb) found. \ +Using automatically generated name \"%s\"." % name) + else: + name = prefix + "-".join([str(serie) for serie in series]) + \ + postfix + print("I: no test-branch name provided (-tb). Using the \ +automatically generated \"%s\"." % name) + return name + + def _delete_test_branch(self, testBranch, baseBranch): + prereq = self._checkout_to_branch(baseBranch) + if prereq != 0: + return 1 + else: + try: + msg = subprocess.check_output( + ['git', 'branch', '-D', testBranch] + ).decode('utf-8').strip() + if msg != "": + print("I: %s" % msg) + return 0 + except subprocess.CalledProcessError: + return 1 + + def _publish_build(self, branch, abUrl, applied, builder, pwDir): + # publish build message only when executig full test-builds + if not applied: + return 1 + m = re.search('(test-)((\d{4,}\-)+)', branch) + if m: + series = re.sub('-', ' ', m.group(2)).strip().split() + else: + return 1 + for serie in series: + if serie not in applied: + continue + try: + tmsg = "A test build was attempted for series \"%s\" in the \ +autobuilder at \"%s\" using builder \"%s\". You may find the test results by \ +querying the series ID in the builder\'s \"reason\" field." % ( + serie, abUrl, builder) + pmsg = subprocess.check_output( + ['%s/../git-pw/git-pw' % pwDir, 'post-result', '--url', + '%s/builders/%s/' % (abUrl, builder), '--summary', tmsg, + '%s' % serie, test_name, initial_result] + ).decode('utf-8').strip() + except subprocess.CalledProcessError: + return 1 + return 0 + + def update_branch(self, branch): + if not branch: + print("I: target branch (--base-branch) not found/provided. \ +Atempting to update current branch.") + branch = self._get_current_branch() + if not branch: + return 1 + prereq = self._checkout_to_branch(branch) + if prereq != 0: + print("E: Failed to checkout to branch \"%s\" to be updated. \ +Aborting" % branch) + return 1 + try: + msg = subprocess.check_output(['git', + 'fetch']).decode('utf-8').strip() + if msg != "": + print("I: %s" % msg) + msg = subprocess.check_output(['git', + 'pull']).decode('utf-8').strip() + if msg != "": + print("I: %s" % msg) + except subprocess.CalledProcessError: + return 1 + print("I: Successfully updated branch \"%s\"." % branch) + return 0 + + def create_branch(self, baseBranch, testBranch, updateBaseBranch, series): + if not baseBranch: + print("I: base-branch not provided. Attempting to use \"%s\" as \ +base-branch for new test-branch." % defaultBaseBranch) + if updateBaseBranch: + prereq = self.update_branch(baseBranch) + if prereq == 1: + print("E: Failed to update branch \"%s\" before applying a \ +series." % baseBranch) + return 1 + prereq = self._checkout_to_branch(baseBranch) + if prereq != 0: + print("E: Failed to checkout to \"%s\" before creating \ +test-branch \"%s\". Aborting." % (baseBranch, testBranch)) + return 1 + if not testBranch: + testBranch = self._name_test_branch(series) + print("I: Attempting to create branch \"%s\" from \"%s\"." + % (testBranch, baseBranch)) + try: + msg = subprocess.check_output(['git', 'checkout', '-b', "%s" + % testBranch] + ).decode('utf-8').strip() + if msg != "": + print("I: %s" % msg) + except subprocess.CalledProcessError: + return 1 + + return testBranch + + def apply_series(self, testBranch, series, testSuccessOnly, pwUrl, pwDir, + baseBranch, updateBaseBranch, newBranch): + if not series: + print("E: At least one series ID must be provided, for example: \ +\"-s 1234 -s 1235\". Aborting.") + return 1 + if not testBranch: + if not newBranch: + testBranch = self._get_current_branch() + if not testBranch: + return 1 + print("I: target branch (--test-branch) not received. Attempting \ +to apply series in current branch: \"%s\"." % testBranch) + else: + testBranch = self.create_branch( + baseBranch, testBranch, updateBaseBranch, series) + prereq = self._checkout_to_branch(testBranch) + if prereq != 0: + print("E: Failed to checkout to branch \"%s\" before applying \ +series: %s. Aborting." % (testBranch, series)) + return 1 + if testSuccessOnly: + if not pwUrl: + msg = subprocess.check_output( + ['git', 'config', 'patchwork.default.url'] + ).decode('utf-8').strip() + if 'error:'in msg or 'fatal:' in msg: + print("E: %s" % msg) + return 1 + elif msg != "": + pwUrl = msg + applied_series = [] + for serie in series: + if testSuccessOnly: + # skip series if test_state in patchwork is not "success" + if requests.get( + '%s/api/1.0/series/%d/' % (pwUrl, int(serie)) + ).json().get("test_state") != "success": + print("I: Skipping series \"%d\" due to non \"success\" \ +test-state in patchwork." % serie) + continue + print("I: Attempting to apply series %d" % int(serie)) + try: + msg = subprocess.check_output( + ['%s/../git-pw/git-pw' % pwDir, 'apply', '%s' % serie] + ).decode('utf-8').strip() + if msg != "": + print("I: %s." % msg) + except FileNotFoundError: + print("E: Invalid local patchwork directory (-pd). Aborting.") + return 1 + except subprocess.CalledProcessError: + print("E: Failed to apply series %s" % serie) + try: + msg = subprocess.check_output( + ['git', 'am', '--skip']).decode('utf-8').strip() + if msg != "": + print("I: %s." % msg) + except subprocess.CalledProcessError: + print("E: Failed to execute \"git am --skip\", you may \ +need to execute it manually or even \"git am --abort\" to clean the current \ +working directory.") + applied_series.append(serie) + return applied_series + + def push_branch(self, branch, repo, no_push): + if no_push: + print("I: push-branch for \"%s\" skipped due to not-push (-np) \ +option present." % branch) + return 0 + else: + if not branch: + print("I: Test-branch (-tb) not provided. Attempting to push \ +current branch.") + branch = self._get_current_branch() + if not branch: + print("E: Failed to get current branch name to be pushed. \ +Aborting.") + return 1 + prereq = self._checkout_to_branch(branch) + if prereq != 0: + print("E: Failed to checkout to branch \"%s\" to push. \ +Aborting." % branch) + return 1 + try: + msg = subprocess.check_output( + ['git', 'push', '--set-upstream', repo, branch] + ).decode('utf-8').strip() + if msg != "": + print("I: %s" % msg) + print("I: Successfully pushed branch \"%s\" to \"%s\"" + % (branch, repo)) + return 0 + except subprocess.CalledProcessError: + return 1 + + def force_build(self, abDir, abUrl, abUser, abPassword, builder, + testBranch, repoUrl, repo, no_build, applied, pwDir): + if not abPassword: + print("E: remote autobuilder password (-p) is required to start a \ +build. Aborting.") + return 1 + if no_build: + print("I: force build skipped due to no-build (-nb) option \ +present.") + return 0 + print("I: Attempting to start a forced build at \"%s\"" % abUrl) + if not repoUrl: + try: + msg = subprocess.check_output( + ['git', 'config', '--get', 'remote.%s.url' + % repo]).decode('utf-8').strip() + if msg != "": + repoUrl = msg + except subprocess.CalledProcessError: + print("E: Failed to get remote repo url from git config. \ +Aborting") + return 1 + print("I: remote repository url (-ru) not provided. Attempting \ +to use url from git remote config \"%s\"" % repoUrl) + if not repoUrl: + return 1 + if not testBranch: + testBranch = self._get_current_branch() + print("I: test branch to build-from not provided. Attempting to \ +start a build using current branch \"%s\"" % testBranch) + if not testBranch: + return 1 + try: + msg = subprocess.check_output( + ['%s/bin/forcebuild.py' % abDir, '-s', '%s' % abUrl, + '-u', '%s' % abUser, '-p', '%s' % abPassword, '--force-build', + '%s' % builder, '-o', + '{\'branch_oecore\':\'%s\', \'repo_oecore\':\'%s\',\ + \'reason\':\'%s\'}' % (testBranch, repoUrl, testBranch)], + stderr=subprocess.STDOUT).decode('utf-8').strip() + if msg != "": + print("E: %s" % msg) + print("I: force-build command completed for branch \"%s\" at \ +the autobuilder in \"%s\" using builder \"%s\". You may later find results \ +by including the branch name in your query." % (testBranch, abUrl, builder)) + self._publish_build(testBranch, abUrl, applied, builder, pwDir) + return 0 + except subprocess.CalledProcessError as e: + print("E: Failed to start a forced build for branch \"%s\" at \ +\"%s\".\n%s" % (testBranch, abUrl, e.output.decode('utf-8').strip())) + return 1 + + def full_test(self, baseBranch, testBranch, updateBaseBranch, series, + testSuccessOnly, pwUrl, repo, no_push, abDir, abUrl, abUser, + abPassword, builder, repoUrl, no_build, pwDir, newBranch): + applied = self.apply_series( + testBranch, series, testSuccessOnly, pwUrl, pwDir, baseBranch, + updateBaseBranch, newBranch) + if applied == 1: + return 1 + step = self.push_branch(testBranch, repo, no_push) + if step == 1: + return step + step = self.force_build(abDir, abUrl, abUser, abPassword, builder, + testBranch, repoUrl, repo, no_build, applied, + pwDir) + if step == 1: + return step + else: + return 0 + + +def main(): + parser = argparse.ArgumentParser( + description="Open Embedded Series test build script.\n\nAvailable \ +commands are:\n update-branch update provided base-branch (-bb)\n\ + create-branch create test-branch (-tb) from base-branch (-bb)\n\ + apply-series apply provided series (-s) on top of test-branch (-tb)\n\ + push-branch push provided branch (-tb) to provided repository (-r)\n", + add_help=False, formatter_class=argparse.RawDescriptionHelpFormatter) + parser.add_argument('command', help='Action to be executed.') + parser.add_argument('-bb', '--base-branch', action='store', + default=default_base_branch, help='Name of the branch \ +from where a new branch will be based on. Default: %s' % default_base_branch) + parser.add_argument('-tb', '--test-branch', action='store', + default=None, help='Provide a custom \ +name for the test branch. Default: \"test-series\" + date-time postfix.') + parser.add_argument('-nw', '--new-branch', action='store_true', + default=None, help='Create a new testing branch \ +to execute command there.') + parser.add_argument('-s', '--series', action='append', help='Series to be \ +merged in top of base-branch (-bb).') + parser.add_argument('-ub', '--update-basebranch', action='store_true', + default=default_update_basebranch, help='Update the \ +base-branch (-bb) before any other operation. Default: %s' + % default_update_basebranch) + parser.add_argument('-pd', '--patchwork-dir', action='store', + default=default_patchwork_dir, help='Provide the \ +local patchwork\'s directory. Default: %s' % default_patchwork_dir) + parser.add_argument('-pu', '--patchwork-url', action='store', + help='Provide the remote patchwork instance\'s url. \ +Default: get it from git config') + parser.add_argument('-ts', '--test-success-only', action='store_true', + default=default_test_success_only, help='Test only \ +those series that have \"success\" as test_state in patchwork. Default: %s' + % default_test_success_only) + parser.add_argument('-ru', '--repo-url', action='store', + default=default_repo_url, help="Url to remote \ +repository to get branches to build. Default: get it from git config") + parser.add_argument('-r', '--repo-name', action='store', + default=default_repo_name, help="Name of remote \ +repository to push branches. Default: %s" % default_repo_name) + parser.add_argument('-np', '--no-push', action='store_true', + default=default_no_push, help="Do not push the test \ +branch to remote repo. Default: %s" % default_no_push) + parser.add_argument('-nb', '--no-build', action='store_true', + default=default_no_build, help="Do not start a build \ +from the test branch. Default: %s" % default_no_build) + parser.add_argument('-ad', '--autobuilder-dir', action='store', + default=default_autobuilder_dir, help="Provide the \ +local autobuilder directory. Default: %s" % default_autobuilder_dir) + parser.add_argument('-au', '--autobuilder-url', action='store', + default=default_autobuilder_url, help="Provide the \ +remote autobuilder\'s url. Default: %s" % default_autobuilder_url) + parser.add_argument('-u', '--user', action='store', default=default_user, + help="Provide the remote autobuilder username. \ +Default: %s" % default_user) + parser.add_argument('-p', '--password', action='store', + help="Provide the remote autobuilder password.") + parser.add_argument('-b', '--builder', action='store', + default=default_builder, help="Provide the remote \ +autobuilder builder name. Default: %s" % default_builder) + parser.add_argument('-d', '--delete_branch', action='store_true', + default=default_delete_branch, help="Delete the \ +created local test branch after push or build. Default: %s" + % default_delete_branch) + parser.add_argument( + '-h', '--help', action='help', default=argparse.SUPPRESS, + help='show this help message and exit') + + args = parser.parse_args() + api = TestSeriesAPI(args) + + if args.command == 'update-branch': + return api.update_branch(args.base_branch) + + elif args.command == 'create-branch': + created = api.create_branch(args.base_branch, args.test_branch, + args.update_basebranch, args.series) + if created != 1: + return 0 + else: + return created + + elif args.command == 'apply-series': + applied = api.apply_series(args.test_branch, args.series, + args.test_success_only, args.patchwork_url, + args.patchwork_dir, args.base_branch, + args.update_basebranch, args.new_branch) + if applied == 1: + return 1 + else: + return 0 + + elif args.command == 'push-branch': + return api.push_branch(args.test_branch, args.repo_name, args.no_push) + + elif args.command == 'force-build': + return api.force_build(args.autobuilder_dir, args.autobuilder_url, + args.user, args.password, args.builder, + args.test_branch, args.repo_url, + args.repo_name, args.no_build, None) + + elif args.command == 'full-test': + return api.full_test(args.base_branch, args.test_branch, + args.update_basebranch, args.series, + args.test_success_only, args.patchwork_url, + args.repo_name, args.no_push, + args.autobuilder_dir, args.autobuilder_url, + args.user, args.password, args.builder, + args.repo_url, args.no_build, args.patchwork_dir, + args.new_branch) + + else: + print("Command \"%s\" not recognized" % args.command) + return 1 + +if __name__ == '__main__': + start_directory = os.getcwd() + localrepo = start_directory + try: + ret = main() + os.chdir(start_directory) + except Exception: + os.chdir(start_directory) + ret = 1 + import traceback + traceback.print_exc() + sys.exit(ret) |