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