blob: f08c93334551b288b78a6091c803e4b4b264a2aa [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):
116 _statuses = ('product_failed', 'failed',
117 'prodfailed', 'blocked')
118 LOG.info("Get results for run - {}".format(run.name))
119 results = t_client.results(run)
120 ret = [(run.id, r) for r in results
121 if r.raw_data()['status_id'] is not None and
122 r.raw_data()['defects'] is not None and
123 r.status.name.lower() in _statuses]
124 for r in ret:
125 run_id, result = r
126 test = fetch_test(result.api, result.raw_data()['test_id'], run_id)
127 LOG.info("Test {} - {} - {}".format(test.title, result.status.name,
128 ','.join(result.defects)))
129 return ret
130
131
132@lru_cache()
133def get_defect_info(j_client, defect):
134 LOG.info("Get info about issue {}".format(defect))
135 try:
136 issue = j_client.issue(defect)
137 except jira.exceptions.JIRAError as e:
138 if e.status_code == 404:
139 LOG.error("Defect {} wasn't found in Jira".format(defect))
140 return {
141 'id': defect,
142 'title': "Title for #{} not found".format(defect),
143 'project': "Not found",
144 'priority': "Not found",
145 'status': "Not found",
146 'url': "Not found"
147 }
148 else:
149 raise
150 return {
151 'id': issue.key,
152 'title': issue.fields.summary,
153 'project': issue.fields.project.key,
154 'priority': issue.fields.priority.name,
155 'status': issue.fields.status.name,
156 'url': issue.permalink()
157 }
158
159
160def get_defects_table(jira_client, list_of_results, sort_by):
161 LOG.info("Collect report table")
162 table = defaultdict(dict)
163 for run_id, result in list_of_results:
164 for defect in result.defects:
165 if defect not in table:
166 info = get_defect_info(jira_client, defect)
167
168 table[defect].update(info)
169 table[defect]['results'] = set([(run_id, result)])
170 if result.status.name.lower() == 'blocked':
171 table[defect]['blocks'] = 1
172 table[defect]['fails'] = 0
173 else:
174 table[defect]['fails'] = 1
175 table[defect]['blocks'] = 0
176 else:
177 table[defect]['results'].add((run_id, result))
178 if result.status.name.lower() == 'blocked':
179 table[defect]['blocks'] += 1
180 else:
181 table[defect]['fails'] += 1
182 return OrderedDict(
183 sorted(table.items(), key=lambda i: i[1][sort_by], reverse=True))
184
185
186def get_text_table(table):
187 LOG.info("Generation text table")
188 lines = []
189 line = ("{fails:^5} | {blocks:^5} | {project:^10} | {priority:^15} | "
190 "{status:^15} | {bug:^100} | {tests} ")
191
192 def title_uid(r):
193 run_id, result = r
194 test = fetch_test(result.api, result.raw_data()['test_id'], run_id)
195 return {
196 "title": test.title,
197 "uid": test.id}
198
199 def list_of_defect_tests(results):
200 ret = ["[{title} #{uid}]".format(**title_uid(r)) for
201 r in results]
202 return ' '.join(ret)
203
204 lines.append(line.format(fails='FAILS', blocks='BLOCKS', project="PROJECT",
205 priority="PRIORITY", status="STATUS", bug="BUG",
206 tests="TESTS"))
207 for k in table:
208 one = table[k]
209 data = {
210 "fails": one['fails'],
211 "blocks": one['blocks'],
212 "project": one['project'],
213 "priority": one['priority'],
214 "status": one['status'],
215 "bug": "{uid} {title}".format(uid=one['id'], title=one['title']),
216 "tests": list_of_defect_tests(one['results'])
217 }
218 lines.append(line.format(**data))
219 return '\n'.join(lines)
220
221
222def get_md_table(table):
223 LOG.info("Generation MD table")
224 lines = []
225 line = ("||{fails} | {blocks} | {project} | {priority} | "
226 "{status} | {bug} | {tests} ")
227
228 def title_uid_link(r):
229 run_id, result = r
230 test = fetch_test(result.api, result.raw_data()['test_id'], run_id)
231
232 return {
233 "title": test.title.replace('[', '{').replace(']', '}'),
234 "uid": test.id,
235 "link": "{url}/index.php?/tests/view/{uid}".format(
236 url=test.api._conf()['url'], uid=test.id)
237 }
238
239 def list_of_defect_tests(results):
240 ret = ["<[{title} #{uid}]({link})>".format(**title_uid_link(r))
241 for r in results]
242 return ' '.join(ret)
243
244 lines.append(line.format(fails='|:FAILS', blocks=':BLOCKS',
245 project=":PROJECT", priority=":PRIORITY",
246 status=":STATUS", bug=":BUG", tests=":TESTS"))
247 for k in table:
248 one = table[k]
249 data = {
250 "fails": one['fails'],
251 "blocks": one['blocks'],
252 "project": one['project'],
253 "priority": one['priority'],
254 "status": one['status'],
255 "bug": "[{uid} {title}]({url})".format(
256 uid=one['id'],
257 title=one['title'].replace('[', '{').replace(']', '}'),
258 url=one['url']),
259 "tests": list_of_defect_tests(one['results'])
260 }
261 lines.append(line.format(**data))
262 return '\n'.join(lines)
263
264
265def get_html_table(table):
266 LOG.info("Generation HTML table")
267 html = "<table>{lines}</table>"
268 lines = []
269 line = ("<tr><th>{fails:^5}</th><th>{blocks:^5}</th><th>{project:^10}</th>"
270 "<th>{priority:^15}</th>"
271 "<th>{status:^15}</th><th>{bug:^100}</th><th>{tests}</th></tr>")
272 lines.append(line.format(fails='FAILS', blocks='BLOCKS', project="PROJECT",
273 priority="PRIORITY", status="STATUS", bug="BUG",
274 tests="TESTS"))
275
276 def title_uid_link(r):
277 run_id, result = r
278 test = fetch_test(result.api, result.raw_data()['test_id'], run_id)
279
280 return {
281 "title": test.title,
282 "uid": test.id,
283 "link": "{url}/index.php?/tests/view/{uid}".format(
284 url=test.api._conf()['url'], uid=test.id)
285 }
286
287 def list_of_defect_tests(results):
288 ret = ["<a href='{link}'>{title} #{uid}</a>".format(
289 **title_uid_link(r)) for r in results]
290 return ' '.join(ret)
291
292 for k in table:
293 one = table[k]
294 data = {
295 "fails": one['fails'],
296 "blocks": one['blocks'],
297 "project": one['project'],
298 "priority": one['priority'],
299 "status": one['status'],
300 "bug": "<a href='{url}'>{uid} {title}</a>".format(
301 uid=one['id'], title=one['title'], url=one['url']),
302 "tests": list_of_defect_tests(one['results'])
303 }
304 lines.append(line.format(**data))
305 return html.format(lines=''.join(lines))
306
307
308def out_table(out_type, table):
309 if out_type == 'none':
310 return
311 elif out_type == 'html':
312 print(get_html_table(table))
313 elif out_type == 'md':
314 print(get_md_table(table))
315 else:
316 print(get_text_table(table))
317
318
319def push_report(t_client, plan_name, table):
320 LOG.info("Push report table into plan - {}".format(plan_name))
321 text = "Bugs Statistics (generated on {date})\n" \
322 "=======================================================\n" \
323 "{table}".format(
324 date=datetime.datetime.now().strftime("%a %b %d %H:%M:%S %Y"),
325 table=get_md_table(table))
326 plan = t_client.plan(plan_name)
327 plan.description = text
328 plan.api._post(
329 'update_plan/{}'.format(plan.id),
330 {
331 'name': plan.name,
332 'description': plan.description,
333 'milestone_id': plan.milestone.id
334 })
335
336
337def create_report(**kwargs):
338 j_host = kwargs.get('jira_host')
339 j_user = kwargs.get('jira_user_id')
340 j_user_pwd = kwargs.get('jira_user_password')
341 t_host = kwargs.get('testrail_host')
342 t_user = kwargs.get('testrail_user')
343 t_user_key = kwargs.get('testrail_user_key')
344 t_plan = kwargs.get('testrail_plan')
345 t_project = kwargs.get('testrail_project')
346 t_a_run = kwargs.get('testrail_only_run')
347 o_type = kwargs.get('out_type')
348 push_report_flag = kwargs.get('push_report_flag')
349 sort_by = kwargs.get('sort_by')
350
351 t_client = TestRail(email=t_user, key=t_user_key, url=t_host)
352 t_client.set_project_id(t_client.project(t_project).id)
353
354 j_client = jira.JIRA(j_host, basic_auth=(j_user, j_user_pwd))
355
356 runs = get_runs(t_client, t_plan, t_a_run)
357 results = get_all_results(t_client, runs)
358 table = get_defects_table(j_client, results, sort_by)
359 out_table(o_type, table)
360 if push_report_flag:
361 push_report(t_client, t_plan, table)
362
363
364COMMAND_MAP = {
365 'create-report': create_report
366}
367
368
369def main():
370 args = run_cli()
371 COMMAND_MAP[args.command](**vars(args))
372
Dina Belovae6fdffb2017-09-19 13:58:34 -0700373
Dmitry Tyzhnenkofb0516d2016-11-17 17:55:26 +0200374if __name__ == '__main__':
375 main()