• 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#
18
19import os
20import platform
21import time
22from ast import literal_eval
23from dataclasses import dataclass
24from xml.etree import ElementTree
25
26from _core.logger import platform_logger
27from _core.report.encrypt import check_pub_key_exist
28from _core.report.encrypt import do_rsa_encrypt
29from _core.exception import ParamError
30from _core.constants import FilePermission
31
32LOG = platform_logger("ReporterHelper")
33
34
35@dataclass
36class ReportConstant:
37    # report name constants
38    summary_data_report = "summary_report.xml"
39    summary_vision_report = "summary_report.html"
40    details_vision_report = "details_report.html"
41    failures_vision_report = "failures_report.html"
42    task_info_record = "task_info.record"
43    summary_ini = "summary.ini"
44    summary_report_hash = "summary_report.hash"
45    title_name = "title_name"
46    summary_title = "Summary Report"
47    details_title = "Details Report"
48    failures_title = "Failures Report"
49
50    # exec_info constants
51    platform = "platform"
52    test_type = "test_type"
53    device_name = "device_name"
54    host_info = "host_info"
55    test_time = "test_time"
56    log_path = "log_path"
57    log_path_title = "Log Path"
58    execute_time = "execute_time"
59
60    # summary constants
61    product_info = "productinfo"
62    product_info_ = "product_info"
63    modules = "modules"
64    run_modules = "runmodules"
65    run_modules_ = "run_modules"
66    name = "name"
67    time = "time"
68    total = "total"
69    tests = "tests"
70    passed = "passed"
71    errors = "errors"
72    disabled = "disabled"
73    failures = "failures"
74    blocked = "blocked"
75    ignored = "ignored"
76    completed = "completed"
77    unavailable = "unavailable"
78    not_run = "notrun"
79    message = "message"
80
81    # case result constants
82    module_name = "modulename"
83    module_name_ = "module_name"
84    result = "result"
85    status = "status"
86    run = "run"
87    true = "true"
88    false = "false"
89    skip = "skip"
90    disable = "disable"
91    class_name = "classname"
92    level = "level"
93    empty_name = "-"
94
95    # time constants
96    time_stamp = "timestamp"
97    start_time = "starttime"
98    end_time = "endtime"
99    time_format = "%Y-%m-%d %H:%M:%S"
100
101    # xml tag constants
102    test_suites = "testsuites"
103    test_suite = "testsuite"
104    test_case = "testcase"
105
106    # report title constants
107    failed = "failed"
108    error = "error"
109    color_normal = "color-normal"
110    color_failed = "color-failed"
111    color_blocked = "color-blocked"
112    color_ignored = "color-ignored"
113    color_unavailable = "color-unavailable"
114
115
116class DataHelper:
117    LINE_BREAK = "\n"
118    LINE_BREAK_INDENT = "\n  "
119    INDENT = "  "
120    DATA_REPORT_SUFFIX = ".xml"
121
122    def __init__(self):
123        pass
124
125    @staticmethod
126    def parse_data_report(data_report):
127        if "<" not in data_report and os.path.exists(data_report):
128            with open(data_report, 'r', encoding='UTF-8', errors="ignore") as \
129                    file_content:
130                data_str = file_content.read()
131        else:
132            data_str = data_report
133
134        for char_index in range(32):
135            if char_index in [10, 13]:  # chr(10): LF, chr(13): CR
136                continue
137            data_str = data_str.replace(chr(char_index), "")
138        try:
139            return ElementTree.fromstring(data_str)
140        except SyntaxError as error:
141            LOG.error("%s %s", data_report, error.args)
142            return ElementTree.Element("empty")
143
144    @staticmethod
145    def set_element_attributes(element, element_attributes):
146        for key, value in element_attributes.items():
147            element.set(key, str(value))
148
149    @classmethod
150    def initial_element(cls, tag, tail, text):
151        element = ElementTree.Element(tag)
152        element.tail = tail
153        element.text = text
154        return element
155
156    def initial_suites_element(self):
157        return self.initial_element(ReportConstant.test_suites,
158                                    self.LINE_BREAK, self.LINE_BREAK_INDENT)
159
160    def initial_suite_element(self):
161        return self.initial_element(ReportConstant.test_suite,
162                                    self.LINE_BREAK_INDENT,
163                                    self.LINE_BREAK_INDENT + self.INDENT)
164
165    def initial_case_element(self):
166        return self.initial_element(ReportConstant.test_case,
167                                    self.LINE_BREAK_INDENT + self.INDENT, "")
168
169    @classmethod
170    def update_suite_result(cls, suite, case):
171        update_time = round(float(suite.get(
172            ReportConstant.time, 0)) + float(
173            case.get(ReportConstant.time, 0)), 3)
174        suite.set(ReportConstant.time, str(update_time))
175        update_tests = str(int(suite.get(ReportConstant.tests, 0))+1)
176        suite.set(ReportConstant.tests, update_tests)
177        if case.findall('failure'):
178            update_failures = str(int(suite.get(ReportConstant.failures, 0))+1)
179            suite.set(ReportConstant.failures, update_failures)
180
181    @classmethod
182    def get_summary_result(cls, report_path, file_name, key=None, **kwargs):
183        reverse = kwargs.get("reverse", False)
184        file_prefix = kwargs.get("file_prefix", None)
185        data_reports = cls._get_data_reports(report_path, file_prefix)
186        if not data_reports:
187            return
188        if key:
189            data_reports.sort(key=key, reverse=reverse)
190        summary_result = None
191        need_update_attributes = [ReportConstant.tests, ReportConstant.errors,
192                                  ReportConstant.failures,
193                                  ReportConstant.disabled,
194                                  ReportConstant.unavailable]
195        for data_report in data_reports:
196            data_report_element = cls.parse_data_report(data_report)
197            if not len(list(data_report_element)):
198                continue
199            if not summary_result:
200                summary_result = data_report_element
201                continue
202            if not summary_result or not data_report_element:
203                continue
204            for data_suite in data_report_element:
205                for summary_suite in summary_result:
206                    if data_suite.get("name", None) == \
207                            summary_suite.get("name", None):
208                        for data_case in data_suite:
209                            for summary_case in summary_suite:
210                                if data_case.get("name", None) == \
211                                        summary_case.get("name", None):
212                                    break
213                            else:
214                                summary_suite.append(data_case)
215                                DataHelper.update_suite_result(summary_result,
216                                                               data_case)
217                                DataHelper.update_suite_result(summary_suite,
218                                                               data_case)
219                        break
220                else:
221                    summary_result.append(data_suite)
222                    DataHelper._update_attributes(summary_result, data_suite,
223                                                  need_update_attributes)
224        if summary_result:
225            cls.generate_report(summary_result, file_name)
226        return summary_result
227
228    @classmethod
229    def _get_data_reports(cls, report_path, file_prefix=None):
230        if not os.path.isdir(report_path):
231            return []
232        data_reports = []
233        for root, _, files in os.walk(report_path):
234            for file_name in files:
235                if not file_name.endswith(cls.DATA_REPORT_SUFFIX):
236                    continue
237                if file_prefix and not file_name.startswith(file_prefix):
238                    continue
239                data_reports.append(os.path.join(root, file_name))
240        return data_reports
241
242    @classmethod
243    def _update_attributes(cls, summary_element, data_element,
244                           need_update_attributes):
245        for attribute in need_update_attributes:
246            updated_value = int(summary_element.get(attribute, 0)) + \
247                            int(data_element.get(attribute, 0))
248            summary_element.set(attribute, str(updated_value))
249        # update time
250        updated_time = round(float(summary_element.get(
251            ReportConstant.time, 0)) + float(
252            data_element.get(ReportConstant.time, 0)), 3)
253        summary_element.set(ReportConstant.time, str(updated_time))
254
255    @staticmethod
256    def generate_report(element, file_name):
257        if check_pub_key_exist():
258            plain_text = DataHelper.to_string(element)
259            try:
260                cipher_text = do_rsa_encrypt(plain_text)
261            except ParamError as error:
262                LOG.error(error, error_no=error.error_no)
263                cipher_text = b""
264            if platform.system() == "Windows":
265                flags = os.O_WRONLY | os.O_CREAT | os.O_APPEND | os.O_BINARY
266            else:
267                flags = os.O_WRONLY | os.O_CREAT | os.O_APPEND
268            file_name_open = os.open(file_name, flags, FilePermission.mode_755)
269            with os.fdopen(file_name_open, "wb") as file_handler:
270                file_handler.write(cipher_text)
271                file_handler.flush()
272        else:
273            tree = ElementTree.ElementTree(element)
274            tree.write(file_name, encoding="UTF-8", xml_declaration=True,
275                       short_empty_elements=True)
276        LOG.info("Generate data report: %s", file_name)
277
278    @staticmethod
279    def to_string(element):
280        return str(
281            ElementTree.tostring(element, encoding='UTF-8', method='xml'),
282            encoding="UTF-8")
283
284
285@dataclass
286class ExecInfo:
287    keys = [ReportConstant.platform, ReportConstant.test_type,
288            ReportConstant.device_name, ReportConstant.host_info,
289            ReportConstant.test_time, ReportConstant.execute_time]
290    test_type = ""
291    device_name = ""
292    host_info = ""
293    test_time = ""
294    log_path = ""
295    platform = ""
296    execute_time = ""
297    product_info = dict()
298
299
300class Result:
301
302    def __init__(self):
303        self.total = 0
304        self.passed = 0
305        self.failed = 0
306        self.blocked = 0
307        self.ignored = 0
308        self.unavailable = 0
309
310    def get_total(self):
311        return self.total
312
313    def get_passed(self):
314        return self.passed
315
316
317class Summary:
318    keys = [ReportConstant.modules, ReportConstant.total,
319            ReportConstant.passed, ReportConstant.failed,
320            ReportConstant.blocked, ReportConstant.unavailable,
321            ReportConstant.ignored, ReportConstant.run_modules_]
322
323    def __init__(self):
324        self.result = Result()
325        self.modules = None
326        self.run_modules = 0
327
328    def get_result(self):
329        return self.result
330
331    def get_modules(self):
332        return self.modules
333
334
335class Suite:
336    keys = [ReportConstant.module_name_, ReportConstant.name,
337            ReportConstant.total, ReportConstant.passed,
338            ReportConstant.failed, ReportConstant.blocked,
339            ReportConstant.ignored, ReportConstant.time]
340    module_name = ReportConstant.empty_name
341    name = ""
342    time = ""
343
344    def __init__(self):
345        self.message = ""
346        self.result = Result()
347        self.cases = []  # need initial to create new object
348
349    def get_cases(self):
350        return self.cases
351
352    def set_cases(self, element):
353        if len(element) == 0:
354            LOG.debug("%s has no testcase",
355                      element.get(ReportConstant.name, ""))
356            return
357
358        # get case context and add to self.cases
359        for child in element:
360            case = Case()
361            case.module_name = self.module_name
362            for key, value in child.items():
363                setattr(case, key, value)
364            if len(child) > 0:
365                if not getattr(case, ReportConstant.result, "") or \
366                        getattr(case, ReportConstant.result, "") == ReportConstant.completed:
367                    setattr(case, ReportConstant.result, ReportConstant.false)
368                message = child[0].get(ReportConstant.message, "")
369                if child[0].text and message != child[0].text:
370                    message = "%s\n%s" % (message, child[0].text)
371                setattr(case, ReportConstant.message, message)
372            self.cases.append(case)
373        self.cases.sort(key=lambda x: (
374            x.is_failed(), x.is_blocked(), x.is_unavailable(), x.is_passed()),
375                        reverse=True)
376
377
378class Case:
379    module_name = ReportConstant.empty_name
380    name = ReportConstant.empty_name
381    classname = ReportConstant.empty_name
382    status = ""
383    result = ""
384    message = ""
385    time = ""
386
387    def is_passed(self):
388        if self.result == ReportConstant.true and \
389                (self.status == ReportConstant.run or self.status == ""):
390            return True
391        if self.result == "" and self.status == ReportConstant.run and \
392                self.message == "":
393            return True
394        return False
395
396    def is_failed(self):
397        return self.result == ReportConstant.false and \
398               (self.status == ReportConstant.run or self.status == "")
399
400    def is_blocked(self):
401        return self.status in [ReportConstant.blocked, ReportConstant.disable,
402                               ReportConstant.error]
403
404    def is_unavailable(self):
405        return self.status in [ReportConstant.unavailable]
406
407    def is_ignored(self):
408        return self.status in [ReportConstant.skip, ReportConstant.not_run]
409
410    def get_result(self):
411        if self.is_failed():
412            return ReportConstant.failed
413        if self.is_blocked():
414            return ReportConstant.blocked
415        if self.is_unavailable():
416            return ReportConstant.unavailable
417        if self.is_ignored():
418            return ReportConstant.ignored
419        return ReportConstant.passed
420
421
422@dataclass
423class ColorType:
424    keys = [ReportConstant.failed, ReportConstant.blocked,
425            ReportConstant.ignored, ReportConstant.unavailable]
426    failed = ReportConstant.color_normal
427    blocked = ReportConstant.color_normal
428    ignored = ReportConstant.color_normal
429    unavailable = ReportConstant.color_normal
430
431
432class VisionHelper:
433    PLACE_HOLDER = "&nbsp;"
434    MAX_LENGTH = 50
435
436    def __init__(self):
437        from xdevice import Variables
438        self.summary_element = None
439        self.template_name = os.path.join(Variables.res_dir, "template",
440                                          "report.html")
441
442    def parse_element_data(self, summary_element, report_path, task_info):
443        self.summary_element = summary_element
444        exec_info = self._set_exec_info(report_path, task_info)
445        suites = self._set_suites_info()
446        summary = self._set_summary_info()
447        return exec_info, summary, suites
448
449    def _set_exec_info(self, report_path, task_info):
450        exec_info = ExecInfo()
451        exec_info.platform = getattr(task_info, ReportConstant.platform,
452                                     "None")
453        exec_info.test_type = getattr(task_info, ReportConstant.test_type,
454                                      "Test")
455        exec_info.device_name = getattr(task_info, ReportConstant.device_name,
456                                        "None")
457        exec_info.host_info = platform.platform()
458        start_time = self.summary_element.get(ReportConstant.start_time, "")
459        if not start_time:
460            start_time = self.summary_element.get("start_time", "")
461        end_time = self.summary_element.get(ReportConstant.end_time, "")
462        if not end_time:
463            end_time = self.summary_element.get("end_time", "")
464        exec_info.test_time = "%s/ %s" % (start_time, end_time)
465        start_time = time.mktime(time.strptime(
466            start_time, ReportConstant.time_format))
467        end_time = time.mktime(time.strptime(
468            end_time, ReportConstant.time_format))
469        exec_info.execute_time = self.get_execute_time(round(
470            end_time - start_time, 3))
471        exec_info.log_path = os.path.abspath(os.path.join(report_path, "log"))
472
473        try:
474            product_info = self.summary_element.get(
475                ReportConstant.product_info, "")
476            if product_info:
477                exec_info.product_info = literal_eval(str(product_info))
478        except SyntaxError as error:
479            LOG.error("Summary report error: %s", error.args)
480        return exec_info
481
482    @classmethod
483    def get_execute_time(cls, second_time):
484        hour, day = 0, 0
485        second, minute = second_time % 60, second_time // 60
486        if minute > 0:
487            minute, hour = minute % 60, minute // 60
488        if hour > 0:
489            hour, day = hour % 24, hour // 24
490        execute_time = "{}sec".format(str(int(second)))
491        if minute > 0:
492            execute_time = "{}min {}".format(str(int(minute)), execute_time)
493        if hour > 0:
494            execute_time = "{}hour {}".format(str(int(hour)), execute_time)
495        if day > 0:
496            execute_time = "{}day {}".format(str(int(day)), execute_time)
497        return execute_time
498
499    def _set_summary_info(self):
500        summary = Summary()
501        summary.modules = self.summary_element.get(
502            ReportConstant.modules, 0)
503        summary.run_modules = self.summary_element.get(
504            ReportConstant.run_modules, 0)
505        summary.result.total = int(self.summary_element.get(
506            ReportConstant.tests, 0))
507        summary.result.failed = int(
508            self.summary_element.get(ReportConstant.failures, 0))
509        summary.result.blocked = int(
510            self.summary_element.get(ReportConstant.errors, 0)) + \
511            int(self.summary_element.get(ReportConstant.disabled, 0))
512        summary.result.ignored = int(
513            self.summary_element.get(ReportConstant.ignored, 0))
514        summary.result.unavailable = int(
515            self.summary_element.get(ReportConstant.unavailable, 0))
516        summary.result.passed = summary.result.total - summary.result.failed \
517            - summary.result.blocked - summary.result.ignored
518        return summary
519
520    def _set_suites_info(self):
521        suites = []
522        for child in self.summary_element:
523            suite = Suite()
524            suite.module_name = child.get(ReportConstant.module_name,
525                                          ReportConstant.empty_name)
526            suite.name = child.get(ReportConstant.name, "")
527            suite.message = child.get(ReportConstant.message, "")
528            suite.result.total = int(child.get(ReportConstant.tests)) if \
529                child.get(ReportConstant.tests) else 0
530            suite.result.failed = int(child.get(ReportConstant.failures)) if \
531                child.get(ReportConstant.failures) else 0
532            suite.result.unavailable = int(child.get(
533                ReportConstant.unavailable)) if child.get(
534                ReportConstant.unavailable) else 0
535            errors = int(child.get(ReportConstant.errors)) if child.get(
536                ReportConstant.errors) else 0
537            disabled = int(child.get(ReportConstant.disabled)) if child.get(
538                ReportConstant.disabled) else 0
539            suite.result.ignored = int(child.get(ReportConstant.ignored)) if \
540                child.get(ReportConstant.ignored) else 0
541            suite.result.blocked = errors + disabled
542            suite.result.passed = suite.result.total - suite.result.failed - \
543                suite.result.blocked - suite.result.ignored
544            suite.time = child.get(ReportConstant.time, "")
545            suite.set_cases(child)
546            suites.append(suite)
547        suites.sort(key=lambda x: (x.result.failed, x.result.blocked,
548                                   x.result.unavailable), reverse=True)
549        return suites
550
551    def render_data(self, title_name, parsed_data,
552                    render_target=ReportConstant.summary_vision_report):
553        exec_info, summary, suites = parsed_data
554        if not os.path.exists(self.template_name):
555            LOG.error("Template file not exists. {}".format(self.template_name))
556            return ""
557
558        with open(self.template_name) as file:
559            file_context = file.read()
560            file_context = self._render_key("", ReportConstant.title_name,
561                                            title_name, file_context)
562            file_context = self._render_exec_info(file_context, exec_info)
563            file_context = self._render_summary(file_context, summary)
564            if render_target == ReportConstant.summary_vision_report:
565                file_context = self._render_suites(file_context, suites)
566            elif render_target == ReportConstant.details_vision_report:
567                file_context = self._render_cases(file_context, suites)
568            elif render_target == ReportConstant.failures_vision_report:
569                file_context = self._render_failure_cases(file_context, suites)
570            else:
571                LOG.error("Unsupported vision report type: %s", render_target)
572            return file_context
573
574    @classmethod
575    def _render_key(cls, prefix, key, new_str, update_context):
576        old_str = "<!--{%s%s}-->" % (prefix, key)
577        return update_context.replace(old_str, new_str)
578
579    def _render_exec_info(self, file_context, exec_info):
580        prefix = "exec_info."
581        for key in ExecInfo.keys:
582            value = self._get_hidden_style_value(getattr(
583                exec_info, key, "None"))
584            file_context = self._render_key(prefix, key, value, file_context)
585        file_context = self._render_product_info(exec_info, file_context,
586                                                 prefix)
587        return file_context
588
589    def _render_product_info(self, exec_info, file_context, prefix):
590        """Construct product info context and render it to file context
591
592        rendered product info sample:
593            <tr>
594                <td class="normal first">key:</td>
595                <td class="normal second">value</td>
596                <td class="normal third">key:</td>
597                <td class="normal fourth">value</td>
598            </tr>
599
600        Args:
601            exec_info: dict that used to update file_content
602            file_context: exist html content
603            prefix: target replace prefix key
604
605        Returns:
606            updated file context that includes rendered product info
607        """
608        row_start = True
609        try:
610            keys = list(exec_info.product_info.keys())
611        except AttributeError as _:
612            LOG.error("Product info error %s", exec_info.product_info)
613            keys = []
614
615        render_value = ""
616        for key in keys:
617            value = exec_info.product_info[key]
618            if row_start:
619                render_value = "%s<tr>\n" % render_value
620            render_value = "{}{}".format(
621                render_value, self._get_exec_info_td(key, value, row_start))
622            if not row_start:
623                render_value = "%s</tr>\n" % render_value
624            row_start = not row_start
625        if not row_start:
626            render_value = "%s</tr>\n" % render_value
627        file_context = self._render_key(prefix, ReportConstant.product_info_,
628                                        render_value, file_context)
629        return file_context
630
631    def _get_exec_info_td(self, key, value, row_start):
632        if not value:
633            value = self.PLACE_HOLDER
634        if key == ReportConstant.log_path_title and row_start:
635            exec_info_td = \
636                "  <td class='normal first'>%s:</td>\n" \
637                "  <td class='normal second' colspan='3'>%s</td>\n" % \
638                (key, value)
639            return exec_info_td
640        value = self._get_hidden_style_value(value)
641        if row_start:
642            exec_info_td = "  <td class='normal first'>%s:</td>\n" \
643                           "  <td class='normal second'>%s</td>\n" % \
644                           (key, value)
645        else:
646            exec_info_td = "  <td class='normal third'>%s:</td>\n" \
647                           "  <td class='normal fourth'>%s</td>\n" % \
648                           (key, value)
649        return exec_info_td
650
651    def _get_hidden_style_value(self, value):
652        if len(value) <= self.MAX_LENGTH:
653            return value
654        return "<div class='hidden' title='%s'>%s</div>" % (value, value)
655
656    def _render_summary(self, file_context, summary):
657        file_context = self._render_data_object(file_context, summary,
658                                                "summary.")
659
660        # render color type
661        color_type = ColorType()
662        if summary.result.failed != 0:
663            color_type.failed = ReportConstant.color_failed
664        if summary.result.blocked != 0:
665            color_type.blocked = ReportConstant.color_blocked
666        if summary.result.ignored != 0:
667            color_type.ignored = ReportConstant.color_ignored
668        if summary.result.unavailable != 0:
669            color_type.unavailable = ReportConstant.color_unavailable
670        return self._render_data_object(file_context, color_type,
671                                        "color_type.")
672
673    def _render_data_object(self, file_context, data_object, prefix,
674                            default=None):
675        """Construct data object context and render it to file context"""
676        if default is None:
677            default = self.PLACE_HOLDER
678        update_context = file_context
679        for key in getattr(data_object, "keys", []):
680            if hasattr(Result(), key) and hasattr(
681                    data_object, ReportConstant.result):
682                result = getattr(data_object, ReportConstant.result, Result())
683                new_str = str(getattr(result, key, default))
684            else:
685                new_str = str(getattr(data_object, key, default))
686            update_context = self._render_key(prefix, key, new_str,
687                                              update_context)
688        return update_context
689
690    def _render_suites(self, file_context, suites):
691        """Construct suites context and render it to file context
692        suite record sample:
693            <table class="suites">
694            <tr>
695                <th class="title" colspan="9">Test detail</th>
696            </tr>
697            <tr>
698                <th class="normal module">Module</th>
699                <th class="normal test-suite">Testsuite</th>
700                <th class="normal total">Total Tests</th>
701                <th class="normal passed">Passed</th>
702                <th class="normal failed">Failed</th>
703                <th class="normal blocked">Blocked</th>
704                <th class="normal ignored">Ignored</th>
705                <th class="normal time">Time</th>
706                <th class="normal operate">Operate</th>
707            </tr>
708            <tr [class="background-color"]>
709                <td class="normal module">{suite.module_name}</td>
710                <td class="normal test-suite">{suite.name}</td>
711                <td class="normal total">{suite.result.total}</td>
712                <td class="normal passed">{suite.result.passed}</td>
713                <td class="normal failed">{suite.result.failed}</td>
714                <td class="normal blocked">{suite.result.blocked}</td>
715                <td class="normal ignored">{suite.result.ignored}</td>
716                <td class="normal time">{suite.time}</td>
717                <td class="normal operate">
718                  <a href="details_report.html#{suite.name}" or
719                          "failures_report.html#{suite.name}">
720                  <div class="operate"></div></a>
721                </td>
722            </tr>
723            ...
724            </table>
725        """
726        replace_str = "<!--{suites.context}-->"
727
728        suites_context = "<table class='suites'>\n"
729        suites_context = "%s%s" % (suites_context, self._get_suites_title())
730        for index, suite in enumerate(suites):
731            # construct suite context
732            suite_name = getattr(suite, "name", self.PLACE_HOLDER)
733            suite_context = "<tr>\n  " if index % 2 == 0 else \
734                "<tr class='background-color'>\n  "
735            for key in Suite.keys:
736                if hasattr(Result(), key):
737                    result = getattr(suite, ReportConstant.result, Result())
738                    text = getattr(result, key, self.PLACE_HOLDER)
739                else:
740                    text = getattr(suite, key, self.PLACE_HOLDER)
741                suite_context = "{}{}".format(
742                    suite_context, self._add_suite_td_context(key, text))
743            if suite.result.total == 0:
744                href = "%s#%s" % (
745                    ReportConstant.failures_vision_report, suite_name)
746            else:
747                href = "%s#%s" % (
748                    ReportConstant.details_vision_report, suite_name)
749            suite_context = "{}{}".format(
750                suite_context,
751                "<td class='normal operate'><a href='%s'><div class='operate'>"
752                "</div></a></td>\n</tr>\n" % href)
753            # add suite context to suites context
754            suites_context = "{}{}".format(suites_context, suite_context)
755
756        suites_context = "%s</table>\n" % suites_context
757        return file_context.replace(replace_str, suites_context)
758
759    @classmethod
760    def _get_suites_title(cls):
761        suites_title = "<tr>\n" \
762                       "  <th class='title' colspan='9'>Test detail</th>\n" \
763                       "</tr>\n" \
764                       "<tr>\n" \
765                       "  <th class='normal module'>Module</th>\n" \
766                       "  <th class='normal test-suite'>Testsuite</th>\n" \
767                       "  <th class='normal total'>Total Tests</th>\n" \
768                       "  <th class='normal passed'>Passed</th>\n" \
769                       "  <th class='normal failed'>Failed</th>\n" \
770                       "  <th class='normal blocked'>Blocked</th>\n" \
771                       "  <th class='normal ignored'>Ignored</th>\n" \
772                       "  <th class='normal time'>Time</th>\n" \
773                       "  <th class='normal operate'>Operate</th>\n" \
774                       "</tr>\n"
775        return suites_title
776
777    @staticmethod
778    def _add_suite_td_context(style, text):
779        if style == ReportConstant.name:
780            style = "test-suite"
781        td_style_class = "normal %s" % style
782        return "<td class='%s'>%s</td>\n  " % (td_style_class, str(text))
783
784    def _render_cases(self, file_context, suites):
785        """Construct cases context and render it to file context
786        case table sample:
787            <table class="test-suite">
788            <tr>
789                <th class="title" colspan="4" id="{suite.name}">
790                    <span class="title">{suite.name}&nbsp;&nbsp;</span>
791                    <a href="summary_report.html#summary">
792                    <span class="return"></span></a>
793                </th>
794            </tr>
795            <tr>
796                <th class="normal module">Module</th>
797                <th class="normal test-suite">Testsuite</th>
798                <th class="normal test">Testcase</th>
799                <th class="normal time">Time</th>
800                <th class="normal status"><div class="circle-normal
801                    circle-white"></div></th>
802                <th class="normal result">Result</th>
803            </tr>
804            <tr [class="background-color"]>
805                <td class="normal module">{case.module_name}</td>
806                <td class="normal test-suite">{case.classname}</td>
807                <td class="normal test">{case.name}</td>
808                <td class="normal time">{case.time}</td>
809                <td class="normal status"><div class="circle-normal
810                    circle-{case.result/status}"></div></td>
811                <td class="normal result">
812                    [<a href="failures_report.html#{suite.name}.{case.name}">]
813                    {case.result/status}[</a>]</td>
814            </tr>
815            ...
816            </table>
817            ...
818        """
819        replace_str = "<!--{cases.context}-->"
820        cases_context = ""
821        for suite in suites:
822            # construct case context
823            suite_name = getattr(suite, "name", self.PLACE_HOLDER)
824            case_context = "<table class='test-suite'>\n"
825            case_context = "{}{}".format(case_context,
826                                         self._get_case_title(suite_name))
827            for index, case in enumerate(suite.cases):
828                case_context = "{}{}".format(
829                    case_context,
830                    self._get_case_td_context(index, case, suite_name))
831            case_context = "%s</table>\n" % case_context
832
833            # add case context to cases context
834            cases_context = "{}{}".format(cases_context, case_context)
835        return file_context.replace(replace_str, cases_context)
836
837    @classmethod
838    def _get_case_td_context(cls, index, case, suite_name):
839        result = case.get_result()
840        rendered_result = result
841        if result != ReportConstant.passed and \
842                result != ReportConstant.ignored:
843            rendered_result = "<a href='%s#%s.%s'>%s</a>" % \
844                              (ReportConstant.failures_vision_report,
845                               suite_name, case.name, result)
846        case_td_context = "<tr>\n" if index % 2 == 0 else \
847            "<tr class='background-color'>\n"
848        case_td_context = "{}{}".format(
849            case_td_context,
850            "  <td class='normal module'>%s</td>\n"
851            "  <td class='normal test-suite'>%s</td>\n"
852            "  <td class='normal test'>%s</td>\n"
853            "  <td class='normal time'>%s</td>\n"
854            "  <td class='normal status'>"
855            "<div class='circle-normal circle-%s'></div></td>\n"
856            "  <td class='normal result'>%s</td>\n"
857            "</tr>\n" % (case.module_name, case.classname, case.name,
858                         case.time, result, rendered_result))
859        return case_td_context
860
861    @classmethod
862    def _get_case_title(cls, suite_name):
863        case_title = \
864            "<tr>\n" \
865            "  <th class='title' colspan='4' id='%s'>\n" \
866            "    <span class='title'>%s&nbsp;&nbsp;</span>\n" \
867            "    <a href='%s#summary'>\n" \
868            "    <span class='return'></span></a>\n" \
869            "  </th>\n"  \
870            "</tr>\n"  \
871            "<tr>\n" \
872            "  <th class='normal module'>Module</th>\n" \
873            "  <th class='normal test-suite'>Testsuite</th>\n" \
874            "  <th class='normal test'>Testcase</th>\n" \
875            "  <th class='normal time'>Time</th>\n" \
876            "  <th class='normal status'><div class='circle-normal " \
877            "circle-white'></div></th>\n" \
878            "  <th class='normal result'>Result</th>\n" \
879            "</tr>\n" % (suite_name, suite_name,
880                         ReportConstant.summary_vision_report)
881        return case_title
882
883    def _render_failure_cases(self, file_context, suites):
884        """Construct failure cases context and render it to file context
885        failure case table sample:
886            <table class="failure-test">
887            <tr>
888                <th class="title" colspan="4" id="{suite.name}">
889                    <span class="title">{suite.name}&nbsp;&nbsp;</span>
890                    <a href="details_report.html#{suite.name}" or
891                            "summary_report.html#summary">
892                    <span class="return"></span></a>
893                </th>
894            </tr>
895            <tr>
896                <th class="normal test">Test</th>
897                <th class="normal status"><div class="circle-normal
898                circle-white"></div></th>
899                <th class="normal result">Result</th>
900                <th class="normal details">Details</th>
901            </tr>
902            <tr [class="background-color"]>
903                <td class="normal test" id="{suite.name}">
904                    {suite.module_name}#{suite.name}</td>
905                or
906                <td class="normal test" id="{suite.name}.{case.name}">
907                    {case.module_name}#{case.classname}#{case.name}</td>
908                <td class="normal status"><div class="circle-normal
909                    circle-{case.result/status}"></div></td>
910                <td class="normal result">{case.result/status}</td>
911                <td class="normal details">{case.message}</td>
912            </tr>
913            ...
914            </table>
915            ...
916        """
917        replace_str = "<!--{failures.context}-->"
918        failure_cases_context = ""
919        for suite in suites:
920            if suite.result.total == (
921                    suite.result.passed + suite.result.ignored) and \
922                    suite.result.unavailable == 0:
923                continue
924
925            # construct failure cases context for failure suite
926            suite_name = getattr(suite, "name", self.PLACE_HOLDER)
927            case_context = "<table class='failure-test'>\n"
928            case_context = \
929                "{}{}".format(case_context, self._get_failure_case_title(
930                        suite_name, suite.result.total))
931            if suite.result.total == 0:
932                case_context = "{}{}".format(
933                    case_context, self._get_failure_case_td_context(
934                       0, suite, suite_name, ReportConstant.unavailable))
935            else:
936                skipped_num = 0
937                for index, case in enumerate(suite.cases):
938                    result = case.get_result()
939                    if result == ReportConstant.passed or \
940                            result == ReportConstant.ignored:
941                        skipped_num += 1
942                        continue
943                    case_context = "{}{}".format(
944                        case_context, self._get_failure_case_td_context(
945                          index - skipped_num, case, suite_name, result))
946
947            case_context = "%s</table>\n" % case_context
948
949            # add case context to cases context
950            failure_cases_context = \
951                "{}{}".format(failure_cases_context, case_context)
952        return file_context.replace(replace_str, failure_cases_context)
953
954    @classmethod
955    def _get_failure_case_td_context(cls, index, case, suite_name, result):
956        failure_case_td_context = "<tr>\n" if index % 2 == 0 else \
957            "<tr class='background-color'>\n"
958        if result == ReportConstant.unavailable:
959            test_context = "%s#%s" % (case.module_name, case.name)
960            href_id = suite_name
961        else:
962            test_context = \
963                "%s#%s#%s" % (case.module_name, case.classname, case.name)
964            href_id = "%s.%s" % (suite_name, case.name)
965        details_context = case.message
966        if details_context:
967            details_context = str(details_context).replace("<", "&lt;"). \
968                replace(">", "&gt;").replace("\\r\\n", "<br/>"). \
969                replace("\\n", "<br/>").replace("\n", "<br/>"). \
970                replace(" ", "&nbsp;")
971        failure_case_td_context = "{}{}".format(
972            failure_case_td_context,
973            "  <td class='normal test' id='%s'>%s</td>\n"
974            "  <td class='normal status'>"
975            "<div class='circle-normal circle-%s'></div></td>\n"
976            "  <td class='normal result'>%s</td>\n"
977            "  <td class='normal details'>%s</td>\n"
978            "</tr>\n" %
979            (href_id, test_context, result, result, details_context))
980        return failure_case_td_context
981
982    @classmethod
983    def _get_failure_case_title(cls, suite_name, total):
984        if total == 0:
985            href = "%s#summary" % ReportConstant.summary_vision_report
986        else:
987            href = "%s#%s" % (ReportConstant.details_vision_report, suite_name)
988        failure_case_title = \
989            "<tr>\n" \
990            "  <th class='title' colspan='4' id='%s'>\n" \
991            "    <span class='title'>%s&nbsp;&nbsp;</span>\n" \
992            "    <a href='%s'>\n" \
993            "    <span class='return'></span></a>\n" \
994            "  </th>\n"  \
995            "</tr>\n" \
996            "<tr>\n" \
997            "  <th class='normal test'>Test</th>\n"  \
998            "  <th class='normal status'><div class='circle-normal " \
999            "circle-white'></div></th>\n" \
1000            "  <th class='normal result'>Result</th>\n" \
1001            "  <th class='normal details'>Details</th>\n" \
1002            "</tr>\n" % (suite_name, suite_name, href)
1003        return failure_case_title
1004
1005    @staticmethod
1006    def generate_report(summary_vision_path, report_context):
1007        if platform.system() == "Windows":
1008            flags = os.O_WRONLY | os.O_CREAT | os.O_APPEND | os.O_BINARY
1009        else:
1010            flags = os.O_WRONLY | os.O_CREAT | os.O_APPEND
1011        vision_file_open = os.open(summary_vision_path, flags,
1012                                   FilePermission.mode_755)
1013        vision_file = os.fdopen(vision_file_open, "wb")
1014        if check_pub_key_exist():
1015            try:
1016                cipher_text = do_rsa_encrypt(report_context)
1017            except ParamError as error:
1018                LOG.error(error, error_no=error.error_no)
1019                cipher_text = b""
1020            vision_file.write(cipher_text)
1021        else:
1022            vision_file.write(bytes(report_context, "utf-8", "ignore"))
1023        vision_file.flush()
1024        vision_file.close()
1025        LOG.info("Generate vision report: %s", summary_vision_path)
1026