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