blob: 03530d6fabb3f34b4f3266b3c6eed9d73332d395 [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)
Dennis Dmitrieva0622182018-05-09 04:39:19 +030091 if plan:
92 for e in plan.entries:
93 for r in e.runs:
94 LOG.info("Run {} #{}".format(r.name, r.id))
95 if run_name is not None and r.name != run_name:
96 continue
97 ret.append(r)
98 else:
99 LOG.warning("Plan {} is empty".format(plan_name))
Dmitry Tyzhnenkofb0516d2016-11-17 17:55:26 +0200100 return ret
101
102
103def get_all_results(t_client, list_of_runs):
104 ret = []
105 for run in list_of_runs:
106 ret.extend(get_results(t_client, run))
107 return ret
108
109
110@lru_cache()
111def fetch_test(api, test_id, run_id):
112 return Test(api.test_with_id(test_id, run_id))
113
114
115def get_results(t_client, run):
Dmitry Tyzhnenkofb0516d2016-11-17 17:55:26 +0200116 LOG.info("Get results for run - {}".format(run.name))
117 results = t_client.results(run)
118 ret = [(run.id, r) for r in results
119 if r.raw_data()['status_id'] is not None and
Dennis Dmitrievef6af332018-05-09 05:21:21 +0300120 r.raw_data()['defects'] is not None]
Dmitry Tyzhnenkofb0516d2016-11-17 17:55:26 +0200121 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)
Dennis Dmitrievef6af332018-05-09 05:21:21 +0300324 if plan:
325 plan.description = text
326 plan.api._post(
327 'update_plan/{}'.format(plan.id),
328 {
329 'name': plan.name,
330 'description': plan.description,
331 'milestone_id': plan.milestone.id
332 })
Dmitry Tyzhnenkofb0516d2016-11-17 17:55:26 +0200333
334
335def create_report(**kwargs):
336 j_host = kwargs.get('jira_host')
337 j_user = kwargs.get('jira_user_id')
338 j_user_pwd = kwargs.get('jira_user_password')
339 t_host = kwargs.get('testrail_host')
340 t_user = kwargs.get('testrail_user')
341 t_user_key = kwargs.get('testrail_user_key')
342 t_plan = kwargs.get('testrail_plan')
343 t_project = kwargs.get('testrail_project')
344 t_a_run = kwargs.get('testrail_only_run')
345 o_type = kwargs.get('out_type')
346 push_report_flag = kwargs.get('push_report_flag')
347 sort_by = kwargs.get('sort_by')
348
349 t_client = TestRail(email=t_user, key=t_user_key, url=t_host)
350 t_client.set_project_id(t_client.project(t_project).id)
351
352 j_client = jira.JIRA(j_host, basic_auth=(j_user, j_user_pwd))
353
354 runs = get_runs(t_client, t_plan, t_a_run)
355 results = get_all_results(t_client, runs)
356 table = get_defects_table(j_client, results, sort_by)
357 out_table(o_type, table)
358 if push_report_flag:
359 push_report(t_client, t_plan, table)
360
361
362COMMAND_MAP = {
363 'create-report': create_report
364}
365
366
367def main():
368 args = run_cli()
369 COMMAND_MAP[args.command](**vars(args))
370
Dina Belovae6fdffb2017-09-19 13:58:34 -0700371
Dmitry Tyzhnenkofb0516d2016-11-17 17:55:26 +0200372if __name__ == '__main__':
373 main()