From 7cbaf473ca385cd64978a2d6f25f2df6af76bdb9 Mon Sep 17 00:00:00 2001 From: Charles Cabergs Date: Tue, 2 Mar 2021 14:33:51 +0100 Subject: Refactoring test.result.Result --- minishell_test/test/result.py | 301 +++++++++++++++++++++--------------------- 1 file changed, 149 insertions(+), 152 deletions(-) (limited to 'minishell_test/test/result.py') diff --git a/minishell_test/test/result.py b/minishell_test/test/result.py index 93c576a..566520a 100644 --- a/minishell_test/test/result.py +++ b/minishell_test/test/result.py @@ -6,64 +6,64 @@ # By: charles +#+ +:+ +#+ # # +#+#+#+#+#+ +#+ # # Created: 2020/09/11 12:17:34 by charles #+# #+# # -# Updated: 2021/02/27 12:28:20 by cacharle ### ########.fr # +# Updated: 2021/03/02 14:17:01 by cacharle ### ########.fr # # # # ############################################################################ # import re -from typing import Match, List, Optional +from typing import Match, List, Optional, Union from minishell_test.config import Config -from minishell_test.test.captured import Captured +from minishell_test.colors import green, red, blue, bold +from minishell_test.test.captured import CapturedCommand, CapturedTimeout, CapturedType 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() + """ + :param cmd: + The command executed to get to the result + """ + self._raw_cmd = cmd @property - def passed(self): + def passed(self) -> bool: """Check if the result passed""" raise NotImplementedError @property - def failed(self): + def failed(self) -> bool: """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) + @property + def __repr__(self) -> str: + raise NotImplementedError - def indicator(self, title: str, prefix: str) -> str: - return self.bold(self.blue(prefix + " " + title)) + def summarize(self, index: int) -> str: + """Summary of the result - def full_diff(self) -> str: - raise NotImplementedError + :param index: + The test index to print when :option:`--show-range` is enabled + :returns: + A summary of the result on one line, + it's length is the width of the terminal. + """ + printed = self._cmd[:] + if Config.show_range: + printed = f"{index:2}: {printed}" + width = Config.term_cols - len(" [PASS]") + if len(printed) > width: + printed = printed[:width - 3] + "..." + if self.passed: + return green(f"{printed:{width}} [PASS]") + else: + return red(f"{printed:{width}} [FAIL]") @property - def _escaped_cmd(self): - """Escape common control characters""" - c = self.cmd + def _cmd(self) -> str: + """The result command with the common control characters escaped""" + c = self._raw_cmd c = c.replace("\t", "\\t") c = c.replace("\n", "\\n") c = c.replace("\v", "\\v") @@ -72,75 +72,109 @@ class BaseResult: 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 _cmd_header(self): + return f"|> WITH {self._cmd}\n" - 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, + cmd: str, file_names: List[str], - expected: Captured, - actual: Captured, + expected: CapturedType, + actual: CapturedType, ): """Result class - cmd: runned command - file_names: names of watched files - expected: expected capture - actual: actual capture + + :param cmd: + runned command + :param file_names: + names of watched files + :param expected: + expected capture + :param actual: + actual capture """ - self.file_names = file_names - self.expected = expected - self.actual = actual super().__init__(cmd) + self.file_names = file_names + self.expected = expected + self.actual = actual @property def passed(self): return self.actual == self.expected - def header(self, title: str) -> str: - return self.bold("|---------------------------------------{:-<40}".format(title)) + def __repr__(self) -> str: + """Concat all difference reports""" + return ( + self._cmd_header + + self._cmd_diff() + + self._files_diff() + + "=" * 80 + '\n' + ) + + def _cmd_diff(self) -> str: + """Difference in command output""" + if isinstance(self.actual, CapturedTimeout): + return "TIMEOUT\n" + out = "" + if self.expected.status != self.actual.status: + out = f"| STATUS: expected {self.expected.status} actual {self.actual.status}\n" + if self.expected.output != self.actual.output: + out += self._content_diff(self.expected.output, self.actual.output) + return out + + _FILE_NOT_CREATED_MESSAGE = "FROM TEST: File not created\n" + + def _files_diff(self): + """Difference between watched files""" + + if isinstance(self.actual, CapturedTimeout): + return "" + + def diff(name, expected, actual): + expected = expected or self._FILE_NOT_CREATED_MESSAGE + actual = actual or self._FILE_NOT_CREATED_MESSAGE + return f"|# FILE {file_name}\n" + self._content_diff(expected, actual) + + return '\n'.join([ + diff(name, expected, actual) + for name, expected, actual in + zip( + self.file_names, + self.expected.files_content, + self.actual.files_content + ) + if expected != actual + ]) + + def _content_diff(self, expected: str, actual: str) -> str: + return ( + self._expected_header + + self._show_newlines(expected) + + self._actual_header + + self._show_newlines(actual) + ) + + def _header(self, title: str) -> str: + """Create a 80 characters wide header in the format ``-- title --``""" + return f"|{'-' * 40}{title:-<40}\n" @property - def expected_header(self) -> str: - return self.green(self.header("EXPECTED")) + '\n' + def _expected_header(self) -> str: + return self._header("EXPECTED") @property - def actual_header(self) -> str: - return self.red(self.header("ACTUAL")) + '\n' + def _actual_header(self) -> str: + return self._header("ACTUAL") - 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" + def _show_newlines(self, s: str) -> str: + """Add a ``$`` at the end of each newline + + If the string doesn't end with a newline add one but doesn't add a + ``$`` to represent it. + """ s = s.replace("\n", "$\n") if len(s) < 2: return s @@ -148,85 +182,48 @@ class Result(BaseResult): 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]) +class LeakResultException(Exception): + def __init__(self, result: 'LeakResult'): + self._result = result - 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' + def __str__(self) -> str: + return f"valgrind output parsing failed for `{self._result._cmd}`:\n{self._result._captured.output}" class LeakResult(BaseResult): - def __init__(self, cmd: str, captured: Captured): - self.captured = captured + def __init__(self, cmd: str, captured: CapturedType): + self._captured = captured super().__init__(cmd) + def __repr__(self) -> str: + return self._cmd_header + self.captured.output + + @property + def passed(self) -> str: + if isinstance(self._captured, CapturedTimeout): + return False + return self._lost_bytes == 0 + + _VALGRIND_OK_MESSAGE = "All heap blocks were freed -- no leaks are possible" + + @property + def _lost_bytes(self): + # Some versions of valgrind don't output `definitely` and `indirectly` + # when no leaks are found. + if self._captured.output.find(self._VALGRIND_OK_MESSAGE) != -1: + return 0 + 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 + 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 + self._captured.output ) if match is None: - raise RuntimeError( - "valgrind output parsing failed for `{}`:\n{}" - .format(self.cmd, self.captured.output) - ) + raise LeakResultException(self) 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 -- cgit