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