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