diff --git a/sktm.py b/sktm.py index 115dca6..0af6c85 100755 --- a/sktm.py +++ b/sktm.py @@ -19,6 +19,7 @@ import logging import os import sktm +import sktm.jenkins def setup_parser(): @@ -120,9 +121,10 @@ def load_config(args): setup_logging(args.verbose) cfg = load_config(args) logging.debug("cfg=%s", cfg) + jenkins_project = sktm.jenkins.Project(cfg.get("jjname"), cfg.get("jurl"), + cfg.get("jlogin"), cfg.get("jpass")) - sw = sktm.watcher(cfg.get("jurl"), cfg.get("jlogin"), cfg.get("jpass"), - cfg.get("jjname"), cfg.get("db"), cfg.get("makeopts")) + sw = sktm.watcher(jenkins_project, cfg.get("db"), cfg.get("makeopts")) args.func(sw, cfg) try: diff --git a/sktm/__init__.py b/sktm/__init__.py index c9ffb92..be5e7dd 100644 --- a/sktm/__init__.py +++ b/sktm/__init__.py @@ -18,18 +18,8 @@ import re import time import sktm.db -import sktm.jenkins import sktm.patchwork - - -class tresult(enum.IntEnum): - """Test result""" - SUCCESS = 0 - MERGE_FAILURE = 1 - BUILD_FAILURE = 2 - PUBLISH_FAILURE = 3 - TEST_FAILURE = 4 - BASELINE_FAILURE = 5 +import sktm.misc class jtype(enum.IntEnum): @@ -40,16 +30,13 @@ class jtype(enum.IntEnum): # TODO This is no longer just a watcher. Rename/refactor/describe accordingly. class watcher(object): - def __init__(self, jenkinsurl, jenkinslogin, jenkinspassword, - jenkinsjobname, dbpath, makeopts=None): + def __init__(self, jenkins_project, dbpath, makeopts=None): """ Initialize a "watcher". Args: - jenkinsurl: Jenkins instance URL. - jenkinslogin: Jenkins user name. - jenkinspassword: Jenkins user password. - jenkinsjobname: Name of the Jenkins job to trigger and watch. + jenkins_project: Interface to the Jenkins project to trigger + and watch (sktm.jenkins.Project). dbpath: Path to the job status database file. makeopts: Extra arguments to pass to "make" when building. @@ -57,11 +44,8 @@ def __init__(self, jenkinsurl, jenkinslogin, jenkinspassword, # FIXME Clarify/fix member variable names # Database instance self.db = sktm.db.skt_db(os.path.expanduser(dbpath)) - # Jenkins interface instance - self.jk = sktm.jenkins.skt_jenkins(jenkinsurl, jenkinslogin, - jenkinspassword) - # Jenkins project name - self.jobname = jenkinsjobname + # Jenkins project interface instance + self.jk = jenkins_project # Extra arguments to pass to "make" self.makeopts = makeopts # List of pending Jenkins builds, each one represented by a 3-tuple @@ -156,8 +140,7 @@ def add_pw(self, baseurl, pname, lpatch=None, apikey=None): def check_baseline(self): """Submit a build for baseline""" self.pj.append((sktm.jtype.BASELINE, - self.jk.build(self.jobname, - baserepo=self.baserepo, + self.jk.build(baserepo=self.baserepo, ref=self.baseref, baseconfig=self.cfgurl, makeopts=self.makeopts), @@ -207,14 +190,13 @@ def check_patchwork(self): # Submit and remember a Jenkins build for the patchset self.pj.append((sktm.jtype.PATCHWORK, self.jk.build( - self.jobname, baserepo=self.baserepo, ref=stablecommit, baseconfig=self.cfgurl, message_id=patchset.message_id, subject=patchset.subject, emails=patchset.email_addr_set, - patchwork=patchset.patch_url_list, + patch_url_list=patchset.patch_url_list, makeopts=self.makeopts), cpw)) logging.info("submitted message ID: %s", patchset.message_id) @@ -224,38 +206,38 @@ def check_patchwork(self): def check_pending(self): for (pjt, bid, cpw) in self.pj: - if self.jk.is_build_complete(self.jobname, bid): + if self.jk.is_build_complete(bid): logging.info("job completed: jjid=%d; type=%d", bid, pjt) self.pj.remove((pjt, bid, cpw)) if pjt == sktm.jtype.BASELINE: self.db.update_baseline( self.baserepo, - self.jk.get_base_hash(self.jobname, bid), - self.jk.get_base_commitdate(self.jobname, bid), - self.jk.get_result(self.jobname, bid), + self.jk.get_base_hash(bid), + self.jk.get_base_commitdate(bid), + self.jk.get_result(bid), bid ) elif pjt == sktm.jtype.PATCHWORK: patches = list() slist = list() series = None - bres = self.jk.get_result(self.jobname, bid) - rurl = self.jk.get_result_url(self.jobname, bid) + bres = self.jk.get_result(bid) + rurl = self.jk.get_result_url(bid) logging.info("result=%s", bres) logging.info("url=%s", rurl) - basehash = self.jk.get_base_hash(self.jobname, bid) + basehash = self.jk.get_base_hash(bid) logging.info("basehash=%s", basehash) - if bres == sktm.tresult.BASELINE_FAILURE: + if bres == sktm.misc.tresult.BASELINE_FAILURE: self.db.update_baseline( self.baserepo, basehash, - self.jk.get_base_commitdate(self.jobname, bid), - sktm.tresult.TEST_FAILURE, + self.jk.get_base_commitdate(bid), + sktm.misc.tresult.TEST_FAILURE, bid ) - patchset = self.jk.get_patchwork(self.jobname, bid) - for purl in patchset: + patch_url_list = self.jk.get_patch_url_list(bid) + for purl in patch_url_list: match = re.match(r"(.*)/patch/(\d+)$", purl) if match: baseurl = match.group(1) @@ -284,7 +266,7 @@ def check_pending(self): except ValueError: pass - if bres != sktm.tresult.BASELINE_FAILURE: + if bres != sktm.misc.tresult.BASELINE_FAILURE: self.db.commit_patchtest(self.baserepo, basehash, patches, bres, bid, series) else: diff --git a/sktm/db.py b/sktm/db.py index 9ef538b..1ee0b73 100644 --- a/sktm/db.py +++ b/sktm/db.py @@ -17,7 +17,7 @@ import os import sqlite3 import time -import sktm +import sktm.misc class skt_db(object): @@ -268,7 +268,7 @@ def get_baselineresult(self, baserepo, commithash): 'ORDER BY baseline.commitdate DESC LIMIT 1', (commithash, brid)) res = self.cur.fetchone() - return None if res is None else sktm.tresult(res[0]) + return None if res is None else sktm.misc.tresult(res[0]) def get_stable(self, baserepo): """ @@ -458,7 +458,7 @@ def dump_baseline_tests(self): for (burl, commit, res, buildid) in self.cur.fetchall(): print("repo url:", burl) print("commit id:", commit) - print("result:", sktm.tresult(res).name) + print("result:", sktm.misc.tresult(res).name) print("build id: #", buildid, sep='') print("---") diff --git a/sktm/jenkins.py b/sktm/jenkins.py index 0096b44..6ba4fbb 100644 --- a/sktm/jenkins.py +++ b/sktm/jenkins.py @@ -18,25 +18,29 @@ import jenkinsapi -import sktm +import sktm.misc -class skt_jenkins(object): - """Jenkins interface""" - def __init__(self, url, username=None, password=None): +class Project(object): + """Jenkins project interface""" + def __init__(self, name, url, username=None, password=None): """ - Initialize a Jenkins interface. + Initialize a Jenkins project interface. Args: + name: Name of the Jenkins project to operate on. url: Jenkins instance URL. username: Jenkins user name. password: Jenkins user password. """ + # Name of the Jenkins project to operate on + self.name = name + # Jenkins server interface # TODO Add support for CSRF protection self.server = jenkinsapi.jenkins.Jenkins(url, username, password) - def _wait_and_get_build(self, jobname, buildid): - job = self.server.get_job(jobname) + def __wait_and_get_build(self, buildid): + job = self.server.get_job(self.name) build = job.get_build(buildid) build.block_until_complete(delay=60) @@ -45,14 +49,13 @@ def _wait_and_get_build(self, jobname, buildid): return build - def get_cfg_data(self, jobname, buildid, stepname, cfgkey, default=None): + def __get_cfg_data(self, buildid, stepname, cfgkey, default=None): """ Get a value from a JSON-formatted output of a test result, of the - specified completed build for the specified project. Wait for the - build to complete, if it hasn't yet. + specified completed build. Wait for the build to complete, if it + hasn't yet. Args: - jobname: Jenkins project name. buildid: Jenkins build ID. stepname: Test (step) path in the result, which output should be parsed as JSON. @@ -63,7 +66,7 @@ def get_cfg_data(self, jobname, buildid, stepname, cfgkey, default=None): Returns: The key value, or the default if not found. """ - build = self._wait_and_get_build(jobname, buildid) + build = self.__wait_and_get_build(buildid) if not build.has_resultset(): raise Exception("No results for build %d (%s)" % @@ -75,70 +78,80 @@ def get_cfg_data(self, jobname, buildid, stepname, cfgkey, default=None): cfg = json.loads(val.stdout) return cfg.get(cfgkey, default) - def get_base_commitdate(self, jobname, buildid): + def get_base_commitdate(self, buildid): """ - Get base commit's committer date of the specified completed build for - the specified project. Wait for the build to complete, if it hasn't - yet. + Get base commit's committer date of the specified completed build. + Wait for the build to complete, if it hasn't yet. Args: - jobname: Jenkins project name. buildid: Jenkins build ID. Return: The epoch timestamp string of the committer date. """ - return self.get_cfg_data(jobname, buildid, "skt.cmd_merge", - "commitdate") + return self.__get_cfg_data(buildid, "skt.cmd_merge", "commitdate") - def get_base_hash(self, jobname, buildid): + def get_base_hash(self, buildid): """ - Get base commit's hash of the specified completed build for the - specified project. Wait for the build to complete, if it hasn't yet. + Get base commit's hash of the specified completed build. + Wait for the build to complete, if it hasn't yet. Args: - jobname: Jenkins project name. buildid: Jenkins build ID. Return: The base commit's hash string. """ - return self.get_cfg_data(jobname, buildid, "skt.cmd_merge", - "basehead") + return self.__get_cfg_data(buildid, "skt.cmd_merge", "basehead") - # FIXME Clarify function name - def get_patchwork(self, jobname, buildid): + def get_patch_url_list(self, buildid): """ - Get the list of Patchwork patch URLs for the specified completed build - for the specified project. Wait for the build to complete, if it - hasn't yet. + Get the list of Patchwork patch URLs for the specified completed + build. Wait for the build to complete, if it hasn't yet. Args: - jobname: Jenkins project name. buildid: Jenkins build ID. Return: - The list of Patchwork patch URLs. + The list of Patchwork patch URLs, in the order the patches should + be applied in. + """ + return self.__get_cfg_data(buildid, "skt.cmd_merge", "pw") + + def __get_baseretcode(self, buildid): + return self.__get_cfg_data(buildid, "skt.cmd_run", "baseretcode", 0) + + def get_result_url(self, buildid): """ - return self.get_cfg_data(jobname, buildid, "skt.cmd_merge", - "pw") + Get the URL of the web representation of the specified build. - def get_baseretcode(self, jobname, buildid): - return self.get_cfg_data(jobname, buildid, "skt.cmd_run", - "baseretcode", 0) + Args: + buildid: Jenkins build ID. - def get_result_url(self, jobname, buildid): - return "%s/job/%s/%s" % (self.server.base_server_url(), jobname, + Result: + The URL of the build result. + """ + return "%s/job/%s/%s" % (self.server.base_server_url(), self.name, buildid) - def get_result(self, jobname, buildid): - build = self._wait_and_get_build(jobname, buildid) + def get_result(self, buildid): + """ + Get result code (sktm.misc.tresult) for the specified build. + Wait for the build to complete, if it hasn't yet. + + Args: + buildid: Jenkins build ID. + + Result: + The build result code (sktm.misc.tresult). + """ + build = self.__wait_and_get_build(buildid) bstatus = build.get_status() logging.info("build_status=%s", bstatus) if bstatus == "SUCCESS": - return sktm.tresult.SUCCESS + return sktm.misc.tresult.SUCCESS if not build.has_resultset(): raise Exception("No results for build %d (%s)" % @@ -147,11 +160,11 @@ def get_result(self, jobname, buildid): if bstatus == "UNSTABLE" and \ (build.get_resultset()["skt.cmd_run"].status in ["PASSED", "FIXED"]): - if self.get_baseretcode(jobname, buildid) != 0: + if self.__get_baseretcode(buildid) != 0: logging.warning("baseline failure found during patch testing") - return sktm.tresult.BASELINE_FAILURE + return sktm.misc.tresult.BASELINE_FAILURE - return sktm.tresult.SUCCESS + return sktm.misc.tresult.SUCCESS for (key, val) in build.get_resultset().iteritems(): if not key.startswith("skt."): @@ -160,36 +173,37 @@ def get_result(self, jobname, buildid): logging.debug("key=%s; value=%s", key, val.status) if val.status == "FAILED" or val.status == "REGRESSION": if key == "skt.cmd_merge": - return sktm.tresult.MERGE_FAILURE + return sktm.misc.tresult.MERGE_FAILURE elif key == "skt.cmd_build": - return sktm.tresult.BUILD_FAILURE + return sktm.misc.tresult.BUILD_FAILURE elif key == "skt.cmd_run": - return sktm.tresult.TEST_FAILURE + return sktm.misc.tresult.TEST_FAILURE logging.warning("Unknown status. marking as test failure") - return sktm.tresult.TEST_FAILURE + return sktm.misc.tresult.TEST_FAILURE # FIXME Clarify/fix argument names - def build(self, jobname, baserepo=None, ref=None, baseconfig=None, - message_id=None, subject=None, emails=set(), patchwork=[], + def build(self, baserepo=None, ref=None, baseconfig=None, + message_id=None, subject=None, emails=set(), patch_url_list=[], makeopts=None): """ Submit a build of a patchset. Args: - jobname: Name of the Jenkins project to build. - baserepo: Baseline Git repo URL. - ref: Baseline Git reference to test. - baseconfig: Kernel configuration URL. - message_id: Value of the "Message-Id" header of the e-mail - message representing the patchset, or None if unknown. - subject: Subject of the message representing the patchset, or - None if unknown. - emails: Set of e-mail addresses involved with the patchset to - send notifications to. - patchwork: List of URLs pointing to patches to apply. - makeopts: String of extra arguments to pass to the build's make - invocation. + baserepo: Baseline Git repo URL. + ref: Baseline Git reference to test. + baseconfig: Kernel configuration URL. + message_id: Value of the "Message-Id" header of the e-mail + message representing the patchset, or None if + unknown. + subject: Subject of the message representing the patchset, + or None if unknown. + emails: Set of e-mail addresses involved with the patchset + to send notifications to. + patch_url_list: List of URLs pointing to patches to apply, in the + order they should be applied in. + makeopts: String of extra arguments to pass to the build's + make invocation. Returns: Submitted build number. @@ -213,27 +227,36 @@ def build(self, jobname, baserepo=None, ref=None, baseconfig=None, if emails: params["emails"] = ",".join(emails) - if patchwork: - params["patchwork"] = " ".join(patchwork) + if patch_url_list: + params["patchwork"] = " ".join(patch_url_list) if makeopts is not None: params["makeopts"] = makeopts logging.debug(params) - job = self.server.get_job(jobname) - expected_id = self.server.get_job(jobname).get_next_build_number() - self.server.build_job(jobname, params) - build = self.find_build(jobname, params, expected_id) + job = self.server.get_job(self.name) + expected_id = self.server.get_job(self.name).get_next_build_number() + self.server.build_job(self.name, params) + build = self.__find_build(params, expected_id) logging.info("submitted build: %s", build) return build.get_number() - def is_build_complete(self, jobname, buildid): - job = self.server.get_job(jobname) + def is_build_complete(self, buildid): + """ + Check if a project build is complete. + + Args: + buildid: Jenkins build ID to get the status of. + + Return: + True if the build is complete. + """ + job = self.server.get_job(self.name) build = job.get_build(buildid) return not build.is_running() - def _params_eq(self, build, params): + def __params_eq(self, build, params): try: build_params = build.get_actions()["parameters"] except (AttributeError, KeyError): @@ -246,8 +269,8 @@ def _params_eq(self, build, params): return True - def find_build(self, jobname, params, eid=None): - job = self.server.get_job(jobname) + def __find_build(self, params, eid=None): + job = self.server.get_job(self.name) lbuild = None while not lbuild: @@ -260,12 +283,12 @@ def find_build(self, jobname, params, eid=None): while lbuild.get_number() < eid: time.sleep(1) lbuild = job.get_last_build() - if self._params_eq(lbuild, params): + if self.__params_eq(lbuild, params): return lbuild # slowpath for bid in job.get_build_ids(): build = job.get_build(bid) - if self._params_eq(build, params): + if self.__params_eq(build, params): return build return None diff --git a/sktm/misc.py b/sktm/misc.py new file mode 100644 index 0000000..c4fc7fd --- /dev/null +++ b/sktm/misc.py @@ -0,0 +1,25 @@ +# Copyright (c) 2017-2018 Red Hat, Inc. All rights reserved. This copyrighted +# material is made available to anyone wishing to use, modify, copy, or +# redistribute it subject to the terms and conditions of the GNU General +# Public License v.2 or later. +# +# 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. + +import enum + + +class tresult(enum.IntEnum): + """Test result""" + SUCCESS = 0 + MERGE_FAILURE = 1 + BUILD_FAILURE = 2 + PUBLISH_FAILURE = 3 + TEST_FAILURE = 4 + BASELINE_FAILURE = 5 diff --git a/sktm/patchwork.py b/sktm/patchwork.py index b1a731f..39dcc31 100644 --- a/sktm/patchwork.py +++ b/sktm/patchwork.py @@ -24,7 +24,7 @@ import re import urllib import xmlrpclib -import sktm +import sktm.misc class PatchsetSummary(object): @@ -435,12 +435,12 @@ def _set_patch_check(self, patch, payload): def set_patch_check(self, pid, jurl, result): """ Add a patch "check" for the specified patch, with the specified - Jenkins build URL and result (sktm.tresult). + Jenkins build URL and result (sktm.misc.tresult). Args: pid: The ID of the patch to add the "check" for. jurl: Jenkins build URL for the "check" to reference. - result: Test result (sktm.tresult) to feature in the "check" + result: Test result (sktm.misc.tresult) to feature in the "check" state. """ if self.apikey is None: @@ -452,9 +452,9 @@ def set_patch_check(self, pid, jurl, result): 'target_url': jurl, 'context': 'skt', 'description': 'skt boot test'} - if result == sktm.tresult.SUCCESS: + if result == sktm.misc.tresult.SUCCESS: payload['state'] = int(pwresult.SUCCESS) - elif result == sktm.tresult.BASELINE_FAILURE: + elif result == sktm.misc.tresult.BASELINE_FAILURE: payload['state'] = int(pwresult.WARNING) payload['description'] = 'Baseline failure found while testing ' 'this patch' @@ -844,12 +844,12 @@ def get_emails(self, pid): def set_patch_check(self, pid, jurl, result): """ Add a patch "check" for the specified patch, with the specified - Jenkins build URL and result (sktm.tresult). + Jenkins build URL and result (sktm.misc.tresult). Args: pid: The ID of the patch to add the "check" for. jurl: Jenkins build URL for the "check" to reference. - result: Test result (sktm.tresult) to feature in the "check" + result: Test result (sktm.misc.tresult) to feature in the "check" state. """ # TODO: Implement this for xmlrpc diff --git a/sktm/scheduler.py b/sktm/scheduler.py new file mode 100644 index 0000000..d6d4e38 --- /dev/null +++ b/sktm/scheduler.py @@ -0,0 +1,287 @@ +# Copyright (c) 2017-2018 Red Hat, Inc. All rights reserved. This copyrighted +# material is made available to anyone wishing to use, modify, copy, or +# redistribute it subject to the terms and conditions of the GNU General +# Public License v.2 or later. +# +# 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. + + +class DirectRun(object): + """Direct test run""" + + def __init__(self, work_dir, work_url, skt_work_dir, + baserepo, baseref, baseconfig, + message_id, subject, emails, + patch_url_list, makeopts) + """ + Initialize a direct test run. + + Args: + dir: Directory to run in. + url: URL through which the directory is shared. + skt_dir: Skt work directory to use. + baserepo: Baseline Git repo URL. + baseref: Baseline Git reference. + baseconfig: Kernel configuration URL. + message_id: Value of the "Message-Id" header of the e-mail + message representing the patchset, or None if + unknown. + subject: Subject of the message representing the patchset, + or None if unknown. + emails: Set of e-mail addresses involved with the patchset + to send notifications to. + patch_url_list: List of URLs pointing to patches to apply, in the + order they should be applied in. + makeopts: String of extra arguments to pass to the build's + make invocation. + + """ + self.dir = dir + self.url = url + self.skt_dir = skt_dir + self.baserepo = baserepo + self.baseref = baseref + self.baseconfig = baseconfig + self.message_id = message_id + self.subject = subject + self.emails = emails + self.patch_url_list = patch_url_list + self.makeopts = makeopts + + def start(): + """ + Start the run. + """ + # Create directory for JUnit results + self.dir_junit = os.path.join(self.dir, "junit") + os.mkdir(self.dir_junit) + # Create directory for build artifacts + self.dir_build = os.path.join(self.dir, "build") + os.mkdir(self.dir_build) + # Format URL for build artifacts + self.url_build = + urlparse.urljoin(self.url, os.path.basename(self.dir_build)) + # Start skt + self.started = True + + def is_started(): + """ + Check if the run is started. + + Returns: + True if the run is started, false otherwise. + """ + + def abort(): + """ + Abort a run: stop the run, if started, and mark it aborted. + """ + def is_finished(): + """ + Check if the run is finished. + + Returns: + True if the run is finished, false otherwise. + """ + + +class Direct(object): + """Direct test run scheduler""" + + def __init__(self, skt_dir, public_hostname, port, + beaker_job_template, parallel_run_num): + """ + Initialize a direct test run scheduler. + """ + # Scheduler data directory + self.dir = None + # HTTP server PID + self.httpd_pid = None + # Scheduler public data URL (as shared by the HTTP server) + self.url = None + # Skt work directory to use + self.skt_dir = skt_dir + # Hostname the published artifacts should be accessible at + self.public_hostname = public_hostname + # Port the published artifacts should be accessible at + self.port = port + # Path to the Beaker job XML template to supply to skt + self.beaker_job_template = beaker_job_template + # Maximum number of parallel runs + self.parallel_run_num = parallel_run_num + # Next run ID + self.run_id = 0 + # Run ID -> object map + self.run_map = {} + + # Create the scheduler directory + self.dir = tempfile.mkdtemp(suffix="sktm_runs_") + # Start an HTTP server for serving artifacts + self.httpd_pid = os.fork() + if self.httpd_pid == 0: + os.chdir(self.dir) + httpd = BaseHTTPServer.HTTPServer( + ("", port), + SimpleHTTPServer.SimpleHTTPRequestHandler) + # TODO Print port number we're listening on, e.g. + # print(httpd.socket.getsockname()[1]) + httpd.serve_forever() + os._exit(0) + # TODO Read and update the port in case it was zero + # TODO That will also ensure the server is functional + # Format our public URL + self.url = "http://" + self.public_hostname + ":" + self.port + + def __del__(): + """ + Cleanup a direct test run scheduler. + """ + if self.httpd_pid: + os.kill(self.httpd_pid) + if self.dir: + shutil.rmtree(self.dir) + + def get_base_commitdate(self, run_id): + """ + Get base commit's committer date of the specified completed run. + Wait for the run to complete, if it hasn't yet. + + Args: + run_id: Run ID. + + Return: + The epoch timestamp string of the committer date. + """ + + def get_base_hash(self, run_id): + """ + Get base commit's hash of the specified completed run. + Wait for the run to complete, if it hasn't yet. + + Args: + run_id: Run ID. + + Return: + The base commit's hash string. + """ + + def get_patch_url_list(self, run_id): + """ + Get the list of Patchwork patch URLs for the specified completed + run. Wait for the run to complete, if it hasn't yet. + + Args: + run_id: Run ID. + + Return: + The list of Patchwork patch URLs, in the order the patches should + be applied in. + """ + + def get_result_url(self, run_id): + """ + Get the URL of the web representation of the specified run. + + Args: + run_id: Run ID. + + Result: + The URL of the run result. + """ + + def get_result(self, run_id): + """ + Get result code (sktm.misc.tresult) for the specified run. + Wait for the run to complete, if it hasn't yet. + + Args: + run_id: Run ID. + + Result: + The run result code (sktm.misc.tresult). + """ + + def __update(): + """ + Update run status. + """ + # Start with the maximum allowed run number + parallel_run_num = self.parallel_run_num + # Subtract runs in progress + for run_id, run in self.run_map.items(): + if run.is_active(): + parallel_run_num -= 1 + # Fill up with new jobs + while parallel_run_num > 0: + for run_id, run in self.run_map.items(): + if not run.is_complete(): + run.start() + + def submit(self, baserepo=None, baseref=None, baseconfig=None, + message_id=None, subject=None, emails=set(), patch_url_list=[], + makeopts=None): + """ + Submit a run. + + Args: + baserepo: Baseline Git repo URL. + baseref: Baseline Git reference to test. + baseconfig: Kernel configuration URL. + message_id: Value of the "Message-Id" header of the e-mail + message representing the patchset, or None if + unknown. + subject: Subject of the message representing the patchset, + or None if unknown. + emails: Set of e-mail addresses involved with the patchset + to send notifications to. + patch_url_list: List of URLs pointing to patches to apply, in the + order they should be applied in. + makeopts: String of extra arguments to pass to the build's + make invocation. + + Returns: + Submitted run number. + """ + # Grab next run ID + run_id = self.run_id + # Create a run directory + run_dir = os.path.join(self.dir, run_id) + os.mkdir(run_dir) + # Format a run URL + run_url = urlparse.urljoin(self.url, run_id) + # Create a run + run = DirectRun(dir=run_dir, + url=run_url, + skt_dir=self.skt_dir, + baserepo=baserepo, + baseref=baseref, + baseconfig=baseconfig, + message_id=message_id, + subject=subject, + emails=emails, + patch_url_list=patch_url_list, + makeopts=makeopts) + self.job_map[run_id] = run + # Submitted, move onto next ID + self.run_id += 1 + # Update run status. + self.__update() + return run_id + + def is_run_complete(self, run_id): + """ + Check if a run is complete. + + Args: + run_id: Run ID. + + Return: + True if the run is complete. + """