1#!/usr/bin/env python3 2# coding=utf-8 3 4# 5# Copyright (c) 2020-2021 Huawei Device Co., Ltd. 6# Licensed under the Apache License, Version 2.0 (the "License"); 7# you may not use this file except in compliance with the License. 8# You may obtain a copy of the License at 9# 10# http://www.apache.org/licenses/LICENSE-2.0 11# 12# Unless required by applicable law or agreed to in writing, software 13# distributed under the License is distributed on an "AS IS" BASIS, 14# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15# See the License for the specific language governing permissions and 16# limitations under the License. 17# 18 19import os 20import platform 21import time 22from ast import literal_eval 23from dataclasses import dataclass 24from xml.etree import ElementTree 25 26from _core.logger import platform_logger 27from _core.report.encrypt import check_pub_key_exist 28from _core.report.encrypt import do_rsa_encrypt 29from _core.exception import ParamError 30 31LOG = platform_logger("ReporterHelper") 32 33 34@dataclass 35class ReportConstant: 36 # report name constants 37 summary_data_report = "summary_report.xml" 38 summary_vision_report = "summary_report.html" 39 details_vision_report = "details_report.html" 40 failures_vision_report = "failures_report.html" 41 task_info_record = "task_info.record" 42 summary_ini = "summary.ini" 43 summary_report_hash = "summary_report.hash" 44 title_name = "title_name" 45 summary_title = "Summary Report" 46 details_title = "Details Report" 47 failures_title = "Failures Report" 48 49 # exec_info constants 50 platform = "platform" 51 test_type = "test_type" 52 device_name = "device_name" 53 host_info = "host_info" 54 test_time = "test_time" 55 log_path = "log_path" 56 log_path_title = "Log Path" 57 execute_time = "execute_time" 58 59 # summary constants 60 product_info = "productinfo" 61 product_info_ = "product_info" 62 modules = "modules" 63 run_modules = "runmodules" 64 run_modules_ = "run_modules" 65 name = "name" 66 time = "time" 67 total = "total" 68 tests = "tests" 69 passed = "passed" 70 errors = "errors" 71 disabled = "disabled" 72 failures = "failures" 73 blocked = "blocked" 74 ignored = "ignored" 75 unavailable = "unavailable" 76 not_run = "notrun" 77 message = "message" 78 79 # case result constants 80 module_name = "modulename" 81 module_name_ = "module_name" 82 result = "result" 83 status = "status" 84 run = "run" 85 true = "true" 86 false = "false" 87 skip = "skip" 88 disable = "disable" 89 class_name = "classname" 90 level = "level" 91 empty_name = "-" 92 93 # time constants 94 time_stamp = "timestamp" 95 start_time = "starttime" 96 end_time = "endtime" 97 time_format = "%Y-%m-%d %H:%M:%S" 98 99 # xml tag constants 100 test_suites = "testsuites" 101 test_suite = "testsuite" 102 test_case = "testcase" 103 104 # report title constants 105 failed = "failed" 106 error = "error" 107 color_normal = "color-normal" 108 color_failed = "color-failed" 109 color_blocked = "color-blocked" 110 color_ignored = "color-ignored" 111 color_unavailable = "color-unavailable" 112 113 114class DataHelper: 115 LINE_BREAK = "\n" 116 LINE_BREAK_INDENT = "\n " 117 INDENT = " " 118 DATA_REPORT_SUFFIX = ".xml" 119 120 def __init__(self): 121 pass 122 123 @staticmethod 124 def parse_data_report(data_report): 125 if "<" not in data_report and os.path.exists(data_report): 126 with open(data_report, 'r', encoding='UTF-8', errors="ignore") as \ 127 file_content: 128 data_str = file_content.read() 129 else: 130 data_str = data_report 131 132 for char_index in range(32): 133 if char_index in [10, 13]: # chr(10): LF, chr(13): CR 134 continue 135 data_str = data_str.replace(chr(char_index), "") 136 try: 137 return ElementTree.fromstring(data_str) 138 except SyntaxError as error: 139 LOG.error("%s %s", data_report, error.args) 140 return ElementTree.Element("empty") 141 142 @staticmethod 143 def set_element_attributes(element, element_attributes): 144 for key, value in element_attributes.items(): 145 element.set(key, str(value)) 146 147 @classmethod 148 def initial_element(cls, tag, tail, text): 149 element = ElementTree.Element(tag) 150 element.tail = tail 151 element.text = text 152 return element 153 154 def initial_suites_element(self): 155 return self.initial_element(ReportConstant.test_suites, 156 self.LINE_BREAK, self.LINE_BREAK_INDENT) 157 158 def initial_suite_element(self): 159 return self.initial_element(ReportConstant.test_suite, 160 self.LINE_BREAK_INDENT, 161 self.LINE_BREAK_INDENT + self.INDENT) 162 163 def initial_case_element(self): 164 return self.initial_element(ReportConstant.test_case, 165 self.LINE_BREAK_INDENT + self.INDENT, "") 166 167 @classmethod 168 def update_suite_result(cls, suite, case): 169 update_time = round(float(suite.get( 170 ReportConstant.time, 0)) + float( 171 case.get(ReportConstant.time, 0)), 3) 172 suite.set(ReportConstant.time, str(update_time)) 173 update_tests = str(int(suite.get(ReportConstant.tests, 0))+1) 174 suite.set(ReportConstant.tests, update_tests) 175 if case.findall('failure'): 176 update_failures = str(int(suite.get(ReportConstant.failures, 0))+1) 177 suite.set(ReportConstant.failures, update_failures) 178 179 @classmethod 180 def get_summary_result(cls, report_path, file_name, key=None, **kwargs): 181 reverse = kwargs.get("reverse", False) 182 file_prefix = kwargs.get("file_prefix", None) 183 data_reports = cls._get_data_reports(report_path, file_prefix) 184 if not data_reports: 185 return 186 if key: 187 data_reports.sort(key=key, reverse=reverse) 188 summary_result = None 189 need_update_attributes = [ReportConstant.tests, ReportConstant.errors, 190 ReportConstant.failures, 191 ReportConstant.disabled, 192 ReportConstant.unavailable] 193 for data_report in data_reports: 194 data_report_element = cls.parse_data_report(data_report) 195 if not len(list(data_report_element)): 196 continue 197 if not summary_result: 198 summary_result = data_report_element 199 continue 200 if not summary_result or not data_report_element: 201 continue 202 for data_suite in data_report_element: 203 for summary_suite in summary_result: 204 if data_suite.get("name", None) == \ 205 summary_suite.get("name", None): 206 for data_case in data_suite: 207 for summary_case in summary_suite: 208 if data_case.get("name", None) == \ 209 summary_case.get("name", None): 210 break 211 else: 212 summary_suite.append(data_case) 213 DataHelper.update_suite_result(summary_result, 214 data_case) 215 DataHelper.update_suite_result(summary_suite, 216 data_case) 217 break 218 else: 219 summary_result.append(data_suite) 220 DataHelper._update_attributes(summary_result, data_suite, 221 need_update_attributes) 222 if summary_result: 223 cls.generate_report(summary_result, file_name) 224 return summary_result 225 226 @classmethod 227 def _get_data_reports(cls, report_path, file_prefix=None): 228 if not os.path.isdir(report_path): 229 return [] 230 data_reports = [] 231 for root, _, files in os.walk(report_path): 232 for file_name in files: 233 if not file_name.endswith(cls.DATA_REPORT_SUFFIX): 234 continue 235 if file_prefix and not file_name.startswith(file_prefix): 236 continue 237 data_reports.append(os.path.join(root, file_name)) 238 return data_reports 239 240 @classmethod 241 def _update_attributes(cls, summary_element, data_element, 242 need_update_attributes): 243 for attribute in need_update_attributes: 244 updated_value = int(summary_element.get(attribute, 0)) + \ 245 int(data_element.get(attribute, 0)) 246 summary_element.set(attribute, str(updated_value)) 247 # update time 248 updated_time = round(float(summary_element.get( 249 ReportConstant.time, 0)) + float( 250 data_element.get(ReportConstant.time, 0)), 3) 251 summary_element.set(ReportConstant.time, str(updated_time)) 252 253 @staticmethod 254 def generate_report(element, file_name): 255 if check_pub_key_exist(): 256 plain_text = DataHelper.to_string(element) 257 try: 258 cipher_text = do_rsa_encrypt(plain_text) 259 except ParamError as error: 260 LOG.error(error, error_no=error.error_no) 261 cipher_text = b"" 262 if platform.system() == "Windows": 263 flags = os.O_WRONLY | os.O_CREAT | os.O_APPEND | os.O_BINARY 264 else: 265 flags = os.O_WRONLY | os.O_CREAT | os.O_APPEND 266 file_name_open = os.open(file_name, flags, 0o755) 267 with os.fdopen(file_name_open, "wb") as file_handler: 268 file_handler.write(cipher_text) 269 file_handler.flush() 270 else: 271 tree = ElementTree.ElementTree(element) 272 tree.write(file_name, encoding="UTF-8", xml_declaration=True, 273 short_empty_elements=True) 274 LOG.info("generate data report: %s", file_name) 275 276 @staticmethod 277 def to_string(element): 278 return str( 279 ElementTree.tostring(element, encoding='UTF-8', method='xml'), 280 encoding="UTF-8") 281 282 283@dataclass 284class ExecInfo: 285 keys = [ReportConstant.platform, ReportConstant.test_type, 286 ReportConstant.device_name, ReportConstant.host_info, 287 ReportConstant.test_time, ReportConstant.execute_time] 288 test_type = "" 289 device_name = "" 290 host_info = "" 291 test_time = "" 292 log_path = "" 293 platform = "" 294 execute_time = "" 295 product_info = dict() 296 297 298class Result: 299 300 def __init__(self): 301 self.total = 0 302 self.passed = 0 303 self.failed = 0 304 self.blocked = 0 305 self.ignored = 0 306 self.unavailable = 0 307 308 def get_total(self): 309 return self.total 310 311 def get_passed(self): 312 return self.passed 313 314 315class Summary: 316 keys = [ReportConstant.modules, ReportConstant.total, 317 ReportConstant.passed, ReportConstant.failed, 318 ReportConstant.blocked, ReportConstant.unavailable, 319 ReportConstant.ignored, ReportConstant.run_modules_] 320 321 def __init__(self): 322 self.result = Result() 323 self.modules = None 324 self.run_modules = 0 325 326 def get_result(self): 327 return self.result 328 329 def get_modules(self): 330 return self.modules 331 332 333class Suite: 334 keys = [ReportConstant.module_name_, ReportConstant.name, 335 ReportConstant.total, ReportConstant.passed, 336 ReportConstant.failed, ReportConstant.blocked, 337 ReportConstant.ignored, ReportConstant.time] 338 module_name = ReportConstant.empty_name 339 name = "" 340 time = "" 341 342 def __init__(self): 343 self.message = "" 344 self.result = Result() 345 self.cases = [] # need initial to create new object 346 347 def get_cases(self): 348 return self.cases 349 350 def set_cases(self, element): 351 if len(element) == 0: 352 LOG.debug("%s has no testcase", 353 element.get(ReportConstant.name, "")) 354 return 355 356 # get case context and add to self.cases 357 for child in element: 358 case = Case() 359 case.module_name = self.module_name 360 for key, value in child.items(): 361 setattr(case, key, value) 362 if len(child) > 0: 363 if not getattr(case, ReportConstant.result, ""): 364 setattr(case, ReportConstant.result, ReportConstant.false) 365 message = child[0].get(ReportConstant.message, "") 366 if child[0].text and message != child[0].text: 367 message = "%s\n%s" % (message, child[0].text) 368 setattr(case, ReportConstant.message, message) 369 self.cases.append(case) 370 self.cases.sort(key=lambda x: ( 371 x.is_failed(), x.is_blocked(), x.is_unavailable(), x.is_passed()), 372 reverse=True) 373 374 375class Case: 376 module_name = ReportConstant.empty_name 377 name = ReportConstant.empty_name 378 classname = ReportConstant.empty_name 379 status = "" 380 result = "" 381 message = "" 382 time = "" 383 384 def is_passed(self): 385 if self.result == ReportConstant.true and \ 386 (self.status == ReportConstant.run or self.status == ""): 387 return True 388 if self.result == "" and self.status == ReportConstant.run and \ 389 self.message == "": 390 return True 391 return False 392 393 def is_failed(self): 394 return self.result == ReportConstant.false and \ 395 (self.status == ReportConstant.run or self.status == "") 396 397 def is_blocked(self): 398 return self.status in [ReportConstant.blocked, ReportConstant.disable, 399 ReportConstant.error] 400 401 def is_unavailable(self): 402 return self.status in [ReportConstant.unavailable] 403 404 def is_ignored(self): 405 return self.status in [ReportConstant.skip, ReportConstant.not_run] 406 407 def get_result(self): 408 if self.is_failed(): 409 return ReportConstant.failed 410 if self.is_blocked(): 411 return ReportConstant.blocked 412 if self.is_unavailable(): 413 return ReportConstant.unavailable 414 if self.is_ignored(): 415 return ReportConstant.ignored 416 return ReportConstant.passed 417 418 419@dataclass 420class ColorType: 421 keys = [ReportConstant.failed, ReportConstant.blocked, 422 ReportConstant.ignored, ReportConstant.unavailable] 423 failed = ReportConstant.color_normal 424 blocked = ReportConstant.color_normal 425 ignored = ReportConstant.color_normal 426 unavailable = ReportConstant.color_normal 427 428 429class VisionHelper: 430 PLACE_HOLDER = " " 431 MAX_LENGTH = 50 432 433 def __init__(self): 434 from xdevice import Variables 435 self.summary_element = None 436 self.template_name = os.path.join(Variables.res_dir, "template", 437 "report.html") 438 439 def parse_element_data(self, summary_element, report_path, task_info): 440 self.summary_element = summary_element 441 exec_info = self._set_exec_info(report_path, task_info) 442 suites = self._set_suites_info() 443 summary = self._set_summary_info() 444 return exec_info, summary, suites 445 446 def _set_exec_info(self, report_path, task_info): 447 exec_info = ExecInfo() 448 exec_info.platform = getattr(task_info, ReportConstant.platform, 449 "None") 450 exec_info.test_type = getattr(task_info, ReportConstant.test_type, 451 "Test") 452 exec_info.device_name = getattr(task_info, ReportConstant.device_name, 453 "None") 454 exec_info.host_info = platform.platform() 455 start_time = self.summary_element.get(ReportConstant.start_time, "") 456 if not start_time: 457 start_time = self.summary_element.get("start_time", "") 458 end_time = self.summary_element.get(ReportConstant.end_time, "") 459 if not end_time: 460 end_time = self.summary_element.get("end_time", "") 461 exec_info.test_time = "%s/ %s" % (start_time, end_time) 462 start_time = time.mktime(time.strptime( 463 start_time, ReportConstant.time_format)) 464 end_time = time.mktime(time.strptime( 465 end_time, ReportConstant.time_format)) 466 exec_info.execute_time = self.get_execute_time(round( 467 end_time - start_time, 3)) 468 exec_info.log_path = os.path.abspath(os.path.join(report_path, "log")) 469 470 try: 471 product_info = self.summary_element.get( 472 ReportConstant.product_info, "") 473 if product_info: 474 exec_info.product_info = literal_eval(str(product_info)) 475 except SyntaxError as error: 476 LOG.error("summary report error: %s", error.args) 477 return exec_info 478 479 @classmethod 480 def get_execute_time(cls, second_time): 481 hour, day = 0, 0 482 second, minute = second_time % 60, second_time // 60 483 if minute > 0: 484 minute, hour = minute % 60, minute // 60 485 if hour > 0: 486 hour, day = hour % 24, hour // 24 487 execute_time = "{}sec".format(str(int(second))) 488 if minute > 0: 489 execute_time = "{}min {}".format(str(int(minute)), execute_time) 490 if hour > 0: 491 execute_time = "{}hour {}".format(str(int(hour)), execute_time) 492 if day > 0: 493 execute_time = "{}day {}".format(str(int(day)), execute_time) 494 return execute_time 495 496 def _set_summary_info(self): 497 summary = Summary() 498 summary.modules = self.summary_element.get( 499 ReportConstant.modules, 0) 500 summary.run_modules = self.summary_element.get( 501 ReportConstant.run_modules, 0) 502 summary.result.total = int(self.summary_element.get( 503 ReportConstant.tests, 0)) 504 summary.result.failed = int( 505 self.summary_element.get(ReportConstant.failures, 0)) 506 summary.result.blocked = int( 507 self.summary_element.get(ReportConstant.errors, 0)) + \ 508 int(self.summary_element.get(ReportConstant.disabled, 0)) 509 summary.result.ignored = int( 510 self.summary_element.get(ReportConstant.ignored, 0)) 511 summary.result.unavailable = int( 512 self.summary_element.get(ReportConstant.unavailable, 0)) 513 summary.result.passed = summary.result.total - summary.result.failed \ 514 - summary.result.blocked - summary.result.ignored 515 return summary 516 517 def _set_suites_info(self): 518 suites = [] 519 for child in self.summary_element: 520 suite = Suite() 521 suite.module_name = child.get(ReportConstant.module_name, 522 ReportConstant.empty_name) 523 suite.name = child.get(ReportConstant.name, "") 524 suite.message = child.get(ReportConstant.message, "") 525 suite.result.total = int(child.get(ReportConstant.tests)) if \ 526 child.get(ReportConstant.tests) else 0 527 suite.result.failed = int(child.get(ReportConstant.failures)) if \ 528 child.get(ReportConstant.failures) else 0 529 suite.result.unavailable = int(child.get( 530 ReportConstant.unavailable)) if child.get( 531 ReportConstant.unavailable) else 0 532 errors = int(child.get(ReportConstant.errors)) if child.get( 533 ReportConstant.errors) else 0 534 disabled = int(child.get(ReportConstant.disabled)) if child.get( 535 ReportConstant.disabled) else 0 536 suite.result.ignored = int(child.get(ReportConstant.ignored)) if \ 537 child.get(ReportConstant.ignored) else 0 538 suite.result.blocked = errors + disabled 539 suite.result.passed = suite.result.total - suite.result.failed - \ 540 suite.result.blocked - suite.result.ignored 541 suite.time = child.get(ReportConstant.time, "") 542 suite.set_cases(child) 543 suites.append(suite) 544 suites.sort(key=lambda x: (x.result.failed, x.result.blocked, 545 x.result.unavailable), reverse=True) 546 return suites 547 548 def render_data(self, title_name, parsed_data, 549 render_target=ReportConstant.summary_vision_report): 550 exec_info, summary, suites = parsed_data 551 if not os.path.exists(self.template_name): 552 LOG.error("template file not exists") 553 return "" 554 with open(self.template_name) as file_temp: 555 file_context = file_temp.read() 556 file_context = self._render_key("", ReportConstant.title_name, 557 title_name, file_context) 558 file_context = self._render_exec_info(file_context, exec_info) 559 file_context = self._render_summary(file_context, summary) 560 if render_target == ReportConstant.summary_vision_report: 561 file_context = self._render_suites(file_context, suites) 562 elif render_target == ReportConstant.details_vision_report: 563 file_context = self._render_cases(file_context, suites) 564 elif render_target == ReportConstant.failures_vision_report: 565 file_context = self._render_failure_cases(file_context, suites) 566 else: 567 LOG.error("unsupported vision report type: %s", render_target) 568 return file_context 569 570 @classmethod 571 def _render_key(cls, prefix, key, new_str, update_context): 572 old_str = "<!--{%s%s}-->" % (prefix, key) 573 return update_context.replace(old_str, new_str) 574 575 def _render_exec_info(self, file_context, exec_info): 576 prefix = "exec_info." 577 for key in ExecInfo.keys: 578 value = self._get_hidden_style_value(getattr( 579 exec_info, key, "None")) 580 file_context = self._render_key(prefix, key, value, file_context) 581 file_context = self._render_product_info(exec_info, file_context, 582 prefix) 583 return file_context 584 585 def _render_product_info(self, exec_info, file_context, prefix): 586 """construct product info context and render it to file context 587 588 rendered product info sample: 589 <tr> 590 <td class="normal first">key:</td> 591 <td class="normal second">value</td> 592 <td class="normal third">key:</td> 593 <td class="normal fourth">value</td> 594 </tr> 595 596 Args: 597 exec_info: dict that used to update file_content 598 file_context: exist html content 599 prefix: target replace prefix key 600 601 Returns: 602 updated file context that includes rendered product info 603 """ 604 row_start = True 605 try: 606 keys = list(exec_info.product_info.keys()) 607 except AttributeError: 608 LOG.error("product info error %s", exec_info.product_info) 609 keys = [] 610 611 render_value = "" 612 for key in keys: 613 value = exec_info.product_info[key] 614 if row_start: 615 render_value = "%s<tr>\n" % render_value 616 render_value = "{}{}".format( 617 render_value, self._get_exec_info_td(key, value, row_start)) 618 if not row_start: 619 render_value = "%s</tr>\n" % render_value 620 row_start = not row_start 621 if not row_start: 622 render_value = "%s</tr>\n" % render_value 623 file_context = self._render_key(prefix, ReportConstant.product_info_, 624 render_value, file_context) 625 return file_context 626 627 def _get_exec_info_td(self, key, value, row_start): 628 if not value: 629 value = self.PLACE_HOLDER 630 if key == ReportConstant.log_path_title and row_start: 631 exec_info_td = \ 632 " <td class='normal first'>%s:</td>\n" \ 633 " <td class='normal second' colspan='3'>%s</td>\n" % \ 634 (key, value) 635 return exec_info_td 636 value = self._get_hidden_style_value(value) 637 if row_start: 638 exec_info_td = " <td class='normal first'>%s:</td>\n" \ 639 " <td class='normal second'>%s</td>\n" % \ 640 (key, value) 641 else: 642 exec_info_td = " <td class='normal third'>%s:</td>\n" \ 643 " <td class='normal fourth'>%s</td>\n" % \ 644 (key, value) 645 return exec_info_td 646 647 def _get_hidden_style_value(self, value): 648 if len(value) <= self.MAX_LENGTH: 649 return value 650 return "<div class='hidden' title='%s'>%s</div>" % (value, value) 651 652 def _render_summary(self, file_context, summary): 653 file_context = self._render_data_object(file_context, summary, 654 "summary.") 655 656 # render color type 657 color_type = ColorType() 658 if summary.result.failed != 0: 659 color_type.failed = ReportConstant.color_failed 660 if summary.result.blocked != 0: 661 color_type.blocked = ReportConstant.color_blocked 662 if summary.result.ignored != 0: 663 color_type.ignored = ReportConstant.color_ignored 664 if summary.result.unavailable != 0: 665 color_type.unavailable = ReportConstant.color_unavailable 666 return self._render_data_object(file_context, color_type, 667 "color_type.") 668 669 def _render_data_object(self, file_context, data_object, prefix, 670 default=None): 671 """construct data object context and render it to file context""" 672 if default is None: 673 default = self.PLACE_HOLDER 674 update_context = file_context 675 for key in getattr(data_object, "keys", []): 676 if hasattr(Result(), key) and hasattr( 677 data_object, ReportConstant.result): 678 result = getattr(data_object, ReportConstant.result, Result()) 679 new_str = str(getattr(result, key, default)) 680 else: 681 new_str = str(getattr(data_object, key, default)) 682 update_context = self._render_key(prefix, key, new_str, 683 update_context) 684 return update_context 685 686 def _render_suites(self, file_context, suites): 687 """construct suites context and render it to file context 688 suite record sample: 689 <table class="suites"> 690 <tr> 691 <th class="title" colspan="9">Test detail</th> 692 </tr> 693 <tr> 694 <th class="normal module">Module</th> 695 <th class="normal test-suite">Testsuite</th> 696 <th class="normal total">Total Tests</th> 697 <th class="normal passed">Passed</th> 698 <th class="normal failed">Failed</th> 699 <th class="normal blocked">Blocked</th> 700 <th class="normal ignored">Ignored</th> 701 <th class="normal time">Time</th> 702 <th class="normal operate">Operate</th> 703 </tr> 704 <tr [class="background-color"]> 705 <td class="normal module">{suite.module_name}</td> 706 <td class="normal test-suite">{suite.name}</td> 707 <td class="normal total">{suite.result.total}</td> 708 <td class="normal passed">{suite.result.passed}</td> 709 <td class="normal failed">{suite.result.failed}</td> 710 <td class="normal blocked">{suite.result.blocked}</td> 711 <td class="normal ignored">{suite.result.ignored}</td> 712 <td class="normal time">{suite.time}</td> 713 <td class="normal operate"> 714 <a href="details_report.html#{suite.name}" or 715 "failures_report.html#{suite.name}"> 716 <div class="operate"></div></a> 717 </td> 718 </tr> 719 ... 720 </table> 721 """ 722 replace_str = "<!--{suites.context}-->" 723 724 suites_context = "<table class='suites'>\n" 725 suites_context = "%s%s" % (suites_context, self._get_suites_title()) 726 for index, suite in enumerate(suites): 727 # construct suite context 728 suite_name = getattr(suite, "name", self.PLACE_HOLDER) 729 suite_context = "<tr>\n " if index % 2 == 0 else \ 730 "<tr class='background-color'>\n " 731 for key in Suite.keys: 732 if hasattr(Result(), key): 733 result = getattr(suite, ReportConstant.result, Result()) 734 text = getattr(result, key, self.PLACE_HOLDER) 735 else: 736 text = getattr(suite, key, self.PLACE_HOLDER) 737 suite_context = "{}{}".format( 738 suite_context, self._add_suite_td_context(key, text)) 739 if suite.result.total == 0: 740 href = "%s#%s" % ( 741 ReportConstant.failures_vision_report, suite_name) 742 else: 743 href = "%s#%s" % ( 744 ReportConstant.details_vision_report, suite_name) 745 suite_context = "{}{}".format( 746 suite_context, 747 "<td class='normal operate'><a href='%s'><div class='operate'>" 748 "</div></a></td>\n</tr>\n" % href) 749 # add suite context to suites context 750 suites_context = "{}{}".format(suites_context, suite_context) 751 752 suites_context = "%s</table>\n" % suites_context 753 return file_context.replace(replace_str, suites_context) 754 755 @classmethod 756 def _get_suites_title(cls): 757 suites_title = "<tr>\n" \ 758 " <th class='title' colspan='9'>Test detail</th>\n" \ 759 "</tr>\n" \ 760 "<tr>\n" \ 761 " <th class='normal module'>Module</th>\n" \ 762 " <th class='normal test-suite'>Testsuite</th>\n" \ 763 " <th class='normal total'>Total Tests</th>\n" \ 764 " <th class='normal passed'>Passed</th>\n" \ 765 " <th class='normal failed'>Failed</th>\n" \ 766 " <th class='normal blocked'>Blocked</th>\n" \ 767 " <th class='normal ignored'>Ignored</th>\n" \ 768 " <th class='normal time'>Time</th>\n" \ 769 " <th class='normal operate'>Operate</th>\n" \ 770 "</tr>\n" 771 return suites_title 772 773 @staticmethod 774 def _add_suite_td_context(style, text): 775 if style == ReportConstant.name: 776 style = "test-suite" 777 td_style_class = "normal %s" % style 778 return "<td class='%s'>%s</td>\n " % (td_style_class, str(text)) 779 780 def _render_cases(self, file_context, suites): 781 """construct cases context and render it to file context 782 case table sample: 783 <table class="test-suite"> 784 <tr> 785 <th class="title" colspan="4" id="{suite.name}"> 786 <span class="title">{suite.name} </span> 787 <a href="summary_report.html#summary"> 788 <span class="return"></span></a> 789 </th> 790 </tr> 791 <tr> 792 <th class="normal module">Module</th> 793 <th class="normal test-suite">Testsuite</th> 794 <th class="normal test">Testcase</th> 795 <th class="normal time">Time</th> 796 <th class="normal status"><div class="circle-normal 797 circle-white"></div></th> 798 <th class="normal result">Result</th> 799 </tr> 800 <tr [class="background-color"]> 801 <td class="normal module">{case.module_name}</td> 802 <td class="normal test-suite">{case.classname}</td> 803 <td class="normal test">{case.name}</td> 804 <td class="normal time">{case.time}</td> 805 <td class="normal status"><div class="circle-normal 806 circle-{case.result/status}"></div></td> 807 <td class="normal result"> 808 [<a href="failures_report.html#{suite.name}.{case.name}">] 809 {case.result/status}[</a>]</td> 810 </tr> 811 ... 812 </table> 813 ... 814 """ 815 replace_str = "<!--{cases.context}-->" 816 cases_context = "" 817 for suite in suites: 818 # construct case context 819 suite_name = getattr(suite, "name", self.PLACE_HOLDER) 820 case_context = "<table class='test-suite'>\n" 821 case_context = "{}{}".format(case_context, 822 self._get_case_title(suite_name)) 823 for index, case in enumerate(suite.cases): 824 case_context = "{}{}".format( 825 case_context, 826 self._get_case_td_context(index, case, suite_name)) 827 case_context = "%s</table>\n" % case_context 828 829 # add case context to cases context 830 cases_context = "{}{}".format(cases_context, case_context) 831 return file_context.replace(replace_str, cases_context) 832 833 @classmethod 834 def _get_case_td_context(cls, index, case, suite_name): 835 result = case.get_result() 836 rendered_result = result 837 if result != ReportConstant.passed and \ 838 result != ReportConstant.ignored: 839 rendered_result = "<a href='%s#%s.%s'>%s</a>" % \ 840 (ReportConstant.failures_vision_report, 841 suite_name, case.name, result) 842 case_td_context = "<tr>\n" if index % 2 == 0 else \ 843 "<tr class='background-color'>\n" 844 case_td_context = "{}{}".format( 845 case_td_context, 846 " <td class='normal module'>%s</td>\n" 847 " <td class='normal test-suite'>%s</td>\n" 848 " <td class='normal test'>%s</td>\n" 849 " <td class='normal time'>%s</td>\n" 850 " <td class='normal status'>" 851 "<div class='circle-normal circle-%s'></div></td>\n" 852 " <td class='normal result'>%s</td>\n" 853 "</tr>\n" % (case.module_name, case.classname, case.name, 854 case.time, result, rendered_result)) 855 return case_td_context 856 857 @classmethod 858 def _get_case_title(cls, suite_name): 859 case_title = \ 860 "<tr>\n" \ 861 " <th class='title' colspan='4' id='%s'>\n" \ 862 " <span class='title'>%s </span>\n" \ 863 " <a href='%s#summary'>\n" \ 864 " <span class='return'></span></a>\n" \ 865 " </th>\n" \ 866 "</tr>\n" \ 867 "<tr>\n" \ 868 " <th class='normal module'>Module</th>\n" \ 869 " <th class='normal test-suite'>Testsuite</th>\n" \ 870 " <th class='normal test'>Testcase</th>\n" \ 871 " <th class='normal time'>Time</th>\n" \ 872 " <th class='normal status'><div class='circle-normal " \ 873 "circle-white'></div></th>\n" \ 874 " <th class='normal result'>Result</th>\n" \ 875 "</tr>\n" % (suite_name, suite_name, 876 ReportConstant.summary_vision_report) 877 return case_title 878 879 def _render_failure_cases(self, file_context, suites): 880 """construct failure cases context and render it to file context 881 failure case table sample: 882 <table class="failure-test"> 883 <tr> 884 <th class="title" colspan="4" id="{suite.name}"> 885 <span class="title">{suite.name} </span> 886 <a href="details_report.html#{suite.name}" or 887 "summary_report.html#summary"> 888 <span class="return"></span></a> 889 </th> 890 </tr> 891 <tr> 892 <th class="normal test">Test</th> 893 <th class="normal status"><div class="circle-normal 894 circle-white"></div></th> 895 <th class="normal result">Result</th> 896 <th class="normal details">Details</th> 897 </tr> 898 <tr [class="background-color"]> 899 <td class="normal test" id="{suite.name}"> 900 {suite.module_name}#{suite.name}</td> 901 or 902 <td class="normal test" id="{suite.name}.{case.name}"> 903 {case.module_name}#{case.classname}#{case.name}</td> 904 <td class="normal status"><div class="circle-normal 905 circle-{case.result/status}"></div></td> 906 <td class="normal result">{case.result/status}</td> 907 <td class="normal details">{case.message}</td> 908 </tr> 909 ... 910 </table> 911 ... 912 """ 913 replace_str = "<!--{failures.context}-->" 914 failure_cases_context = "" 915 for suite in suites: 916 if suite.result.total == ( 917 suite.result.passed + suite.result.ignored) and \ 918 suite.result.unavailable == 0: 919 continue 920 921 # construct failure cases context for failure suite 922 suite_name = getattr(suite, "name", self.PLACE_HOLDER) 923 case_context = "<table class='failure-test'>\n" 924 case_context = \ 925 "{}{}".format(case_context, self._get_failure_case_title( 926 suite_name, suite.result.total)) 927 if suite.result.total == 0: 928 case_context = "{}{}".format( 929 case_context, self._get_failure_case_td_context( 930 0, suite, suite_name, ReportConstant.unavailable)) 931 else: 932 skipped_num = 0 933 for index, case in enumerate(suite.cases): 934 result = case.get_result() 935 if result == ReportConstant.passed or \ 936 result == ReportConstant.ignored: 937 skipped_num += 1 938 continue 939 case_context = "{}{}".format( 940 case_context, self._get_failure_case_td_context( 941 index - skipped_num, case, suite_name, result)) 942 943 case_context = "%s</table>\n" % case_context 944 945 # add case context to cases context 946 failure_cases_context = \ 947 "{}{}".format(failure_cases_context, case_context) 948 return file_context.replace(replace_str, failure_cases_context) 949 950 @classmethod 951 def _get_failure_case_td_context(cls, index, case, suite_name, result): 952 failure_case_td_context = "<tr>\n" if index % 2 == 0 else \ 953 "<tr class='background-color'>\n" 954 if result == ReportConstant.unavailable: 955 test_context = "%s#%s" % (case.module_name, case.name) 956 href_id = suite_name 957 else: 958 test_context = \ 959 "%s#%s#%s" % (case.module_name, case.classname, case.name) 960 href_id = "%s.%s" % (suite_name, case.name) 961 details_context = case.message 962 if details_context: 963 details_context = str(details_context).replace("<", "<"). \ 964 replace(">", ">").replace("\\r\\n", "<br/>"). \ 965 replace("\\n", "<br/>").replace("\n", "<br/>"). \ 966 replace(" ", " ") 967 failure_case_td_context = "{}{}".format( 968 failure_case_td_context, 969 " <td class='normal test' id='%s'>%s</td>\n" 970 " <td class='normal status'>" 971 "<div class='circle-normal circle-%s'></div></td>\n" 972 " <td class='normal result'>%s</td>\n" 973 " <td class='normal details'>%s</td>\n" 974 "</tr>\n" % 975 (href_id, test_context, result, result, details_context)) 976 return failure_case_td_context 977 978 @classmethod 979 def _get_failure_case_title(cls, suite_name, total): 980 if total == 0: 981 href = "%s#summary" % ReportConstant.summary_vision_report 982 else: 983 href = "%s#%s" % (ReportConstant.details_vision_report, suite_name) 984 failure_case_title = \ 985 "<tr>\n" \ 986 " <th class='title' colspan='4' id='%s'>\n" \ 987 " <span class='title'>%s </span>\n" \ 988 " <a href='%s'>\n" \ 989 " <span class='return'></span></a>\n" \ 990 " </th>\n" \ 991 "</tr>\n" \ 992 "<tr>\n" \ 993 " <th class='normal test'>Test</th>\n" \ 994 " <th class='normal status'><div class='circle-normal " \ 995 "circle-white'></div></th>\n" \ 996 " <th class='normal result'>Result</th>\n" \ 997 " <th class='normal details'>Details</th>\n" \ 998 "</tr>\n" % (suite_name, suite_name, href) 999 return failure_case_title 1000 1001 @staticmethod 1002 def generate_report(summary_vision_path, report_context): 1003 if platform.system() == "Windows": 1004 flags = os.O_WRONLY | os.O_CREAT | os.O_APPEND | os.O_BINARY 1005 else: 1006 flags = os.O_WRONLY | os.O_CREAT | os.O_APPEND 1007 vision_file_open = os.open(summary_vision_path, flags, 0o755) 1008 vision_file = os.fdopen(vision_file_open, "wb") 1009 if check_pub_key_exist(): 1010 try: 1011 cipher_text = do_rsa_encrypt(report_context) 1012 except ParamError as error: 1013 LOG.error(error, error_no=error.error_no) 1014 cipher_text = b"" 1015 vision_file.write(cipher_text) 1016 else: 1017 vision_file.write(bytes(report_context, "utf-8", "ignore")) 1018 vision_file.flush() 1019 vision_file.close() 1020 LOG.info("generate vision report: %s", summary_vision_path) 1021