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