# ############################################################################ # # # # ::: :::::::: # # result.py :+: :+: :+: # # +:+ +:+ +:+ # # By: charles +#+ +:+ +#+ # # +#+#+#+#+#+ +#+ # # Created: 2020/09/11 12:17:34 by charles #+# #+# # # Updated: 2021/02/27 12:28:20 by cacharle ### ########.fr # # # # ############################################################################ # import re from typing import Match, List, Optional from minishell_test import config from minishell_test.test.captured import Captured class BaseResult: RED_CHARS = "\033[31m" GREEN_CHARS = "\033[32m" BLUE_CHARS = "\033[34m" BOLD_CHARS = "\033[1m" CLOSE_CHARS = "\033[0m" def __init__(self, cmd: str): self.cmd = cmd self.colored = True self.set_colors() @property def passed(self): """Check if the result passed""" raise NotImplementedError @property def failed(self): """Check if the result failed""" return not self.passed def __repr__(self): """Returns a representation of the result based on the verbosity""" printed = self._escaped_cmd[:] if config.SHOW_RANGE: printed = "{:2}: ".format(self.index) + printed if len(printed) > config.TERM_COLS - 7: printed = printed[:config.TERM_COLS - 10] + "..." fmt = self.green("{:{width}} [PASS]") if self.passed else self.red("{:{width}} [FAIL]") return fmt.format(printed, width=config.TERM_COLS - 7) def put(self, index: int) -> None: """Print a summary of the result""" self.index = index print(self) def indicator(self, title: str, prefix: str) -> str: return self.bold(self.blue(prefix + " " + title)) def full_diff(self) -> str: raise NotImplementedError @property def _escaped_cmd(self): """Escape common control characters""" c = self.cmd c = c.replace("\t", "\\t") c = c.replace("\n", "\\n") c = c.replace("\v", "\\v") c = c.replace("\r", "\\r") c = c.replace("\f", "\\f") return c @property def _header_with(self): return self.indicator("WITH {}".format(self._escaped_cmd), "|>") + '\n' def set_colors(self): """Set colors strings on or off based on self.colored""" if self.colored: self.color_red = self.RED_CHARS self.color_green = self.GREEN_CHARS self.color_blue = self.BLUE_CHARS self.color_bold = self.BOLD_CHARS self.color_close = self.CLOSE_CHARS else: self.color_red = "" self.color_green = "" self.color_blue = "" self.color_bold = "" self.color_close = "" def green(self, s): return self.color_green + s + self.color_close def red(self, s): return self.color_red + s + self.color_close def blue(self, s): return self.color_blue + s + self.color_close def bold(self, s): return self.color_bold + s + self.color_close class Result(BaseResult): def __init__( self, cmd: str, file_names: List[str], expected: Captured, actual: Captured, ): """Result class cmd: runned command file_names: names of watched files expected: expected capture actual: actual capture """ self.file_names = file_names self.expected = expected self.actual = actual super().__init__(cmd) @property def passed(self): return self.actual == self.expected def header(self, title: str) -> str: return self.bold("|---------------------------------------{:-<40}".format(title)) @property def expected_header(self) -> str: return self.green(self.header("EXPECTED")) + '\n' @property def actual_header(self) -> str: return self.red(self.header("ACTUAL")) + '\n' def cat_e(self, s: Optional[str]) -> str: """Pass a string through a cat -e like output""" if s is None: return "FROM TEST: File not created\n" s = s.replace("\n", "$\n") if len(s) < 2: return s if s[-1] != '\n': s += '\n' return s def file_diff(self, file_name: str, expected: Optional[str], actual: Optional[str]) -> str: """Difference between 2 files""" if expected == actual: return "" file_header = self.indicator("FILE {}".format(file_name), "|#") + '\n' return ( file_header + self.expected_header + self.cat_e(expected) + self.actual_header + self.cat_e(actual) ) def files_diff(self): """Difference between watched files""" return '\n'.join([self.file_diff(n, e, a) for n, e, a in zip(self.file_names, self.expected.files_content, self.actual.files_content) if e != a]) def output_diff(self) -> str: """Difference in command output""" out = "" if self.actual.is_timeout: return "TIMEOUT\n" if self.expected.status != self.actual.status: out += self.indicator( "STATUS: expected {} actual {}" .format(self.expected.status, self.actual.status), "| " ) + '\n' if self.expected.output != self.actual.output: out += (self.expected_header + self.cat_e(self.expected.output) + self.actual_header + self.cat_e(self.actual.output)) return out def full_diff(self) -> str: """Concat all difference reports""" return self._header_with + self.output_diff() + self.files_diff() + "=" * 80 + '\n' class LeakResult(BaseResult): def __init__(self, cmd: str, captured: Captured): self.captured = captured super().__init__(cmd) def _search_leak_kind(self, kind: str) -> Match: match = re.search( r"==\d+==\s+" + kind + r" lost: (?P[0-9,]+) bytes in [0-9,]+ blocks", self.captured.output ) if match is None: raise RuntimeError( "valgrind output parsing failed for `{}`:\n{}" .format(self.cmd, self.captured.output) ) return match @property def _lost_bytes(self): if self.captured.output.find("All heap blocks were freed -- no leaks are possible") != -1: definite_bytes = 0 indirect_bytes = 0 else: definite_match = self._search_leak_kind("definitely") indirect_match = self._search_leak_kind("indirectly") definite_bytes = int(definite_match.group("bytes").replace(",", "")) indirect_bytes = int(indirect_match.group("bytes").replace(",", "")) return definite_bytes + indirect_bytes @property def passed(self): """Check if the result passed""" if self.captured.is_timeout: return False return self._lost_bytes == 0 def full_diff(self) -> str: """Concat all difference reports""" return self._header_with + self.captured.output