aboutsummaryrefslogtreecommitdiff
path: root/minishell_test/test
diff options
context:
space:
mode:
Diffstat (limited to 'minishell_test/test')
-rw-r--r--minishell_test/test/captured.py48
-rw-r--r--minishell_test/test/result.py301
-rw-r--r--minishell_test/test/test.py32
3 files changed, 190 insertions, 191 deletions
diff --git a/minishell_test/test/captured.py b/minishell_test/test/captured.py
index 6481dcf..bb09579 100644
--- a/minishell_test/test/captured.py
+++ b/minishell_test/test/captured.py
@@ -6,43 +6,49 @@
# By: charles <me@cacharle.xyz> +#+ +:+ +#+ #
# +#+#+#+#+#+ +#+ #
# Created: 2020/09/11 12:16:25 by charles #+# #+# #
-# Updated: 2021/03/01 16:15:31 by cacharle ### ########.fr #
+# Updated: 2021/03/02 10:32:19 by cacharle ### ########.fr #
# #
# ############################################################################ #
-from typing import List, Optional
+from typing import List, Optional, Union
-class Captured:
+class CapturedCommand:
def __init__(
self,
output: str,
status: int,
files_content: List[Optional[str]],
- is_timeout: bool = False
):
- """Captured class
- output: captured content
- status: command status
- files_content: content of the files altered by the command
- is_timeout: the command has timed out
+ """Captured command
+
+ :param output:
+ Command output
+ :param status:
+ Command return status code
+ :param files_content:
+ Content of the files altered by the command
"""
self.output = output
self.status = status
self.files_content = files_content
- self.is_timeout = is_timeout
def __eq__(self, other: object) -> bool:
- if not isinstance(other, Captured):
+ if not isinstance(other, CapturedCommand):
return False
- if self.is_timeout:
- return self.is_timeout == other.is_timeout
- return (self.output == other.output and
- self.status == other.status and
- all(x == y for x, y in zip(self.files_content, other.files_content)))
-
- @staticmethod
- def timeout():
- """Create a new captured timeout"""
- return Captured("", 0, [], is_timeout=True)
+ return (
+ self.output == other.output and
+ self.status == other.status and
+ all(x == y for x, y in zip(self.files_content, other.files_content))
+ )
+
+
+class CapturedTimeout():
+ """Captured timeout"""
+
+ def __eq__(self, other: object) -> bool:
+ return isinstance(other, CapturedTimeout)
+
+
+CapturedType = Union[CapturedCommand, CapturedTimeout]
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 <me@cacharle.xyz> +#+ +:+ +#+ #
# +#+#+#+#+#+ +#+ #
# 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<bytes>[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
diff --git a/minishell_test/test/test.py b/minishell_test/test/test.py
index f45b8b4..372f9f2 100644
--- a/minishell_test/test/test.py
+++ b/minishell_test/test/test.py
@@ -6,7 +6,7 @@
# By: charles <charles.cabergs@gmail.com> +#+ +:+ +#+ #
# +#+#+#+#+#+ +#+ #
# Created: 2020/06/16 21:48:50 by charles #+# #+# #
-# Updated: 2021/03/01 16:02:35 by cacharle ### ########.fr #
+# Updated: 2021/03/02 11:10:28 by cacharle ### ########.fr #
# #
# ############################################################################ #
@@ -18,7 +18,7 @@ from pathlib import Path
from typing import Optional, List, Dict, Union, Callable
from minishell_test.config import Config
-from minishell_test.test.captured import Captured
+from minishell_test.test.captured import CapturedCommand, CapturedTimeout, CapturedType
from minishell_test.test.result import Result, LeakResult
from minishell_test import sandbox
@@ -50,7 +50,6 @@ class Test:
self.setup = setup
self.files = files
self.exports = exports
- self.result: Optional[Union[Result, LeakResult]] = None
self.timeout = timeout if timeout > 0 else Config.timeout_test
if not isinstance(hook, list):
hook = [hook]
@@ -59,23 +58,20 @@ class Test:
self.hook = hook
self.hook_status = hook_status
- def run(self, index: int) -> None:
+ def run(self) -> Union[Result, LeakResult]:
""" Run the test for minishell and the reference shell and print the result out """
if Config.check_leaks:
self.hook = []
self.hook_status = []
captured = self._run_sandboxed([*Config.valgrind_cmd, "-c"])
- self.result = LeakResult(self.full_cmd, captured)
- self.result.put(index)
- return
+ return LeakResult(self.full_cmd, captured)
expected = self._run_sandboxed([Config.shell_reference_path, *Config.shell_reference_args, "-c"])
actual = self._run_sandboxed([Config.minishell_exec_path, "-c"])
- self.result = Result(self.full_cmd, self.files, expected, actual)
- self.result.put(index)
+ return Result(self.full_cmd, self.files, expected, actual)
- def _run_sandboxed(self, shell_cmd: List[Union[str, Path]]) -> Captured:
+ def _run_sandboxed(self, shell_cmd: List[Union[str, Path]]) -> CapturedType:
""" Run the command in a sandbox environment """
with sandbox.context():
if self.setup != "":
@@ -97,7 +93,7 @@ class Test:
sys.exit(1)
return self._run_capture(shell_cmd)
- def _run_capture(self, shell_cmd: List[Union[str, Path]]) -> Captured:
+ def _run_capture(self, shell_cmd: List[Union[str, Path]]) -> CapturedType:
""" Capture the output (stdout and stderr)
Capture the content of the watched files after the command is run
"""
@@ -121,7 +117,7 @@ class Test:
except subprocess.TimeoutExpired:
process.kill()
# _, _ = process.communicate(timeout=2)
- return Captured.timeout()
+ return CapturedTimeout()
try:
output = stdout.decode()
except UnicodeDecodeError:
@@ -150,7 +146,7 @@ class Test:
lines[i] = Config.minishell_prefix + line[len(Config.shell_reference_prefix):]
output = '\n'.join(lines)
- return Captured(output, process.returncode, files_content)
+ return CapturedCommand(output, process.returncode, files_content)
@property
def full_cmd(self) -> str:
@@ -166,10 +162,10 @@ class Test:
@classmethod
def try_run(cls, cmd: str) -> str:
test = Test(cmd)
- test.run(0)
- if isinstance(test.result, LeakResult):
- return test.result.captured.output
- elif isinstance(test.result, Result):
- return test.result.actual.output
+ result = test.run(0)
+ if isinstance(result, LeakResult):
+ return result.captured.output
+ elif isinstance(result, Result):
+ return result.actual.output
else:
return "No output"