You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
536 lines
18 KiB
536 lines
18 KiB
6 years ago
|
#!/usr/bin/python3
|
||
|
# -*- coding: utf-8 -*-
|
||
|
|
||
|
# Copyright (C) 2011, 2012 Canonical Ltd.
|
||
|
# Author: Stéphane Graber <stgraber@ubuntu.com>
|
||
|
|
||
|
# This library is free software; you can redistribute it and/or
|
||
|
# modify it under the terms of the GNU Lesser General Public
|
||
|
# License as published by the Free Software Foundation; either
|
||
|
# version 2.1 of the License, or (at your option) any later version.
|
||
|
|
||
|
# This library 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
|
||
|
# Lesser General Public License for more details.
|
||
|
|
||
|
# You should have received a copy of the GNU Lesser General Public
|
||
|
# License along with this library; if not, write to the Free Software
|
||
|
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301
|
||
|
# USA
|
||
|
|
||
|
try:
|
||
|
import xmlrpc.client as xmlrpclib
|
||
|
except ImportError:
|
||
|
import xmlrpclib
|
||
|
|
||
|
import base64
|
||
|
from datetime import datetime
|
||
|
|
||
|
# Taken from qatracker/qatracker.modules (PHP code)
|
||
|
# cat qatracker.module | grep " = array" | sed -e 's/^\$//g' \
|
||
|
# -e 's/array(/[/g' -e 's/);/]/g' -e "s/t('/\"/g" -e "s/')/\"/g"
|
||
|
### AUTO-GENERATED ->
|
||
|
qatracker_build_milestone_status = ["Active", "Re-building", "Disabled",
|
||
|
"Superseded", "Ready"]
|
||
|
qatracker_milestone_notify = ["No", "Yes"]
|
||
|
qatracker_milestone_autofill = ["No", "Yes"]
|
||
|
qatracker_milestone_status = ["Testing", "Released", "Archived"]
|
||
|
qatracker_milestone_series_status = ["Active", "Disabled"]
|
||
|
qatracker_milestone_series_manifest_status = ["Active", "Disabled"]
|
||
|
qatracker_product_status = ["Active", "Disabled"]
|
||
|
qatracker_product_type = ["iso", "package", "hardware"]
|
||
|
qatracker_product_download_type = ["HTTP", "RSYNC", "ZSYNC",
|
||
|
"GPG signature", "MD5 checksum", "Comment",
|
||
|
"Torrent"]
|
||
|
qatracker_testsuite_testcase_status = ["Mandatory", "Disabled", "Run-once",
|
||
|
"Optional"]
|
||
|
qatracker_result_result = ["Failed", "Passed", "In progress"]
|
||
|
qatracker_result_status = ["Active", "Disabled"]
|
||
|
qatracker_rebuild_status = ["Requested", "Queued", "Building", "Built",
|
||
|
"Published", "Canceled"]
|
||
|
### <- AUTO-GENERATED
|
||
|
|
||
|
|
||
|
class QATrackerRPCObject():
|
||
|
"""Base class for objects received over XML-RPC"""
|
||
|
|
||
|
CONVERT_BOOL = []
|
||
|
CONVERT_DATE = []
|
||
|
CONVERT_INT = []
|
||
|
|
||
|
def __init__(self, tracker, rpc_dict):
|
||
|
# Convert the dict we get from the API into an object
|
||
|
|
||
|
for key in rpc_dict:
|
||
|
if key in self.CONVERT_INT:
|
||
|
try:
|
||
|
setattr(self, key, int(rpc_dict[key]))
|
||
|
except ValueError:
|
||
|
setattr(self, key, None)
|
||
|
elif key in self.CONVERT_BOOL:
|
||
|
setattr(self, key, rpc_dict[key] == "true")
|
||
|
elif key in self.CONVERT_DATE:
|
||
|
try:
|
||
|
setattr(self, key, datetime.strptime(rpc_dict[key],
|
||
|
'%Y-%m-%d %H:%M:%S'))
|
||
|
except ValueError:
|
||
|
setattr(self, key, None)
|
||
|
else:
|
||
|
setattr(self, key, str(rpc_dict[key]))
|
||
|
|
||
|
self.tracker = tracker
|
||
|
|
||
|
def __repr__(self):
|
||
|
return "%s: %s" % (self.__class__.__name__, self.title)
|
||
|
|
||
|
|
||
|
class QATrackerBug(QATrackerRPCObject):
|
||
|
"""A bug entry"""
|
||
|
|
||
|
CONVERT_INT = ['bugnumber', 'count']
|
||
|
CONVERT_DATE = ['earliest_report', 'latest_report']
|
||
|
|
||
|
def __repr__(self):
|
||
|
return "%s: %s" % (self.__class__.__name__, self.bugnumber)
|
||
|
|
||
|
|
||
|
class QATrackerBuild(QATrackerRPCObject):
|
||
|
"""A build entry"""
|
||
|
|
||
|
CONVERT_INT = ['id', 'productid', 'userid', 'status']
|
||
|
CONVERT_DATE = ['date']
|
||
|
|
||
|
def __repr__(self):
|
||
|
return "%s: %s" % (self.__class__.__name__, self.id)
|
||
|
|
||
|
def add_result(self, testcase, result, comment='', hardware='', bugs={}):
|
||
|
"""Add a result to the build"""
|
||
|
|
||
|
if (self.tracker.access not in ("user", "admin") and
|
||
|
self.tracker.access is not None):
|
||
|
raise Exception("Access denied, you need 'user' but are '%s'" %
|
||
|
self.tracker.access)
|
||
|
|
||
|
build_testcase = None
|
||
|
|
||
|
# FIXME: Supporting 'str' containing the testcase name would be nice
|
||
|
if isinstance(testcase, QATrackerTestcase):
|
||
|
build_testcase = testcase.id
|
||
|
elif isinstance(testcase, int):
|
||
|
build_testcase = testcase
|
||
|
|
||
|
if not build_testcase:
|
||
|
raise IndexError("Couldn't find testcase: %s" % (testcase,))
|
||
|
|
||
|
if isinstance(result, list):
|
||
|
raise TypeError("result must be a string or an integer")
|
||
|
|
||
|
build_result = self.tracker._get_valid_id_list(qatracker_result_result,
|
||
|
result)
|
||
|
|
||
|
if not isinstance(bugs, dict):
|
||
|
raise TypeError("bugs must be a dict")
|
||
|
|
||
|
for bug in bugs:
|
||
|
if not isinstance(bug, int) or bug <= 0:
|
||
|
raise ValueError("A bugnumber must be a number >= 0")
|
||
|
|
||
|
if not isinstance(bugs[bug], int) or bugs[bug] not in (0, 1):
|
||
|
raise ValueError("A bugimportance must be in (0,1)")
|
||
|
|
||
|
resultid = int(self.tracker.tracker.results.add(self.id,
|
||
|
build_testcase,
|
||
|
build_result[0],
|
||
|
str(comment),
|
||
|
str(hardware),
|
||
|
bugs))
|
||
|
if resultid == -1:
|
||
|
raise Exception("Couldn't post your result.")
|
||
|
|
||
|
new_result = None
|
||
|
for entry in self.get_results(build_testcase, 0):
|
||
|
if entry.id == resultid:
|
||
|
new_result = entry
|
||
|
break
|
||
|
|
||
|
return new_result
|
||
|
|
||
|
def get_results(self, testcase, status=qatracker_result_status):
|
||
|
"""Get a list of results for the given build and testcase"""
|
||
|
|
||
|
build_testcase = None
|
||
|
|
||
|
# FIXME: Supporting 'str' containing the testcase name would be nice
|
||
|
if isinstance(testcase, QATrackerTestcase):
|
||
|
build_testcase = testcase.id
|
||
|
elif isinstance(testcase, int):
|
||
|
build_testcase = testcase
|
||
|
|
||
|
if not build_testcase:
|
||
|
raise IndexError("Couldn't find testcase: %s" % (testcase,))
|
||
|
|
||
|
record_filter = self.tracker._get_valid_id_list(
|
||
|
qatracker_result_status,
|
||
|
status)
|
||
|
|
||
|
if len(record_filter) == 0:
|
||
|
return []
|
||
|
|
||
|
results = []
|
||
|
for entry in self.tracker.tracker.results.get_list(
|
||
|
self.id, build_testcase, list(record_filter)):
|
||
|
results.append(QATrackerResult(self.tracker, entry))
|
||
|
|
||
|
return results
|
||
|
|
||
|
|
||
|
class QATrackerMilestone(QATrackerRPCObject):
|
||
|
"""A milestone entry"""
|
||
|
|
||
|
CONVERT_INT = ['id', 'status', 'series']
|
||
|
CONVERT_BOOL = ['notify']
|
||
|
|
||
|
def get_bugs(self):
|
||
|
"""Returns a list of all bugs linked to this milestone"""
|
||
|
|
||
|
bugs = []
|
||
|
for entry in self.tracker.tracker.bugs.get_list(self.id):
|
||
|
bugs.append(QATrackerBug(self.tracker, entry))
|
||
|
|
||
|
return bugs
|
||
|
|
||
|
def add_build(self, product, version, note="", notify=True):
|
||
|
"""Add a build to the milestone"""
|
||
|
|
||
|
if self.status != 0:
|
||
|
raise TypeError("Only active milestones are accepted")
|
||
|
|
||
|
if self.tracker.access != "admin" and self.tracker.access is not None:
|
||
|
raise Exception("Access denied, you need 'admin' but are '%s'" %
|
||
|
self.tracker.access)
|
||
|
|
||
|
if not isinstance(notify, bool):
|
||
|
raise TypeError("notify must be a boolean")
|
||
|
|
||
|
build_product = None
|
||
|
|
||
|
if isinstance(product, QATrackerProduct):
|
||
|
build_product = product
|
||
|
else:
|
||
|
valid_products = self.tracker.get_products(0)
|
||
|
|
||
|
for entry in valid_products:
|
||
|
if (entry.title.lower() == str(product).lower() or
|
||
|
entry.id == product):
|
||
|
build_product = entry
|
||
|
break
|
||
|
|
||
|
if not build_product:
|
||
|
raise IndexError("Couldn't find product: %s" % product)
|
||
|
|
||
|
if build_product.status != 0:
|
||
|
raise TypeError("Only active products are accepted")
|
||
|
|
||
|
self.tracker.tracker.builds.add(build_product.id, self.id,
|
||
|
str(version), str(note), notify)
|
||
|
|
||
|
new_build = None
|
||
|
for entry in self.get_builds(0):
|
||
|
if (entry.productid == build_product.id
|
||
|
and entry.version == str(version)):
|
||
|
new_build = entry
|
||
|
break
|
||
|
|
||
|
return new_build
|
||
|
|
||
|
def get_builds(self, status=qatracker_build_milestone_status):
|
||
|
"""Get a list of builds for the milestone"""
|
||
|
|
||
|
record_filter = self.tracker._get_valid_id_list(
|
||
|
qatracker_build_milestone_status, status)
|
||
|
|
||
|
if len(record_filter) == 0:
|
||
|
return []
|
||
|
|
||
|
builds = []
|
||
|
for entry in self.tracker.tracker.builds.get_list(self.id,
|
||
|
list(record_filter)):
|
||
|
builds.append(QATrackerBuild(self.tracker, entry))
|
||
|
|
||
|
return builds
|
||
|
|
||
|
|
||
|
class QATrackerProduct(QATrackerRPCObject):
|
||
|
CONVERT_INT = ['id', 'type', 'status']
|
||
|
|
||
|
def get_testcases(self, series,
|
||
|
status=qatracker_testsuite_testcase_status):
|
||
|
"""Get a list of testcases associated with the product"""
|
||
|
|
||
|
record_filter = self.tracker._get_valid_id_list(
|
||
|
qatracker_testsuite_testcase_status, status)
|
||
|
|
||
|
if len(record_filter) == 0:
|
||
|
return []
|
||
|
|
||
|
if isinstance(series, QATrackerMilestone):
|
||
|
seriesid = series.series
|
||
|
elif isinstance(series, int):
|
||
|
seriesid = series
|
||
|
else:
|
||
|
raise TypeError("series needs to be a valid QATrackerMilestone"
|
||
|
" instance or an integer")
|
||
|
|
||
|
testcases = []
|
||
|
for entry in self.tracker.tracker.testcases.get_list(
|
||
|
self.id, seriesid, list(record_filter)):
|
||
|
testcases.append(QATrackerTestcase(self.tracker, entry))
|
||
|
|
||
|
return testcases
|
||
|
|
||
|
|
||
|
class QATrackerRebuild(QATrackerRPCObject):
|
||
|
CONVERT_INT = ['id', 'seriesid', 'productid', 'milestoneid', 'requestedby',
|
||
|
'changedby', 'status']
|
||
|
CONVERT_DATE = ['requestedat', 'changedat']
|
||
|
|
||
|
def __repr__(self):
|
||
|
return "%s: %s" % (self.__class__.__name__, self.id)
|
||
|
|
||
|
def save(self):
|
||
|
"""Save any change that happened on this entry.
|
||
|
NOTE: At the moment only supports the status field."""
|
||
|
|
||
|
if (self.tracker.access != "admin" and
|
||
|
self.tracker.access is not None):
|
||
|
raise Exception("Access denied, you need 'admin' but are '%s'" %
|
||
|
self.tracker.access)
|
||
|
|
||
|
retval = self.tracker.tracker.rebuilds.update_status(self.id,
|
||
|
self.status)
|
||
|
if retval is not True:
|
||
|
raise Exception("Failed to update rebuild")
|
||
|
|
||
|
return retval
|
||
|
|
||
|
|
||
|
class QATrackerResult(QATrackerRPCObject):
|
||
|
CONVERT_INT = ['id', 'reporterid', 'revisionid', 'result', 'changedby',
|
||
|
'status']
|
||
|
CONVERT_DATE = ['date', 'lastchange']
|
||
|
__deleted = False
|
||
|
|
||
|
def __repr__(self):
|
||
|
return "%s: %s" % (self.__class__.__name__, self.id)
|
||
|
|
||
|
def delete(self):
|
||
|
"""Remove the result from the tracker"""
|
||
|
|
||
|
if (self.tracker.access not in ("user", "admin") and
|
||
|
self.tracker.access is not None):
|
||
|
raise Exception("Access denied, you need 'user' but are '%s'" %
|
||
|
self.tracker.access)
|
||
|
|
||
|
if self.__deleted:
|
||
|
raise IndexError("Result has already been removed")
|
||
|
|
||
|
retval = self.tracker.tracker.results.delete(self.id)
|
||
|
if retval is not True:
|
||
|
raise Exception("Failed to remove result")
|
||
|
|
||
|
self.status = 1
|
||
|
self.__deleted = True
|
||
|
|
||
|
def save(self):
|
||
|
"""Save any change that happened on this entry"""
|
||
|
|
||
|
if (self.tracker.access not in ("user", "admin") and
|
||
|
self.tracker.access is not None):
|
||
|
raise Exception("Access denied, you need 'user' but are '%s'" %
|
||
|
self.tracker.access)
|
||
|
|
||
|
if self.__deleted:
|
||
|
raise IndexError("Result no longer exists")
|
||
|
|
||
|
retval = self.tracker.tracker.results.update(self.id, self.result,
|
||
|
self.comment,
|
||
|
self.hardware,
|
||
|
self.bugs)
|
||
|
if retval is not True:
|
||
|
raise Exception("Failed to update result")
|
||
|
|
||
|
return retval
|
||
|
|
||
|
|
||
|
class QATrackerSeries(QATrackerRPCObject):
|
||
|
CONVERT_INT = ['id', 'status']
|
||
|
|
||
|
def get_manifest(self, status=qatracker_milestone_series_manifest_status):
|
||
|
"""Get a list of products in the series' manifest"""
|
||
|
|
||
|
record_filter = self.tracker._get_valid_id_list(
|
||
|
qatracker_milestone_series_manifest_status, status)
|
||
|
|
||
|
if len(record_filter) == 0:
|
||
|
return []
|
||
|
|
||
|
manifest_entries = []
|
||
|
for entry in self.tracker.tracker.series.get_manifest(
|
||
|
self.id, list(record_filter)):
|
||
|
manifest_entries.append(QATrackerSeriesManifest(
|
||
|
self.tracker, entry))
|
||
|
|
||
|
return manifest_entries
|
||
|
|
||
|
|
||
|
class QATrackerSeriesManifest(QATrackerRPCObject):
|
||
|
CONVERT_INT = ['id', 'productid', 'status']
|
||
|
|
||
|
def __repr__(self):
|
||
|
return "%s: %s" % (self.__class__.__name__, self.product_title)
|
||
|
|
||
|
|
||
|
class QATrackerTestcase(QATrackerRPCObject):
|
||
|
CONVERT_INT = ['id', 'status', 'weight', 'suite']
|
||
|
|
||
|
|
||
|
class QATracker():
|
||
|
def __init__(self, url, username=None, password=None):
|
||
|
class AuthTransport(xmlrpclib.Transport):
|
||
|
def set_auth(self, auth):
|
||
|
self.auth = auth
|
||
|
|
||
|
def get_host_info(self, host):
|
||
|
host, extra_headers, x509 = \
|
||
|
xmlrpclib.Transport.get_host_info(self, host)
|
||
|
if extra_headers is None:
|
||
|
extra_headers = []
|
||
|
extra_headers.append(('Authorization', 'Basic %s' % auth))
|
||
|
return host, extra_headers, x509
|
||
|
|
||
|
if username and password:
|
||
|
try:
|
||
|
auth = str(base64.b64encode(
|
||
|
bytes('%s:%s' % (username, password), 'utf-8')),
|
||
|
'utf-8')
|
||
|
except TypeError:
|
||
|
auth = base64.b64encode('%s:%s' % (username, password))
|
||
|
|
||
|
transport = AuthTransport()
|
||
|
transport.set_auth(auth)
|
||
|
drupal = xmlrpclib.ServerProxy(url, transport=transport)
|
||
|
else:
|
||
|
drupal = xmlrpclib.ServerProxy(url)
|
||
|
|
||
|
# Call listMethods() so if something is wrong we know it immediately
|
||
|
drupal.system.listMethods()
|
||
|
|
||
|
# Get our current access
|
||
|
self.access = drupal.qatracker.get_access()
|
||
|
|
||
|
self.tracker = drupal.qatracker
|
||
|
|
||
|
def _get_valid_id_list(self, status_list, status):
|
||
|
""" Get a list of valid keys and a list or just a single
|
||
|
entry of input to check against the list of valid keys.
|
||
|
The function looks for valid indexes and content, doing
|
||
|
case insensitive checking for strings and returns a list
|
||
|
of indexes for the list of valid keys. """
|
||
|
|
||
|
def process(status_list, status):
|
||
|
valid_status = [entry.lower() for entry in status_list]
|
||
|
|
||
|
if isinstance(status, int):
|
||
|
if status < 0 or status >= len(valid_status):
|
||
|
raise IndexError("Invalid status: %s" % status)
|
||
|
return int(status)
|
||
|
|
||
|
if isinstance(status, str):
|
||
|
status = status.lower()
|
||
|
if status not in valid_status:
|
||
|
raise IndexError("Invalid status: %s" % status)
|
||
|
return valid_status.index(status)
|
||
|
|
||
|
raise TypeError("Invalid status type: %s (expected str or int)" %
|
||
|
type(status))
|
||
|
|
||
|
record_filter = set()
|
||
|
|
||
|
if isinstance(status, list):
|
||
|
for entry in status:
|
||
|
record_filter.add(process(status_list, entry))
|
||
|
else:
|
||
|
record_filter.add(process(status_list, status))
|
||
|
|
||
|
return list(record_filter)
|
||
|
|
||
|
def get_bugs(self):
|
||
|
"""Get a list of all bugs reported on the site"""
|
||
|
|
||
|
bugs = []
|
||
|
for entry in self.tracker.bugs.get_list(0):
|
||
|
bugs.append(QATrackerBug(self, entry))
|
||
|
|
||
|
return bugs
|
||
|
|
||
|
def get_milestones(self, status=qatracker_milestone_status):
|
||
|
"""Get a list of all milestones"""
|
||
|
|
||
|
record_filter = self._get_valid_id_list(qatracker_milestone_status,
|
||
|
status)
|
||
|
|
||
|
if len(record_filter) == 0:
|
||
|
return []
|
||
|
|
||
|
milestones = []
|
||
|
for entry in self.tracker.milestones.get_list(list(record_filter)):
|
||
|
milestones.append(QATrackerMilestone(self, entry))
|
||
|
|
||
|
return milestones
|
||
|
|
||
|
def get_products(self, status=qatracker_product_status):
|
||
|
"""Get a list of all products"""
|
||
|
|
||
|
record_filter = self._get_valid_id_list(qatracker_product_status,
|
||
|
status)
|
||
|
|
||
|
if len(record_filter) == 0:
|
||
|
return []
|
||
|
|
||
|
products = []
|
||
|
for entry in self.tracker.products.get_list(list(record_filter)):
|
||
|
products.append(QATrackerProduct(self, entry))
|
||
|
|
||
|
return products
|
||
|
|
||
|
def get_rebuilds(self, status=qatracker_rebuild_status):
|
||
|
"""Get a list of all rebuilds"""
|
||
|
|
||
|
record_filter = self._get_valid_id_list(
|
||
|
qatracker_rebuild_status, status)
|
||
|
|
||
|
if len(record_filter) == 0:
|
||
|
return []
|
||
|
|
||
|
rebuilds = []
|
||
|
for entry in self.tracker.rebuilds.get_list(list(record_filter)):
|
||
|
rebuilds.append(QATrackerRebuild(self, entry))
|
||
|
|
||
|
return rebuilds
|
||
|
|
||
|
def get_series(self, status=qatracker_milestone_series_status):
|
||
|
"""Get a list of all series"""
|
||
|
|
||
|
record_filter = self._get_valid_id_list(
|
||
|
qatracker_milestone_series_status, status)
|
||
|
|
||
|
if len(record_filter) == 0:
|
||
|
return []
|
||
|
|
||
|
series = []
|
||
|
for entry in self.tracker.series.get_list(list(record_filter)):
|
||
|
series.append(QATrackerSeries(self, entry))
|
||
|
|
||
|
return series
|