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