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