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