blob: fe1322bb0c7a5c8c9e5b49b36e19df43f4935f16 [file] [log] [blame]
Pavel Glazov21812c32024-05-03 04:50:17 -07001#!/usr/bin/env python
2import datetime
3import sys
4import logging
5from collections import defaultdict, OrderedDict
6import jira
7import ipdb
8import argparse
9from testrail import TestRail
10from testrail.test import Test
11from functools import lru_cache
12
13logging.basicConfig(format="%(levelname)s:%(message)s", level=logging.INFO)
14LOG = logging.getLogger(__name__)
15
16
17def run_cli():
18 cli = argparse.ArgumentParser(
19 prog="Report generator",
20 description="Command line tool for generate summary report",
21 )
22 commands = cli.add_subparsers(title="Operation commands", dest="command")
23 cli_process = commands.add_parser(
24 "create-report",
25 help="Create summary report",
26 description="Create summary report",
27 )
28 cli_process_link = commands.add_parser(
29 "mark-fails",
30 help="Extract linked bugs from previous reports",
31 description="Extract linked bugs from previous reports"
32 " and mark current",
33 )
34 cli_process.add_argument(
35 "-T",
36 "--testrail-host",
37 dest="testrail_host",
38 required=True,
39 help="TestRail hostname",
40 )
41 cli_process.add_argument(
42 "-U",
43 "--testrail-user",
44 dest="testrail_user",
45 required=True,
46 help="TestRail user email",
47 )
48 cli_process.add_argument(
49 "-K",
50 "--testrail-user-key",
51 dest="testrail_user_key",
52 required=True,
53 help="TestRail user key",
54 )
55 cli_process.add_argument(
56 "-R",
57 "--testrail-plan",
58 dest="testrail_plan",
59 required=True,
60 help="TestRail test plan for analize",
61 )
62 cli_process.add_argument(
63 "-P",
64 "--testrail-project",
65 dest="testrail_project",
66 required=True,
67 help="TestRail project name",
68 )
69 cli_process.add_argument(
70 "--testrail-only-run",
71 dest="testrail_only_run",
72 help="Analize only one run in selected plan",
73 )
74 cli_process.add_argument(
75 "--out-type",
76 dest="out_type",
77 choices=["text", "html", "md", "none"],
78 default="none",
79 help="Select output format for report table. "
80 "By default print nothing (none).",
81 )
82 cli_process.add_argument(
83 "--sort-by",
84 dest="sort_by",
85 default="fails",
86 choices=["fails", "blocks", "project", "priority", "status"],
87 help="Select sorting column. By deafult table sort by fails",
88 )
89 cli_process.add_argument(
90 "--push-to-testrail",
91 dest="push_report_flag",
92 action="store_true",
93 default=False,
94 help="Save report in plan description",
95 )
96 cli_process.add_argument(
97 "-j", "--jira-host", dest="jira_host", required=True,
98 help="JIRA hostname"
99 )
100 cli_process.add_argument(
101 "-u", "--jira-user", dest="jira_user_id", required=True,
102 help="JIRA username"
103 )
104 cli_process.add_argument(
105 "-p",
106 "--jira-password",
107 dest="jira_user_password",
108 required=True,
109 help="JIRA user password",
110 )
111 # link fail bugs parameters
112 cli_process_link.add_argument(
113 "-T",
114 "--testrail-host",
115 dest="testrail_host",
116 required=True,
117 help="TestRail hostname",
118 )
119 cli_process_link.add_argument(
120 "-U",
121 "--testrail-user",
122 dest="testrail_user",
123 required=True,
124 help="TestRail user email",
125 )
126 cli_process_link.add_argument(
127 "-K",
128 "--testrail-user-key",
129 dest="testrail_user_key",
130 required=True,
131 help="TestRail user key",
132 )
133 cli_process_link.add_argument(
134 "-R",
135 "--testrail-plan",
136 dest="testrail_plan",
137 required=True,
138 help="TestRail test plan for analize",
139 )
140 cli_process_link.add_argument(
141 "-M",
142 "--testrail-marked-plan",
143 dest="testrail_marked_plan",
144 required=False,
145 help="TestRail test plan for parse",
146 )
147 cli_process_link.add_argument(
148 "-P",
149 "--testrail-project",
150 dest="testrail_project",
151 required=True,
152 help="TestRail project name",
153 )
154 cli_process_link.add_argument(
155 "--testrail-only-run",
156 dest="testrail_only_run",
157 help="Name to update only specified run in selected plan",
158 )
159 cli_process_link.add_argument(
160 "--push-to-testrail",
161 dest="update_report_flag",
162 action="store_true",
163 default=False,
164 help="Save report in plan description",
165 )
166 if len(sys.argv) == 1:
167 cli.print_help()
168 sys.exit(1)
169 return cli.parse_args()
170
171
172def get_runs(t_client, plan_name, run_name):
173 LOG.info("Get runs from plan - {}".format(plan_name))
174 ret = []
175 plan = t_client.plan(plan_name)
176 if plan:
177 for e in plan.entries:
178 for r in e.runs:
179 LOG.info("Run {} #{}".format(r.name, r.id))
180 if run_name is not None and r.name != run_name:
181 continue
182 ret.append(r)
183 else:
184 LOG.warning("Plan {} is empty".format(plan_name))
185 return ret
186
187
188def get_all_results(t_client, list_of_runs):
189 ret = []
190 for run in list_of_runs:
191 ret.extend(get_results(t_client, run))
192 return ret
193
194
195def get_all_failed_results(t_client, list_of_runs, result_type):
196 """
197 returned result format:
198 [[run(id,name), result(id,status,defects...), test(id,name..)],
199 [run(id,name), result(id,status,defects...), test(id,name..)],
200 ...]
201 """
202 ret = []
203 for run in list_of_runs:
204 ret.extend(get_failed_results(t_client, run, result_type))
205 return ret
206
207
208@lru_cache()
209def fetch_test(api, test_id, run_id):
210 return Test(api.test_with_id(test_id, run_id))
211
212
213def get_results(t_client, run):
214 LOG.info("Get results for run - {}".format(run.name))
215 results = t_client.results(run)
216 ret = [
217 (run.id, r)
218 for r in results
219 if r.raw_data()["status_id"] is not None
220 and r.raw_data()["defects"] is not None
221 ]
222 for r in ret:
223 run_id, result = r
224 test = fetch_test(result.api, result.raw_data()["test_id"], run_id)
225 LOG.info(
226 "Test {} - {} - {}".format(
227 test.title, result.status.name, ",".join(result.defects)
228 )
229 )
230 return ret
231
232
233def get_failed_results(t_client, run, result_type):
234 """
235 returned result format:
236 [run(id,name),
237 result(id,status,defects...),
238 test(id,name..)]
239 """
240 LOG.info("Get results for run - {}".format(run.name))
241 results = t_client.results(run)
242 results_with_test = []
243 if result_type == "5":
244 ret = [
245 (run, r)
246 for r in results
247 if r.raw_data()["status_id"] is int(result_type)
248 and r.raw_data()["defects"] is None
249 ]
250 else:
251 ret = [
252 (run, r)
253 for r in results
254 if r.raw_data()["status_id"] is not None
255 and r.raw_data()["defects"] is not None
256 ]
257 for r in ret:
258 run, result = r
259 test = fetch_test(result.api, result.raw_data()["test_id"], run.id)
260 LOG.info(
261 "Test {} - {} - {} - {}".format(
262 test.title,
263 result.status.name,
264 result.raw_data()["status_id"],
265 ",".join(result.defects),
266 )
267 )
268 results_with_test.append([run, result, test])
269 return results_with_test
270
271
272def mark_failed_results(t_cl, marked_res, failed_res, t_h):
273 """
274 Extract list tests with defect and compare it with tests to be marked,
275 and add defects and result from marked tests
276 Returned result format:
277 [[target_tests_to_update_with_defect, target_run_id],
278 [target_tests_to_update_with_defect, target_run_id],
279 ...]
280 """
281 LOG.info("Extract marked tests and attach to failed")
282
283 def generate_result(t_c, tst, m_r, m_t):
284 link_comment = "{url}/index.php?/tests/view/{uid}".format(
285 url=t_h,
286 uid=m_t.id)
287 tmp_result = t_c.result()
288 tmp_result.test = tst
289 tmp_result.status = m_r.status
290 tmp_result.comment = "Result taked from: " + link_comment
291 tmp_result.defects = [str(m_r.defects[0])]
292 return tmp_result
293
294 # def check_if_marked():
295 # if ret.count()
296 ret = []
297 for run, result, test in failed_res:
298 for m_run, m_result, m_test in marked_res:
299 if run.name == m_run.name and test.title == m_test.title:
300 LOG.info(
301 " MARKED FOUND: Run:{} test: .. {}-{}".format(
302 run.id, test.title[-72:], m_result.defects[0]
303 )
304 )
305 ret.append([generate_result(t_cl,
306 test,
307 m_result,
308 m_test),
309 run.id])
310 return ret
311
312
313@lru_cache()
314def get_defect_info(j_client, defect):
315 LOG.info("Get info about issue {}".format(defect))
316 try:
317 issue = j_client.issue(defect)
318 except jira.exceptions.JIRAError as e:
319 if e.status_code == 404:
320 LOG.error("Defect {} wasn't found in Jira".format(defect))
321 return {
322 "id": defect,
323 "title": "Title for #{} not found".format(defect),
324 "project": "Not found",
325 "priority": "Not found",
326 "status": "Not found",
327 "url": "Not found",
328 }
329 else:
330 raise
331 return {
332 "id": issue.key,
333 "title": issue.fields.summary,
334 "project": issue.fields.project.key,
335 "priority": issue.fields.priority.name,
336 "status": issue.fields.status.name,
337 "url": issue.permalink(),
338 }
339
340
341def get_defects_table(jira_client, list_of_results, sort_by):
342 LOG.info("Collect report table")
343 table = defaultdict(dict)
344 for run_id, result in list_of_results:
345 for defect in result.defects:
346 if defect not in table:
347 info = get_defect_info(jira_client, defect)
348 table[defect].update(info)
349 table[defect]["results"] = set([(run_id, result)])
350 if result.status.name.lower() == "blocked":
351 table[defect]["blocks"] = 1
352 table[defect]["fails"] = 0
353 else:
354 table[defect]["fails"] = 1
355 table[defect]["blocks"] = 0
356 else:
357 table[defect]["results"].add((run_id, result))
358 if result.status.name.lower() == "blocked":
359 table[defect]["blocks"] += 1
360 else:
361 table[defect]["fails"] += 1
362 return OrderedDict(sorted(table.items(),
363 key=lambda i: i[1][sort_by],
364 reverse=True))
365
366
367def get_text_table(table):
368 LOG.info("Generation text table")
369 lines = []
370 line = (
371 "{fails:^5} | {blocks:^5} | {project:^10} | {priority:^15} | "
372 "{status:^15} | {bug:^100} | {tests} "
373 )
374
375 def title_uid(r):
376 run_id, result = r
377 test = fetch_test(result.api, result.raw_data()["test_id"], run_id)
378 return {"title": test.title, "uid": test.id}
379
380 def list_of_defect_tests(results):
381 ret = ["[{title} #{uid}]".format(**title_uid(r)) for r in results]
382 return " ".join(ret)
383
384 lines.append(
385 line.format(
386 fails="FAILS",
387 blocks="BLOCKS",
388 project="PROJECT",
389 priority="PRIORITY",
390 status="STATUS",
391 bug="BUG",
392 tests="TESTS",
393 )
394 )
395 for k in table:
396 one = table[k]
397 data = {
398 "fails": one["fails"],
399 "blocks": one["blocks"],
400 "project": one["project"],
401 "priority": one["priority"],
402 "status": one["status"],
403 "bug": "{uid} {title}".format(uid=one["id"], title=one["title"]),
404 "tests": list_of_defect_tests(one["results"]),
405 }
406 lines.append(line.format(**data))
407 return "\n".join(lines)
408
409
410def get_md_table(table):
411 LOG.info("Generation MD table")
412 lines = []
413 line = (
414 "||{fails} | {blocks} | {priority} | "
415 "{status} | <div style='width:200px'>{bug}</div> | {tests} |"
416 )
417
418 def title_uid_link(r):
419 run_id, result = r
420 test = fetch_test(result.api, result.raw_data()["test_id"], run_id)
421 return {
422 "title": test.title.replace("[", "{").replace("]", "}"),
423 "uid": test.id,
424 "link": "{url}/index.php?/tests/view/{uid}".format(
425 url=test.api._conf()["url"], uid=test.id
426 ),
427 }
428
429 def list_of_defect_tests(results):
430 ret = [
431 "<[{title} #{uid}]({link})>".format(**title_uid_link(r))
432 for r in results
433 ]
434 return " ".join(ret)
435
436 lines.append(
437 line.format(
438 fails="|:FAILS",
439 blocks=":BLOCKS",
440 project=":PROJECT",
441 priority=":PRIORITY",
442 status=":STATUS",
443 bug=":BUG",
444 tests=":TESTS",
445 )
446 )
447 for k in table:
448 one = table[k]
449 data = {
450 "fails": one["fails"],
451 "blocks": one["blocks"],
452 "project": one["project"],
453 "priority": one["priority"],
454 "status": one["status"],
455 "bug": "[{uid} {title}]({url})".format(
456 uid=one["id"],
457 title=one["title"].replace("[", "{").replace("]", "}"),
458 url=one["url"],
459 ),
460 "tests": list_of_defect_tests(one["results"]),
461 }
462 lines.append(line.format(**data))
463 return "\n".join(lines)
464
465
466def get_html_table(table):
467 LOG.info("Generation HTML table")
468 html = "<table>{lines}</table>"
469 lines = []
470 line = (
471 "<tr><th>{fails:^5}</th><th>{blocks:^5}</th><th>{project:^10}</th>"
472 "<th>{priority:^15}</th>"
473 "<th>{status:^15}</th><th>{bug:^100}</th><th>{tests}</th></tr>"
474 )
475 lines.append(
476 line.format(
477 fails="FAILS",
478 blocks="BLOCKS",
479 project="PROJECT",
480 priority="PRIORITY",
481 status="STATUS",
482 bug="BUG",
483 tests="TESTS",
484 )
485 )
486
487 def title_uid_link(r):
488 run_id, result = r
489 test = fetch_test(result.api, result.raw_data()["test_id"], run_id)
490 return {
491 "title": test.title,
492 "uid": test.id,
493 "link": "{url}/index.php?/tests/view/{uid}".format(
494 url=test.api._conf()["url"], uid=test.id
495 ),
496 }
497
498 def list_of_defect_tests(results):
499 ret = [
500 "<a href='{link}'>{title} #{uid}</a>".format(**title_uid_link(r))
501 for r in results
502 ]
503 return " ".join(ret)
504
505 for k in table:
506 one = table[k]
507 data = {
508 "fails": one["fails"],
509 "blocks": one["blocks"],
510 "project": one["project"],
511 "priority": one["priority"],
512 "status": one["status"],
513 "bug": "<a href='{url}'>{uid} {title}</a>".format(
514 uid=one["id"], title=one["title"], url=one["url"]
515 ),
516 "tests": list_of_defect_tests(one["results"]),
517 }
518 lines.append(line.format(**data))
519 return html.format(lines="".join(lines))
520
521
522def out_table(out_type, table):
523 if out_type == "none":
524 return
525 elif out_type == "html":
526 print(get_html_table(table))
527 elif out_type == "md":
528 print(get_md_table(table))
529 else:
530 print(get_text_table(table))
531
532
533def push_report(t_client, plan_name, table):
534 LOG.info("Push report table into plan - {}".format(plan_name))
535 text = (
536 "Bugs Statistics (generated on {date})\n"
537 "=======================================================\n"
538 "{table}".format(
539 date=datetime.datetime.now().strftime("%a %b %d %H:%M:%S %Y"),
540 table=get_md_table(table),
541 )
542 )
543 plan = t_client.plan(plan_name)
544 if plan:
545 plan.description = text
546 plan.api._post(
547 "update_plan/{}".format(plan.id),
548 {
549 "name": plan.name,
550 "description": plan.description,
551 "milestone_id": plan.milestone.id,
552 },
553 )
554
555
556def update_report(t_client, plan_name, tests_table):
557 LOG.info(
558 "Update report table into plan - {}".format(plan_name)
559 + "\n===\nList tests to udate:"
560 )
561 plan = t_client.plan(plan_name)
562 if plan:
563 for r_test, run in tests_table:
564 t_client.add(r_test)
565 print(r_test.test.title)
566 LOG.info("\n===\nUpdate plan finished - {}".format(plan_name))
567
568
569def create_report(**kwargs):
570 j_host = kwargs.get("jira_host")
571 j_user = kwargs.get("jira_user_id")
572 j_user_pwd = kwargs.get("jira_user_password")
573 t_host = kwargs.get("testrail_host")
574 t_user = kwargs.get("testrail_user")
575 t_user_key = kwargs.get("testrail_user_key")
576 t_plan = kwargs.get("testrail_plan")
577 t_project = kwargs.get("testrail_project")
578 t_a_run = kwargs.get("testrail_only_run")
579 o_type = kwargs.get("out_type")
580 push_report_flag = kwargs.get("push_report_flag")
581 sort_by = kwargs.get("sort_by")
582 t_client = TestRail(email=t_user, key=t_user_key, url=t_host)
583 t_client.set_project_id(t_client.project(t_project).id)
584 j_client = jira.JIRA(j_host, basic_auth=(j_user, j_user_pwd))
585 runs = get_runs(t_client, t_plan, t_a_run)
586 results = get_all_results(t_client, runs)
587 table = get_defects_table(j_client, results, sort_by)
588 out_table(o_type, table)
589 if push_report_flag:
590 push_report(t_client, t_plan, table)
591
592
593def mark_fails(**kwargs):
594 testrail_host = kwargs.get("testrail_host")
595 testrail_user = kwargs.get("testrail_user")
596 testrail_user_key = kwargs.get("testrail_user_key")
597 testrail_plan = kwargs.get("testrail_plan")
598 testrail_m_plan = kwargs.get("testrail_marked_plan")
599 testrail_project = kwargs.get("testrail_project")
600 testrail_active_run = kwargs.get("testrail_only_run")
601 if testrail_active_run == "":
602 testrail_active_run = None
603 update_report_flag = kwargs.get("update_report_flag")
604 testrail_client = TestRail(
605 email=testrail_user, key=testrail_user_key, url=testrail_host
606 )
607 testrail_client.set_project_id(
608 testrail_client.project(testrail_project).id)
609 # Get list runs with marked results
610 marked_runs = get_runs(testrail_client, testrail_m_plan,
611 testrail_active_run)
612 # Get list runs to update
613 runs = get_runs(testrail_client, testrail_plan, testrail_active_run)
614 # Get list (failed, prod_failed, test_failed,skipped..) tests with defects
615 marked_results = get_all_failed_results(
616 testrail_client, marked_runs, "2,3,4,5,6,7,8,9"
617 )
618 # Get list (failed) tests without defects to mark
619 # 5-failed
620 failed_results = get_all_failed_results(testrail_client, runs, "5")
621 # Generate list tests to update based on compare (defected
622 # results for tests with failed and not defected)
623 tests_to_update = mark_failed_results(
624 testrail_client, marked_results, failed_results, testrail_host
625 )
626 if update_report_flag:
627 update_report(testrail_client, testrail_plan, tests_to_update)
628
629
630COMMAND_MAP = {"create-report": create_report, "mark-fails": mark_fails}
631
632
633def main():
634 args = run_cli()
635 COMMAND_MAP[args.command](**vars(args))
636
637
638if __name__ == "__main__":
639 with ipdb.launch_ipdb_on_exception():
640 main()