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