Skip to content

test: detect unexpected OLDPWD change #527

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 5 commits into from
May 27, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
238 changes: 191 additions & 47 deletions test/t/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -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
Expand All @@ -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:
Expand Down Expand Up @@ -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))
Expand All @@ -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
Expand Down Expand Up @@ -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:
Expand Down
23 changes: 5 additions & 18 deletions test/t/test_evince.py
Original file line number Diff line number Diff line change
@@ -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 "
Expand All @@ -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])
Expand Down
23 changes: 5 additions & 18 deletions test/t/test_kdvi.py
Original file line number Diff line number Diff line change
@@ -1,33 +1,20 @@
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"
).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, "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])
Expand Down
Loading