• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1#!/usr/bin/env python3
2# -*- coding: utf-8 -*-
3
4# Copyright (c) 2021-2024 Huawei Device Co., Ltd.
5# Licensed under the Apache License, Version 2.0 (the "License");
6# you may not use this file except in compliance with the License.
7# You may obtain a copy of the License at
8#
9# http://www.apache.org/licenses/LICENSE-2.0
10#
11# Unless required by applicable law or agreed to in writing, software
12# distributed under the License is distributed on an "AS IS" BASIS,
13# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14# See the License for the specific language governing permissions and
15# limitations under the License.
16#
17
18import logging
19from abc import abstractmethod, ABC
20from os import path
21from typing import Dict, Mapping, Type, List, Tuple, Any
22
23from runner.enum_types.params import TestReport
24from runner.reports.report_format import ReportFormat
25from runner.enum_types.params import TestEnv
26from runner.utils import write_2_file
27from runner.logger import Log
28
29_LOGGER = logging.getLogger("runner.reports.report")
30
31
32class ReportGenerator:
33    def __init__(self, test_id: str, test_env: TestEnv) -> None:
34        self.__test_id = test_id
35        self.__test_env = test_env
36
37    def generate_fail_reports(self, test_result: Any) -> Dict[ReportFormat, str]:
38        if test_result.passed:
39            return {}
40
41        reports_format_to_paths = {}
42        format_to_report: Mapping[ReportFormat, Type[Report]] = {
43            ReportFormat.HTML: HtmlReport,
44            ReportFormat.MD: MdReport,
45            ReportFormat.LOG: TextReport
46        }
47
48        report_root = path.join(self.__test_env.work_dir.report, "known" if test_result.ignored else "new")
49        for report_format in list(self.__test_env.report_formats):
50            report_path = path.join(report_root,
51                                    f"{test_result.test_id}.report-{self.__test_env.timestamp}.{report_format.value}")
52            report = format_to_report[report_format](test_result)
53            Log.all(_LOGGER, f"{self.__test_id}: Report is saved to {report_path}")
54            write_2_file(report_path, report.make_report())
55            reports_format_to_paths[report_format] = report_path
56
57        return reports_format_to_paths
58
59
60REPORT_TITLE = "${Title}"
61REPORT_PATH = "${Path}"
62REPORT_STATUS_CLASS = "${status_class}"
63REPORT_STATUS = "${Status}"
64REPORT_REPRODUCE = "${Reproduce}"
65REPORT_RESULT = "${Result}"
66REPORT_EXPECTED = "${Expected}"
67REPORT_ACTUAL = "${Actual}"
68REPORT_ERROR = "${Error}"
69REPORT_RETURN_CODE = "${ReturnCode}"
70REPORT_TIME = "${Time}"
71
72STATUS_PASSED = "PASSED"
73STATUS_PASSED_CLASS = "test_status--passed"
74STATUS_FAILED = "FAILED"
75STATUS_FAILED_CLASS = "test_status--failed"
76
77NO_TIME = "not measured"
78
79
80def convert_to_array(output: str) -> List[str]:
81    return [line.strip() for line in output.split("\n") if len(line.strip()) > 0]
82
83
84class Report(ABC):
85    def __init__(self, test: Any) -> None:
86        self.test = test
87
88    @abstractmethod
89    def make_report(self) -> str:
90        pass
91
92
93class HtmlReport(Report):
94    @staticmethod
95    def __get_good_line(line: str) -> str:
96        return f'<span class="output_line">{line}</span>'
97
98    @staticmethod
99    def __get_failed_line(line: str) -> str:
100        return f'<span class="output_line output_line--failed">{line}</span>'
101
102    def make_report(self) -> str:
103        actual_report = self.test.report if self.test.report is not None else TestReport("", "", -1)
104        expected, actual = self.__make_output_diff_html(self.test.expected, actual_report.output)
105        test_expected, test_actual = "\n".join(expected), "\n".join(actual)
106
107        report_path = path.join(path.dirname(path.abspath(__file__)), "report_template.html")
108        with open(report_path, "r", encoding="utf-8") as file_pointer:
109            report = file_pointer.read()
110
111        report = report.replace(REPORT_TITLE, self.test.test_id)
112        report = report.replace(REPORT_PATH, self.test.path)
113        if self.test.passed:
114            report = report.replace(REPORT_STATUS_CLASS, STATUS_PASSED_CLASS)
115            report = report.replace(REPORT_STATUS, STATUS_PASSED)
116        else:
117            report = report.replace(REPORT_STATUS_CLASS, STATUS_FAILED_CLASS)
118            report = report.replace(REPORT_STATUS, STATUS_FAILED)
119        if self.test.time is not None:
120            report = report.replace(REPORT_TIME, f"{round(self.test.time, 2)} sec")
121        else:
122            report = report.replace(REPORT_TIME, NO_TIME)
123
124        report = report.replace(REPORT_REPRODUCE, self.test.reproduce)
125        report = report.replace(REPORT_EXPECTED, test_expected)
126        report = report.replace(REPORT_ACTUAL, test_actual)
127        report = report.replace(REPORT_ERROR, actual_report.error)
128        if self.test.report is None:
129            report = report.replace(REPORT_RETURN_CODE, "Not defined")
130        else:
131            report = report.replace(REPORT_RETURN_CODE, str(actual_report.return_code))
132
133        return report
134
135    def __make_output_diff_html(self, expected: str, actual: str) -> Tuple[List[str], List[str]]:
136        expected_list = convert_to_array(expected)
137        actual_list = convert_to_array(actual)
138        result_expected = []
139        result_actual = []
140
141        min_len = min(len(expected_list), len(actual_list))
142        for i in range(min_len):
143            expected_line = expected_list[i].strip()
144            actual_line = actual_list[i].strip()
145            if expected_line == actual_line:
146                result_expected.append(self.__get_good_line(expected_line))
147                result_actual.append(self.__get_good_line(actual_line))
148            else:
149                result_expected.append(self.__get_failed_line(expected_line))
150                result_actual.append(self.__get_failed_line(actual_line))
151
152        max_len = max(len(expected_list), len(actual_list))
153        is_expected_remains = len(expected_list) > len(actual_list)
154        for i in range(min_len, max_len):
155            if is_expected_remains:
156                result_expected.append(self.__get_good_line(expected_list[i]))
157            else:
158                result_actual.append(self.__get_good_line(actual_list[i]))
159
160        return result_expected, result_actual
161
162
163class MdReport(Report):
164    @staticmethod
165    def __get_md_good_line(expected: str, actual: str) -> str:
166        return f"| {expected} | {actual} |"
167
168    @staticmethod
169    def __get_md_failed_line(expected: str, actual: str) -> str:
170        if expected.strip() != "":
171            expected = f"**{expected}**"
172        if actual.strip() != "":
173            actual = f"**{actual}**"
174        return f"| {expected} | {actual} |"
175
176    def make_report(self) -> str:
177        actual_report = self.test.report if self.test.report is not None else TestReport("", "", -1)
178        result = self.__make_output_diff_md(self.test.expected, actual_report.output)
179        test_result = "\n".join(result)
180
181        report_path = path.join(path.dirname(path.abspath(__file__)), "report_template.md")
182        with open(report_path, "r", encoding="utf-8") as file_pointer:
183            report = file_pointer.read()
184
185        report = report.replace(REPORT_TITLE, self.test.test_id)
186        report = report.replace(REPORT_PATH, self.test.path)
187        if self.test.passed:
188            report = report.replace(REPORT_STATUS_CLASS, STATUS_PASSED_CLASS)
189            report = report.replace(REPORT_STATUS, STATUS_PASSED)
190        else:
191            report = report.replace(REPORT_STATUS_CLASS, STATUS_FAILED_CLASS)
192            report = report.replace(REPORT_STATUS, STATUS_FAILED)
193        if self.test.time is not None:
194            report = report.replace(REPORT_TIME, f"{round(self.test.time, 2)} sec")
195        else:
196            report = report.replace(REPORT_TIME, NO_TIME)
197
198        report = report.replace(REPORT_REPRODUCE, self.test.reproduce)
199        report = report.replace(REPORT_RESULT, test_result)
200        report = report.replace(REPORT_ERROR, actual_report.error)
201        if self.test.report is None:
202            report = report.replace(REPORT_RETURN_CODE, "Not defined")
203        else:
204            report = report.replace(REPORT_RETURN_CODE, str(actual_report.return_code))
205
206        return report
207
208    def __make_output_diff_md(self, expected: str, actual: str) -> List[str]:
209        expected_list = convert_to_array(expected)
210        actual_list = convert_to_array(actual)
211        result = []
212
213        min_len = min(len(expected_list), len(actual_list))
214        for i in range(min_len):
215            expected_line = expected_list[i].strip()
216            actual_line = actual_list[i].strip()
217            if expected_line == actual_line:
218                result.append(self.__get_md_good_line(expected_line, actual_line))
219            else:
220                result.append(self.__get_md_failed_line(expected_line, actual_line))
221
222        max_len = max(len(expected_list), len(actual_list))
223        is_expected_remains = len(expected_list) > len(actual_list)
224        for i in range(min_len, max_len):
225            if is_expected_remains:
226                result.append(self.__get_md_good_line(expected_list[i], " "))
227            else:
228                result.append(self.__get_md_failed_line(" ", actual_list[i]))
229
230        return result
231
232
233class TextReport(Report):
234    def make_report(self) -> str:
235        result = "PASSED" if self.test.passed else "FAILED"
236        time_line = f"{round(self.test.time, 2)} sec" if self.test.time is not None else NO_TIME
237        return "\n".join([
238            f"{self.test.test_id}",
239            f"{self.test.path}\n",
240            f"Result: {result}",
241            f"Execution time: {time_line}",
242            f"Steps to reproduce:{self.test.reproduce}\n",
243            f"Expected output:\n{self.test.expected}\n",
244            f"Actual output (stdout):\n{self.test.report.output if self.test.report is not None else ''}\n",
245            f"Actual error (stderr):\n{self.test.report.error if self.test.report is not None else ''}\n",
246            f"Actual return code:\n{self.test.report.return_code if self.test.report is not None else ''}\n"])
247