#!/usr/bin/env python # # Copyright 2016 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 unicode_literals import json import os import re import sys import time import argparse from collections import OrderedDict from logging import CRITICAL from logging import DEBUG from fuelweb_test.testrail.builds import Build from fuelweb_test.testrail.launchpad_client import LaunchpadBug from fuelweb_test.testrail.report import get_version 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 def inspect_bug(bug): # Return target which matches defined in settings project/milestone and # has 'open' status. If there are no such targets, then just return first # one available target. for target in bug.targets: if target['project'] == LaunchpadSettings.project and \ LaunchpadSettings.milestone in target['milestone'] and\ target['status'] not in LaunchpadSettings.closed_statuses: return target return bug.targets[0] def generate_test_plan_name(job_name, build_number): # Generate name of TestPlan basing on iso image name # taken from Jenkins job build parameters runner_build = Build(job_name, build_number) milestone, iso_number, prefix = get_version(runner_build.build_data) if 'snapshot' not in prefix: return ' '.join(filter(lambda x: bool(x), (milestone, prefix, 'iso', '#' + str(iso_number)))) else: return ' '.join(filter(lambda x: bool(x), (milestone, prefix))) def get_testrail(): logger.info('Initializing TestRail Project configuration...') return TestRailProject(url=TestRailSettings.url, user=TestRailSettings.user, password=TestRailSettings.password, project=TestRailSettings.project) class TestRunStatistics(object): """Statistics for attached bugs in TestRun """ def __init__(self, project, run_id, check_blocked=False): self.project = project self.run = self.project.get_run(run_id) self.tests = self.project.get_tests(run_id) self.results = self.get_results() logger.info('Found TestRun "{0}" on "{1}" with {2} tests and {3} ' 'results'.format(self.run['name'], self.run['config'] or 'default config', len(self.tests), len(self.results))) self.blocked_statuses = [self.project.get_status(s)['id'] for s in TestRailSettings.stauses['blocked']] self.failed_statuses = [self.project.get_status(s)['id'] for s in TestRailSettings.stauses['failed']] self.check_blocked = check_blocked self._bugs_statistics = {} def __getitem__(self, item): return self.run.__getitem__(item) def get_results(self): results = [] stop = 0 offset = 0 while not stop: new_results = self.project.get_results_for_run( self.run['id'], limit=TestRailSettings.max_results_per_request, offset=offset) results += new_results offset += len(new_results) stop = TestRailSettings.max_results_per_request - len(new_results) return results def get_test_by_group(self, group, version): if group in GROUPS_TO_EXPAND: m = re.search(r'^\d+_(\S+)_on_[\d\.]+', version) if m: tests_thread = m.group(1) group = '{0}_{1}'.format(group, tests_thread) for test in self.tests: if test['custom_test_group'] == group: return test logger.error('Test with group "{0}" not found!'.format(group)) def handle_blocked(self, test, result): if result['custom_launchpad_bug']: return False m = re.search(r'Blocked by "(\S+)" test.', result['comment'] or '') if m: blocked_test_group = m.group(1) else: logger.warning('Blocked result #{0} for test {1} does ' 'not have upstream test name in its ' 'comments!'.format(result['id'], test['custom_test_group'])) return False if not result['version']: logger.debug('Blocked result #{0} for test {1} does ' 'not have version, can\'t find upstream ' 'test case!'.format(result['id'], test['custom_test_group'])) return False bug_link = None blocked_test = self.get_test_by_group(blocked_test_group, result['version']) if not blocked_test: return False logger.debug('Test {0} was blocked by failed test {1}'.format( test['custom_test_group'], blocked_test_group)) blocked_results = self.project.get_results_for_test( blocked_test['id']) # Since we manually add results to failed tests with statuses # ProdFailed, TestFailed, etc. and attach bugs links to them, # we could skip original version copying. So look for test # results with target version, but allow to copy links to bugs # from other results of the same test (newer are checked first) if not any(br['version'] == result['version'] and br['status_id'] in self.failed_statuses for br in blocked_results): logger.debug('Did not find result for test {0} with version ' '{1}!'.format(blocked_test_group, result['version'])) return False for blocked_result in sorted(blocked_results, key=lambda x: x['id'], reverse=True): if blocked_result['status_id'] not in self.failed_statuses: continue if blocked_result['custom_launchpad_bug']: bug_link = blocked_result['custom_launchpad_bug'] break if bug_link is not None: result['custom_launchpad_bug'] = bug_link self.project.add_raw_results_for_test(test['id'], result) logger.info('Added bug {0} to blocked result of {1} test.'.format( bug_link, test['custom_test_group'])) return bug_link return False @property def bugs_statistics(self): if self._bugs_statistics != {}: return self._bugs_statistics logger.info('Collecting stats for TestRun "{0}" on "{1}"...'.format( self.run['name'], self.run['config'] or 'default config')) for test in self.tests: logger.debug('Checking "{0}" test...'.format(test['title'])) test_results = sorted( self.project.get_results_for_test(test['id'], self.results), key=lambda x: x['id'], reverse=True) linked_bugs = [] is_blocked = False for result in test_results: if result['status_id'] in self.blocked_statuses: if self.check_blocked: new_bug_link = self.handle_blocked(test, result) if new_bug_link: linked_bugs.append(new_bug_link) is_blocked = True break if result['custom_launchpad_bug']: linked_bugs.append(result['custom_launchpad_bug']) is_blocked = True break if result['status_id'] in self.failed_statuses \ and result['custom_launchpad_bug']: linked_bugs.append(result['custom_launchpad_bug']) bug_ids = set([re.search(r'.*bugs?/(\d+)/?', link).group(1) for link in linked_bugs if re.search(r'.*bugs?/(\d+)/?', link)]) for bug_id in bug_ids: if bug_id in self._bugs_statistics: self._bugs_statistics[bug_id][test['id']] = { 'group': test['custom_test_group'] or 'manual', 'config': self.run['config'] or 'default', 'blocked': is_blocked } else: self._bugs_statistics[bug_id] = { test['id']: { 'group': test['custom_test_group'] or 'manual', 'config': self.run['config'] or 'default', 'blocked': is_blocked } } return self._bugs_statistics class StatisticsGenerator(object): """Generate statistics for bugs attached to TestRuns in TestPlan """ def __init__(self, project, plan_id, run_ids=(), handle_blocked=False): self.project = project self.test_plan = self.project.get_plan(plan_id) logger.info('Found TestPlan "{0}"'.format(self.test_plan['name'])) self.test_runs_stats = [ TestRunStatistics(self.project, r['id'], handle_blocked) for e in self.test_plan['entries'] for r in e['runs'] if r['id'] in run_ids or len(run_ids) == 0 ] self.bugs_statistics = {} def generate(self): for test_run in self.test_runs_stats: test_run_stats = test_run.bugs_statistics self.bugs_statistics[test_run['id']] = dict() for bug, tests in test_run_stats.items(): if bug in self.bugs_statistics[test_run['id']]: self.bugs_statistics[test_run['id']][bug].update(tests) else: self.bugs_statistics[test_run['id']][bug] = tests logger.info('Found {0} linked bug(s)'.format( len(self.bugs_statistics[test_run['id']]))) def update_desription(self, stats): old_description = self.test_plan['description'] new_description = '' for line in old_description.split('\n'): if not re.match(r'^Bugs Statistics \(generated on .*\)$', line): new_description += line + '\n' else: break new_description += '\n' + stats return self.project.update_plan(plan_id=self.test_plan['id'], description=new_description) def dump(self, run_id=None): stats = dict() if not run_id: joint_bugs_statistics = dict() for run in self.bugs_statistics: for bug, tests in self.bugs_statistics[run].items(): if bug in joint_bugs_statistics: joint_bugs_statistics[bug].update(tests) else: joint_bugs_statistics[bug] = tests else: for _run_id, _stats in self.bugs_statistics.items(): if _run_id == run_id: joint_bugs_statistics = _stats for bug_id in joint_bugs_statistics: try: lp_bug = LaunchpadBug(bug_id).get_duplicate_of() except KeyError: logger.warning("Bug with ID {0} not found! Most probably it's " "private or private security.".format(bug_id)) continue bug_target = inspect_bug(lp_bug) if lp_bug.bug.id in stats: stats[lp_bug.bug.id]['tests'].update( joint_bugs_statistics[bug_id]) else: stats[lp_bug.bug.id] = { 'title': bug_target['title'], 'importance': bug_target['importance'], 'status': bug_target['status'], 'project': bug_target['project'], 'link': lp_bug.bug.web_link, 'tests': joint_bugs_statistics[bug_id] } stats[lp_bug.bug.id]['failed_num'] = len( [t for t, v in stats[lp_bug.bug.id]['tests'].items() if not v['blocked']]) stats[lp_bug.bug.id]['blocked_num'] = len( [t for t, v in stats[lp_bug.bug.id]['tests'].items() if v['blocked']]) return OrderedDict(sorted(stats.items(), key=lambda x: (x[1]['failed_num'] + x[1]['blocked_num']), reverse=True)) def dump_html(self, stats=None, run_id=None): if stats is None: stats = self.dump() html = '\n' html += '