diff --git a/test/t/conftest.py b/test/t/conftest.py index 9c31e99f043..da6d293dddf 100644 --- a/test/t/conftest.py +++ b/test/t/conftest.py @@ -8,13 +8,24 @@ import tempfile import time from pathlib import Path -from typing import Callable, Iterable, Iterator, List, Optional, Tuple +from types import TracebackType +from typing import ( + Callable, + Dict, + Iterable, + Iterator, + List, + Optional, + Tuple, + Type, +) import pexpect # type: ignore[import] import pytest PS1 = "/@" MAGIC_MARK = "__MaGiC-maRKz!__" +MAGIC_MARK2 = "Re8SCgEdfN" def find_unique_completion_pair( @@ -387,6 +398,141 @@ def assert_bash_exec( return output +class bash_env_saved: + def __init__(self, bash: pexpect.spawn, sendintr: bool = False): + self.bash = bash + self.cwd: Optional[str] = None + self.saved_shopt: Dict[str, int] = {} + self.saved_variables: Dict[str, int] = {} + self.sendintr = sendintr + + def __enter__(self): + return self + + def __exit__( + self, + exc_type: Optional[Type[BaseException]], + exc_value: Optional[BaseException], + exc_traceback: Optional[TracebackType], + ) -> None: + self._restore_env() + return None + + def _copy_variable(self, src_var: str, dst_var: str): + assert_bash_exec( + self.bash, + "if [[ ${%s+set} ]]; then %s=${%s}; else unset -v %s; fi" + % (src_var, dst_var, src_var, dst_var), + ) + + def _unset_variable(self, varname: str): + assert_bash_exec(self.bash, "unset -v %s" % varname) + + def _save_cwd(self): + if not self.cwd: + self.cwd = self.bash.cwd + + def _check_shopt(self, name: str): + assert_bash_exec( + self.bash, + '[[ $(shopt -p %s) == "${_BASHCOMP_TEST_NEWSHOPT_%s}" ]]' + % (name, name), + ) + + def _unprotect_shopt(self, name: str): + if name not in self.saved_shopt: + self.saved_shopt[name] = 1 + assert_bash_exec( + self.bash, + "_BASHCOMP_TEST_OLDSHOPT_%s=$(shopt -p %s; true)" + % (name, name), + ) + else: + self._check_shopt(name) + + def _protect_shopt(self, name: str): + assert_bash_exec( + self.bash, + "_BASHCOMP_TEST_NEWSHOPT_%s=$(shopt -p %s; true)" % (name, name), + ) + + def _check_variable(self, varname: str): + assert_bash_exec( + self.bash, + '[[ ${%s-%s} == "${_BASHCOMP_TEST_NEWVAR_%s-%s}" ]]' + % (varname, MAGIC_MARK2, varname, MAGIC_MARK2), + ) + + def _unprotect_variable(self, varname: str): + if varname not in self.saved_variables: + self.saved_variables[varname] = 1 + self._copy_variable(varname, "_BASHCOMP_TEST_OLDVAR_" + varname) + else: + self._check_variable(varname) + + def _protect_variable(self, varname: str): + self._copy_variable(varname, "_BASHCOMP_TEST_NEWVAR_" + varname) + + def _restore_env(self): + if self.sendintr: + self.bash.sendintr() + self.bash.expect_exact(PS1) + + # We first go back to the original directory before restoring + # variables because "cd" affects "OLDPWD". + if self.cwd: + self._unprotect_variable("OLDPWD") + assert_bash_exec(self.bash, "cd %s" % shlex.quote(str(self.cwd))) + self._protect_variable("OLDPWD") + self.cwd = None + + for name in self.saved_shopt: + self._check_shopt(name) + assert_bash_exec( + self.bash, 'eval "$_BASHCOMP_TEST_OLDSHOPT_%s"' % name + ) + self._unset_variable("_BASHCOMP_TEST_OLDSHOPT_" + name) + self._unset_variable("_BASHCOMP_TEST_NEWSHOPT_" + name) + self.saved_shopt = {} + + for varname in self.saved_variables: + self._check_variable(varname) + self._copy_variable("_BASHCOMP_TEST_OLDVAR_" + varname, varname) + self._unset_variable("_BASHCOMP_TEST_OLDVAR_" + varname) + self._unset_variable("_BASHCOMP_TEST_NEWVAR_" + varname) + self.saved_variables = {} + + def chdir(self, path: str): + self._save_cwd() + self._unprotect_variable("OLDPWD") + assert_bash_exec(self.bash, "cd %s" % shlex.quote(path)) + self._protect_variable("OLDPWD") + + def shopt(self, name: str, value: bool): + self._unprotect_shopt(name) + if value: + assert_bash_exec(self.bash, "shopt -s %s" % name) + else: + assert_bash_exec(self.bash, "shopt -u %s" % name) + self._protect_shopt(name) + + def write_variable(self, varname: str, new_value: str, quote: bool = True): + if quote: + new_value = shlex.quote(new_value) + self._unprotect_variable(varname) + assert_bash_exec(self.bash, "%s=%s" % (varname, new_value)) + self._protect_variable(varname) + + # TODO: We may restore the "export" attribute as well though it is + # not currently tested in "diff_env" + def write_env(self, envname: str, new_value: str, quote: bool = True): + if quote: + new_value = shlex.quote(new_value) + self._unprotect_variable(envname) + assert_bash_exec(self.bash, "export %s=%s" % (envname, new_value)) + self._protect_variable(envname) + + def get_env(bash: pexpect.spawn) -> List[str]: return [ x @@ -410,7 +556,11 @@ def diff_env(before: List[str], after: List[str], ignore: str): # Remove unified diff markers: if not re.search(r"^(---|\+\+\+|@@ )", x) # Ignore variables expected to change: - and not re.search("^[-+](_|PPID|BASH_REMATCH|OLDPWD)=", x) + and not re.search( + r"^[-+](_|PPID|BASH_REMATCH|_BASHCOMP_TEST_\w+)=", + x, + re.ASCII, + ) # Ignore likely completion functions added by us: and not re.search(r"^\+declare -f _.+", x) # ...and additional specified things: @@ -494,22 +644,19 @@ def assert_complete( pass else: pytest.xfail(xfail) - cwd = kwargs.get("cwd") - if cwd: - assert_bash_exec(bash, "cd '%s'" % cwd) - env_prefix = "_BASHCOMP_TEST_" - env = kwargs.get("env", {}) - if env: - # Back up environment and apply new one - assert_bash_exec( - bash, - " ".join('%s%s="${%s-}"' % (env_prefix, k, k) for k in env.keys()), - ) - assert_bash_exec( - bash, - "export %s" % " ".join("%s=%s" % (k, v) for k, v in env.items()), - ) - try: + + with bash_env_saved(bash, sendintr=True) as bash_env: + + cwd = kwargs.get("cwd") + if cwd: + bash_env.chdir(str(cwd)) + + for k, v in kwargs.get("env", {}).items(): + bash_env.write_env(k, v, quote=False) + + for k, v in kwargs.get("shopt", {}).items(): + bash_env.shopt(k, v) + bash.send(cmd + "\t") # Sleep a bit if requested, to avoid `.*` matching too early time.sleep(kwargs.get("sleep_after_tab", 0)) @@ -531,36 +678,13 @@ def assert_complete( output = bash.before if output.endswith(MAGIC_MARK): output = bash.before[: -len(MAGIC_MARK)] - result = CompletionResult(output) + return CompletionResult(output) elif got == 2: output = bash.match.group(1) - result = CompletionResult(output) + return CompletionResult(output) else: # TODO: warn about EOF/TIMEOUT? - result = CompletionResult() - finally: - bash.sendintr() - bash.expect_exact(PS1) - if env: - # Restore environment, and clean up backup - # TODO: Test with declare -p if a var was set, backup only if yes, and - # similarly restore only backed up vars. Should remove some need - # for ignore_env. - assert_bash_exec( - bash, - "export %s" - % " ".join( - '%s="$%s%s"' % (k, env_prefix, k) for k in env.keys() - ), - ) - assert_bash_exec( - bash, - "unset -v %s" - % " ".join("%s%s" % (env_prefix, k) for k in env.keys()), - ) - if cwd: - assert_bash_exec(bash, "cd - >/dev/null") - return result + return CompletionResult() @pytest.fixture @@ -676,21 +800,41 @@ def prepare_fixture_dir( tempdir = Path(tempfile.mkdtemp(prefix="bash-completion-fixture-dir")) request.addfinalizer(lambda: shutil.rmtree(str(tempdir))) + old_cwd = os.getcwd() + try: + os.chdir(tempdir) + new_files, new_dirs = create_dummy_filedirs(files, dirs) + finally: + os.chdir(old_cwd) + + return tempdir, new_files, new_dirs + + +def create_dummy_filedirs( + files: Iterable[str], dirs: Iterable[str] +) -> Tuple[List[str], List[str]]: + """ + Create dummy files and directories on the fly in the current directory. + + Tests that contain filenames differing only by case should use this to + prepare a dir on the fly rather than including their fixtures in git and + the tarball. This is to work better with case insensitive file systems. + """ new_files = [] new_dirs = [] for dir_ in dirs: - path = tempdir / dir_ + path = Path(dir_) if not path.exists(): path.mkdir() new_dirs.append(dir_) for file_ in files: - path = tempdir / file_ + path = Path(file_) if not path.exists(): path.touch() new_files.append(file_) - return tempdir, sorted(new_files), sorted(new_dirs) + return sorted(new_files), sorted(new_dirs) class TestUnitBase: diff --git a/test/t/test_evince.py b/test/t/test_evince.py index 8de1b7c75b4..74a3ccf9611 100644 --- a/test/t/test_evince.py +++ b/test/t/test_evince.py @@ -1,17 +1,12 @@ -import shlex -from pathlib import Path -from typing import List, Tuple - import pytest -from conftest import assert_bash_exec, assert_complete, prepare_fixture_dir +from conftest import assert_complete, create_dummy_filedirs +@pytest.mark.bashcomp(temp_cwd=True) class TestEvince: - @pytest.fixture(scope="class") - def setup_fixture(self, request) -> Tuple[Path, List[str], List[str]]: - return prepare_fixture_dir( - request, + def test_1(self, bash): + files, dirs = create_dummy_filedirs( ( ".bmp .BMP .cbr .CBR .cbz .CBZ .djv .DJV .djvu .DJVU .dvi " ".DVI .dvi.bz2 .dvi.BZ2 .DVI.bz2 .DVI.BZ2 .dvi.gz .dvi.GZ " @@ -27,15 +22,7 @@ def setup_fixture(self, request) -> Tuple[Path, List[str], List[str]]: "foo".split(), ) - def test_1(self, bash, setup_fixture): - fixture_dir, files, dirs = setup_fixture - - assert_bash_exec(bash, "cd %s" % shlex.quote(str(fixture_dir))) - try: - completion = assert_complete(bash, "evince ") - finally: - assert_bash_exec(bash, "cd -", want_output=None) - + completion = assert_complete(bash, "evince ") assert completion == [ x for x in sorted(files + ["%s/" % d for d in dirs]) diff --git a/test/t/test_kdvi.py b/test/t/test_kdvi.py index 03ab476f4cc..114e024e400 100644 --- a/test/t/test_kdvi.py +++ b/test/t/test_kdvi.py @@ -1,17 +1,12 @@ -import shlex -from pathlib import Path -from typing import List, Tuple - import pytest -from conftest import assert_bash_exec, assert_complete, prepare_fixture_dir +from conftest import assert_complete, create_dummy_filedirs +@pytest.mark.bashcomp(temp_cwd=True) class TestKdvi: - @pytest.fixture(scope="class") - def setup_fixture(self, request) -> Tuple[Path, List[str], List[str]]: - return prepare_fixture_dir( - request, + def test_1(self, bash): + files, dirs = create_dummy_filedirs( ( ".dvi .DVI .dvi.bz2 .DVI.bz2 .dvi.gz .DVI.gz .dvi.Z .DVI.Z " ".txt" @@ -19,15 +14,7 @@ def setup_fixture(self, request) -> Tuple[Path, List[str], List[str]]: "foo".split(), ) - def test_1(self, bash, setup_fixture): - fixture_dir, files, dirs = setup_fixture - - assert_bash_exec(bash, "cd %s" % shlex.quote(str(fixture_dir))) - try: - completion = assert_complete(bash, "kdvi ") - finally: - assert_bash_exec(bash, "cd -", want_output=None) - + completion = assert_complete(bash, "kdvi ") assert completion == [ x for x in sorted(files + ["%s/" % d for d in dirs]) diff --git a/test/t/test_kpdf.py b/test/t/test_kpdf.py index 87f7d61ca63..b7e658fd625 100644 --- a/test/t/test_kpdf.py +++ b/test/t/test_kpdf.py @@ -1,30 +1,17 @@ -import shlex -from pathlib import Path -from typing import List, Tuple - import pytest -from conftest import assert_bash_exec, assert_complete, prepare_fixture_dir +from conftest import assert_complete, create_dummy_filedirs +@pytest.mark.bashcomp(temp_cwd=True) class TestKpdf: - @pytest.fixture(scope="class") - def setup_fixture(self, request) -> Tuple[Path, List[str], List[str]]: - return prepare_fixture_dir( - request, + def test_1(self, bash): + files, dirs = create_dummy_filedirs( ".eps .EPS .pdf .PDF .ps .PS .txt".split(), "foo".split(), ) - def test_1(self, bash, setup_fixture): - fixture_dir, files, dirs = setup_fixture - - assert_bash_exec(bash, "cd %s" % shlex.quote(str(fixture_dir))) - try: - completion = assert_complete(bash, "kpdf ") - finally: - assert_bash_exec(bash, "cd -", want_output=None) - + completion = assert_complete(bash, "kpdf ") assert completion == [ x for x in sorted(files + ["%s/" % d for d in dirs]) diff --git a/test/t/test_man.py b/test/t/test_man.py index 015e85fb714..366b54d29cc 100644 --- a/test/t/test_man.py +++ b/test/t/test_man.py @@ -1,6 +1,11 @@ import pytest -from conftest import assert_bash_exec, assert_complete, prepare_fixture_dir +from conftest import ( + assert_bash_exec, + assert_complete, + bash_env_saved, + prepare_fixture_dir, +) @pytest.mark.bashcomp( @@ -95,24 +100,22 @@ def test_8(self, completion): "man %s" % assumed_present, require_cmd=True, cwd="shared/empty_dir", - pre_cmds=("shopt -s failglob",), + shopt=dict(failglob=True), ) def test_9(self, bash, completion): assert self.assumed_present in completion - assert_bash_exec(bash, "shopt -u failglob") @pytest.mark.complete(require_cmd=True) def test_10(self, request, bash, colonpath): - assert_bash_exec( - bash, - 'manpath=${MANPATH-}; export MANPATH="%s:%s/man"' - % (TestMan.manpath, colonpath), - ) - request.addfinalizer( - lambda: assert_bash_exec(bash, "MANPATH=$manpath") - ) - completion = assert_complete(bash, "man Bash::C") - assert completion == "ompletion" + with bash_env_saved(bash) as bash_env: + bash_env.write_env( + "MANPATH", + "%s:%s/man" % (TestMan.manpath, colonpath), + quote=False, + ) + + completion = assert_complete(bash, "man Bash::C") + assert completion == "ompletion" @pytest.mark.complete("man -", require_cmd=True) def test_11(self, completion): diff --git a/test/t/test_tar.py b/test/t/test_tar.py index 3fbaa54a1f8..4d526900350 100644 --- a/test/t/test_tar.py +++ b/test/t/test_tar.py @@ -13,10 +13,9 @@ def gnu_tar(self, bash): if not re.search(r"\bGNU ", got): pytest.skip("Not GNU tar") - @pytest.mark.complete("tar ", pre_cmds=("shopt -s failglob",)) + @pytest.mark.complete("tar ", shopt=dict(failglob=True)) def test_1(self, bash, completion): assert completion - assert_bash_exec(bash, "shopt -u failglob") # Test "f" when mode is not as first option @pytest.mark.complete("tar zfc ", cwd="tar") diff --git a/test/t/unit/test_unit_known_hosts_real.py b/test/t/unit/test_unit_known_hosts_real.py index ac5205e1478..327f0d5983e 100644 --- a/test/t/unit/test_unit_known_hosts_real.py +++ b/test/t/unit/test_unit_known_hosts_real.py @@ -2,7 +2,7 @@ import pytest -from conftest import assert_bash_exec +from conftest import assert_bash_exec, bash_env_saved @pytest.mark.bashcomp( @@ -126,33 +126,28 @@ def test_included_configs(self, bash, hosts): # fixtures/_known_hosts_real/.ssh/config_question_mark expected.append("question_mark") - assert_bash_exec( - bash, 'OLDHOME="$HOME"; HOME="%s/_known_hosts_real"' % bash.cwd - ) - output = assert_bash_exec( - bash, - "unset -v COMPREPLY COMP_KNOWN_HOSTS_WITH_HOSTFILE; " - "_known_hosts_real -aF _known_hosts_real/config_include ''; " - r'printf "%s\n" "${COMPREPLY[@]}"', - want_output=True, - ) - assert_bash_exec(bash, 'HOME="$OLDHOME"') + with bash_env_saved(bash) as bash_env: + bash_env.write_variable("HOME", "%s/_known_hosts_real" % bash.cwd) + output = assert_bash_exec( + bash, + "unset -v COMPREPLY COMP_KNOWN_HOSTS_WITH_HOSTFILE; " + "_known_hosts_real -aF _known_hosts_real/config_include ''; " + r'printf "%s\n" "${COMPREPLY[@]}"', + want_output=True, + ) assert sorted(set(output.strip().split())) == sorted(expected) def test_no_globbing(self, bash): - assert_bash_exec( - bash, 'OLDHOME="$HOME"; HOME="%s/_known_hosts_real"' % bash.cwd - ) - output = assert_bash_exec( - bash, - "cd _known_hosts_real; " - "unset -v COMPREPLY COMP_KNOWN_HOSTS_WITH_HOSTFILE; " - "_known_hosts_real -aF config ''; " - r'printf "%s\n" "${COMPREPLY[@]}"; ' - "cd - &>/dev/null", - want_output=True, - ) - assert_bash_exec(bash, 'HOME="$OLDHOME"') + with bash_env_saved(bash) as bash_env: + bash_env.write_variable("HOME", "%s/_known_hosts_real" % bash.cwd) + bash_env.chdir("_known_hosts_real") + output = assert_bash_exec( + bash, + "unset -v COMPREPLY COMP_KNOWN_HOSTS_WITH_HOSTFILE; " + "_known_hosts_real -aF config ''; " + r'printf "%s\n" "${COMPREPLY[@]}"', + want_output=True, + ) completion = sorted(set(output.strip().split())) assert "gee" in completion assert "gee-filename-canary" not in completion diff --git a/test/t/unit/test_unit_quote_readline.py b/test/t/unit/test_unit_quote_readline.py index a7ac52ec5d1..1df70bd92a6 100644 --- a/test/t/unit/test_unit_quote_readline.py +++ b/test/t/unit/test_unit_quote_readline.py @@ -2,7 +2,7 @@ import pytest -from conftest import assert_bash_exec, assert_complete +from conftest import assert_bash_exec, assert_complete, bash_env_saved @pytest.mark.bashcomp(cmd=None, temp_cwd=True) @@ -75,6 +75,21 @@ def test_github_issue_492_3(self, bash): os.mkdir("./ret=$(echo injected >&2)") assert_bash_exec(bash, "quote_readline $'\\'$*' >/dev/null") + def test_github_issue_492_4(self, bash): + """Test error messages through unintended pathname expansions + + When "shopt -s failglob" is set by the user, the completion of the word + containing glob character and special characters (e.g. TAB) results in + the failure of pathname expansions. + + $ shopt -s failglob + $ echo a\\ b*[TAB] + + """ + with bash_env_saved(bash) as bash_env: + bash_env.shopt("failglob", True) + assert_bash_exec(bash, "quote_readline $'a\\\\\\tb*' >/dev/null") + def test_github_issue_526_1(self, bash): r"""Regression tests for unprocessed escape sequences after quotes @@ -103,19 +118,3 @@ def test_github_issue_526_1(self, bash): ) == "eta/" ) - - -@pytest.mark.bashcomp(cmd=None, temp_cwd=True, pre_cmds=("shopt -s failglob",)) -class TestUnitQuoteReadlineWithFailglob: - def test_github_issue_492_4(self, bash): - """Test error messages through unintended pathname expansions - - When "shopt -s failglob" is set by the user, the completion of the word - containing glob character and special characters (e.g. TAB) results in - the failure of pathname expansions. - - $ shopt -s failglob - $ echo a\\ b*[TAB] - - """ - assert_bash_exec(bash, "quote_readline $'a\\\\\\tb*' >/dev/null")