aboutsummaryrefslogtreecommitdiff
path: root/minishell_test
diff options
context:
space:
mode:
Diffstat (limited to 'minishell_test')
-rwxr-xr-xminishell_test/__main__.py93
-rw-r--r--minishell_test/args.py87
-rw-r--r--minishell_test/config.py120
-rw-r--r--minishell_test/hooks.py123
-rw-r--r--minishell_test/sandbox.py48
-rw-r--r--minishell_test/suite/__init__.py2
-rw-r--r--minishell_test/suite/decorator.py47
-rw-r--r--minishell_test/suite/suite.py179
-rw-r--r--minishell_test/suites/__init__.py17
-rw-r--r--minishell_test/suites/builtin.py403
-rw-r--r--minishell_test/suites/cmd.py331
-rw-r--r--minishell_test/suites/flow.py290
-rw-r--r--minishell_test/suites/misc.py100
-rw-r--r--minishell_test/suites/path.py137
-rw-r--r--minishell_test/suites/preprocess.py463
-rw-r--r--minishell_test/test/__init__.py13
-rw-r--r--minishell_test/test/captured.py56
-rw-r--r--minishell_test/test/result.py246
-rw-r--r--minishell_test/test/test.py155
19 files changed, 2910 insertions, 0 deletions
diff --git a/minishell_test/__main__.py b/minishell_test/__main__.py
new file mode 100755
index 0000000..fe48b5e
--- /dev/null
+++ b/minishell_test/__main__.py
@@ -0,0 +1,93 @@
+#!/usr/bin/env python3
+
+# ############################################################################ #
+# #
+# ::: :::::::: #
+# main.py :+: :+: :+: #
+# +:+ +:+ +:+ #
+# By: charles <charles.cabergs@gmail.com> +#+ +:+ +#+ #
+# +#+#+#+#+#+ +#+ #
+# Created: 2020/07/15 15:11:52 by charles #+# #+# #
+# Updated: 2020/07/15 15:11:52 by charles ### ########.fr #
+# #
+# ############################################################################ #
+
+import os
+import sys
+import shutil
+import distutils.spawn
+import subprocess
+
+import config
+import sandbox
+from args import parse_args
+from suite import Suite
+from suites import * # noqa: F403,F401
+
+
+def main():
+ args = parse_args()
+ if args.list:
+ Suite.list()
+ sys.exit(0)
+
+ if config.MINISHELL_MAKE or args.make:
+ try:
+ print("{:=^{width}}".format("MAKE", width=config.TERM_COLS))
+ subprocess.run(["make", "--no-print-directory", "-C", config.MINISHELL_DIR],
+ check=True,
+ env={"MINISHELL_TEST_FLAGS": "-DMINISHELL_TEST", **os.environ})
+ print("=" * config.TERM_COLS)
+ except subprocess.CalledProcessError:
+ sys.exit(1)
+ if args.make:
+ sys.exit(0)
+ if os.path.exists(config.EXECUTABLES_PATH):
+ shutil.rmtree(config.EXECUTABLES_PATH)
+ os.mkdir(config.EXECUTABLES_PATH)
+ for cmd in config.AVAILABLE_COMMANDS:
+ cmd_path = distutils.spawn.find_executable(cmd)
+ if cmd_path is None:
+ raise RuntimeError
+ shutil.copy(cmd_path,
+ os.path.join(config.EXECUTABLES_PATH, cmd))
+
+ reference_args = os.environ.get("MINISHELL_TEST_ARGS")
+ if reference_args is not None:
+ config.REFERENCE_ARGS.extend(reference_args.split(','))
+
+ pager = os.environ.get("MINISHELL_TEST_PAGER")
+ if pager is not None:
+ config.PAGER = pager
+
+ config.VERBOSE_LEVEL = args.verbose
+ if args.bonus or os.environ.get("MINISHELL_TEST_BONUS") == "yes":
+ config.BONUS = True
+ if args.no_bonus:
+ config.BONUS = False
+ config.EXIT_FIRST = args.exit_first
+ config.CHECK_LEAKS = args.check_leaks
+ config.RANGE = args.range
+ config.SHOW_RANGE = args.show_range
+ if config.RANGE is not None or config.CHECK_LEAKS:
+ config.SHOW_RANGE = True
+
+ Suite.setup(args.suites)
+ try:
+ Suite.run_all()
+ except KeyboardInterrupt:
+ sandbox.remove()
+
+ Suite.summarize()
+ Suite.save_log()
+ print("See", config.LOG_PATH, "for more information")
+ if config.CHECK_LEAKS:
+ print("HELP: Valgrind is really slow the -x and --range options could be useful"
+ " (./run -h for more details)")
+
+ if args.pager:
+ subprocess.run([config.PAGER, config.LOG_PATH])
+
+
+if __name__ == "__main__":
+ main()
diff --git a/minishell_test/args.py b/minishell_test/args.py
new file mode 100644
index 0000000..b7fcca6
--- /dev/null
+++ b/minishell_test/args.py
@@ -0,0 +1,87 @@
+# ############################################################################ #
+# #
+# ::: :::::::: #
+# args.py :+: :+: :+: #
+# +:+ +:+ +:+ #
+# By: charles <charles.cabergs@gmail.com> +#+ +:+ +#+ #
+# +#+#+#+#+#+ +#+ #
+# Created: 2020/07/15 18:24:32 by charles #+# #+# #
+# Updated: 2021/01/11 22:20:16 by charles ### ########.fr #
+# #
+# ############################################################################ #
+
+import argparse
+import textwrap
+
+
+def parse_args():
+ """Parse command line arguments"""
+
+ parser = argparse.ArgumentParser(
+ description=textwrap.dedent(r"""
+ ___ ____ _ _ _ _ _ _
+ | \/ (_) (_) | | | | | | | | |
+ | . . |_ _ __ _ ___| |__ ___| | | | |_ ___ ___| |_
+ | |\/| | | '_ \| / __| '_ \ / _ \ | | | __/ _ \/ __| __|
+ | | | | | | | | \__ \ | | | __/ | | | || __/\__ \ |_
+ \_| |_/_|_| |_|_|___/_| |_|\___|_|_| \__\___||___/\__|
+ """),
+ formatter_class=argparse.RawTextHelpFormatter,
+ epilog=textwrap.dedent("""\
+ Signal handling is not tested
+ There is a commented glob suite in src/suites/preprocess.py.
+ Good luck handling `*'.*'`.
+ """)
+ )
+ parser.add_argument(
+ "-k", "--check-leaks", action="store_true",
+ help="Run valgrind on tests (disable usual comparison with bash)"
+ )
+ parser.add_argument(
+ "-r", "--range", nargs=2, type=int, metavar=("BEGIN", "END"),
+ help="Range of test index to run (imply --show-index)"
+ )
+ parser.add_argument(
+ "--show-range", action="store_true",
+ help="Show test index (useful with --range)"
+ )
+ parser.add_argument(
+ "-x", "--exit-first", action="store_true",
+ help="Exit on first fail"
+ )
+ parser.add_argument(
+ "-v", "--verbose", action="count",
+ help="Increase verbosity level (e.g -vv == 2)"
+ )
+ parser.add_argument(
+ "-b", "--bonus", action="store_true",
+ help="Enable bonus tests"
+ )
+ parser.add_argument(
+ "-n", "--no-bonus", action="store_true",
+ help="Disable bonus tests"
+ )
+ parser.add_argument(
+ "-l", "--list", action="store_true",
+ help="Print available test suites"
+ )
+ parser.add_argument(
+ "-m", "--make", action="store_true",
+ help="Make minishell and exit"
+ )
+ parser.add_argument(
+ "-g", "--pager", action="store_true",
+ help="After running the test, display the result in a pager of your choice"
+ )
+ parser.add_argument(
+ "suites", nargs='*', metavar="suite",
+ help=textwrap.dedent("""\
+ Test suites/group to run.
+ It tries to be smart and autocomplete the suite name
+ (e.g ./run int -> ./run preprocess/interpolation)
+ """)
+ )
+ tmp = parser.parse_args()
+ if tmp.verbose is None:
+ tmp.verbose = 1
+ return tmp
diff --git a/minishell_test/config.py b/minishell_test/config.py
new file mode 100644
index 0000000..493652c
--- /dev/null
+++ b/minishell_test/config.py
@@ -0,0 +1,120 @@
+# ############################################################################ #
+# #
+# ::: :::::::: #
+# config.py :+: :+: :+: #
+# +:+ +:+ +:+ #
+# By: charles <charles.cabergs@gmail.com> +#+ +:+ +#+ #
+# +#+#+#+#+#+ +#+ #
+# Created: 2020/07/15 18:24:19 by charles #+# #+# #
+# Updated: 2021/01/31 04:41:29 by charles ### ########.fr #
+# #
+# ############################################################################ #
+
+################################################################################
+# Minishell configuration file #
+################################################################################
+
+import os
+import shutil
+import distutils.spawn
+from typing import List
+
+# run the bonus tests
+# can be changed with `export MINISHELL_TEST_BONUS=yes` in your shell rc file.
+BONUS = False
+
+# minishell dir path
+MINISHELL_DIR = "../minishell"
+
+# minishell executable
+MINISHELL_EXEC = "minishell"
+
+# make minishell before executing the test if set to True
+MINISHELL_MAKE = True
+
+# path to reference shell (shell which will be compared minishell)
+# has to support the -c option (sh, bash and zsh support it)
+REFERENCE_PATH = "/bin/bash"
+# can be changed with `export MINISHELL_TEST_ARGS=--poxix,--otherarg`
+REFERENCE_ARGS: List[str] = [] # ["--posix"]
+
+# pager to use with --pager option
+# can be changed with `export MINISHELL_TEST_PAGER=yourpager`
+PAGER = "less"
+
+# log file path
+LOG_PATH = "result.log"
+
+# path to the sandbox directory
+# WARNING: will be rm -rf so be careful
+SANDBOX_PATH = "sandbox"
+
+# where the availables commands are stored
+EXECUTABLES_PATH = "./bin"
+
+# commands available in test"
+AVAILABLE_COMMANDS = ["rmdir", "env", "cat", "touch", "ls", "grep", "sh", "head"]
+
+# $PATH environment variable passed to the shell
+PATH_VARIABLE = os.path.abspath(EXECUTABLES_PATH)
+
+# test timeout
+TIMEOUT = 0.5
+
+# check leaks test timeout
+CHECK_LEAKS_TIMEOUT = 10
+
+LOREM = """
+Mollitia asperiores assumenda excepturi et ipsa. Nihil corporis facere aut a rem consequatur.
+Quas molestiae corporis et quibusdam maiores. Molestiae sed unde aut at sed.
+Deserunt quidem quidem aspernatur pariatur vel illum voluptatum. Culpa unde dolor aspernatur sit.
+Mollitia tenetur sed eaque autem placeat a aut in. Ipsam ea consequuntur omnis.
+Non et qui vel corrupti similique eum aut voluptatibus. Iste consequatur voluptatum et omnis debitis.
+Sit quia neque nihil consequatur sint. Velit libero ut aut et et rerum.
+Placeat cumque incidunt non repellat sunt perspiciatis ullam.
+Repellendus repudiandae nostrum quia quis corrupti.
+Rerum veniam earum cumque pariatur accusantium voluptatum omnis.
+Alias ut et et adipisci. Tempore omnis numquam ullam et animi et eveniet.
+Dolor itaque distinctio in. Magnam rerum quia est laboriosam repellat perspiciatis eos.
+Consequuntur quae corrupti atque. Numquam enim ut ut.
+Perspiciatis ut maxime et libero quo voluptas consequatur illum. Pariatur porro dolor cumque molestiae harum.
+"""
+LOREM = ' '.join(LOREM.split('\n'))
+
+###############################################################################
+# You probably shouldn't edit after #
+###############################################################################
+
+MINISHELL_PATH = os.path.abspath(
+ os.path.join(MINISHELL_DIR, MINISHELL_EXEC)
+)
+
+VALGRIND_CMD: List[str] = [
+ distutils.spawn.find_executable("valgrind") or "couldn't find valgrind",
+ # "valgrind",
+ "--trace-children=no",
+ "--leak-check=yes",
+ "--child-silent-after-fork=yes",
+ "--show-leak-kinds=definite",
+ MINISHELL_PATH,
+]
+
+# 0, 1, 2
+VERBOSE_LEVEL = 1
+
+MINISHELL_ERROR_BEGIN = os.path.basename(MINISHELL_PATH) + ": "
+REFERENCE_ERROR_BEGIN = REFERENCE_PATH + ": line 0: "
+
+TERM_COLS = shutil.get_terminal_size().columns
+if TERM_COLS < 40:
+ raise RuntimeError("You're terminal isn't wide enough")
+
+PLATFORM = os.uname().sysname
+
+EXIT_FIRST = False
+
+CHECK_LEAKS = False
+
+SHOW_RANGE = False
+
+RANGE = None
diff --git a/minishell_test/hooks.py b/minishell_test/hooks.py
new file mode 100644
index 0000000..e37f2aa
--- /dev/null
+++ b/minishell_test/hooks.py
@@ -0,0 +1,123 @@
+# ############################################################################ #
+# #
+# ::: :::::::: #
+# hooks.py :+: :+: :+: #
+# +:+ +:+ +:+ #
+# By: charles <me@cacharle.xyz> +#+ +:+ +#+ #
+# +#+#+#+#+#+ +#+ #
+# Created: 2020/09/11 16:10:20 by charles #+# #+# #
+# Updated: 2020/11/25 21:36:18 by charles ### ########.fr #
+# #
+# ############################################################################ #
+
+import re
+import sys
+import os
+
+import config
+
+
+def sort_lines(output):
+ """Sort lines of output"""
+ return '\n'.join(sorted(output.split('\n')))
+
+
+def error_line0(output):
+ """Replace "/bin/bash: -c: line 0:" by "minishell:" and delete the second line"""
+ error_message = os.environ.get("MINISHELL_TEST_DONT_CHECK_ERROR_MESSAGE")
+ if error_message is not None and error_message == "yes":
+ return "DISCARDED BY TEST"
+
+ lines = output.split('\n')
+ if len(lines) != 3:
+ return output
+ prefix = "{}: -c: line 0: ".format(config.REFERENCE_PATH)
+ if lines[0].find(prefix) != 0:
+ return output
+ return lines[0].replace(prefix, "minishell: ") + "\n"
+
+
+def discard(output):
+ """Discard the output"""
+ return "DISCARDED BY TEST"
+
+
+def export_singleton(output):
+ """Remove variable that are not set to anything in a call to export without arguments"""
+ prefix = "export " if ("--posix" in config.REFERENCE_ARGS) else "declare -x "
+ return sort_lines(
+ '\n'.join([line for line in output.split('\n')
+ if re.match("^{}[a-zA-Z]+$".format(prefix), line) is None])
+ )
+
+
+def replace_double_slash(output):
+ """Replace occurence of double slash by one"""
+ return output.replace("//", "/")
+
+
+def replace_double_semi_colon(output):
+ """Replace occurence of double semi-colon by one"""
+ return output.replace(";;", ";")
+
+
+def platform_status(darwin_status, linux_status, windows_status=None):
+ def hook(status):
+ if config.PLATFORM == "Darwin":
+ return status
+ elif config.PLATFORM == "Linux":
+ return (darwin_status if status == linux_status else status)
+ else:
+ raise RuntimeError("This platform exit codes are not supported yet,"
+ "feel free to contact me to add it.")
+ sys.exit(2)
+ return status
+ return hook
+
+
+def is_directory(output):
+ if config.PLATFORM == "Linux":
+ return output.replace("Is a directory", "is a directory")
+ else:
+ return output
+
+
+# def no_cd_too_many_arguments(output):
+# for i, line in output.split("\n"):
+# if line.find("too many arguments")
+
+
+def shlvl_0_to_1(output):
+ if config.PLATFORM == "Linux":
+ return output.replace("SHLVL=0", "SHLVL=1")
+ else:
+ return output
+
+
+def delete_escape(output):
+ if config.PLATFORM == "Linux":
+ return output.replace("\\", "")
+ else:
+ return output
+
+
+def error_eof_to_expected_token(output):
+ return output.replace(
+ "-c: line 1: syntax error: unexpected end of file",
+ "syntax error expected token"
+ )
+
+
+def linux_discard(output):
+ if config.PLATFORM == "Linux":
+ return "DISCARDED BY MINISHELL TEST"
+ else:
+ return output
+
+
+def should_not_be(not_expected):
+ def hook(output):
+ if output == not_expected:
+ return "OUTPUT SHOULD NOT BE " + output
+ return "DISCARDED BY TEST"
+ return hook
diff --git a/minishell_test/sandbox.py b/minishell_test/sandbox.py
new file mode 100644
index 0000000..bd49d1e
--- /dev/null
+++ b/minishell_test/sandbox.py
@@ -0,0 +1,48 @@
+# ############################################################################ #
+# #
+# ::: :::::::: #
+# sandbox.py :+: :+: :+: #
+# +:+ +:+ +:+ #
+# By: charles <me@cacharle.xyz> +#+ +:+ +#+ #
+# +#+#+#+#+#+ +#+ #
+# Created: 2020/09/11 13:48:07 by charles #+# #+# #
+# Updated: 2021/01/31 03:59:30 by charles ### ########.fr #
+# #
+# ############################################################################ #
+
+import os
+import glob
+import shutil
+import subprocess
+from contextlib import contextmanager
+
+import config
+
+
+def create():
+ """Create a new sandbox directory"""
+ try:
+ os.mkdir(config.SANDBOX_PATH)
+ except OSError:
+ pass
+
+
+def remove():
+ """Remove the sandbox directory
+ Brute force rm -rf if clean removal doesn't work due to permissions.
+ """
+ try:
+ shutil.rmtree(config.SANDBOX_PATH)
+ except PermissionError:
+ subprocess.run(["chmod", "777", *glob.glob(config.SANDBOX_PATH + "/*")], check=True)
+ subprocess.run(["rm", "-rf", config.SANDBOX_PATH], check=True)
+ except FileNotFoundError:
+ pass
+
+
+@contextmanager
+def context():
+ """Sandbox context manager"""
+ create()
+ yield
+ remove()
diff --git a/minishell_test/suite/__init__.py b/minishell_test/suite/__init__.py
new file mode 100644
index 0000000..6f7f321
--- /dev/null
+++ b/minishell_test/suite/__init__.py
@@ -0,0 +1,2 @@
+from suite.suite import Suite # noqa: F401
+from suite.decorator import suite # noqa: F401
diff --git a/minishell_test/suite/decorator.py b/minishell_test/suite/decorator.py
new file mode 100644
index 0000000..fdc7fb6
--- /dev/null
+++ b/minishell_test/suite/decorator.py
@@ -0,0 +1,47 @@
+# ############################################################################ #
+# #
+# ::: :::::::: #
+# decorator.py :+: :+: :+: #
+# +:+ +:+ +:+ #
+# By: charles <me@cacharle.xyz> +#+ +:+ +#+ #
+# +#+#+#+#+#+ +#+ #
+# Created: 2020/09/11 12:28:00 by charles #+# #+# #
+# Updated: 2021/02/04 16:18:11 by charles ### ########.fr #
+# #
+# ############################################################################ #
+
+import inspect
+from typing import List
+
+from suite import Suite
+from test import Test
+
+
+def suite(groups: List[str] = [], bonus: bool = False): # type: ignore
+ """Decorator generator for suites arguments"""
+
+ def suite_wrapper(origin):
+ """Decorator for a suite function (fmt: suite_[name]) """
+
+ mod = inspect.getmodule(origin)
+ if mod is None:
+ raise NotImplementedError
+ mod_name = mod.__name__[len("suites."):]
+ name = "{}/{}".format(mod_name, origin.__name__[len("suite_"):])
+ description = origin.__doc__
+ if description is None:
+ print("You should had a doc string to the {} suite".format(name))
+ description = "no description"
+ description = description.split("\n")[0].strip()
+ s = Suite(name, groups + [mod_name], bonus, description)
+
+ def test_generator():
+ def test(*args, **kwargs):
+ s.add(Test(*args, **kwargs))
+ origin(test)
+
+ s.generator_func = test_generator
+ Suite.available.append(s)
+ return test_generator
+
+ return suite_wrapper
diff --git a/minishell_test/suite/suite.py b/minishell_test/suite/suite.py
new file mode 100644
index 0000000..836cac0
--- /dev/null
+++ b/minishell_test/suite/suite.py
@@ -0,0 +1,179 @@
+# ############################################################################ #
+# #
+# ::: :::::::: #
+# suite.py :+: :+: :+: #
+# +:+ +:+ +:+ #
+# By: charles <charles.cabergs@gmail.com> +#+ +:+ +#+ #
+# +#+#+#+#+#+ +#+ #
+# Created: 2020/07/15 18:24:29 by charles #+# #+# #
+# Updated: 2021/02/04 16:13:08 by charles ### ########.fr #
+# #
+# ############################################################################ #
+
+import sys
+from typing import List, Tuple, Optional, Callable
+
+import config
+from test import Test
+
+
+class Suite:
+ available: List['Suite'] = []
+
+ @classmethod
+ def run_all(cls):
+ """Run all available suites"""
+ for s in cls.available:
+ if not s.run() and config.EXIT_FIRST:
+ break
+
+ @classmethod
+ def setup(cls, asked_names: List[str]) -> None:
+ """ Remove not asked suite from available suites
+ Tries to autocomplete the asked names
+ """
+ if not config.BONUS:
+ cls.available = [s for s in cls.available if not s.bonus]
+ if len(asked_names) == 0:
+ asked_names = [s.name for s in cls.available]
+
+ suite_names = [s.name for s in cls.available]
+ names = []
+ for i, name in enumerate(asked_names):
+ if name in suite_names:
+ names.append(name)
+ continue
+ matches = [n for n in suite_names
+ if n.find("/") != -1
+ and n[n.find("/") + 1:].startswith(name)
+ or n.startswith(name)]
+ if len(matches) == 1:
+ names.append(matches[0])
+ elif len(matches) != 0 and all(n.startswith(name) for n in matches):
+ names.extend(matches)
+ elif len(matches) > 2:
+ print(("Ambiguous name `{}` match the following suites\n\t{}\n"
+ "Try to run with -l to see the available suites")
+ .format(name, ', '.join(matches)))
+ sys.exit(1)
+ elif len(matches) == 0:
+ print(("Name `{}` doesn't match any suite/group name\n\t"
+ "Try to run with -l to see the available suites")
+ .format(name))
+ sys.exit(1)
+
+ cls.available = list(set(
+ [s for s in cls.available if s.name in names]
+ + [s for s in cls.available if any(g for g in s.groups if g in names)]
+ ))
+ cls.available.sort(key=lambda s: s.name)
+ for s in cls.available:
+ if s.generator_func is not None:
+ s.generator_func()
+
+ @classmethod
+ def available_names(cls) -> List[str]:
+ """List of available suites names"""
+ return [s.name for s in cls.available]
+
+ @classmethod
+ def list(cls):
+ print("Groups:")
+ print("\n".join({" - " + ', '.join(s.groups) for s in Suite.available}))
+ print("The available suites are:")
+ max_name_width = max(len(s.name) for s in Suite.available) + 5
+ lines = [
+ " - {:.<{max_name_width}} {}".format(
+ s.name + " ",
+ s.description,
+ max_name_width=max_name_width
+ )
+ for s in Suite.available
+ ]
+ print("\n".join(lines))
+
+ def __init__(
+ self,
+ name: str,
+ groups: List[str],
+ bonus: bool = False,
+ description: str = "no description",
+ ):
+ """Suite class
+ name: suite id
+ groups: list of suite groups
+ bonus: is this suite bonus
+ """
+ self.name = name
+ self.groups = groups
+ self.description = description
+ self.bonus = bonus
+ self.generator_func: Optional[Callable] = None
+ self.tests: List[Test] = []
+
+ def add(self, test):
+ """Append a test to the suite"""
+ self.tests.append(test)
+
+ BLUE_CHARS = "\033[34m"
+ CLOSE_CHARS = "\033[0m"
+
+ def run(self) -> bool:
+ """Run all test in the suite"""
+ if config.VERBOSE_LEVEL == 0:
+ print(self.name + ": ", end="")
+ else:
+ print("{}{:#^{width}}{}".format(
+ self.BLUE_CHARS,
+ " " + self.name + " ",
+ self.CLOSE_CHARS,
+ width=config.TERM_COLS
+ ))
+ for i, t in enumerate(self.tests):
+ if config.RANGE is not None:
+ if not (config.RANGE[0] <= i <= config.RANGE[1]):
+ continue
+ t.run(i)
+ if config.EXIT_FIRST and t.result is not None and t.result.failed:
+ return False
+ if config.VERBOSE_LEVEL == 0:
+ print()
+ return True
+
+ def total(self) -> Tuple[int, int]:
+ """Returns the total of passed and failed tests"""
+ passed_total = 0
+ for t in self.tests:
+ if t.result is None:
+ return (-1, -1)
+ if t.result.passed:
+ passed_total += 1
+ return passed_total, len(self.tests) - passed_total
+
+ @classmethod
+ def summarize(cls):
+ """Print a summary of all runned suites"""
+ pass_sum = 0
+ fail_sum = 0
+ print("\nSummary:")
+ for s in cls.available:
+ (pass_total, fail_total) = s.total()
+ if pass_total == -1:
+ continue
+ pass_sum += pass_total
+ fail_sum += fail_total
+ print("{:.<{width}} \033[32m{:4} [PASS]\033[0m \033[31m{:4} [FAIL]\033[0m"
+ .format(s.name + " ", pass_total, fail_total, width=config.TERM_COLS - 24))
+ print("{:.<{width}} \033[32m{:4} [PASS]\033[0m \033[31m{:4} [FAIL]\033[0m"
+ .format("TOTAL ", pass_sum, fail_sum, width=config.TERM_COLS - 24))
+
+ @classmethod
+ def save_log(cls):
+ """Save the result of all suites to a file"""
+ with open(config.LOG_PATH, "w") as log_file:
+ for s in cls.available:
+ for t in s.tests:
+ if t.result is not None and t.result.failed:
+ t.result.colored = False
+ t.result.set_colors()
+ log_file.write(t.result.full_diff() + '\n')
diff --git a/minishell_test/suites/__init__.py b/minishell_test/suites/__init__.py
new file mode 100644
index 0000000..b6b3b68
--- /dev/null
+++ b/minishell_test/suites/__init__.py
@@ -0,0 +1,17 @@
+# ############################################################################ #
+# #
+# ::: :::::::: #
+# __init__.py :+: :+: :+: #
+# +:+ +:+ +:+ #
+# By: charles <charles.cabergs@gmail.com> +#+ +:+ +#+ #
+# +#+#+#+#+#+ +#+ #
+# Created: 2020/07/15 18:24:48 by charles #+# #+# #
+# Updated: 2020/09/11 13:25:26 by charles ### ########.fr #
+# #
+# ############################################################################ #
+
+import os
+import glob
+
+modules = glob.glob(os.path.join(os.path.dirname(__file__), "*.py"))
+__all__ = [os.path.basename(f)[:-3] for f in modules if os.path.isfile(f) and not f.endswith("__init__.py")]
diff --git a/minishell_test/suites/builtin.py b/minishell_test/suites/builtin.py
new file mode 100644
index 0000000..9ab2af8
--- /dev/null
+++ b/minishell_test/suites/builtin.py
@@ -0,0 +1,403 @@
+# **************************************************************************** #
+# #
+# ::: :::::::: #
+# builtin.py :+: :+: :+: #
+# +:+ +:+ +:+ #
+# By: juligonz <juligonz@student.42.fr> +#+ +:+ +#+ #
+# +#+#+#+#+#+ +#+ #
+# Created: 2020/07/15 18:24:43 by charles #+# #+# #
+# Updated: 2020/11/28 06:17:19 by charles ### ########.fr #
+# Updated: 2020/09/11 18:01:27 by juligonz ### ########.fr #
+# #
+# **************************************************************************** #
+
+import os
+
+import config
+import hooks
+from suite import suite
+from hooks import linux_discard
+
+
+@suite()
+def suite_echo(test):
+ """ echo builtin tests """
+ test("echo")
+ test("echo bonjour")
+ test("echo lalalala lalalalal alalalalal alalalala")
+ test("echo lalalala lalalalal alalalalal alalalala")
+ test("echo " + config.LOREM)
+ test("echo -n")
+ test("echo -n bonjour")
+ test("echo -n lalalala lalalalal alalalalal alalalala")
+ test("echo -n lalalala lalalalal alalalalal alalalala")
+ test("echo -n " + config.LOREM)
+ test("echo bonjour -n")
+ test("echo -n bonjour -n")
+ test(" echo bonjour je")
+ test(" echo -n bonjour je")
+ test("echo a '' b '' c '' d")
+ test('echo a "" b "" c "" d')
+ test("echo -n a '' b '' c '' d")
+ test('echo -n a "" b "" c "" d')
+ test("echo '' '' ''")
+ test("Echo bonjour")
+ test("eCho bonjour")
+ test("ecHo bonjour")
+ test("echO bonjour")
+ test("EchO bonjour")
+ test("eCHo bonjour")
+ test("EcHo bonjour")
+ test("eChO bonjour")
+ test("Echo bonjour", exports={"PATH": "/bin:/usr/bin"})
+ test("eCho bonjour", exports={"PATH": "/bin:/usr/bin"})
+ test("ecHo bonjour", exports={"PATH": "/bin:/usr/bin"})
+ test("echO bonjour", exports={"PATH": "/bin:/usr/bin"})
+ test("EchO bonjour", exports={"PATH": "/bin:/usr/bin"})
+ test("eCHo bonjour", exports={"PATH": "/bin:/usr/bin"})
+ test("EcHo bonjour", exports={"PATH": "/bin:/usr/bin"})
+ test("eChO bonjour", exports={"PATH": "/bin:/usr/bin"})
+ test("eChO -e 'bonjo\\nur'", exports={"PATH": "/bin:/usr/bin"})
+ test("echo -n -n -n -n bonjour")
+ test("echo -nnnnnnnnnnnnnnnnnnnnn bonjour")
+ test("echo -nnnnnnnnnnnnnnnnnnnnn -n -n -n bonjour -n -n")
+
+
+@suite()
+def suite_export(test):
+ """ export builtin tests """
+ test("export", hook=hooks.export_singleton)
+ test("export", exports={"A": ""}, hook=hooks.export_singleton)
+ test("export", exports={"A": "\""}, hook=hooks.export_singleton)
+ test("export", exports={"A": "\\"}, hook=hooks.export_singleton)
+ test("export", exports={"A": "$"}, hook=hooks.export_singleton)
+ test("export", exports={"A": "\t"}, hook=hooks.export_singleton)
+ test("export", exports={"A": "'"}, hook=hooks.export_singleton)
+ test("export", exports={"A": "a"}, hook=hooks.export_singleton)
+ test("export A=a; echo $A")
+ test("export A=a B=b C=c; echo $A$B$C")
+ test("export A=a B=b C=c D=d E=e F=f G=g H=h I=i J=j K=k L=l"
+ "M=m N=n O=o P=p Q=q R=r S=s T=t U=u V=v W=w