blob: 46f5995161c0c6c61739b373037419d8cb3be20a [file] [log] [blame]
Dmitry Tyzhnenkofb0516d2016-11-17 17:55:26 +02001#!/usr/bin/env python
2import datetime
3import sys
4import logging
5from collections import defaultdict, OrderedDict
6
7import jira
8import argparse
9
10from testrail import TestRail
11from testrail.test import Test
12from functools32 import lru_cache
13
14logging.basicConfig(format='%(levelname)s:%(message)s', level=logging.INFO)
15LOG = logging.getLogger(__name__)
16
17
18def run_cli():
19 cli = argparse.ArgumentParser(
20 prog="Report generator",
21 description="Command line tool for generate summary report")
22
23 commands = cli.add_subparsers(
24 title="Operation commands",
25 dest="command")
26
27 cli_process = commands.add_parser(
28 "create-report",
29 help="Create summary report",
30 description="Create summary report")
31 cli_process.add_argument(
32 "-T", "--testrail-host", dest="testrail_host",
33 required=True,
34 help="TestRail hostname")
35 cli_process.add_argument(
36 "-U", "--testrail-user", dest="testrail_user",
37 required=True,
38 help="TestRail user email")
39 cli_process.add_argument(
40 "-K", "--testrail-user-key", dest="testrail_user_key",
41 required=True,
42 help="TestRail user key")
43 cli_process.add_argument(
44 "-R", "--testrail-plan", dest="testrail_plan",
45 required=True,
46 help="TestRail test plan for analize")
47 cli_process.add_argument(
48 "-P", "--testrail-project", dest="testrail_project",
49 required=True,
50 help="TestRail project name")
51 cli_process.add_argument(
52 "--testrail-only-run", dest="testrail_only_run",
53 help="Analize only one run in selected plan")
54 cli_process.add_argument(
55 "--out-type", dest="out_type", choices=["text", "html", 'md', 'none'],
56 default='none',
57 help="Select output format for report table. "
58 "By default print nothing (none).")
59 cli_process.add_argument(
60 "--sort-by", dest="sort_by", default='fails',
61 choices=["fails", "blocks", 'project', 'priority', 'status'],
62 help="Select sorting column. By deafult table sort by fails")
63 cli_process.add_argument(
64 "--push-to-testrail", dest="push_report_flag", action="store_true",
65 default=False,
66 help="Save report in plan description")
67 cli_process.add_argument(
68 "-j", "--jira-host", dest="jira_host",
69 required=True,
70 help="JIRA hostname")
71 cli_process.add_argument(
72 "-u", "--jira-user", dest="jira_user_id",
73 required=True,
74 help="JIRA username")
75 cli_process.add_argument(
76 "-p", "--jira-password", dest="jira_user_password",
77 required=True,
78 help="JIRA user password")
79
80 if len(sys.argv) == 1:
81 cli.print_help()
82 sys.exit(1)
83
84 return cli.parse_args()
85
86
87def get_runs(t_client, plan_name, run_name):
88 LOG.info("Get runs from plan - {}".format(plan_name))
89 ret = []
90 plan = t_client.plan(plan_name)
91 for e in plan.entries:
92 for r in e.runs:
93 LOG.info("Run {} #{}".format(r.name, r.id))
94 if run_name is not None and r.name != run_name:
95 continue
96 ret.append(r)
97 return ret
98
99
100def get_all_results(t_client, list_of_runs):
101 ret = []
102 for run in list_of_runs:
103 ret.extend(get_results(t_client, run))
104 return ret
105
106
107@lru_cache()
108def fetch_test(api, test_id, run_id):
109 return Test(api.test_with_id(test_id, run_id))
110
111
112def get_results(t_client, run):
113 _statuses = ('product_failed', 'failed',
114 'prodfailed', 'blocked')
115 LOG.info("Get results for run - {}".format(run.name))
116 results = t_client.results(run)
117 ret = [(run.id, r) for r in results
118 if r.raw_data()['status_id'] is not None and
119 r.raw_data()['defects'] is not None and
120 r.status.name.lower() in _statuses]
121 for r in ret:
122 run_id, result = r
123 test = fetch_test(result.api, result.raw_data()['test_id'], run_id)
124 LOG.info("Test {} - {} - {}".format(test.title, result.status.name,
125 ','.join(result.defects)))
126 return ret
127
128
129@lru_cache()
130def get_defect_info(j_client, defect):
131 LOG.info("Get info about issue {}".format(defect))
132 try:
133 issue = j_client.issue(defect)
134 except jira.exceptions.JIRAError as e:
135 if e.status_code == 404:
136 LOG.error("Defect {} wasn't found in Jira".format(defect))
137 return {
138 'id': defect,
139 'title': "Title for #{} not found".format(defect),
140 'project': "Not found",
141 'priority': "Not found",
142 'status': "Not found",
143 'url': "Not found"
144 }
145 else:
146 raise
147 return {
148 'id': issue.key,
149 'title': issue.fields.summary,
150 'project': issue.fields.project.key,
151 'priority': issue.fields.priority.name,
152 'status': issue.fields.status.name,
153 'url': issue.permalink()
154 }
155
156
157def get_defects_table(jira_client, list_of_results, sort_by):
158 LOG.info("Collect report table")
159 table = defaultdict(dict)
160 for run_id, result in list_of_results:
161 for defect in result.defects:
162 if defect not in table:
163 info = get_defect_info(jira_client, defect)
164
165 table[defect].update(info)
166 table[defect]['results'] = set([(run_id, result)])
167 if result.status.name.lower() == 'blocked':
168 table[defect]['blocks'] = 1
169 table[defect]['fails'] = 0
170 else:
171 table[defect]['fails'] = 1
172 table[defect]['blocks'] = 0
173 else:
174 table[defect]['results'].add((run_id, result))
175 if result.status.name.lower() == 'blocked':
176 table[defect]['blocks'] += 1
177 else:
178 table[defect]['fails'] += 1
179 return OrderedDict(
180 sorted(table.items(), key=lambda i: i[1][sort_by], reverse=True))
181
182
183def get_text_table(table):
184 LOG.info("Generation text table")
185 lines = []
186 line = ("{fails:^5} | {blocks:^5} | {project:^10} | {priority:^15} | "
187 "{status:^15} | {bug:^100} | {tests} ")
188
189 def title_uid(r):
190 run_id, result = r
191 test = fetch_test(result.api, result.raw_data()['test_id'], run_id)
192 return {
193 "title": test.title,
194 "uid": test.id}
195
196 def list_of_defect_tests(results):
197 ret = ["[{title} #{uid}]".format(**title_uid(r)) for
198 r in results]
199 return ' '.join(ret)
200
201 lines.append(line.format(fails='FAILS', blocks='BLOCKS', project="PROJECT",
202 priority="PRIORITY", status="STATUS", bug="BUG",
203 tests="TESTS"))
204 for k in table:
205 one = table[k]
206 data = {
207 "fails": one['fails'],
208 "blocks": one['blocks'],
209 "project": one['project'],
210 "priority": one['priority'],
211 "status": one['status'],
212 "bug": "{uid} {title}".format(uid=one['id'], title=one['title']),
213 "tests": list_of_defect_tests(one['results'])
214 }
215 lines.append(line.format(**data))
216 return '\n'.join(lines)
217
218
219def get_md_table(table):
220 LOG.info("Generation MD table")
221 lines = []
222 line = ("||{fails} | {blocks} | {project} | {priority} | "
223 "{status} | {bug} | {tests} ")
224
225 def title_uid_link(r):
226 run_id, result = r
227 test = fetch_test(result.api, result.raw_data()['test_id'], run_id)
228
229 return {
230 "title": test.title.replace('[', '{').replace(']', '}'),
231 "uid": test.id,
232 "link": "{url}/index.php?/tests/view/{uid}".format(
233 url=test.api._conf()['url'], uid=test.id)
234 }
235
236 def list_of_defect_tests(results):
237 ret = ["<[{title} #{uid}]({link})>".format(**title_uid_link(r))
238 for r in results]
239 return ' '.join(ret)
240
241 lines.append(line.format(fails='|:FAILS', blocks=':BLOCKS',
242 project=":PROJECT", priority=":PRIORITY",
243 status=":STATUS", bug=":BUG", tests=":TESTS"))
244 for k in table:
245 one = table[k]
246 data = {
247 "fails": one['fails'],
248 "blocks": one['blocks'],
249 "project": one['project'],
250 "priority": one['priority'],
251 "status": one['status'],
252 "bug": "[{uid} {title}]({url})".format(
253 uid=one['id'],
254 title=one['title'].replace('[', '{').replace(']', '}'),
255 url=one['url']),
256 "tests": list_of_defect_tests(one['results'])
257 }
258 lines.append(line.format(**data))
259 return '\n'.join(lines)
260
261
262def get_html_table(table):
263 LOG.info("Generation HTML table")
264 html = "<table>{lines}</table>"
265 lines = []
266 line = ("<tr><th>{fails:^5}</th><th>{blocks:^5}</th><th>{project:^10}</th>"
267 "<th>{priority:^15}</th>"
268 "<th>{status:^15}</th><th>{bug:^100}</th><th>{tests}</th></tr>")
269 lines.append(line.format(fails='FAILS', blocks='BLOCKS', project="PROJECT",
270 priority="PRIORITY", status="STATUS", bug="BUG",
271 tests="TESTS"))
272
273 def title_uid_link(r):
274 run_id, result = r
275 test = fetch_test(result.api, result.raw_data()['test_id'], run_id)
276
277 return {
278 "title": test.title,
279 "uid": test.id,
280 "link": "{url}/index.php?/tests/view/{uid}".format(
281 url=test.api._conf()['url'], uid=test.id)
282 }
283
284 def list_of_defect_tests(results):
285 ret = ["<a href='{link}'>{title} #{uid}</a>".format(
286 **title_uid_link(r)) for r in results]
287 return ' '.join(ret)
288
289 for k in table:
290 one = table[k]
291 data = {
292 "fails": one['fails'],
293 "blocks": one['blocks'],
294 "project": one['project'],
295 "priority": one['priority'],
296 "status": one['status'],
297 "bug": "<a href='{url}'>{uid} {title}</a>".format(
298 uid=one['id'], title=one['title'], url=one['url']),
299 "tests": list_of_defect_tests(one['results'])
300 }
301 lines.append(line.format(**data))
302 return html.format(lines=''.join(lines))
303
304
305def out_table(out_type, table):
306 if out_type == 'none':
307 return
308 elif out_type == 'html':
309 print(get_html_table(table))
310 elif out_type == 'md':
311 print(get_md_table(table))
312 else:
313 print(get_text_table(table))
314
315
316def push_report(t_client, plan_name, table):
317 LOG.info("Push report table into plan - {}".format(plan_name))
318 text = "Bugs Statistics (generated on {date})\n" \
319 "=======================================================\n" \
320 "{table}".format(
321 date=datetime.datetime.now().strftime("%a %b %d %H:%M:%S %Y"),
322 table=get_md_table(table))
323 plan = t_client.plan(plan_name)
324 plan.description = text
325 plan.api._post(
326 'update_plan/{}'.format(plan.id),
327 {
328 'name': plan.name,
329 'description': plan.description,
330 'milestone_id': plan.milestone.id
331 })
332
333
334def create_report(**kwargs):
335 j_host = kwargs.get('jira_host')
336 j_user = kwargs.get('jira_user_id')
337 j_user_pwd = kwargs.get('jira_user_password')
338 t_host = kwargs.get('testrail_host')
339 t_user = kwargs.get('testrail_user')
340 t_user_key = kwargs.get('testrail_user_key')
341 t_plan = kwargs.get('testrail_plan')
342 t_project = kwargs.get('testrail_project')
343 t_a_run = kwargs.get('testrail_only_run')
344 o_type = kwargs.get('out_type')
345 push_report_flag = kwargs.get('push_report_flag')
346 sort_by = kwargs.get('sort_by')
347
348 t_client = TestRail(email=t_user, key=t_user_key, url=t_host)
349 t_client.set_project_id(t_client.project(t_project).id)
350
351 j_client = jira.JIRA(j_host, basic_auth=(j_user, j_user_pwd))
352
353 runs = get_runs(t_client, t_plan, t_a_run)
354 results = get_all_results(t_client, runs)
355 table = get_defects_table(j_client, results, sort_by)
356 out_table(o_type, table)
357 if push_report_flag:
358 push_report(t_client, t_plan, table)
359
360
361COMMAND_MAP = {
362 'create-report': create_report
363}
364
365
366def main():
367 args = run_cli()
368 COMMAND_MAP[args.command](**vars(args))
369
Dina Belovae6fdffb2017-09-19 13:58:34 -0700370
Dmitry Tyzhnenkofb0516d2016-11-17 17:55:26 +0200371if __name__ == '__main__':
372 main()