fuel-qa/fuelweb_test/testrail/report.py
Artem Panchenko d5002204fc Search target Test Runs across all available Plans
Previously TESTRAIL_TESTS_DEPTH setting was applied while
getting a list of latest test plans created for given
milestone.
Now it's applied directly for test runs count with
provided test suite and configuration, so test plans which
do not contain such runs are just skipped. This improvement
is needed, because currently we create a lot of test plans
w/o SWARM run, so bugs links copying mechanism doesn't work
as expected.

Change-Id: Iac76a3461c44ca113ce334a397f9628d7cf6863a
(cherry picked from commit 4545efcc2b)
2016-04-21 09:06:11 +00:00

616 lines
25 KiB
Python
Executable File

#!/usr/bin/env python
#
# Copyright 2015 Mirantis, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from __future__ import division
from __future__ import unicode_literals
import functools
import re
import time
from logging import DEBUG
from optparse import OptionParser
from fuelweb_test.testrail.builds import Build
from fuelweb_test.testrail.builds import get_build_artifact
from fuelweb_test.testrail.builds import get_downstream_builds_from_html
from fuelweb_test.testrail.builds import get_jobs_for_view
from fuelweb_test.testrail.launchpad_client import LaunchpadBug
from fuelweb_test.testrail.settings import JENKINS
from fuelweb_test.testrail.settings import GROUPS_TO_EXPAND
from fuelweb_test.testrail.settings import LaunchpadSettings
from fuelweb_test.testrail.settings import logger
from fuelweb_test.testrail.settings import TestRailSettings
from fuelweb_test.testrail.testrail_client import TestRailProject
class TestResult(object):
"""TestResult.""" # TODO documentation
def __init__(self, name, group, status, duration, url=None,
version=None, description=None, comments=None,
launchpad_bug=None, steps=None):
self.name = name
self.group = group
self._status = status
self.duration = duration
self.url = url
self._version = version
self.description = description
self.comments = comments
self.launchpad_bug = launchpad_bug
self.available_statuses = {
'passed': ['passed', 'fixed'],
'failed': ['failed', 'regression'],
'skipped': ['skipped'],
'blocked': ['blocked'],
'custom_status2': ['in_progress']
}
self._steps = steps
@property
def version(self):
# Version string length is limited by 250 symbols because field in
# TestRail has type 'String'. This limitation can be removed by
# changing field type to 'Text'
return (self._version or '')[:250]
@version.setter
def version(self, value):
self._version = value[:250]
@property
def status(self):
for s in self.available_statuses.keys():
if self._status in self.available_statuses[s]:
return s
logger.error('Unsupported result status: "{0}"!'.format(self._status))
return self._status
@status.setter
def status(self, value):
self._status = value
@property
def steps(self):
return self._steps
def __str__(self):
result_dict = {
'name': self.name,
'group': self.group,
'status': self.status,
'duration': self.duration,
'url': self.url,
'version': self.version,
'description': self.description,
'comments': self.comments
}
return str(result_dict)
def retry(count=3):
def wrapped(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
i = 0
while True:
try:
return func(*args, **kwargs)
except:
i += 1
if i >= count:
raise
return wrapper
return wrapped
def get_downstream_builds(jenkins_build_data, status=None):
if 'subBuilds' not in jenkins_build_data.keys():
return get_downstream_builds_from_html(jenkins_build_data['url'])
return [{'name': b['jobName'], 'number': b['buildNumber'],
'result': b['result']} for b in jenkins_build_data['subBuilds']]
def get_version(jenkins_build_data):
version = get_version_from_parameters(jenkins_build_data)
if not version:
version = get_version_from_artifacts(jenkins_build_data)
if not version:
version = get_version_from_upstream_job(jenkins_build_data)
if not version:
raise Exception('Failed to get iso version from Jenkins jobs '
'parameters/artifacts!')
return version
def get_version_from_upstream_job(jenkins_build_data):
upstream_job = get_job_parameter(jenkins_build_data, 'UPSTREAM_JOB_URL')
if not upstream_job:
return
causes = [a['causes'] for a in jenkins_build_data['actions']
if 'causes' in a.keys()][0]
if len(causes) > 0:
upstream_job_name = causes[0]['upstreamProject']
upstream_build_number = causes[0]['upstreamBuild']
upstream_build = Build(upstream_job_name, upstream_build_number)
return (get_version_from_artifacts(upstream_build.build_data) or
get_version_from_parameters(upstream_build.build_data))
def get_job_parameter(jenkins_build_data, parameter):
parameters = [a['parameters'] for a in jenkins_build_data['actions']
if 'parameters' in a.keys()][0]
target_params = [p['value'] for p in parameters
if p['name'].lower() == str(parameter).lower()]
if len(target_params) > 0:
return target_params[0]
def get_version_from_parameters(jenkins_build_data):
custom_version = get_job_parameter(jenkins_build_data, 'CUSTOM_VERSION')
if custom_version:
swarm_timestamp = jenkins_build_data['timestamp'] // 1000 \
if 'timestamp' in jenkins_build_data else None
return (TestRailSettings.milestone,
time.strftime("%D %H:%M", time.localtime(swarm_timestamp)),
custom_version)
iso_link = get_job_parameter(jenkins_build_data, 'magnet_link')
if iso_link:
return get_version_from_iso_name(iso_link)
def get_version_from_artifacts(jenkins_build_data):
if not any([artifact for artifact in jenkins_build_data['artifacts']
if artifact['fileName'] == JENKINS['magnet_link_artifact']]):
return
iso_link = (get_build_artifact(url=jenkins_build_data['url'],
artifact=JENKINS['magnet_link_artifact']))
if iso_link:
return get_version_from_iso_name(iso_link)
def get_version_from_iso_name(iso_link):
match = re.search(r'.*\bfuel-(?P<prefix1>[a-zA-Z]*)-?(?P<version>\d+'
r'(?P<version2>\.\d+)+)-(?P<prefix2>[a-zA-Z]*)-?'
r'(?P<buildnum>\d+)-.*', iso_link)
if match:
return (match.group('version'),
int(match.group('buildnum')),
match.group('prefix1') or match.group('prefix2'))
def expand_test_group(group, systest_build_name, os):
"""Expand specified test names with the group name of the job
which is taken from the build name, for example:
group: 'setup_master'
systest_build_name: '7.0.system_test.ubuntu.bonding_ha_one_controller'
os: str, release name in lower case, for example: 'ubuntu'
return: 'setup_master_bonding_ha_one_controller'
"""
if group in GROUPS_TO_EXPAND:
if os in systest_build_name:
sep = '.' + os + '.'
else:
sep = '.'
systest_group_name = systest_build_name.split(sep)[-1]
if systest_group_name:
group = '_'.join([group, systest_group_name])
return group
def check_blocked(test):
"""Change test result status to 'blocked' if it was
skipped due to failure of another dependent test
:param test: dict, test result info
:return: None
"""
if test['status'].lower() != 'skipped':
return
match = re.match(r'^Failure in <function\s+(\w+)\s+at\s0x[a-f0-9]+>',
test['skippedMessage'])
if match:
failed_func_name = match.group(1)
if test['name'] != failed_func_name:
test['status'] = 'blocked'
test['skippedMessage'] = 'Blocked by "{0}" test.'.format(
failed_func_name)
def check_untested(test):
"""Check if test result is fake
:param test: dict
:return: bool
"""
if test['name'] == 'jenkins' and 'skippedMessage' not in test:
return True
return False
@retry(count=3)
def get_tests_results(systest_build, os):
tests_results = []
test_build = Build(systest_build['name'], systest_build['number'])
run_test_data = test_build.test_data()
test_classes = {}
for one in run_test_data['suites'][0]['cases']:
class_name = one['className']
if class_name not in test_classes:
test_classes[class_name] = {}
test_classes[class_name]['child'] = []
test_classes[class_name]['duration'] = 0
test_classes[class_name]["failCount"] = 0
test_classes[class_name]["passCount"] = 0
test_classes[class_name]["skipCount"] = 0
else:
if one['className'] == one['name']:
logger.warning("Found duplicate test in run - {}".format(
one['className']))
continue
test_class = test_classes[class_name]
test_class['child'].append(one)
test_class['duration'] += float(one['duration'])
if one['status'].lower() in ('failed', 'error'):
test_class["failCount"] += 1
if one['status'].lower() == 'passed':
test_class["passCount"] += 1
if one['status'].lower() == 'skipped':
test_class["skipCount"] += 1
for klass in test_classes:
klass_result = test_classes[klass]
if len(klass_result['child']) == 1:
test = klass_result['child'][0]
if check_untested(test):
continue
check_blocked(test)
test_result = TestResult(
name=test['name'],
group=expand_test_group(test['className'],
systest_build['name'],
os),
status=test['status'].lower(),
duration='{0}s'.format(int(test['duration']) + 1),
url='{0}testReport/(root)/{1}/'.format(test_build.url,
test['name']),
version='_'.join(
[test_build.build_data["id"]] + (
test_build.build_data["description"] or
test['name']).split()),
description=test_build.build_data["description"] or
test['name'],
comments=test['skippedMessage']
)
else:
case_steps = []
test_duration = sum(
[float(c['duration']) for c in klass_result['child']])
steps = [c for c in klass_result['child']
if c['name'].startswith('Step')]
steps = sorted(steps, key=lambda k: k['name'])
test_name = steps[0]['className']
test_group = steps[0]['className']
test_comments = None
is_test_failed = any([s['status'].lower() in ('failed', 'error')
for s in steps])
for step in steps:
if step['status'].lower() in ('failed', 'error'):
case_steps.append({
"content": step['name'],
"actual": step['errorStackTrace'] or
step['errorDetails'],
"status": step['status'].lower()})
test_comments = "{err}\n\n\n{stack}".format(
err=step['errorDetails'],
stack=step['errorStackTrace'])
else:
case_steps.append({
"content": step['name'],
"actual": "pass",
"status": step['status'].lower()
})
test_result = TestResult(
name=test_name,
group=expand_test_group(test_group,
systest_build['name'],
os),
status='failed' if is_test_failed else 'passed',
duration='{0}s'.format(int(test_duration) + 1),
url='{0}testReport/(root)/{1}/'.format(test_build.url,
test_name),
version='_'.join(
[test_build.build_data["id"]] + (
test_build.build_data["description"] or
test_name).split()),
description=test_build.build_data["description"] or
test_name,
comments=test_comments,
steps=case_steps,
)
tests_results.append(test_result)
return tests_results
def publish_results(project, milestone_id, test_plan,
suite_id, config_id, results):
test_run_ids = [run['id'] for entry in test_plan['entries']
for run in entry['runs'] if suite_id == run['suite_id'] and
config_id in run['config_ids']]
logger.debug('Looking for previous tests runs on "{0}" using tests suite '
'"{1}"...'.format(project.get_config(config_id)['name'],
project.get_suite(suite_id)['name']))
previous_tests_runs = project.get_previous_runs(
milestone_id=milestone_id,
suite_id=suite_id,
config_id=config_id,
limit=TestRailSettings.previous_results_depth)
logger.debug('Found next test runs: {0}'.format(
[test_run['description'] for test_run in previous_tests_runs]))
cases = project.get_cases(suite_id=suite_id)
tests = project.get_tests(run_id=test_run_ids[0])
results_to_publish = []
for result in results:
test = project.get_test_by_group(run_id=test_run_ids[0],
group=result.group,
tests=tests)
if not test:
logger.error("Test for '{0}' group not found: {1}".format(
result.group, result.url))
continue
existing_results_versions = [r['version'] for r in
project.get_results_for_test(test['id'])]
if result.version in existing_results_versions:
continue
if result.status not in ('passed', 'blocked'):
case_id = project.get_case_by_group(suite_id=suite_id,
group=result.group,
cases=cases)['id']
run_ids = [run['id'] for run in previous_tests_runs[0:
int(TestRailSettings.previous_results_depth)]]
previous_results = project.get_all_results_for_case(
run_ids=run_ids,
case_id=case_id)
lp_bug = get_existing_bug_link(previous_results)
if lp_bug:
result.launchpad_bug = lp_bug['bug_link']
results_to_publish.append(result)
try:
if len(results_to_publish) > 0:
project.add_results_for_cases(run_id=test_run_ids[0],
suite_id=suite_id,
tests_results=results_to_publish)
except:
logger.error('Failed to add new results for tests: {0}'.format(
[r.group for r in results_to_publish]
))
raise
return results_to_publish
@retry(count=3)
def get_existing_bug_link(previous_results):
results_with_bug = [result for result in previous_results if
result["custom_launchpad_bug"] is not None]
if not results_with_bug:
return
for result in sorted(results_with_bug,
key=lambda k: k['created_on'],
reverse=True):
try:
bug_id = int(result["custom_launchpad_bug"].strip('/').split(
'/')[-1])
except ValueError:
logger.warning('Link "{0}" doesn\'t contain bug id.'.format(
result["custom_launchpad_bug"]))
continue
try:
bug = LaunchpadBug(bug_id).get_duplicate_of()
except KeyError:
logger.warning("Bug with id '{bug_id}' is private or \
doesn't exist.".format(bug_id=bug_id))
continue
except Exception:
logger.exception("Strange situation with '{bug_id}' \
issue".format(bug_id=bug_id))
continue
for target in bug.targets:
if target['project'] == LaunchpadSettings.project and\
target['milestone'] == LaunchpadSettings.milestone and\
target['status'] not in LaunchpadSettings.closed_statuses:
target['bug_link'] = result["custom_launchpad_bug"]
return target
def main():
parser = OptionParser(
description="Publish results of system tests from Jenkins build to "
"TestRail. See settings.py for configuration."
)
parser.add_option('-j', '--job-name', dest='job_name', default=None,
help='Jenkins swarm runner job name')
parser.add_option('-N', '--build-number', dest='build_number',
default='latest',
help='Jenkins swarm runner build number')
parser.add_option('-o', '--one-job', dest='one_job_name',
default=None,
help=('Process only one job name from the specified '
'parent job or view'))
parser.add_option("-w", "--view", dest="jenkins_view", default=False,
help="Get system tests jobs from Jenkins view")
parser.add_option("-l", "--live", dest="live_report", action="store_true",
help="Get tests results from running swarm")
parser.add_option("-m", "--manual", dest="manual_run", action="store_true",
help="Manually add tests cases to TestRun (tested only)")
parser.add_option('-c', '--create-plan-only', action="store_true",
dest="create_plan_only", default=False,
help='Jenkins swarm runner job name')
parser.add_option("-v", "--verbose",
action="store_true", dest="verbose", default=False,
help="Enable debug output")
(options, _) = parser.parse_args()
if options.verbose:
logger.setLevel(DEBUG)
if options.live_report and options.build_number == 'latest':
options.build_number = 'latest_started'
# STEP #1
# Initialize TestRail Project and define configuration
logger.info('Initializing TestRail Project configuration...')
project = TestRailProject(url=TestRailSettings.url,
user=TestRailSettings.user,
password=TestRailSettings.password,
project=TestRailSettings.project)
tests_suite = project.get_suite_by_name(TestRailSettings.tests_suite)
operation_systems = [{'name': config['name'], 'id': config['id'],
'distro': config['name'].split()[0].lower()}
for config in project.get_config_by_name(
'Operation System')['configs'] if
config['name'] in TestRailSettings.operation_systems]
tests_results = {os['distro']: [] for os in operation_systems}
# STEP #2
# Get tests results from Jenkins
logger.info('Getting tests results from Jenkins...')
if options.jenkins_view:
jobs = get_jobs_for_view(options.jenkins_view)
tests_jobs = [{'name': j, 'number': 'latest'}
for j in jobs if 'system_test' in j] if \
not options.create_plan_only else []
runner_job = [j for j in jobs if 'runner' in j][0]
runner_build = Build(runner_job, 'latest')
elif options.job_name:
runner_build = Build(options.job_name, options.build_number)
tests_jobs = get_downstream_builds(runner_build.build_data) if \
not options.create_plan_only else []
else:
logger.error("Please specify either Jenkins swarm runner job name (-j)"
" or Jenkins view with system tests jobs (-w). Exiting..")
return
for systest_build in tests_jobs:
if (options.one_job_name and
options.one_job_name != systest_build['name']):
logger.debug("Skipping '{0}' because --one-job is specified"
.format(systest_build['name']))
continue
if options.job_name:
if 'result' not in systest_build.keys():
logger.debug("Skipping '{0}' job because it does't run tests "
"(build #{1} contains no results)".format(
systest_build['name'],
systest_build['number']))
continue
if systest_build['result'] is None:
logger.debug("Skipping '{0}' job (build #{1}) because it's sti"
"ll running...".format(systest_build['name'],
systest_build['number'],))
continue
for os in tests_results.keys():
if os in systest_build['name'].lower():
tests_results[os].extend(get_tests_results(systest_build, os))
# STEP #3
# Create new TestPlan in TestRail (or get existing) and add TestRuns
milestone, iso_number, prefix = get_version(runner_build.build_data)
milestone = project.get_milestone_by_name(name=milestone)
test_plan_name = ' '.join(
filter(lambda x: bool(x),
(milestone['name'], prefix, 'iso', '#' + str(iso_number))))
test_plan = project.get_plan_by_name(test_plan_name)
iso_link = '/'.join([JENKINS['url'], 'job',
'{0}.all'.format(milestone['name']), str(iso_number)])
if not test_plan:
test_plan = project.add_plan(test_plan_name,
description=iso_link,
milestone_id=milestone['id'],
entries=[]
)
logger.info('Created new TestPlan "{0}".'.format(test_plan_name))
else:
logger.info('Found existing TestPlan "{0}".'.format(test_plan_name))
if options.create_plan_only:
return
plan_entries = []
all_cases = project.get_cases(suite_id=tests_suite['id'])
for os in operation_systems:
cases_ids = []
if options.manual_run:
all_results_groups = [r.group for r in tests_results[os['distro']]]
for case in all_cases:
if case['custom_test_group'] in all_results_groups:
cases_ids.append(case['id'])
plan_entries.append(
project.test_run_struct(
name='{suite_name}'.format(suite_name=tests_suite['name']),
suite_id=tests_suite['id'],
milestone_id=milestone['id'],
description='Results of system tests ({tests_suite}) on is'
'o #"{iso_number}"'.format(tests_suite=tests_suite['name'],
iso_number=iso_number),
config_ids=[os['id']],
include_all=True,
case_ids=cases_ids
)
)
if not any(entry['suite_id'] == tests_suite['id']
for entry in test_plan['entries']):
if project.add_plan_entry(plan_id=test_plan['id'],
suite_id=tests_suite['id'],
config_ids=[os['id'] for os
in operation_systems],
runs=plan_entries):
test_plan = project.get_plan(test_plan['id'])
# STEP #4
# Upload tests results to TestRail
logger.info('Uploading tests results to TestRail...')
for os in operation_systems:
logger.info('Checking tests results for "{0}"...'.format(os['name']))
results_to_publish = publish_results(
project=project,
milestone_id=milestone['id'],
test_plan=test_plan,
suite_id=tests_suite['id'],
config_id=os['id'],
results=tests_results[os['distro']]
)
logger.debug('Added new results for tests ({os}): {tests}'.format(
os=os['name'], tests=[r.group for r in results_to_publish]
))
logger.info('Report URL: {0}'.format(test_plan['url']))
if __name__ == "__main__":
main()