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# 18import json 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 35class CaseResult: 36 passed = "Passed" 37 failed = "Failed" 38 blocked = "Blocked" 39 ignored = "Ignored" 40 unavailable = "Unavailable" 41 investigated = "Investigated" 42 43 44@dataclass 45class ReportConstant: 46 # report name constants 47 summary_data_report = "summary_report.xml" 48 summary_vision_report = "summary_report.html" 49 details_vision_report = "details_report.html" 50 failures_vision_report = "failures_report.html" 51 passes_vision_report = "passes_report.html" 52 ignores_vision_report = "ignores_report.html" 53 task_info_record = "task_info.record" 54 summary_ini = "summary.ini" 55 summary_report_hash = "summary_report.hash" 56 title_name = "title_name" 57 summary_title = "Summary Report" 58 details_title = "Details Report" 59 failures_title = "Failures Report" 60 passes_title = "Passes Report" 61 ignores_title = "Ignores Report" 62 63 # exec_info constants 64 platform = "platform" 65 test_type = "test_type" 66 device_name = "device_name" 67 host_info = "host_info" 68 test_time = "test_time" 69 log_path = "log_path" 70 log_path_title = "Log Path" 71 execute_time = "execute_time" 72 device_label = "device_label" 73 74 # summary constants 75 product_info = "productinfo" 76 product_info_ = "product_info" 77 modules = "modules" 78 run_modules = "runmodules" 79 run_modules_ = "run_modules" 80 name = "name" 81 time = "time" 82 total = "total" 83 tests = "tests" 84 passed = "passed" 85 errors = "errors" 86 disabled = "disabled" 87 failures = "failures" 88 blocked = "blocked" 89 ignored = "ignored" 90 completed = "completed" 91 unavailable = "unavailable" 92 not_run = "notrun" 93 message = "message" 94 report = "report" 95 devices = "devices" 96 97 # case result constants 98 module_name = "modulename" 99 module_name_ = "module_name" 100 result = "result" 101 result_kind = "result_kind" 102 status = "status" 103 run = "run" 104 true = "true" 105 false = "false" 106 skip = "skip" 107 disable = "disable" 108 class_name = "classname" 109 level = "level" 110 empty_name = "-" 111 112 # time constants 113 time_stamp = "timestamp" 114 start_time = "starttime" 115 end_time = "endtime" 116 time_format = "%Y-%m-%d %H:%M:%S" 117 118 # xml tag constants 119 test_suites = "testsuites" 120 test_suite = "testsuite" 121 test_case = "testcase" 122 123 # report title constants 124 failed = "failed" 125 error = "error" 126 color_normal = "color-normal" 127 color_failed = "color-failed" 128 color_blocked = "color-blocked" 129 color_ignored = "color-ignored" 130 color_unavailable = "color-unavailable" 131 132 133class DataHelper: 134 LINE_BREAK = "\n" 135 LINE_BREAK_INDENT = "\n " 136 INDENT = " " 137 DATA_REPORT_SUFFIX = ".xml" 138 139 def __init__(self): 140 pass 141 142 @staticmethod 143 def parse_data_report(data_report): 144 if "<" not in data_report and os.path.exists(data_report): 145 with open(data_report, 'r', encoding='UTF-8', errors="ignore") as \ 146 file_content: 147 data_str = file_content.read() 148 else: 149 data_str = data_report 150 151 for char_index in range(32): 152 if char_index in [10, 13]: # chr(10): LF, chr(13): CR 153 continue 154 data_str = data_str.replace(chr(char_index), "") 155 try: 156 return ElementTree.fromstring(data_str) 157 except SyntaxError as error: 158 LOG.error("%s %s", data_report, error.args) 159 return ElementTree.Element("empty") 160 161 @staticmethod 162 def set_element_attributes(element, element_attributes): 163 for key, value in element_attributes.items(): 164 element.set(key, str(value)) 165 166 @classmethod 167 def initial_element(cls, tag, tail, text): 168 element = ElementTree.Element(tag) 169 element.tail = tail 170 element.text = text 171 return element 172 173 def initial_suites_element(self): 174 return self.initial_element(ReportConstant.test_suites, 175 self.LINE_BREAK, self.LINE_BREAK_INDENT) 176 177 def initial_suite_element(self): 178 return self.initial_element(ReportConstant.test_suite, 179 self.LINE_BREAK_INDENT, 180 self.LINE_BREAK_INDENT + self.INDENT) 181 182 def initial_case_element(self): 183 return self.initial_element(ReportConstant.test_case, 184 self.LINE_BREAK_INDENT + self.INDENT, "") 185 186 @classmethod 187 def update_suite_result(cls, suite, case): 188 update_time = round(float(suite.get( 189 ReportConstant.time, 0)) + float( 190 case.get(ReportConstant.time, 0)), 3) 191 suite.set(ReportConstant.time, str(update_time)) 192 update_tests = str(int(suite.get(ReportConstant.tests, 0))+1) 193 suite.set(ReportConstant.tests, update_tests) 194 if case.findall('failure'): 195 update_failures = str(int(suite.get(ReportConstant.failures, 0))+1) 196 suite.set(ReportConstant.failures, update_failures) 197 198 @classmethod 199 def get_summary_result(cls, report_path, file_name, key=None, **kwargs): 200 reverse = kwargs.get("reverse", False) 201 file_prefix = kwargs.get("file_prefix", None) 202 data_reports = cls._get_data_reports(report_path, file_prefix) 203 if not data_reports: 204 return None 205 if key: 206 data_reports.sort(key=key, reverse=reverse) 207 summary_result = None 208 need_update_attributes = [ReportConstant.tests, ReportConstant.errors, 209 ReportConstant.failures, 210 ReportConstant.disabled, 211 ReportConstant.unavailable] 212 for data_report in data_reports: 213 data_report_element = cls.parse_data_report(data_report) 214 if not list(data_report_element): 215 continue 216 if not summary_result: 217 summary_result = data_report_element 218 continue 219 if not summary_result or not data_report_element: 220 continue 221 for data_suite in data_report_element: 222 for summary_suite in summary_result: 223 if data_suite.get("name", None) == \ 224 summary_suite.get("name", None): 225 for data_case in data_suite: 226 for summary_case in summary_suite: 227 if data_case.get("name", None) == \ 228 summary_case.get("name", None): 229 break 230 else: 231 summary_suite.append(data_case) 232 DataHelper.update_suite_result(summary_result, 233 data_case) 234 DataHelper.update_suite_result(summary_suite, 235 data_case) 236 break 237 else: 238 summary_result.append(data_suite) 239 DataHelper._update_attributes(summary_result, data_suite, 240 need_update_attributes) 241 if summary_result: 242 cls.generate_report(summary_result, file_name) 243 return summary_result 244 245 @classmethod 246 def _get_data_reports(cls, report_path, file_prefix=None): 247 if not os.path.isdir(report_path): 248 return [] 249 data_reports = [] 250 for root, _, files in os.walk(report_path): 251 for file_name in files: 252 if not file_name.endswith(cls.DATA_REPORT_SUFFIX): 253 continue 254 if file_prefix and not file_name.startswith(file_prefix): 255 continue 256 data_reports.append(os.path.join(root, file_name)) 257 return data_reports 258 259 @classmethod 260 def _update_attributes(cls, summary_element, data_element, 261 need_update_attributes): 262 for attribute in need_update_attributes: 263 updated_value = int(summary_element.get(attribute, 0)) + \ 264 int(data_element.get(attribute, 0)) 265 summary_element.set(attribute, str(updated_value)) 266 # update time 267 updated_time = round(float(summary_element.get( 268 ReportConstant.time, 0)) + float( 269 data_element.get(ReportConstant.time, 0)), 3) 270 summary_element.set(ReportConstant.time, str(updated_time)) 271 272 @staticmethod 273 def generate_report(element, file_name): 274 if check_pub_key_exist(): 275 plain_text = DataHelper.to_string(element) 276 try: 277 cipher_text = do_rsa_encrypt(plain_text) 278 except ParamError as error: 279 LOG.error(error, error_no=error.error_no) 280 cipher_text = b"" 281 if platform.system() == "Windows": 282 flags = os.O_WRONLY | os.O_CREAT | os.O_APPEND | os.O_BINARY 283 else: 284 flags = os.O_WRONLY | os.O_CREAT | os.O_APPEND 285 file_name_open = os.open(file_name, flags, FilePermission.mode_755) 286 with os.fdopen(file_name_open, "wb") as file_handler: 287 file_handler.write(cipher_text) 288 file_handler.flush() 289 else: 290 tree = ElementTree.ElementTree(element) 291 tree.write(file_name, encoding="UTF-8", xml_declaration=True, 292 short_empty_elements=True) 293 LOG.info("Generate data report: %s", file_name) 294 295 @staticmethod 296 def to_string(element): 297 return str( 298 ElementTree.tostring(element, encoding='UTF-8', method='xml'), 299 encoding="UTF-8") 300 301 302@dataclass 303class ExecInfo: 304 keys = [ReportConstant.platform, ReportConstant.test_type, 305 ReportConstant.device_name, ReportConstant.host_info, 306 ReportConstant.test_time, ReportConstant.execute_time, 307 ReportConstant.device_label] 308 test_type = "" 309 device_name = "" 310 host_info = "" 311 test_time = "" 312 log_path = "" 313 platform = "" 314 execute_time = "" 315 product_info = dict() 316 device_label = "" 317 318 319class Result: 320 321 def __init__(self): 322 self.total = 0 323 self.passed = 0 324 self.failed = 0 325 self.blocked = 0 326 self.ignored = 0 327 self.unavailable = 0 328 329 def get_total(self): 330 return self.total 331 332 def get_passed(self): 333 return self.passed 334 335 336class Summary: 337 keys = [ReportConstant.modules, ReportConstant.total, 338 ReportConstant.passed, ReportConstant.failed, 339 ReportConstant.blocked, ReportConstant.unavailable, 340 ReportConstant.ignored, ReportConstant.run_modules_] 341 342 def __init__(self): 343 self.result = Result() 344 self.modules = None 345 self.run_modules = 0 346 347 def get_result(self): 348 return self.result 349 350 def get_modules(self): 351 return self.modules 352 353 354class Suite: 355 keys = [ReportConstant.module_name_, ReportConstant.name, 356 ReportConstant.time, ReportConstant.total, ReportConstant.passed, 357 ReportConstant.failed, ReportConstant.blocked, ReportConstant.ignored] 358 module_name = ReportConstant.empty_name 359 name = "" 360 time = "" 361 report = "" 362 363 def __init__(self): 364 self.message = "" 365 self.result = Result() 366 self.cases = [] # need initial to create new object 367 368 def get_cases(self): 369 return self.cases 370 371 def set_cases(self, element): 372 if not element: 373 LOG.debug("%s has no testcase", 374 element.get(ReportConstant.name, "")) 375 return 376 377 # get case context and add to self.cases 378 for child in element: 379 case = Case() 380 case.module_name = self.module_name 381 for key, value in child.items(): 382 setattr(case, key, value) 383 if len(child) > 0: 384 if not getattr(case, ReportConstant.result, "") or \ 385 getattr(case, ReportConstant.result, "") == ReportConstant.completed: 386 setattr(case, ReportConstant.result, ReportConstant.false) 387 message = child[0].get(ReportConstant.message, "") 388 if child[0].text and message != child[0].text: 389 message = "%s\n%s" % (message, child[0].text) 390 setattr(case, ReportConstant.message, message) 391 self.cases.append(case) 392 self.cases.sort(key=lambda x: ( 393 x.is_failed(), x.is_blocked(), x.is_unavailable(), x.is_passed()), 394 reverse=True) 395 396 397class Case: 398 module_name = ReportConstant.empty_name 399 name = ReportConstant.empty_name 400 classname = ReportConstant.empty_name 401 status = "" 402 result = "" 403 message = "" 404 time = "" 405 report = "" 406 407 def is_passed(self): 408 if self.result == ReportConstant.true and \ 409 (self.status == ReportConstant.run or self.status == ""): 410 return True 411 if self.result == "" and self.status == ReportConstant.run and \ 412 self.message == "": 413 return True 414 return False 415 416 def is_failed(self): 417 return self.result == ReportConstant.false and \ 418 (self.status == ReportConstant.run or self.status == "") 419 420 def is_blocked(self): 421 return self.status in [ReportConstant.blocked, ReportConstant.disable, 422 ReportConstant.error] 423 424 def is_unavailable(self): 425 return self.status in [ReportConstant.unavailable] 426 427 def is_ignored(self): 428 return self.status in [ReportConstant.skip, ReportConstant.not_run] 429 430 def is_completed(self): 431 return self.result == ReportConstant.completed 432 433 def get_result(self): 434 if self.is_failed(): 435 return ReportConstant.failed 436 if self.is_blocked(): 437 return ReportConstant.blocked 438 if self.is_unavailable(): 439 return ReportConstant.unavailable 440 if self.is_ignored(): 441 return ReportConstant.ignored 442 return ReportConstant.passed 443 444 445@dataclass 446class ColorType: 447 keys = [ReportConstant.failed, ReportConstant.blocked, 448 ReportConstant.ignored, ReportConstant.unavailable] 449 failed = ReportConstant.color_normal 450 blocked = ReportConstant.color_normal 451 ignored = ReportConstant.color_normal 452 unavailable = ReportConstant.color_normal 453 454 455class VisionHelper: 456 PLACE_HOLDER = " " 457 MAX_LENGTH = 50 458 459 def __init__(self): 460 from xdevice import Variables 461 self.summary_element = None 462 self.device_logs = None 463 self.report_path = "" 464 self.template_name = os.path.join(Variables.res_dir, "template", 465 "report.html") 466 467 def parse_element_data(self, summary_element, report_path, task_info): 468 self.summary_element = summary_element 469 exec_info = self._set_exec_info(report_path, task_info) 470 suites = self._set_suites_info() 471 if exec_info.test_type == "SSTS": 472 suites.sort(key=lambda x: x.module_name, reverse=True) 473 summary = self._set_summary_info() 474 return exec_info, summary, suites 475 476 def _set_exec_info(self, report_path, task_info): 477 exec_info = ExecInfo() 478 exec_info.platform = getattr(task_info, ReportConstant.platform, 479 "None") 480 exec_info.test_type = getattr(task_info, ReportConstant.test_type, 481 "Test") 482 exec_info.device_name = getattr(task_info, ReportConstant.device_name, 483 "None") 484 exec_info.host_info = platform.platform() 485 start_time = self.summary_element.get(ReportConstant.start_time, "") 486 if not start_time: 487 start_time = self.summary_element.get("start_time", "") 488 end_time = self.summary_element.get(ReportConstant.end_time, "") 489 if not end_time: 490 end_time = self.summary_element.get("end_time", "") 491 exec_info.test_time = "%s/ %s" % (start_time, end_time) 492 start_time = time.mktime(time.strptime( 493 start_time, ReportConstant.time_format)) 494 end_time = time.mktime(time.strptime( 495 end_time, ReportConstant.time_format)) 496 exec_info.execute_time = self.get_execute_time(round( 497 end_time - start_time, 3)) 498 exec_info.device_label = getattr(task_info, 499 ReportConstant.device_label, 500 "None") 501 exec_info.log_path = os.path.abspath(os.path.join(report_path, "log")) 502 503 try: 504 product_info = self.summary_element.get( 505 ReportConstant.product_info, "") 506 if product_info: 507 exec_info.product_info = literal_eval(str(product_info)) 508 except SyntaxError as error: 509 LOG.error("Summary report error: %s", error.args) 510 return exec_info 511 512 @classmethod 513 def get_execute_time(cls, second_time): 514 hour, day = 0, 0 515 second, minute = second_time % 60, second_time // 60 516 if minute > 0: 517 minute, hour = minute % 60, minute // 60 518 if hour > 0: 519 hour, day = hour % 24, hour // 24 520 execute_time = "{}sec".format(str(int(second))) 521 if minute > 0: 522 execute_time = "{}min {}".format(str(int(minute)), execute_time) 523 if hour > 0: 524 execute_time = "{}hour {}".format(str(int(hour)), execute_time) 525 if day > 0: 526 execute_time = "{}day {}".format(str(int(day)), execute_time) 527 return execute_time 528 529 def _set_summary_info(self): 530 summary = Summary() 531 summary.modules = self.summary_element.get( 532 ReportConstant.modules, 0) 533 summary.run_modules = self.summary_element.get( 534 ReportConstant.run_modules, 0) 535 summary.result.total = int(self.summary_element.get( 536 ReportConstant.tests, 0)) 537 summary.result.failed = int( 538 self.summary_element.get(ReportConstant.failures, 0)) 539 summary.result.blocked = int( 540 self.summary_element.get(ReportConstant.errors, 0)) + \ 541 int(self.summary_element.get(ReportConstant.disabled, 0)) 542 summary.result.ignored = int( 543 self.summary_element.get(ReportConstant.ignored, 0)) 544 summary.result.unavailable = int( 545 self.summary_element.get(ReportConstant.unavailable, 0)) 546 summary.result.passed = summary.result.total - summary.result.failed \ 547 - summary.result.blocked - summary.result.ignored 548 return summary 549 550 def _set_suites_info(self): 551 suites = [] 552 for child in self.summary_element: 553 suite = Suite() 554 suite.module_name = child.get(ReportConstant.module_name, 555 ReportConstant.empty_name) 556 suite.name = child.get(ReportConstant.name, "") 557 suite.message = child.get(ReportConstant.message, "") 558 suite.report = child.get(ReportConstant.report, "") 559 suite.result.total = int(child.get(ReportConstant.tests)) if \ 560 child.get(ReportConstant.tests) else 0 561 suite.result.failed = int(child.get(ReportConstant.failures)) if \ 562 child.get(ReportConstant.failures) else 0 563 suite.result.unavailable = int(child.get( 564 ReportConstant.unavailable)) if child.get( 565 ReportConstant.unavailable) else 0 566 errors = int(child.get(ReportConstant.errors)) if child.get( 567 ReportConstant.errors) else 0 568 disabled = int(child.get(ReportConstant.disabled)) if child.get( 569 ReportConstant.disabled) else 0 570 suite.result.ignored = int(child.get(ReportConstant.ignored)) if \ 571 child.get(ReportConstant.ignored) else 0 572 suite.result.blocked = errors + disabled 573 suite.result.passed = suite.result.total - suite.result.failed - \ 574 suite.result.blocked - suite.result.ignored 575 suite.time = child.get(ReportConstant.time, "") 576 suite.set_cases(child) 577 suites.append(suite) 578 suites.sort(key=lambda x: (x.result.failed, x.result.blocked, 579 x.result.unavailable), reverse=True) 580 return suites 581 582 def render_data(self, title_name, parsed_data, 583 render_target=ReportConstant.summary_vision_report, devices=None): 584 exec_info, summary, suites = parsed_data 585 if not os.path.exists(self.template_name): 586 LOG.error("Template file not exists, {}".format(self.template_name)) 587 return "" 588 with open(self.template_name) as file: 589 file_context = file.read() 590 file_context = self._render_key("", ReportConstant.title_name, 591 title_name, file_context) 592 file_context = self._render_exec_info(file_context, exec_info) 593 file_context = self._render_summary(file_context, summary) 594 if devices is not None and len(devices) != 0: 595 file_context = self._render_product_info(file_context, devices) 596 file_context = self._render_devices(file_context, devices) 597 if render_target == ReportConstant.summary_vision_report: 598 file_context = self._render_suites(file_context, suites) 599 elif render_target == ReportConstant.details_vision_report: 600 file_context = self._render_cases(file_context, suites) 601 elif render_target == ReportConstant.failures_vision_report: 602 file_context = self._render_failure_cases(file_context, suites) 603 elif render_target == ReportConstant.passes_vision_report: 604 file_context = self._render_pass_cases(file_context, suites) 605 elif render_target == ReportConstant.ignores_vision_report: 606 file_context = self._render_ignore_cases(file_context, suites) 607 else: 608 LOG.error("Unsupported vision report type: {}".format(render_target)) 609 return file_context 610 611 @classmethod 612 def _render_devices(cls, file_context, devices): 613 """render devices""" 614 table_body_content = "" 615 keys = ["index", "sn", "model", "type", "platform", "version", "others"] 616 for index, device in enumerate(devices, 1): 617 tds = [] 618 for key in keys: 619 value = device.get(key, "") 620 if key == "index": 621 td_content = index 622 elif key == "others": 623 if len(value) == 0: 624 td_content = f"""<div style="display: flex;"> 625 <div class="ellipsis">{value}</div> 626 </div>""" 627 else: 628 td_content = f"""<div style="display: flex;"> 629 <div class="ellipsis">{value}</div> 630 <div class="operate" onclick="showDialog('dialog{index}')"></div> 631 </div>""" 632 else: 633 td_content = value 634 tds.append("<td class='normal device-{}'>{}</td>".format(key, td_content)) 635 table_body_content += "<tr>\n" + "\n ".join(tds) + "\n</tr>" 636 637 render_result = """<table class="devices"> 638 <thead> 639 <tr> 640 <th class="normal device-index">#</th> 641 <th class="normal device-sn">SN</th> 642 <th class="normal device-model">Model</th> 643 <th class="normal device-type">Type</th> 644 <th class="normal device-platform">Platform</th> 645 <th class="normal device-version">Version</th> 646 <th class="normal device-others">Others</th> 647 </tr> 648 </thead> 649 <tbody> 650 {} 651 </tbody> 652</table>""".format(table_body_content) 653 replace_str = "<!--{devices.context}-->" 654 return file_context.replace(replace_str, render_result) 655 656 @classmethod 657 def _render_key(cls, prefix, key, new_str, update_context): 658 old_str = "<!--{%s%s}-->" % (prefix, key) 659 return update_context.replace(old_str, new_str) 660 661 def _render_exec_info(self, file_context, exec_info): 662 prefix = "exec_info." 663 for key in ExecInfo.keys: 664 value = self._get_hidden_style_value(getattr( 665 exec_info, key, "None")) 666 file_context = self._render_key(prefix, key, value, file_context) 667 replace_str = "<!--{exec_info.task_log}-->" 668 file_context = file_context.replace(replace_str, self._get_task_log()) 669 return file_context 670 671 @staticmethod 672 def _render_product_info(file_context, devices): 673 """Construct product info context and render it to file context""" 674 render_result = "" 675 for index, device in enumerate(devices, 1): 676 others = device.get("others") 677 if len(others) == 0: 678 continue 679 tmp, count = "", 0 680 tbody_content = "" 681 for k, v in others.items(): 682 tmp += f'<td class="key">{k}:</td>\n<td class="value">{v}</td>\n' 683 count += 1 684 if count == 2: 685 tbody_content += "<tr>" + tmp + "<tr>\n" 686 tmp, count = "", 0 687 if tmp != "": 688 tbody_content += "<tr>" + tmp + "<tr>\n" 689 render_dialog = f"""<div id="dialog{index}" , class="el-dialog"> 690 <div style="margin: 15% auto; width: 60%;"> 691 <div class="el-dialog__header"> 692 <button class="el-dialog__close" onclick="hideDialog()">关闭</button> 693 </div> 694 <div class="el-dialog__body"> 695 <table class="el-dialog__table"> 696 <tbody> 697 {tbody_content} 698 </tbody> 699 </table> 700 </div> 701 </div> 702 </div> 703 """ 704 render_result += render_dialog 705 replace_str = "<!--{devices.dialogs}-->" 706 return file_context.replace(replace_str, render_result) 707 708 def _get_exec_info_td(self, key, value, row_start): 709 if not value: 710 value = self.PLACE_HOLDER 711 if key == ReportConstant.log_path_title and row_start: 712 exec_info_td = \ 713 " <td class='normal first'>%s:</td>\n" \ 714 " <td class='normal second' colspan='3'>%s</td>\n" % \ 715 (key, value) 716 return exec_info_td 717 value = self._get_hidden_style_value(value) 718 if row_start: 719 exec_info_td = " <td class='normal first'>%s:</td>\n" \ 720 " <td class='normal second'>%s</td>\n" % \ 721 (key, value) 722 else: 723 exec_info_td = " <td class='normal third'>%s:</td>\n" \ 724 " <td class='normal fourth'>%s</td>\n" % \ 725 (key, value) 726 return exec_info_td 727 728 def _get_hidden_style_value(self, value): 729 if len(value) <= self.MAX_LENGTH: 730 return value 731 return "<div class='hidden' title='%s'>%s</div>" % (value, value) 732 733 def _render_summary(self, file_context, summary): 734 file_context = self._render_data_object(file_context, summary, 735 "summary.") 736 737 # render color type 738 color_type = ColorType() 739 if summary.result.failed != 0: 740 color_type.failed = ReportConstant.color_failed 741 if summary.result.blocked != 0: 742 color_type.blocked = ReportConstant.color_blocked 743 if summary.result.ignored != 0: 744 color_type.ignored = ReportConstant.color_ignored 745 if summary.result.unavailable != 0: 746 color_type.unavailable = ReportConstant.color_unavailable 747 return self._render_data_object(file_context, color_type, 748 "color_type.") 749 750 def _render_data_object(self, file_context, data_object, prefix, 751 default=None): 752 """Construct data object context and render it to file context""" 753 if default is None: 754 default = self.PLACE_HOLDER 755 update_context = file_context 756 for key in getattr(data_object, "keys", []): 757 if hasattr(Result(), key) and hasattr( 758 data_object, ReportConstant.result): 759 result = getattr(data_object, ReportConstant.result, Result()) 760 new_str = str(getattr(result, key, default)) 761 else: 762 new_str = str(getattr(data_object, key, default)) 763 update_context = self._render_key(prefix, key, new_str, 764 update_context) 765 return update_context 766 767 def _render_suites(self, file_context, suites): 768 """Construct suites context and render it to file context 769 suite record sample: 770 <table class="suites"> 771 <tr> 772 <td class='tasklog'>TaskLog:</td> 773 <td class='normal' colspan='8' style="border-bottom: 1px #E8F0FD solid;"> 774 <a href='log/task_log.log'>task_log.log</a> 775 </td> 776 </tr> 777 <tr> 778 <th class="normal module">Module</th> 779 <th class="normal testsuite">Testsuite</th> 780 <th class="normal time">Time(sec)</th> 781 <th class="normal total">Total Tests</th> 782 <th class="normal passed">Passed</th> 783 <th class="normal failed">Failed</th> 784 <th class="normal blocked">Blocked</th> 785 <th class="normal ignored">Ignored</th> 786 <th class="normal operate">Operate</th> 787 </tr> 788 <tr [class="background-color"]> 789 <td class="normal module">{suite.module_name}</td> 790 <td class='normal testsuite'> 791 <a href='{suite.report}'>{suite.name}</a> or {suite.name} 792 </td> 793 <td class="normal time">{suite.time}</td> 794 <td class="normal total">{suite.result.total}</td> 795 <td class="normal passed">{suite.result.passed}</td> 796 <td class="normal failed">{suite.result.failed}</td> 797 <td class="normal blocked">{suite.result.blocked}</td> 798 <td class="normal ignored">{suite.result.ignored}</td> 799 <td class="normal operate"> 800 <a href="details_report.html#{suite.name}" or 801 "failures_report.html#{suite.name}"> 802 <div class="operate"></div></a> 803 </td> 804 </tr> 805 ... 806 </table> 807 """ 808 replace_str = "<!--{suites.context}-->" 809 810 suites_context = "<table class='suites'>\n" 811 suites_context += self._get_suites_title() 812 for index, suite in enumerate(suites): 813 # construct suite context 814 suite_name = getattr(suite, "name", self.PLACE_HOLDER) 815 suite_context = "<tr>\n " if index % 2 == 0 else \ 816 "<tr class='background-color'>\n " 817 for key in Suite.keys: 818 if hasattr(Result(), key): 819 result = getattr(suite, ReportConstant.result, Result()) 820 text = getattr(result, key, self.PLACE_HOLDER) 821 else: 822 text = getattr(suite, key, self.PLACE_HOLDER) 823 if key == ReportConstant.name: 824 report = getattr(suite, ReportConstant.report, self.PLACE_HOLDER) 825 temp = "<td class='normal testsuite'>{}</td>\n ".format( 826 "<a href='{}'>{}</a>".format(report, text) if report else text) 827 else: 828 temp = self._add_suite_td_context(key, text) 829 suite_context = "{}{}".format(suite_context, temp) 830 if suite.result.total == 0: 831 href = "%s#%s" % ( 832 ReportConstant.failures_vision_report, suite_name) 833 else: 834 href = "%s#%s" % ( 835 ReportConstant.details_vision_report, suite_name) 836 suite_context = "{}{}".format( 837 suite_context, 838 "<td class='normal operate'><a href='%s'><div class='operate'>" 839 "</div></a></td>\n</tr>\n" % href) 840 # add suite context to suites context 841 suites_context = "{}{}".format(suites_context, suite_context) 842 843 suites_context = "%s</table>\n" % suites_context 844 return file_context.replace(replace_str, suites_context) 845 846 def _get_task_log(self): 847 logs = [f for f in os.listdir(os.path.join(self.report_path, 'log')) if f.startswith('task_log.log')] 848 link = ["<a href='log/{task_log}'>{task_log}</a>".format(task_log=file_name) for file_name in logs] 849 return ' '.join(link) 850 851 def _get_testsuite_device_log(self, module_name, suite_name): 852 log_index, log_name = 0, 'device_log' 853 hilog_index, hilog_name = 0, 'device_hilog' 854 logs = [] 855 for r in self._get_device_logs(): 856 if (r.startswith(log_name) or r.startswith(hilog_name)) \ 857 and ((module_name and module_name in r) or suite_name in r): 858 logs.append(r) 859 if not logs: 860 return '' 861 link = [] 862 for name in sorted(logs): 863 display_name = '' 864 if name.startswith(log_name): 865 display_name = log_name 866 if log_index != 0: 867 display_name = log_name + str(log_index) 868 log_index += 1 869 if name.startswith(hilog_name): 870 display_name = hilog_name 871 if hilog_index != 0: 872 display_name = hilog_name + str(hilog_index) 873 hilog_index += 1 874 link.append("<a href='{}'>{}</a>".format(os.path.join('log', name), display_name)) 875 ele = "<tr>\n" \ 876 " <td class='devicelog' style='border-bottom: 1px #E8F0FD solid;'>DeviceLog:</td>\n" \ 877 " <td class='normal' colspan='6' style='border-bottom: 1px #E8F0FD solid;'>\n" \ 878 " {}\n" \ 879 " </td>\n" \ 880 "</tr>".format(' | '.join(link)) 881 return ele 882 883 def _get_testcase_device_log(self, case_name): 884 log_name, hilog_name = 'device_log', 'device_hilog' 885 logs = [r for r in self._get_device_logs() 886 if case_name in r and (log_name in r or hilog_name in r) and r.endswith('.log')] 887 if not logs: 888 return '-' 889 link = [] 890 for name in sorted(logs): 891 display_name = '' 892 if log_name in name: 893 display_name = log_name 894 if hilog_name in name: 895 display_name = hilog_name 896 link.append("<a href='{}'>{}</a>".format(os.path.join('log', name), display_name)) 897 return '<br>'.join(link) 898 899 def _get_device_logs(self): 900 if self.device_logs is not None: 901 return self.device_logs 902 result = [] 903 pth = os.path.join(self.report_path, 'log') 904 for top, _, nondirs in os.walk(pth): 905 for filename in nondirs: 906 if filename.startswith('device_log') or filename.startswith('device_hilog'): 907 result.append(os.path.join(top, filename).replace(pth, '')[1:]) 908 self.device_logs = result 909 return result 910 911 @classmethod 912 def _get_suites_title(cls): 913 suites_title = "<tr>\n" \ 914 " <th class='normal module'>Module</th>\n" \ 915 " <th class='normal testsuite'>Testsuite</th>\n" \ 916 " <th class='normal time'>Time(sec)</th>\n" \ 917 " <th class='normal total'>Tests</th>\n" \ 918 " <th class='normal passed'>Passed</th>\n" \ 919 " <th class='normal failed'>Failed</th>\n" \ 920 " <th class='normal blocked'>Blocked</th>\n" \ 921 " <th class='normal ignored'>Ignored</th>\n" \ 922 " <th class='normal operate'>Operate</th>\n" \ 923 "</tr>\n" 924 return suites_title 925 926 @staticmethod 927 def _add_suite_td_context(style, text): 928 if style == ReportConstant.name: 929 style = "test-suite" 930 td_style_class = "normal %s" % style 931 return "<td class='%s'>%s</td>\n " % (td_style_class, str(text)) 932 933 def _render_cases(self, file_context, suites): 934 """Construct cases context and render it to file context 935 case table sample: 936 <table class="test-suite"> 937 <tr> 938 <th class="title" colspan="4" id="{suite.name}"> 939 <span class="title">{suite.name} </span> 940 <a href="summary_report.html#summary"> 941 <span class="return"></span></a> 942 </th> 943 </tr> 944 <tr> 945 <td class='devicelog' style='border-bottom: 1px #E8F0FD solid;'>DeviceLog:</td> 946 <td class='normal' colspan='5' style='border-bottom: 1px #E8F0FD solid;'> 947 <a href='log/device_log_xx.log'>device_log</a> | <a href='log/device_hilog_xx.log'>device_hilog</a> 948 </td> 949 </tr> 950 <tr> 951 <th class="normal module">Module</th> 952 <th class="normal testsuite">Testsuite</th> 953 <th class="normal test">Testcase</th> 954 <th class="normal time">Time(sec)</th> 955 <th class="normal status"> 956 <div class="circle-normal circle-white"></div> 957 </th> 958 <th class="normal result">Result</th> 959 <th class='normal logs'>Logs</th> 960 </tr> 961 <tr [class="background-color"]> 962 <td class="normal module">{case.module_name}</td> 963 <td class="normal testsuite">{case.classname}</td> 964 <td class="normal test"> 965 <a href='{case.report}'>{case.name}</a> or {case.name} 966 </td> 967 <td class="normal time">{case.time}</td> 968 <td class="normal status"><div class="circle-normal 969 circle-{case.result/status}"></div></td> 970 <td class="normal result"> 971 [<a href="failures_report.html#{suite.name}.{case.name}">] 972 {case.result/status}[</a>] 973 </td> 974 <td class='normal logs'>-</td> 975 </tr> 976 ... 977 </table> 978 ... 979 """ 980 replace_str = "<!--{cases.context}-->" 981 cases_context = "" 982 for suite in suites: 983 # construct case context 984 module_name = suite.cases[0].module_name if suite.cases else "" 985 suite_name = getattr(suite, "name", self.PLACE_HOLDER) 986 case_context = "<table class='test-suite'>\n" 987 case_context += self._get_case_title(module_name, suite_name) 988 for index, case in enumerate(suite.cases): 989 case_context += self._get_case_td_context(index, case, suite_name) 990 case_context += "\n</table>\n" 991 cases_context += case_context 992 return file_context.replace(replace_str, cases_context) 993 994 def _get_case_td_context(self, index, case, suite_name): 995 result = case.get_result() 996 rendered_result = result 997 if result != ReportConstant.passed and \ 998 result != ReportConstant.ignored: 999 rendered_result = "<a href='%s#%s.%s'>%s</a>" % \ 1000 (ReportConstant.failures_vision_report, 1001 suite_name, case.name, result) 1002 if result == ReportConstant.passed: 1003 rendered_result = "<a href='{}#{}.{}'>{}</a>".format( 1004 ReportConstant.passes_vision_report, suite_name, case.name, result) 1005 1006 if result == ReportConstant.ignored: 1007 rendered_result = "<a href='{}#{}.{}'>{}</a>".format( 1008 ReportConstant.ignores_vision_report, suite_name, case.name, result) 1009 1010 report = case.report 1011 test_name = "<a href='{}'>{}</a>".format(report, case.name) if report else case.name 1012 case_td_context = "<tr>\n" if index % 2 == 0 else \ 1013 "<tr class='background-color'>\n" 1014 case_td_context = "{}{}".format( 1015 case_td_context, 1016 " <td class='normal module'>%s</td>\n" 1017 " <td class='normal testsuite'>%s</td>\n" 1018 " <td class='normal test'>%s</td>\n" 1019 " <td class='normal time'>%s</td>\n" 1020 " <td class='normal status'>\n" 1021 " <div class='circle-normal circle-%s'></div>\n" 1022 " </td>\n" 1023 " <td class='normal result'>%s</td>\n" 1024 " <td class='normal logs'>%s</td>\n" 1025 "</tr>\n" % (case.module_name, case.classname, test_name, 1026 case.time, result, rendered_result, self._get_testcase_device_log(case.name))) 1027 return case_td_context 1028 1029 def _get_case_title(self, module_name, suite_name): 1030 case_title = \ 1031 "<tr>\n" \ 1032 " <th class='title' colspan='4' id='%s'>\n" \ 1033 " <span class='title'>%s </span>\n" \ 1034 " <a href='%s#summary'>\n" \ 1035 " <span class='return'></span></a>\n" \ 1036 " </th>\n" \ 1037 "</tr>\n" \ 1038 "%s\n" \ 1039 "<tr>\n" \ 1040 " <th class='normal module'>Module</th>\n" \ 1041 " <th class='normal testsuite'>Testsuite</th>\n" \ 1042 " <th class='normal test'>Testcase</th>\n" \ 1043 " <th class='normal time'>Time(sec)</th>\n" \ 1044 " <th class='normal status'><div class='circle-normal " \ 1045 "circle-white'></div></th>\n" \ 1046 " <th class='normal result'>Result</th>\n" \ 1047 " <th class='normal logs'>Logs</th>\n" \ 1048 "</tr>\n" % (suite_name, suite_name, 1049 ReportConstant.summary_vision_report, self._get_testsuite_device_log(module_name, suite_name)) 1050 return case_title 1051 1052 def _render_failure_cases(self, file_context, suites): 1053 """Construct failure cases context and render it to file context 1054 failure case table sample: 1055 <table class="failure-test"> 1056 <tr> 1057 <th class="title" colspan="4" id="{suite.name}"> 1058 <span class="title">{suite.name} </span> 1059 <a href="details_report.html#{suite.name}" or 1060 "summary_report.html#summary"> 1061 <span class="return"></span></a> 1062 </th> 1063 </tr> 1064 <tr> 1065 <th class="normal test">Test</th> 1066 <th class="normal status"><div class="circle-normal 1067 circle-white"></div></th> 1068 <th class="normal result">Result</th> 1069 <th class="normal details">Details</th> 1070 </tr> 1071 <tr [class="background-color"]> 1072 <td class="normal test" id="{suite.name}"> 1073 {suite.module_name}#{suite.name}</td> 1074 or 1075 <td class="normal test" id="{suite.name}.{case.name}"> 1076 {case.module_name}#{case.classname}#{case.name}</td> 1077 <td class="normal status"><div class="circle-normal 1078 circle-{case.result/status}"></div></td> 1079 <td class="normal result">{case.result/status}</td> 1080 <td class="normal details">{case.message}</td> 1081 </tr> 1082 ... 1083 </table> 1084 ... 1085 """ 1086 replace_str = "<!--{failures.context}-->" 1087 failure_cases_context = "" 1088 for suite in suites: 1089 if suite.result.total == ( 1090 suite.result.passed + suite.result.ignored) and \ 1091 suite.result.unavailable == 0: 1092 continue 1093 1094 # construct failure cases context for failure suite 1095 suite_name = getattr(suite, "name", self.PLACE_HOLDER) 1096 case_context = "<table class='failure-test'>\n" 1097 case_context = \ 1098 "{}{}".format(case_context, self._get_failure_case_title( 1099 suite_name, suite.result.total)) 1100 if suite.result.total == 0: 1101 case_context = "{}{}".format( 1102 case_context, self._get_failure_case_td_context( 1103 0, suite, suite_name, ReportConstant.unavailable)) 1104 else: 1105 skipped_num = 0 1106 for index, case in enumerate(suite.cases): 1107 result = case.get_result() 1108 if result == ReportConstant.passed or \ 1109 result == ReportConstant.ignored: 1110 skipped_num += 1 1111 continue 1112 case_context = "{}{}".format( 1113 case_context, self._get_failure_case_td_context( 1114 index - skipped_num, case, suite_name, result)) 1115 1116 case_context = "%s</table>\n" % case_context 1117 1118 # add case context to cases context 1119 failure_cases_context = \ 1120 "{}{}".format(failure_cases_context, case_context) 1121 return file_context.replace(replace_str, failure_cases_context) 1122 1123 def _render_pass_cases(self, file_context, suites): 1124 """construct pass cases context and render it to file context 1125 failure case table sample: 1126 <table class="pass-test"> 1127 <tr> 1128 <th class="title" colspan="4" id="{suite.name}"> 1129 <span class="title">{suite.name} </span> 1130 <a href="details_report.html#{suite.name}" or 1131 "summary_report.html#summary"> 1132 <span class="return"></span></a> 1133 </th> 1134 </tr> 1135 <tr> 1136 <th class="normal test">Test</th> 1137 <th class="normal status"><div class="circle-normal 1138 circle-white"></div></th> 1139 <th class="normal result">Result</th> 1140 <th class="normal details">Details</th> 1141 </tr> 1142 <tr [class="background-color"]> 1143 <td class="normal test" id="{suite.name}"> 1144 {suite.module_name}#{suite.name}</td> 1145 or 1146 <td class="normal test" id="{suite.name}.{case.name}"> 1147 {case.module_name}#{case.classname}#{case.name}</td> 1148 <td class="normal status"><div class="circle-normal 1149 circle-{case.result/status}"></div></td> 1150 <td class="normal result">{case.result/status}</td> 1151 <td class="normal details">{case.message}</td> 1152 </tr> 1153 ... 1154 </table> 1155 ... 1156 """ 1157 file_context = file_context.replace("failure-test", "pass-test") 1158 replace_str = "<!--{failures.context}-->" 1159 pass_cases_context = "" 1160 for suite in suites: 1161 if (suite.result.total > 0 and suite.result.total == ( 1162 suite.result.failed + suite.result.ignored + suite.result.blocked)) or \ 1163 suite.result.unavailable != 0: 1164 continue 1165 1166 # construct pass cases context for pass suite 1167 suite_name = getattr(suite, "name", self.PLACE_HOLDER) 1168 case_context = "<table class='pass-test'>\n" 1169 case_context = \ 1170 "{}{}".format(case_context, self._get_failure_case_title( 1171 suite_name, suite.result.total)) 1172 skipped_num = 0 1173 for index, case in enumerate(suite.cases): 1174 result = case.get_result() 1175 if result == ReportConstant.failed or \ 1176 result == ReportConstant.ignored or result == ReportConstant.blocked: 1177 skipped_num += 1 1178 continue 1179 case_context = "{}{}".format( 1180 case_context, self._get_pass_case_td_context( 1181 index - skipped_num, case, suite_name, result)) 1182 1183 case_context = "{}</table>\n".format(case_context) 1184 1185 # add case context to cases context 1186 pass_cases_context = \ 1187 "{}{}".format(pass_cases_context, case_context) 1188 return file_context.replace(replace_str, pass_cases_context) 1189 1190 def _render_ignore_cases(self, file_context, suites): 1191 file_context = file_context.replace("failure-test", "ignore-test") 1192 replace_str = "<!--{failures.context}-->" 1193 ignore_cases_context = "" 1194 for suite in suites: 1195 if (suite.result.total > 0 and suite.result.total == ( 1196 suite.result.failed + suite.result.ignored + suite.result.blocked)) or \ 1197 suite.result.unavailable != 0: 1198 continue 1199 1200 # construct pass cases context for pass suite 1201 suite_name = getattr(suite, "name", self.PLACE_HOLDER) 1202 case_context = "<table class='ignore-test'>\n" 1203 case_context = \ 1204 "{}{}".format(case_context, self._get_failure_case_title( 1205 suite_name, suite.result.total)) 1206 skipped_num = 0 1207 for index, case in enumerate(suite.cases): 1208 result = case.get_result() 1209 if result == ReportConstant.failed or \ 1210 result == ReportConstant.passed or result == ReportConstant.blocked: 1211 skipped_num += 1 1212 continue 1213 case_context = "{}{}".format( 1214 case_context, self._get_ignore_case_td_context( 1215 index - skipped_num, case, suite_name, result)) 1216 1217 case_context = "{}</table>\n".format(case_context) 1218 1219 # add case context to cases context 1220 ignore_cases_context = "{}{}".format(ignore_cases_context, case_context) 1221 return file_context.replace(replace_str, ignore_cases_context) 1222 1223 @classmethod 1224 def _get_pass_case_td_context(cls, index, case, suite_name, result): 1225 pass_case_td_context = "<tr>\n" if index % 2 == 0 else \ 1226 "<tr class='background-color'>\n" 1227 test_context = "{}#{}#{}".format(case.module_name, case.classname, case.name) 1228 href_id = "{}.{}".format(suite_name, case.name) 1229 1230 detail_data = "-" 1231 if hasattr(case, "normal_screen_urls"): 1232 detail_data += "Screenshot: {}<br>".format( 1233 cls._get_screenshot_url_context(case.normal_screen_urls)) 1234 1235 pass_case_td_context += " <td class='normal test' id='{}'>{}</td>\n" \ 1236 " <td class='normal status'>\n" \ 1237 " <div class='circle-normal circle-{}'></div>\n" \ 1238 " </td>\n" \ 1239 " <td class='normal result'>{}</td>\n" \ 1240 " <td class='normal details'>\n" \ 1241 " {}\n" \ 1242 " </td>\n" \ 1243 "</tr>\n".format(href_id, test_context, result, result, detail_data) 1244 return pass_case_td_context 1245 1246 @classmethod 1247 def _get_ignore_case_td_context(cls, index, case, suite_name, result): 1248 ignore_case_td_context = "<tr>\n" if index % 2 == 0 else \ 1249 "<tr class='background-color'>\n" 1250 test_context = "{}#{}#{}".format(case.module_name, case.classname, case.name) 1251 href_id = "{}.{}".format(suite_name, case.name) 1252 1253 result_info = {} 1254 if hasattr(case, "result_info") and case.result_info: 1255 result_info = json.loads(case.result_info) 1256 detail_data = "" 1257 actual_info = result_info.get("actual", "") 1258 if actual_info: 1259 detail_data += "actual: {}<br>".format(actual_info) 1260 except_info = result_info.get("except", "") 1261 if except_info: 1262 detail_data += "except: {}<br>".format(except_info) 1263 filter_info = result_info.get("filter", "") 1264 if filter_info: 1265 detail_data += "filter: {}<br>".format(filter_info) 1266 if not detail_data: 1267 detail_data = "-" 1268 1269 ignore_case_td_context += " <td class='normal test' id='{}'>{}</td>\n" \ 1270 " <td class='normal status'>\n" \ 1271 " <div class='circle-normal circle-{}'></div></td>\n" \ 1272 " <td class='normal result'>{}</td>\n" \ 1273 " <td class='normal details'>\n" \ 1274 " {}\n" \ 1275 " </td>\n" \ 1276 "</tr>\n".format( 1277 href_id, test_context, result, result, detail_data) 1278 return ignore_case_td_context 1279 1280 @classmethod 1281 def _get_screenshot_url_context(cls, url): 1282 context = "" 1283 if not url: 1284 return "" 1285 paths = cls._find_png_file_path(url) 1286 for path in paths: 1287 context += "<br><a href='{0}'>{1}</a>".format(path, path) 1288 return context 1289 1290 @classmethod 1291 def _find_png_file_path(cls, url): 1292 if not url: 1293 return [] 1294 last_index = url.rfind("\\") 1295 if last_index < 0: 1296 return [] 1297 start_str = url[0:last_index] 1298 end_str = url[last_index + 1:len(url)] 1299 if not os.path.exists(start_str): 1300 return [] 1301 paths = [] 1302 for file in os.listdir(start_str): 1303 if end_str in file: 1304 whole_path = os.path.join(start_str, file) 1305 l_index = whole_path.rfind("screenshot") 1306 relative_path = whole_path[l_index:] 1307 paths.append(relative_path) 1308 return paths 1309 1310 @classmethod 1311 def _get_failure_case_td_context(cls, index, case, suite_name, result): 1312 failure_case_td_context = "<tr>\n" if index % 2 == 0 else \ 1313 "<tr class='background-color'>\n" 1314 if result == ReportConstant.unavailable: 1315 test_context = "{}#{}".format(case.module_name, case.name) 1316 href_id = suite_name 1317 else: 1318 test_context = "{}#{}#{}".format(case.module_name, case.classname, case.name) 1319 href_id = "{}.{}".format(suite_name, case.name) 1320 details_context = case.message 1321 1322 detail_data = "" 1323 if hasattr(case, "normal_screen_urls"): 1324 detail_data += "Screenshot: {}<br>".format( 1325 cls._get_screenshot_url_context(case.normal_screen_urls)) 1326 if hasattr(case, "failure_screen_urls"): 1327 detail_data += "Screenshot_On_Failure: {}<br>".format( 1328 cls._get_screenshot_url_context(case.failure_screen_urls)) 1329 1330 if details_context: 1331 detail_data += str(details_context).replace("<", "<"). \ 1332 replace(">", ">").replace("\\r\\n", "<br>"). \ 1333 replace("\\n", "<br>").replace("\n", "<br>"). \ 1334 replace(" ", " ") 1335 1336 failure_case_td_context += " <td class='normal test' id='{}'>{}</td>\n" \ 1337 " <td class='normal status'>" \ 1338 " <div class='circle-normal circle-{}'></div>" \ 1339 " </td>\n" \ 1340 " <td class='normal result'>{}</td>\n" \ 1341 " <td class='normal details'>\n" \ 1342 " {}" \ 1343 " </td>\n" \ 1344 "</tr>\n".format(href_id, test_context, result, result, detail_data) 1345 return failure_case_td_context 1346 1347 @classmethod 1348 def _get_failure_case_title(cls, suite_name, total): 1349 if total == 0: 1350 href = "%s#summary" % ReportConstant.summary_vision_report 1351 else: 1352 href = "%s#%s" % (ReportConstant.details_vision_report, suite_name) 1353 failure_case_title = \ 1354 "<tr>\n" \ 1355 " <th class='title' colspan='4' id='%s'>\n" \ 1356 " <span class='title'>%s </span>\n" \ 1357 " <a href='%s'>\n" \ 1358 " <span class='return'></span></a>\n" \ 1359 " </th>\n" \ 1360 "</tr>\n" \ 1361 "<tr>\n" \ 1362 " <th class='normal test'>Test</th>\n" \ 1363 " <th class='normal status'><div class='circle-normal " \ 1364 "circle-white'></div></th>\n" \ 1365 " <th class='normal result'>Result</th>\n" \ 1366 " <th class='normal details'>Details</th>\n" \ 1367 "</tr>\n" % (suite_name, suite_name, href) 1368 return failure_case_title 1369 1370 @staticmethod 1371 def generate_report(summary_vision_path, report_context): 1372 if platform.system() == "Windows": 1373 flags = os.O_WRONLY | os.O_CREAT | os.O_APPEND | os.O_BINARY 1374 else: 1375 flags = os.O_WRONLY | os.O_CREAT | os.O_APPEND 1376 vision_file_open = os.open(summary_vision_path, flags, 1377 FilePermission.mode_755) 1378 vision_file = os.fdopen(vision_file_open, "wb") 1379 if check_pub_key_exist(): 1380 try: 1381 cipher_text = do_rsa_encrypt(report_context) 1382 except ParamError as error: 1383 LOG.error(error, error_no=error.error_no) 1384 cipher_text = b"" 1385 vision_file.write(cipher_text) 1386 else: 1387 vision_file.write(bytes(report_context, "utf-8", "ignore")) 1388 vision_file.flush() 1389 vision_file.close() 1390 LOG.info("Generate vision report: file:///%s", summary_vision_path.replace("\\", "/")) 1391