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