diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-05-04 01:03:18 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-05-04 01:03:18 +0000 |
commit | f4acb49ea148cdd899f7f29f1591c7bc853c2135 (patch) | |
tree | b62d60873864065d6428a84a119dd8a3c90f1397 /test/t | |
parent | Adding upstream version 1:2.11. (diff) | |
download | bash-completion-f4acb49ea148cdd899f7f29f1591c7bc853c2135.tar.xz bash-completion-f4acb49ea148cdd899f7f29f1591c7bc853c2135.zip |
Adding upstream version 1:2.12.0.upstream/1%2.12.0
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'test/t')
180 files changed, 4015 insertions, 630 deletions
diff --git a/test/t/Makefile.am b/test/t/Makefile.am index 801841f..5a59969 100644 --- a/test/t/Makefile.am +++ b/test/t/Makefile.am @@ -21,6 +21,7 @@ EXTRA_DIST = \ test_apt_build.py \ test_apt_cache.py \ test_apt_get.py \ + test_apt_mark.py \ test_aptitude.py \ test_arch.py \ test_arp.py \ @@ -36,8 +37,10 @@ EXTRA_DIST = \ test_autoscan.py \ test_autossh.py \ test_autoupdate.py \ + test_avahi_browse.py \ test_avctrl.py \ test_awk.py \ + test_b2sum.py \ test_badblocks.py \ test_base64.py \ test_bash.py \ @@ -47,6 +50,7 @@ EXTRA_DIST = \ test_bk.py \ test_bmake.py \ test_brctl.py \ + test_bsdtar.py \ test_btdownloadcurses_py.py \ test_btdownloadgui_py.py \ test_btdownloadheadless_py.py \ @@ -214,13 +218,16 @@ EXTRA_DIST = \ test_growisofs.py \ test_grpck.py \ test_grub.py \ + test_gssdp_device_sniffer.py \ test_gssdp_discover.py \ test_gzip.py \ + test_hash.py \ test_hciattach.py \ test_hciconfig.py \ test_hcitool.py \ test_hddtemp.py \ test_head.py \ + test_help.py \ test_hexdump.py \ test_hid2hci.py \ test_host.py \ @@ -253,6 +260,7 @@ EXTRA_DIST = \ test_ip.py \ test_ipcalc.py \ test_iperf.py \ + test_iperf3.py \ test_ipmitool.py \ test_ipsec.py \ test_iptables.py \ @@ -395,6 +403,7 @@ EXTRA_DIST = \ test_mysqladmin.py \ test_nc.py \ test_ncftp.py \ + test_neomutt.py \ test_nethogs.py \ test_netstat.py \ test_newgrp.py \ @@ -413,7 +422,6 @@ EXTRA_DIST = \ test_objdump.py \ test_od.py \ test_oggdec.py \ - test_op.py \ test_openssl.py \ test_opera.py \ test_optipng.py \ @@ -422,6 +430,7 @@ EXTRA_DIST = \ test_passwd.py \ test_paste.py \ test_patch.py \ + test_pdftoppm.py \ test_pdftotext.py \ test_perl.py \ test_perlcritic.py \ @@ -439,6 +448,7 @@ EXTRA_DIST = \ test_pkg_get.py \ test_pkg_info.py \ test_pkgadd.py \ + test_pkgconf.py \ test_pkgrm.py \ test_pkgtool.py \ test_pkgutil.py \ @@ -461,6 +471,7 @@ EXTRA_DIST = \ test_prelink.py \ test_printenv.py \ test_protoc.py \ + test_ps.py \ test_psql.py \ test_ptx.py \ test_puppet.py \ @@ -483,11 +494,13 @@ EXTRA_DIST = \ test_pyflakes.py \ test_pylint.py \ test_pylint_3.py \ + test_pyston.py \ test_pytest.py \ test_python.py \ test_python3.py \ test_pyvenv.py \ test_qemu.py \ + test_qemu_system_x86_64.py \ test_qrunner.py \ test_querybts.py \ test_quota.py \ @@ -540,6 +553,10 @@ EXTRA_DIST = \ test_sftp.py \ test_sh.py \ test_sha1sum.py \ + test_sha224sum.py \ + test_sha256sum.py \ + test_sha384sum.py \ + test_sha512sum.py \ test_shar.py \ test_shellcheck.py \ test_sitecopy.py \ @@ -564,6 +581,7 @@ EXTRA_DIST = \ test_ssh_add.py \ test_ssh_copy_id.py \ test_ssh_keygen.py \ + test_ssh_keyscan.py \ test_sshfs.py \ test_sshmitm.py \ test_sshow.py \ @@ -600,6 +618,8 @@ EXTRA_DIST = \ test_tox.py \ test_tr.py \ test_tracepath.py \ + test_tree.py \ + test_truncate.py \ test_tshark.py \ test_tsig_keygen.py \ test_tune2fs.py \ @@ -663,6 +683,7 @@ EXTRA_DIST = \ test_wvdial.py \ test_xdg_mime.py \ test_xdg_settings.py \ + test_xev.py \ test_xfreerdp.py \ test_xgamma.py \ test_xhost.py \ @@ -691,8 +712,14 @@ all: PYTEST = @PYTEST@ -check-local: - $(PYTEST) $(PYTESTFLAGS) $(srcdir) +# Some tests require completions/ symlinks to be in place, which would be a +# chore to achieve in the build dir with VPATH builds (well not the symlinks, +# but the "main" files they target), e.g. "make distcheck". Therefore we test +# the installed tree instead, which isn't a bad idea in the first place. +installcheck-local: + ABS_TOP_BUILDDIR="$(abs_top_builddir)" \ + BASH_COMPLETION_TEST_BASH_COMPLETION="$(DESTDIR)/$(pkgdatadir)/bash_completion" \ + $(PYTEST) $(PYTESTFLAGS) $(srcdir) clean-local: $(RM) -R __pycache__ diff --git a/test/t/conftest.py b/test/t/conftest.py index 5c1603d..874ef1c 100644 --- a/test/t/conftest.py +++ b/test/t/conftest.py @@ -2,15 +2,32 @@ import difflib import os import re import shlex +import shutil import subprocess +import sys +import tempfile import time -from typing import Callable, Iterable, Iterator, List, Optional, Tuple - -import pexpect +from enum import Enum +from pathlib import Path +from types import TracebackType +from typing import ( + Callable, + Dict, + Iterable, + Iterator, + List, + Optional, + TextIO, + Tuple, + Type, +) + +import pexpect # type: ignore[import] import pytest PS1 = "/@" -MAGIC_MARK = "__MaGiC-maRKz!__" +MAGIC_MARK = "__MaGiC-maRKz-NEtXZVZfKC__" +MAGIC_MARK2 = "Re8SCgEdfN" def find_unique_completion_pair( @@ -115,8 +132,8 @@ def _avahi_hosts(bash: pexpect.spawn) -> List[str]: def known_hosts(bash: pexpect.spawn) -> List[str]: output = assert_bash_exec( bash, - '_known_hosts_real ""; ' - r'printf "%s\n" "${COMPREPLY[@]}"; unset COMPREPLY', + '_comp_compgen_known_hosts ""; ' + r'printf "%s\n" "${COMPREPLY[@]}"; unset -v COMPREPLY', want_output=True, ) return sorted(set(output.split())) @@ -127,7 +144,9 @@ def user_home(bash: pexpect.spawn) -> Tuple[str, str]: user = assert_bash_exec( bash, 'id -un 2>/dev/null || echo "$USER"', want_output=True ).strip() - home = assert_bash_exec(bash, 'echo "$HOME"', want_output=True).strip() + # We used to echo $HOME here, but we expect that it will be consistent with + # ~user as far as bash is concerned which may not hold. + home = assert_bash_exec(bash, "echo ~%s" % user, want_output=True).strip() return (user, home) @@ -167,107 +186,175 @@ def partialize( @pytest.fixture(scope="class") def bash(request) -> pexpect.spawn: + logfile: Optional[TextIO] = None + histfile = None + tmpdir = None + bash = None + + if os.environ.get("BASH_COMPLETION_TEST_LOGFILE"): + logfile = open(os.environ["BASH_COMPLETION_TEST_LOGFILE"], "w") + elif os.environ.get("CI"): + logfile = sys.stdout - logfile = None - if os.environ.get("BASHCOMP_TEST_LOGFILE"): - logfile = open(os.environ["BASHCOMP_TEST_LOGFILE"], "w") testdir = os.path.abspath( os.path.join(os.path.dirname(__file__), os.pardir) ) - env = os.environ.copy() - env.update( - dict( - SRCDIR=testdir, # TODO needed at least by bashrc - SRCDIRABS=testdir, # TODO needed? - PS1=PS1, - INPUTRC="%s/config/inputrc" % testdir, - TERM="dumb", - LC_COLLATE="C", # to match Python's default locale unaware sort - ) - ) - fixturesdir = os.path.join(testdir, "fixtures") - os.chdir(fixturesdir) - - # Start bash - bash = pexpect.spawn( - "%s --norc" % os.environ.get("BASHCOMP_TEST_BASH", "bash"), - maxread=os.environ.get("BASHCOMP_TEST_PEXPECT_MAXREAD", 20000), - logfile=logfile, - cwd=fixturesdir, - env=env, - encoding="utf-8", # TODO? or native or...? - # FIXME: Tests shouldn't depend on dimensions, but it's difficult to - # expect robustly enough for Bash to wrap lines anywhere (e.g. inside - # MAGIC_MARK). Increase window width to reduce wrapping. - dimensions=(24, 160), - # TODO? codec_errors="replace", + # Create an empty temporary file for HISTFILE. + # + # To prevent the tested Bash processes from writing to the user's + # history file or any other files, we prepare an empty temporary + # file for each test. + # + # - Note that HISTFILE=/dev/null may not work. It results in the + # removal of the device /dev/null and the creation of a regular + # file at /dev/null when the number of commands reach + # HISTFILESIZE due to a bug in bash 4.3. This causes execution of + # garbage through BASH_COMPLETION_USER_FILE=/dev/null. + # - Note also that "unset -v HISTFILE" in "test/config/bashrc" was not + # adopted because "test/config/bashrc" is loaded after the + # history is read from the history file. + # + histfile = tempfile.NamedTemporaryFile( + prefix="bash-completion-test_", delete=False ) - bash.expect_exact(PS1) - # Load bashrc and bash_completion - assert_bash_exec(bash, "source '%s/config/bashrc'" % testdir) - assert_bash_exec(bash, "source '%s/../bash_completion'" % testdir) - - # Use command name from marker if set, or grab from test filename - cmd = None # type: Optional[str] - cmd_found = False - marker = request.node.get_closest_marker("bashcomp") - if marker: - cmd = marker.kwargs.get("cmd") - cmd_found = "cmd" in marker.kwargs - # Run pre-test commands, early so they're usable in skipif - for pre_cmd in marker.kwargs.get("pre_cmds", []): - assert_bash_exec(bash, pre_cmd) - # Process skip and xfail conditions - skipif = marker.kwargs.get("skipif") - if skipif: - try: - assert_bash_exec(bash, skipif, want_output=None) - except AssertionError: - pass - else: - bash.close() - pytest.skip(skipif) - xfail = marker.kwargs.get("xfail") - if xfail: - try: - assert_bash_exec(bash, xfail, want_output=None) - except AssertionError: - pass - else: - pytest.xfail(xfail) - if not cmd_found: - match = re.search( - r"^test_(.+)\.py$", os.path.basename(str(request.fspath)) + try: + # release the file handle so that Bash can open the file. + histfile.close() + + env = os.environ.copy() + env.update( + dict( + SRCDIR=testdir, # TODO needed at least by bashrc + SRCDIRABS=testdir, + PS1=PS1, + INPUTRC="%s/config/inputrc" % testdir, + TERM="dumb", + LC_COLLATE="C", # to match Python's default locale unaware sort + HISTFILE=histfile.name, + ) + ) + + marker = request.node.get_closest_marker("bashcomp") + + # Set up the current working directory + cwd = None + if marker: + if "cwd" in marker.kwargs and marker.kwargs.get("cwd") is not None: + cwd = os.path.join( + testdir, "fixtures", marker.kwargs.get("cwd") + ) + elif "temp_cwd" in marker.kwargs and marker.kwargs.get("temp_cwd"): + tmpdir = tempfile.TemporaryDirectory( + prefix="bash-completion-test_" + ) + cwd = tmpdir.name + if cwd is None: + cwd = os.path.join(testdir, "fixtures") + os.chdir(cwd) + + # Start bash + bash = pexpect.spawn( + "%s --norc" % os.environ.get("BASH_COMPLETION_TEST_BASH", "bash"), + maxread=os.environ.get( + "BASH_COMPLETION_TEST_PEXPECT_MAXREAD", 20000 + ), + logfile=logfile, + cwd=cwd, + env=env, + encoding="utf-8", # TODO? or native or...? + # FIXME: Tests shouldn't depend on dimensions, but it's difficult to + # expect robustly enough for Bash to wrap lines anywhere (e.g. inside + # MAGIC_MARK). Increase window width to reduce wrapping. + dimensions=(24, 240), + # TODO? codec_errors="replace", ) - if match: - cmd = match.group(1) - - request.cls.cmd = cmd - - if (cmd_found and cmd is None) or is_testable(bash, cmd): - before_env = get_env(bash) - yield bash - # Not exactly sure why, but some errors leave bash in state where - # getting the env here would fail and trash our test output. So - # reset to a good state first (Ctrl+C, expect prompt). - bash.sendintr() bash.expect_exact(PS1) - diff_env( - before_env, - get_env(bash), - marker.kwargs.get("ignore_env") if marker else "", + + # Load bashrc and bash_completion + bash_completion = os.environ.get( + "BASH_COMPLETION_TEST_BASH_COMPLETION", + "%s/../bash_completion" % testdir, ) + assert_bash_exec(bash, "source '%s/config/bashrc'" % testdir) + assert_bash_exec(bash, "source '%s'" % bash_completion) + + # Use command name from marker if set, or grab from test filename + cmd = None # type: Optional[str] + cmd_found = False + if marker: + cmd = marker.kwargs.get("cmd") + cmd_found = "cmd" in marker.kwargs + # Run pre-test commands, early so they're usable in skipif + for pre_cmd in marker.kwargs.get("pre_cmds", []): + assert_bash_exec(bash, pre_cmd, want_output=None) + # Process skip and xfail conditions + skipif = marker.kwargs.get("skipif") + if skipif: + try: + assert_bash_exec(bash, skipif, want_output=None) + except AssertionError: + pass + else: + bash.close() + bash = None + pytest.skip(skipif) + xfail = marker.kwargs.get("xfail") + if xfail: + try: + assert_bash_exec(bash, xfail, want_output=None) + except AssertionError: + pass + else: + pytest.xfail(xfail) + if not cmd_found: + match = re.search( + r"^test_(.+)\.py$", os.path.basename(str(request.fspath)) + ) + if match: + cmd = match.group(1) + if ( + marker + and marker.kwargs + and marker.kwargs.get("require_cmd", False) + ): + if not is_bash_type(bash, cmd): + pytest.skip("Command not found") + + request.cls.cmd = cmd + + if (cmd_found and cmd is None) or is_testable(bash, cmd): + before_env = get_env(bash) + yield bash + # Not exactly sure why, but some errors leave bash in state where + # getting the env here would fail and trash our test output. So + # reset to a good state first (Ctrl+C, expect prompt). + bash.sendintr() + bash.expect_exact(PS1) + diff_env( + before_env, + get_env(bash), + marker.kwargs.get("ignore_env") if marker else "", + ) - if marker: - for post_cmd in marker.kwargs.get("post_cmds", []): - assert_bash_exec(bash, post_cmd) + if marker: + for post_cmd in marker.kwargs.get("post_cmds", []): + assert_bash_exec(bash, post_cmd, want_output=None) - # Clean up - bash.close() - if logfile: - logfile.close() + finally: + # Clean up + if bash: + bash.close() + if tmpdir: + tmpdir.cleanup() + if histfile: + try: + os.remove(histfile.name) + except OSError: + pass + if logfile and logfile != sys.stdout: + logfile.close() def is_testable(bash: pexpect.spawn, cmd: Optional[str]) -> bool: @@ -292,9 +379,9 @@ def is_bash_type(bash: pexpect.spawn, cmd: Optional[str]) -> bool: def load_completion_for(bash: pexpect.spawn, cmd: str) -> bool: try: - # Allow __load_completion to fail so we can test completions + # Allow _comp_load to fail so we can test completions # that are directly loaded in bash_completion without a separate file. - assert_bash_exec(bash, "__load_completion %s || :" % cmd) + assert_bash_exec(bash, "_comp_load %s || :" % cmd) assert_bash_exec(bash, "complete -p %s &>/dev/null" % cmd) except AssertionError: return False @@ -341,27 +428,301 @@ def assert_bash_exec( if output: assert want_output, ( 'Unexpected output from "%s": exit status=%s, output="%s"' - % (cmd, status, output) + % ( + cmd, + status, + output, + ) ) else: assert not want_output, ( 'Expected output from "%s": exit status=%s, output="%s"' - % (cmd, status, output) + % ( + cmd, + status, + output, + ) ) return output +class bash_env_saved: + counter: int = 0 + + class saved_state(Enum): + ChangesDetected = 1 + ChangesIgnored = 2 + + def __init__(self, bash: pexpect.spawn, sendintr: bool = False): + bash_env_saved.counter += 1 + self.prefix: str = "_comp__test_%d" % bash_env_saved.counter + + self.bash = bash + self.cwd_changed: bool = False + self.saved_set: Dict[str, bash_env_saved.saved_state] = {} + self.saved_shopt: Dict[str, bash_env_saved.saved_state] = {} + self.saved_variables: Dict[str, bash_env_saved.saved_state] = {} + self.sendintr = sendintr + + self.noexcept: bool = False + self.captured_error: Optional[Exception] = None + + 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 _safe_sendintr(self): + try: + self.bash.sendintr() + self.bash.expect_exact(PS1) + except Exception as e: + if self.noexcept: + self.captured_error = e + else: + raise + + def _safe_exec(self, cmd: str): + try: + self.bash.sendline(cmd) + self.bash.expect_exact(cmd) + self.bash.expect_exact("\r\n" + PS1) + except Exception as e: + if self.noexcept: + self._safe_sendintr() + self.captured_error = e + else: + raise + + def _safe_assert(self, cmd: str): + try: + assert_bash_exec(self.bash, cmd, want_output=None) + except Exception as e: + if self.noexcept: + self._safe_sendintr() + self.captured_error = e + else: + raise + + def _copy_variable(self, src_var: str, dst_var: str): + self._safe_exec( + "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): + self._safe_exec("unset -v %s" % varname) + + def _save_cwd(self): + if not self.cwd_changed: + self.cwd_changed = True + self._copy_variable("PWD", "%s_OLDPWD" % self.prefix) + + def _check_set(self, name: str): + if self.saved_set[name] != bash_env_saved.saved_state.ChangesDetected: + return + self._safe_assert( + '[[ $(shopt -po %s) == "${%s_NEWSHOPT_%s}" ]]' + % (name, self.prefix, name), + ) + + def _unprotect_set(self, name: str): + if name not in self.saved_set: + self.saved_set[name] = bash_env_saved.saved_state.ChangesDetected + self._safe_exec( + "%s_OLDSHOPT_%s=$(shopt -po %s || true)" + % (self.prefix, name, name), + ) + else: + self._check_set(name) + + def _protect_set(self, name: str): + self._safe_exec( + "%s_NEWSHOPT_%s=$(shopt -po %s || true)" + % (self.prefix, name, name), + ) + + def _check_shopt(self, name: str): + if ( + self.saved_shopt[name] + != bash_env_saved.saved_state.ChangesDetected + ): + return + self._safe_assert( + '[[ $(shopt -p %s) == "${%s_NEWSHOPT_%s}" ]]' + % (name, self.prefix, name), + ) + + def _unprotect_shopt(self, name: str): + if name not in self.saved_shopt: + self.saved_shopt[name] = bash_env_saved.saved_state.ChangesDetected + self._safe_exec( + "%s_OLDSHOPT_%s=$(shopt -p %s || true)" + % (self.prefix, name, name), + ) + else: + self._check_shopt(name) + + def _protect_shopt(self, name: str): + self._safe_exec( + "%s_NEWSHOPT_%s=$(shopt -p %s || true)" + % (self.prefix, name, name), + ) + + def _check_variable(self, varname: str): + if ( + self.saved_variables[varname] + != bash_env_saved.saved_state.ChangesDetected + ): + return + try: + self._safe_assert( + '[[ ${%s-%s} == "${%s_NEWVAR_%s-%s}" ]]' + % (varname, MAGIC_MARK2, self.prefix, varname, MAGIC_MARK2), + ) + except Exception: + self._copy_variable( + "%s_NEWVAR_%s" % (self.prefix, varname), varname + ) + raise + else: + if self.noexcept and self.captured_error: + self._copy_variable( + "%s_NEWVAR_%s" % (self.prefix, varname), varname + ) + + def _unprotect_variable(self, varname: str): + if varname not in self.saved_variables: + self.saved_variables[ + varname + ] = bash_env_saved.saved_state.ChangesDetected + self._copy_variable( + varname, "%s_OLDVAR_%s" % (self.prefix, varname) + ) + else: + self._check_variable(varname) + + def _protect_variable(self, varname: str): + self._copy_variable(varname, "%s_NEWVAR_%s" % (self.prefix, varname)) + + def _restore_env(self): + self.noexcept = True + + if self.sendintr: + self._safe_sendintr() + + # We first go back to the original directory before restoring + # variables because "cd" affects "OLDPWD". + if self.cwd_changed: + self._unprotect_variable("OLDPWD") + self._safe_exec('command cd -- "$%s_OLDPWD"' % self.prefix) + self._protect_variable("OLDPWD") + self._unset_variable("%s_OLDPWD" % self.prefix) + self.cwd_changed = False + + for varname in self.saved_variables: + self._check_variable(varname) + self._copy_variable( + "%s_OLDVAR_%s" % (self.prefix, varname), varname + ) + self._unset_variable("%s_OLDVAR_%s" % (self.prefix, varname)) + self._unset_variable("%s_NEWVAR_%s" % (self.prefix, varname)) + self.saved_variables = {} + + for name in self.saved_shopt: + self._check_shopt(name) + self._safe_exec('eval "$%s_OLDSHOPT_%s"' % (self.prefix, name)) + self._unset_variable("%s_OLDSHOPT_%s" % (self.prefix, name)) + self._unset_variable("%s_NEWSHOPT_%s" % (self.prefix, name)) + self.saved_shopt = {} + + for name in self.saved_set: + self._check_set(name) + self._safe_exec('eval "$%s_OLDSHOPT_%s"' % (self.prefix, name)) + self._unset_variable("%s_OLDSHOPT_%s" % (self.prefix, name)) + self._unset_variable("%s_NEWSHOPT_%s" % (self.prefix, name)) + self.saved_set = {} + + self.noexcept = False + if self.captured_error: + raise self.captured_error + + def chdir(self, path: str): + self._save_cwd() + self.cwd_changed = True + self._unprotect_variable("OLDPWD") + self._safe_exec("command cd -- %s" % shlex.quote(path)) + self._protect_variable("OLDPWD") + + def set(self, name: str, value: bool): + self._unprotect_set(name) + if value: + self._safe_exec("set -u %s" % name) + else: + self._safe_exec("set +o %s" % name) + self._protect_set(name) + + def save_set(self, name: str): + self._unprotect_set(name) + self.saved_set[name] = bash_env_saved.saved_state.ChangesIgnored + + def shopt(self, name: str, value: bool): + self._unprotect_shopt(name) + if value: + self._safe_exec("shopt -s %s" % name) + else: + self._safe_exec("shopt -u %s" % name) + self._protect_shopt(name) + + def save_shopt(self, name: str): + self._unprotect_shopt(name) + self.saved_shopt[name] = bash_env_saved.saved_state.ChangesIgnored + + def write_variable(self, varname: str, new_value: str, quote: bool = True): + if quote: + new_value = shlex.quote(new_value) + self._unprotect_variable(varname) + self._safe_exec("%s=%s" % (varname, new_value)) + self._protect_variable(varname) + + def save_variable(self, varname: str): + self._unprotect_variable(varname) + self.saved_variables[ + varname + ] = bash_env_saved.saved_state.ChangesIgnored + + # 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) + self._safe_exec("export %s=%s" % (envname, new_value)) + self._protect_variable(envname) + + def get_env(bash: pexpect.spawn) -> List[str]: - return ( - assert_bash_exec( + return [ + x + for x in assert_bash_exec( bash, - "{ (set -o posix ; set); declare -F; shopt -p; set -o; }", + "_comp__test_get_env", want_output=True, ) .strip() .splitlines() - ) + # Sometimes there are empty lines in the output due to unknown + # reasons, e.g. in GitHub Actions' macos-latest OS. Filter them out. + if x + ] def diff_env(before: List[str], after: List[str], ignore: str): @@ -371,7 +732,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|(BASH_)?LINENO)=", + x, + re.ASCII, + ) # Ignore likely completion functions added by us: and not re.search(r"^\+declare -f _.+", x) # ...and additional specified things: @@ -455,73 +820,72 @@ 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: - bash.send(cmd + "\t") + + 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) + + input_cmd = cmd + rendered_cmd = kwargs.get("rendered_cmd", cmd) + re_MAGIC_MARK = re.escape(MAGIC_MARK) + + trail = kwargs.get("trail") + if trail: + # \002 = ^B = cursor left + input_cmd += trail + "\002" * len(trail) + rendered_cmd += trail + "\b" * len(trail) + + # After reading the results, something weird happens. For most test + # setups, as expected (pun intended!), MAGIC_MARK follows as + # is. But for some others (e.g. CentOS 6, Ubuntu 14 test + # containers), we get MAGIC_MARK one character a time, followed + # each time by trail and the corresponding number of \b's. Don't + # know why, but accept it until/if someone finds out. Or just be + # fine with it indefinitely, the visible and practical end result + # on a terminal is the same anyway. + maybe_trail = "(%s%s)?" % (re.escape(trail), "\b" * len(trail)) + re_MAGIC_MARK = "".join( + re.escape(x) + maybe_trail for x in MAGIC_MARK + ) + + bash.send(input_cmd + "\t") # Sleep a bit if requested, to avoid `.*` matching too early time.sleep(kwargs.get("sleep_after_tab", 0)) - bash.expect_exact(cmd) + bash.expect_exact(rendered_cmd) bash.send(MAGIC_MARK) got = bash.expect( [ # 0: multiple lines, result in .before - r"\r\n" + re.escape(PS1 + cmd) + ".*" + re.escape(MAGIC_MARK), + r"\r\n" + re.escape(PS1 + rendered_cmd) + ".*" + re_MAGIC_MARK, # 1: no completion - r"^" + re.escape(MAGIC_MARK), + r"^" + re_MAGIC_MARK, # 2: on same line, result in .match - r"^([^\r]+)%s$" % re.escape(MAGIC_MARK), + r"^([^\r]+)%s$" % re_MAGIC_MARK, + # 3: error messages + r"^([^\r].*)%s$" % re_MAGIC_MARK, pexpect.EOF, pexpect.TIMEOUT, ] ) if got == 0: - output = bash.before - if output.endswith(MAGIC_MARK): - output = bash.before[: -len(MAGIC_MARK)] - result = CompletionResult(output) + output = re.sub(re_MAGIC_MARK + "$", "", bash.before) + return CompletionResult(output) elif got == 2: output = bash.match.group(1) - result = CompletionResult(output) + return CompletionResult(output) + elif got == 3: + output = bash.match.group(1) + raise AssertionError("Unexpected output: [%s]" % 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 @@ -530,7 +894,7 @@ def completion(request, bash: pexpect.spawn) -> CompletionResult: if not marker: return CompletionResult() for pre_cmd in marker.kwargs.get("pre_cmds", []): - assert_bash_exec(bash, pre_cmd) + assert_bash_exec(bash, pre_cmd, want_output=None) cmd = getattr(request.cls, "cmd", None) if marker.kwargs.get("require_longopt"): # longopt completions require both command presence and that it @@ -539,66 +903,18 @@ def completion(request, bash: pexpect.spawn) -> CompletionResult: marker.kwargs["require_cmd"] = True if "xfail" not in marker.kwargs: marker.kwargs["xfail"] = ( + # --help is required to exit with zero in order to not get a + # positive for cases where it errors out with a message like + # "foo: unrecognized option '--help'" "! %s --help &>/dev/null || " "! %s --help 2>&1 | command grep -qF -- --help" ) % ((cmd,) * 2) if marker.kwargs.get("require_cmd") and not is_bash_type(bash, cmd): pytest.skip("Command not found") - if "trail" in marker.kwargs: - return assert_complete_at_point( - bash, cmd=marker.args[0], trail=marker.kwargs["trail"] - ) - return assert_complete(bash, marker.args[0], **marker.kwargs) -def assert_complete_at_point( - bash: pexpect.spawn, cmd: str, trail: str -) -> CompletionResult: - # TODO: merge to assert_complete - fullcmd = "%s%s%s" % ( - cmd, - trail, - "\002" * len(trail), - ) # \002 = ^B = cursor left - bash.send(fullcmd + "\t") - bash.send(MAGIC_MARK) - bash.expect_exact(fullcmd.replace("\002", "\b")) - - got = bash.expect_exact( - [ - # 0: multiple lines, result in .before - PS1 + fullcmd.replace("\002", "\b"), - # 1: no completion - MAGIC_MARK, - pexpect.EOF, - pexpect.TIMEOUT, - ] - ) - if got == 0: - output = bash.before - result = CompletionResult(output) - - # At this point, something weird happens. For most test setups, as - # expected (pun intended!), MAGIC_MARK follows as is. But for some - # others (e.g. CentOS 6, Ubuntu 14 test containers), we get MAGIC_MARK - # one character a time, followed each time by trail and the corresponding - # number of \b's. Don't know why, but accept it until/if someone finds out. - # Or just be fine with it indefinitely, the visible and practical end - # result on a terminal is the same anyway. - repeat = "(%s%s)?" % (re.escape(trail), "\b" * len(trail)) - fullexpected = "".join( - "%s%s" % (re.escape(x), repeat) for x in MAGIC_MARK - ) - bash.expect(fullexpected) - else: - # TODO: warn about EOF/TIMEOUT? - result = CompletionResult() - - return result - - def in_container() -> bool: try: container = subprocess.check_output( @@ -624,6 +940,56 @@ def in_container() -> bool: return False +def prepare_fixture_dir( + request, files: Iterable[str], dirs: Iterable[str] +) -> Tuple[Path, List[str], List[str]]: + """ + Fixture to prepare a test dir with dummy contents on the fly. + + 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. + """ + 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 = Path(dir_) + if not path.exists(): + path.mkdir() + new_dirs.append(dir_) + for file_ in files: + path = Path(file_) + if not path.exists(): + path.touch() + new_files.append(file_) + + return sorted(new_files), sorted(new_dirs) + + class TestUnitBase: def _test_unit( self, func, bash, comp_words, comp_cword, comp_line, comp_point, arg="" diff --git a/test/t/test_2to3.py b/test/t/test_2to3.py index 4bce44e..1fc14e7 100644 --- a/test/t/test_2to3.py +++ b/test/t/test_2to3.py @@ -6,6 +6,6 @@ class Test2to3: def test_1(self, completion): assert completion - @pytest.mark.complete("2to3 -", require_cmd=True, require_longopt=True) + @pytest.mark.complete("2to3 -", require_longopt=True) def test_2(self, completion): assert completion diff --git a/test/t/test_7z.py b/test/t/test_7z.py index d4308d9..67681a5 100644 --- a/test/t/test_7z.py +++ b/test/t/test_7z.py @@ -17,7 +17,7 @@ class Test7z: @pytest.mark.complete("7z x ", cwd="7z") def test_4(self, completion): - assert completion == "a.7z" + assert completion == "a.7z hello.7z.001".split() @pytest.mark.complete("7z d a.7z ", cwd="7z", require_cmd=True) def test_5(self, completion): @@ -25,7 +25,9 @@ class Test7z: @pytest.mark.complete("7z a -air@", cwd="7z") def test_6(self, completion): - assert completion == sorted("-air@a.7z -air@f.txt".split()) + assert completion == sorted( + "-air@a.7z -air@hello.7z.001 -air@hello.7z.002 -air@f.txt".split() + ) @pytest.mark.complete("7z a -o") def test_7(self, completion): diff --git a/test/t/test_alias.py b/test/t/test_alias.py index cc592a8..0a9eb3b 100644 --- a/test/t/test_alias.py +++ b/test/t/test_alias.py @@ -19,3 +19,11 @@ class TestAlias: @pytest.mark.complete("alias ", trail="foo") def test_alias_at_point(self, completion): assert completion == "bar foo".split() + + @pytest.mark.complete("alias -") + def test_options(self, completion): + assert completion + + @pytest.mark.complete("alias -p ") + def test_p(self, completion): + assert not completion diff --git a/test/t/test_ant.py b/test/t/test_ant.py index 94acea1..de4c414 100644 --- a/test/t/test_ant.py +++ b/test/t/test_ant.py @@ -3,28 +3,43 @@ import pytest from conftest import assert_bash_exec -@pytest.mark.bashcomp(ignore_env=r"^\+ANT_ARGS=") +@pytest.mark.bashcomp( + ignore_env=r"^\+ANT_ARGS=", + temp_cwd=True, + pre_cmds=('cp "$SRCDIRABS"/fixtures/ant/*.xml .',), +) class TestAnt: + @pytest.fixture(scope="class") + def has_complete_ant_cmd_pl(self, bash): + output = assert_bash_exec(bash, "complete -p ant", want_output=True) + return "complete-ant-cmd.pl" in output + @pytest.mark.complete("ant -", require_cmd=True) def test_1(self, completion): assert completion - @pytest.mark.complete("ant ", cwd="ant") + @pytest.mark.complete("ant ") def test_2(self, completion): assert completion == "bashcomp clean init realclean".split() - @pytest.mark.complete("ant -f build-with-import.xml ", cwd="ant") - def test_3(self, completion): - assert completion == "build-with-import imported-build".split() + @pytest.mark.complete("ant -f build-with-import.xml ") + def test_3(self, completion, has_complete_ant_cmd_pl): + if has_complete_ant_cmd_pl: + # Some versions of complete-ant-cmd.pl add "import-project-name." + # prefix to imported targets, just check that the ones we add + # are there. + assert all( + x in completion + for x in "build-with-import imported-build".split() + ) + else: + assert completion == "build-with-import imported-build".split() - @pytest.mark.complete( - "ant ", cwd="ant", env=dict(ANT_ARGS="'-f named-build.xml'") - ) - def test_4(self, bash, completion): - output = assert_bash_exec(bash, "complete -p ant", want_output=True) - if "complete-ant-cmd.pl" in output: + @pytest.mark.complete("ant ", env=dict(ANT_ARGS="'-f named-build.xml'")) + def test_4(self, bash, completion, has_complete_ant_cmd_pl): + if has_complete_ant_cmd_pl: # Some versions of complete-ant-cmd.pl don't treat ANT_ARGS right; - # in those cases we get the correct completion produced by _ant + # in those cases we get the correct completion produced by us # plus whatever complete-ant-cmd.pl was able to get from build.xml assert "named-build" in completion else: diff --git a/test/t/test_apt_mark.py b/test/t/test_apt_mark.py new file mode 100644 index 0000000..541dbe5 --- /dev/null +++ b/test/t/test_apt_mark.py @@ -0,0 +1,39 @@ +import pytest + + +@pytest.mark.bashcomp(cmd="apt-mark") +class TestAptMark: + @pytest.mark.complete("apt-mark ") + def test_1(self, completion): + assert all( + x in completion + for x in ( + "auto manual remove showinstall showremove " + "hold minimize-manual showauto showmanual unhold install " + "purge showhold showpurge" + ).split() + ) + + @pytest.mark.complete("apt-mark minimize-manual ") + def test_2(self, completion): + assert not completion + + @pytest.mark.complete("apt-mark --file=", cwd="dpkg") + def test_3(self, completion): + assert ( + completion + == "bash-completion-test-nonsubject.txt bash-completion-test-subject.deb".split() + ) + + @pytest.mark.complete("apt-mark --config-file ", cwd="apt-mark") + def test_4(self, completion): + assert completion == "example.conf" + + @pytest.mark.complete("apt-mark --option ") + def test_5(self, completion): + assert not completion + + @pytest.mark.complete("apt-mark --dont-fail-in-unset-mode") + def test_unknown_option(self, completion): + # Just see that it does not error out + pass diff --git a/test/t/test_arp.py b/test/t/test_arp.py index cd038bd..f8a9543 100644 --- a/test/t/test_arp.py +++ b/test/t/test_arp.py @@ -3,7 +3,7 @@ import pytest class TestArp: @pytest.mark.complete( - "arp ", require_cmd=True, skipif='test -z "$(arp 2>/dev/null)"' + "arp ", require_cmd=True, skipif='test ! "$(arp 2>/dev/null)"' ) def test_1(self, completion): assert completion diff --git a/test/t/test_arpspoof.py b/test/t/test_arpspoof.py index 74c09a4..c02f5c4 100644 --- a/test/t/test_arpspoof.py +++ b/test/t/test_arpspoof.py @@ -5,8 +5,13 @@ class TestArpspoof: @pytest.mark.complete( "arpspoof -", require_cmd=True, - # May require privileges even for outputting the usage message - skipif="arpspoof 2>&1 | command grep -qF libnet_open_link", + # May require privileges or network interfaces available even for + # outputting the usage message. Unfortunately --help provokes a + # non-zero exit status so we cannot test for that. + skipif=( + "arpspoof 2>&1 | " + "command grep -qE 'libnet_(open_link|select_device)'" + ), ) def test_1(self, completion): assert completion diff --git a/test/t/test_avahi_browse.py b/test/t/test_avahi_browse.py new file mode 100644 index 0000000..2c3cf0d --- /dev/null +++ b/test/t/test_avahi_browse.py @@ -0,0 +1,27 @@ +import pytest + + +@pytest.mark.bashcomp( + cmd="avahi-browse", +) +class TestAvahiBrowse: + @pytest.mark.complete("avahi-browse --", require_cmd=True) + def test_options(self, completion): + assert completion + + @pytest.mark.complete( + "avahi-browse _", + require_cmd=True, + xfail='test ! "$(avahi-browse --dump-db 2>/dev/null)"', + ) + def test_service_types(self, completion): + assert completion + + @pytest.mark.complete("avahi-browse -a _") + def test_no_service_type_with_a(self, completion): + assert not completion + + @pytest.mark.complete("avahi-browse --dont-fail-in-unset-mode") + def test_unknown_option(self, completion): + # Just see that it does not error out + pass diff --git a/test/t/test_b2sum.py b/test/t/test_b2sum.py new file mode 100644 index 0000000..b184457 --- /dev/null +++ b/test/t/test_b2sum.py @@ -0,0 +1,11 @@ +import pytest + + +class TestB2sum: + @pytest.mark.complete("b2sum ") + def test_1(self, completion): + assert completion + + @pytest.mark.complete("b2sum --", require_longopt=True) + def test_options(self, completion): + assert completion diff --git a/test/t/test_bsdtar.py b/test/t/test_bsdtar.py new file mode 100644 index 0000000..ec3acd1 --- /dev/null +++ b/test/t/test_bsdtar.py @@ -0,0 +1,11 @@ +import pytest + + +class TestBsdtar: + @pytest.mark.complete("bsdtar xf bsdtar/") + def test_readable_archives(self, completion): + assert completion == "test.pax test.rar".split() + + @pytest.mark.complete("bsdtar uf bsdtar/") + def test_writable_archives(self, completion): + assert completion == "test.pax test.shar".split() diff --git a/test/t/test_cd.py b/test/t/test_cd.py index 5b7789a..7243b93 100644 --- a/test/t/test_cd.py +++ b/test/t/test_cd.py @@ -7,11 +7,11 @@ class TestCd: def test_1(self, completion): assert completion == ["bar bar.d/", "foo.d/"] - @pytest.mark.complete("cd fo", env=dict(CDPATH="shared/default")) + @pytest.mark.complete("cd foo", env=dict(CDPATH="shared/default")) def test_2(self, completion): - assert completion == "o.d/" + assert completion == ".d/" - @pytest.mark.complete("cd fo") + @pytest.mark.complete("cd foo") def test_3(self, completion): assert not completion @@ -24,3 +24,7 @@ class TestCd: @pytest.mark.complete("cd shared/default/", trail="foo") def test_dir_at_point(self, completion): assert completion == ["bar bar.d/", "foo.d/"] + + @pytest.mark.complete("cd -") + def test_options(self, completion): + assert completion diff --git a/test/t/test_chmod.py b/test/t/test_chmod.py index 3838b55..e1a7119 100644 --- a/test/t/test_chmod.py +++ b/test/t/test_chmod.py @@ -2,7 +2,6 @@ import pytest class TestChmod: - # No completion here until mode completion is implemented @pytest.mark.complete("chmod ") def test_1(self, completion): diff --git a/test/t/test_chown.py b/test/t/test_chown.py index 9643f3e..ee81346 100644 --- a/test/t/test_chown.py +++ b/test/t/test_chown.py @@ -8,7 +8,7 @@ from conftest import assert_complete @pytest.mark.bashcomp( pre_cmds=( # Fake root command to get all users/groups completed at least for now - "root_command=sudo", + "_comp_root_command=sudo", ) ) class TestChown: diff --git a/test/t/test_chsh.py b/test/t/test_chsh.py index fe1c7f6..c2d8344 100644 --- a/test/t/test_chsh.py +++ b/test/t/test_chsh.py @@ -1,3 +1,5 @@ +import os + import pytest @@ -7,9 +9,16 @@ class TestChsh: assert completion @pytest.mark.complete("chsh -s ") - def test_2(self, completion): - assert completion + def test_shells(self, completion): + if os.path.exists("/etc/shells"): + assert completion + else: + assert not completion @pytest.mark.complete("chsh -", require_cmd=True) def test_3(self, completion): assert completion + + @pytest.mark.complete("chsh --root shells -s ") + def test_chroot_shells(self, completion): + assert completion == "/bash/completion/canary" diff --git a/test/t/test_configure.py b/test/t/test_configure.py index 0fc6117..c3b3393 100644 --- a/test/t/test_configure.py +++ b/test/t/test_configure.py @@ -15,3 +15,11 @@ class TestConfigure: @pytest.mark.complete("configure --prefix ") def test_2(self, completion): assert completion + + @pytest.mark.complete("configure --unknown-option-with-split=") + def test_unknown_split_filedir_fallback(self, completion): + assert "shared/" in completion + + @pytest.mark.complete("configure --unknown-option ") + def test_unknown_filedir_fallback(self, completion): + assert "shared/" in completion diff --git a/test/t/test_convert.py b/test/t/test_convert.py index c903ea0..b355c7a 100644 --- a/test/t/test_convert.py +++ b/test/t/test_convert.py @@ -6,7 +6,7 @@ class TestConvert: def test_1(self, completion): assert completion - @pytest.mark.complete("convert -format ") + @pytest.mark.complete("convert -format ", require_cmd=True) def test_2(self, completion): assert completion diff --git a/test/t/test_cpan2dist.py b/test/t/test_cpan2dist.py index 1ab5de1..ff64dbd 100644 --- a/test/t/test_cpan2dist.py +++ b/test/t/test_cpan2dist.py @@ -2,8 +2,6 @@ import pytest class TestCpan2dist: - @pytest.mark.complete( - "cpan2dist -", require_cmd=True, require_longopt=True - ) + @pytest.mark.complete("cpan2dist -", require_longopt=True) def test_1(self, completion): assert completion diff --git a/test/t/test_createdb.py b/test/t/test_createdb.py index 030338a..6ac255a 100644 --- a/test/t/test_createdb.py +++ b/test/t/test_createdb.py @@ -2,7 +2,6 @@ import pytest class TestCreatedb: - # --help can fail due to missing package dependencies, e.g. on Ubuntu 14 @pytest.mark.complete( "createdb -", require_cmd=True, xfail="! createdb --help &>/dev/null" diff --git a/test/t/test_createuser.py b/test/t/test_createuser.py index ea8d0e3..f3b1c67 100644 --- a/test/t/test_createuser.py +++ b/test/t/test_createuser.py @@ -2,7 +2,6 @@ import pytest class TestCreateuser: - # --help can fail due to missing package dependencies, e.g. on Ubuntu 14 @pytest.mark.complete( "createuser -", xfail="! createuser --help &>/dev/null" diff --git a/test/t/test_csplit.py b/test/t/test_csplit.py index 609c7e5..f1ead89 100644 --- a/test/t/test_csplit.py +++ b/test/t/test_csplit.py @@ -6,6 +6,6 @@ class TestCsplit: def test_1(self, completion): assert completion - @pytest.mark.complete("csplit -", require_cmd=True) + @pytest.mark.complete("csplit -", require_longopt=True) def test_options(self, completion): assert completion diff --git a/test/t/test_curl.py b/test/t/test_curl.py index 63e969f..07050e9 100644 --- a/test/t/test_curl.py +++ b/test/t/test_curl.py @@ -26,3 +26,20 @@ class TestCurl: def test_data_atfile_dir(self, completion): assert completion == "d/" assert not completion.endswith(" ") + + @pytest.mark.complete("curl --dont-fail-in-unset-mode") + def test_unknown_option(self, completion): + # Just see that it does not error out + pass + + @pytest.mark.complete("curl --data-bina", require_cmd=True) + def test_help_all_option(self, completion): + """ + The option used as a canary here is one that should be available + in all curl versions. It should be only listed in `--help all` output + for curl versions that have their help output split to multiple + categories (i.e. ones that support `--help all` to get the complete + list), as well as the basic `--help` output for earlier versions that + do not have that. + """ + assert completion diff --git a/test/t/test_cvs.py b/test/t/test_cvs.py index 97361e9..6d2deb3 100644 --- a/test/t/test_cvs.py +++ b/test/t/test_cvs.py @@ -18,3 +18,23 @@ class TestCvs: @pytest.mark.complete("cvs -", require_cmd=True) def test_4(self, completion): assert completion + + @pytest.mark.complete("cvs update -AdP foo/", cwd="cvs") + def test_5(self, completion): + assert completion == "bar" + + @pytest.mark.complete("cvs log -v foo/", cwd="cvs") + def test_6(self, completion): + assert completion == "bar" + + @pytest.mark.complete("cvs diff foo/", cwd="cvs") + def test_7(self, completion): + assert completion == "bar" + + @pytest.mark.complete("cvs status -v foo/", cwd="cvs") + def test_8(self, completion): + assert completion == "bar" + + @pytest.mark.complete("cvs status foo/", cwd="cvs") + def test_9(self, completion): + assert completion == "bar" diff --git a/test/t/test_cvsps.py b/test/t/test_cvsps.py index 4039893..2095832 100644 --- a/test/t/test_cvsps.py +++ b/test/t/test_cvsps.py @@ -1,7 +1,10 @@ import pytest -@pytest.mark.bashcomp(pre_cmds=("HOME=$PWD/cvs",)) +@pytest.mark.bashcomp( + pre_cmds=("HOME=$PWD/cvs",), + ignore_env=r"^[+-]COMP_CVS_REMOTE=", +) class TestCvsps: @pytest.mark.complete("cvsps -", require_cmd=True) def test_1(self, completion): diff --git a/test/t/test_date.py b/test/t/test_date.py index 57d61b8..f0f93b6 100644 --- a/test/t/test_date.py +++ b/test/t/test_date.py @@ -6,6 +6,6 @@ class TestDate: def test_1(self, completion): assert completion - @pytest.mark.complete("date -", require_cmd=True) + @pytest.mark.complete("date -", require_longopt=True) def test_options(self, completion): assert completion diff --git a/test/t/test_dcop.py b/test/t/test_dcop.py index 5c3c04d..3e6d363 100644 --- a/test/t/test_dcop.py +++ b/test/t/test_dcop.py @@ -2,6 +2,8 @@ import pytest class TestDcop: - @pytest.mark.complete("dcop ", require_cmd=True) + @pytest.mark.complete( + "dcop ", require_cmd=True, skipif="! dcop &>/dev/null" + ) def test_1(self, completion): assert completion diff --git a/test/t/test_dd.py b/test/t/test_dd.py index e082faa..c0978f3 100644 --- a/test/t/test_dd.py +++ b/test/t/test_dd.py @@ -2,13 +2,7 @@ import pytest class TestDd: - @pytest.mark.complete( - "dd --", - xfail=( - "! dd --help &>/dev/null || " - "! dd --help 2>&1 | command grep -qF -- --help" - ), - ) + @pytest.mark.complete("dd --", require_longopt=True) def test_1(self, completion): assert completion diff --git a/test/t/test_declare.py b/test/t/test_declare.py index a61d926..924eef0 100644 --- a/test/t/test_declare.py +++ b/test/t/test_declare.py @@ -15,6 +15,18 @@ class TestDeclare: # bash 5.0 has BASH_ARGV0 too assert all(x in completion for x in "BASH_ARGC BASH_ARGV".split()) - @pytest.mark.complete("declare -f _parse_") + @pytest.mark.complete("declare -f _comp_comp") def test_4(self, completion): - assert "_parse_help" in completion + assert "_comp_compgen" in completion + + @pytest.mark.complete("declare -a BASH_VERS") + def test_arrayvar(self, completion): + assert "INFO" in completion + + @pytest.mark.complete("declare -f BASH_VERS") + def test_no_arrayvar_for_f(self, completion): + assert "INFO" not in completion + + @pytest.mark.complete("declare -i BASH_VERS") + def test_no_arrayvar_for_i(self, completion): + assert "INFO" not in completion diff --git a/test/t/test_dict.py b/test/t/test_dict.py index 99c4a21..64adb54 100644 --- a/test/t/test_dict.py +++ b/test/t/test_dict.py @@ -1,3 +1,5 @@ +import os + import pytest @@ -5,3 +7,25 @@ class TestDict: @pytest.mark.complete("dict -", require_cmd=True) def test_1(self, completion): assert completion + + @pytest.mark.xfail( + os.environ.get("NETWORK") == "none", + reason="The database list is unavailable without network", + ) + @pytest.mark.complete("dict --database ", require_cmd=True) + def test_database(self, completion): + # Ensure the directory name "_comp_load/" not generated because + # filenames in the current dictory (i.e., test/fixtures) are generated + # by "-o default" when "_comp_cmd_dict" fails to generate any + # completions. + assert completion and "_comp_load/" not in completion + + @pytest.mark.xfail( + os.environ.get("NETWORK") == "none", + reason="The database list is unavailable without network", + ) + @pytest.mark.complete( + "dict -h dict.org --database ", require_cmd=True, env=dict(IFS="") + ) + def test_database_IFS(self, completion): + assert completion and "_comp_load/" not in completion diff --git a/test/t/test_dmesg.py b/test/t/test_dmesg.py index a081fb6..e1001b0 100644 --- a/test/t/test_dmesg.py +++ b/test/t/test_dmesg.py @@ -1,7 +1,12 @@ +import sys + import pytest class TestDmesg: @pytest.mark.complete("dmesg -", require_cmd=True) def test_1(self, completion): - assert completion + if sys.platform == "darwin": + assert not completion # takes no options + else: + assert completion diff --git a/test/t/test_dmypy.py b/test/t/test_dmypy.py index 4c031dd..6253d91 100644 --- a/test/t/test_dmypy.py +++ b/test/t/test_dmypy.py @@ -9,6 +9,6 @@ class TestDmypy: assert "help" in completion assert not any("," in x for x in completion) - @pytest.mark.complete("dmypy -", require_cmd=True, require_longopt=True) + @pytest.mark.complete("dmypy -", require_longopt=True) def test_options(self, completion): assert "--help" in completion diff --git a/test/t/test_dot.py b/test/t/test_dot.py index a4aa674..733555c 100644 --- a/test/t/test_dot.py +++ b/test/t/test_dot.py @@ -5,3 +5,11 @@ class TestDot: @pytest.mark.complete("dot ") def test_1(self, completion): assert completion + + @pytest.mark.complete("dot t", cwd="dot") + def test_2(self, completion): + assert completion == ["test1.gv", "test2.dot"] + + @pytest.mark.complete("dot test1", cwd="dot") + def test_3(self, completion): + assert completion == ".gv" diff --git a/test/t/test_dpkg.py b/test/t/test_dpkg.py index eb1228b..ce6a047 100644 --- a/test/t/test_dpkg.py +++ b/test/t/test_dpkg.py @@ -6,10 +6,18 @@ class TestDpkg: def test_1(self, completion): assert completion - @pytest.mark.complete("dpkg -L ", xfail='test -z "$(dpkg -l 2>/dev/null)"') + @pytest.mark.complete("dpkg -L ", xfail='test ! "$(dpkg -l 2>/dev/null)"') def test_2(self, completion): assert completion @pytest.mark.complete("dpkg -i ~") def test_3(self, completion): assert completion + + @pytest.mark.complete("dpkg -i dpkg/") + def test_i_deb(self, completion): + assert completion == "bash-completion-test-subject.deb" + + @pytest.mark.complete("dpkg -") + def test_no_trailing_dash_options(self, completion): + assert not any(x.endswith("-") for x in completion) diff --git a/test/t/test_dpkg_query.py b/test/t/test_dpkg_query.py index 37c5621..743d668 100644 --- a/test/t/test_dpkg_query.py +++ b/test/t/test_dpkg_query.py @@ -3,7 +3,7 @@ import os.path import pytest -@pytest.mark.bashcomp(cmd="dpkg-query",) +@pytest.mark.bashcomp(cmd="dpkg-query") class TestDpkgQuery: @pytest.mark.complete("dpkg-query --", require_cmd=True) def test_options(self, completion): @@ -13,6 +13,10 @@ class TestDpkgQuery: not os.path.exists("/etc/debian_version"), reason="Likely fails on systems not based on Debian", ) - @pytest.mark.complete("dpkg-query -W dpk", require_cmd=True) + @pytest.mark.complete( + "dpkg-query -W dpk", + require_cmd=True, + xfail="! apt-cache show &>/dev/null", # empty cache? + ) def test_show(self, completion): assert "dpkg" in completion diff --git a/test/t/test_dropdb.py b/test/t/test_dropdb.py index 2f65857..f75979a 100644 --- a/test/t/test_dropdb.py +++ b/test/t/test_dropdb.py @@ -2,7 +2,6 @@ import pytest class TestDropdb: - # --help can fail due to missing package dependencies, e.g. on Ubuntu 14 @pytest.mark.complete( "dropdb -", require_cmd=True, xfail="! dropdb --help &>/dev/null" diff --git a/test/t/test_evince.py b/test/t/test_evince.py index 9e9245d..f89e97c 100644 --- a/test/t/test_evince.py +++ b/test/t/test_evince.py @@ -1,22 +1,32 @@ import pytest +from conftest import assert_complete, create_dummy_filedirs + +@pytest.mark.bashcomp(temp_cwd=True) class TestEvince: - @pytest.mark.complete("evince ", cwd="evince") - def test_1(self, completion): - # .txt should not be here - assert completion == sorted( - "foo/ .bmp .BMP .cbr .CBR .cbz .CBZ .djv .DJV .djvu .DJVU .dvi " - ".DVI .dvi.bz2 .dvi.BZ2 .DVI.bz2 .DVI.BZ2 .dvi.gz .dvi.GZ " - ".DVI.gz .DVI.GZ .eps .EPS .eps.bz2 .eps.BZ2 .EPS.bz2 .EPS.BZ2 " - ".eps.gz .eps.GZ .EPS.gz .EPS.GZ .gif .GIF .ico .ICO .jpeg " - ".JPEG .jpg .JPG .miff .MIFF .pbm .PBM .pcx .PCX .pdf .PDF " - ".pdf.bz2 .pdf.BZ2 .PDF.bz2 .PDF.BZ2 .pdf.gz .pdf.GZ .PDF.gz " - ".PDF.GZ .pgm .PGM .png .PNG .pnm .PNM .ppm .PPM .ps .PS " - ".ps.bz2 .ps.BZ2 .PS.bz2 .PS.BZ2 .ps.gz .ps.GZ .PS.gz .PS.GZ " - ".tga .TGA .tif .TIF .tiff .TIFF .xpm .XPM .xwd .XWD".split() + def test_1(self, bash): + files, dirs = create_dummy_filedirs( + ( + ".cb7 .CB7 .cbr .CBR .cbt .CBT .cbz .CBZ .djv .DJV .djvu " + ".DJVU .dvi .DVI .dvi.bz2 .dvi.BZ2 .DVI.bz2 .DVI.BZ2 .dvi.gz " + ".dvi.GZ .DVI.gz .DVI.GZ .eps .EPS .eps.bz2 .eps.BZ2 .EPS.bz2 " + ".EPS.BZ2 .eps.gz .eps.GZ .EPS.gz .EPS.GZ .oxps .OXPS .pdf " + ".PDF .pdf.bz2 .pdf.BZ2 .PDF.bz2 .PDF.BZ2 .pdf.gz .pdf.GZ " + ".PDF.gz .PDF.GZ .ps .PS .ps.bz2 .ps.BZ2 .PS.bz2 .PS.BZ2 " + ".ps.gz .ps.GZ .PS.gz .PS.GZ .tif .TIF .tiff .TIFF .txt .TXT " + ".xps .XPS" + ).split(), + "foo".split(), ) + completion = assert_complete(bash, "evince ") + assert completion == [ + x + for x in sorted(files + ["%s/" % d for d in dirs]) + if x.lower() != ".txt" + ] + @pytest.mark.complete("evince -", require_cmd=True) def test_2(self, completion): assert completion diff --git a/test/t/test_export.py b/test/t/test_export.py index 8738913..8abb564 100644 --- a/test/t/test_export.py +++ b/test/t/test_export.py @@ -1,5 +1,7 @@ import pytest +from conftest import assert_bash_exec + class TestExport: @pytest.mark.complete("export BASH") @@ -22,10 +24,9 @@ class TestExport: def test_5(self, completion): assert completion == ["foo", "foo.d/"] - @pytest.mark.complete("export -fn _ex") + @pytest.mark.complete("export -fn _comp_cmd_ex") def test_6(self, completion): - assert "_expand" in completion - assert "_export" in completion + assert completion == "port" @pytest.mark.complete(r"export FOO=$BASH") def test_7(self, completion): @@ -34,3 +35,13 @@ class TestExport: @pytest.mark.complete("export -", require_cmd=True) def test_8(self, completion): assert completion + + @pytest.fixture(scope="class") + def export_f_canary(self, request, bash): + assert_bash_exec(bash, "_comp__test_export_f_canary() { return; }") + + @pytest.mark.complete("export -f _comp__test_export_f_canar") + def test_no_equals_sign_for_function(self, completion, export_f_canary): + assert completion + assert "=" not in "".join(completion) + assert completion.endswith(" ") diff --git a/test/t/test_finger.py b/test/t/test_finger.py index d765fdd..4aca977 100644 --- a/test/t/test_finger.py +++ b/test/t/test_finger.py @@ -26,8 +26,8 @@ class TestFinger: def test_partial_hostname(self, bash, known_hosts): first_char, partial_hosts = partialize(bash, known_hosts) user = "test" - completion = assert_complete(bash, "finger %s@%s" % (user, first_char)) + completion = assert_complete(bash, f"finger {user}@{first_char}") if len(completion) == 1: assert completion == partial_hosts[0][1:] else: - assert completion == ["%s@%s" % (user, x) for x in partial_hosts] + assert completion == [f"{user}@{x}" for x in partial_hosts] diff --git a/test/t/test_fio.py b/test/t/test_fio.py index 0f6eba7..dadb5ac 100644 --- a/test/t/test_fio.py +++ b/test/t/test_fio.py @@ -10,6 +10,44 @@ class TestFio: def test_2(self, completion): assert completion - @pytest.mark.complete("fio --debug=foo,") + @pytest.mark.complete("fio --debug=foo,", require_cmd=True) def test_3(self, completion): assert completion + + @pytest.mark.complete("fio --ioengin", require_cmd=True) + def test_cmdhelp_all(self, completion): + """Test we got a "known present" option from --cmdhelp=all.""" + assert completion == "e=" or "e" in completion + + @pytest.mark.complete("fio --ioengine=", require_cmd=True) + def test_enghelp(self, completion): + """Test --enghelp parsing.""" + assert completion + + @pytest.mark.complete("fio --unlink=", require_cmd=True) + def test_cmdhelp_boolean(self, completion): + """Test --cmdhelp=COMMAND boolean parsing.""" + assert completion == "0 1".split() + + @pytest.mark.complete("fio --kb_base=", require_cmd=True) + def test_cmdhelp_valid_values(self, completion): + """Test --cmdhelp=COMMAND valid values parsing.""" + # We expect kb_base args to be stable, no additions/removals. + assert completion == "1000 1024".split() + + @pytest.mark.complete("fio --non_exist3nt_option=", require_cmd=True) + def test_cmdhelp_nonexistent(self, completion): + """Test --cmdhelp=COMMAND errors out gracefully.""" + assert not completion + + @pytest.mark.complete( + "fio --crctest=", + require_cmd=True, + xfail="! fio --help 2>&1 | command grep -q -- --crctest", + ) + def test_crctest(self, completion): + assert "sha1" in completion + + @pytest.mark.complete("fio --debug=", require_cmd=True) + def test_debug(self, completion): + assert "process" in completion diff --git a/test/t/test_firefox.py b/test/t/test_firefox.py index 2e05255..619318c 100644 --- a/test/t/test_firefox.py +++ b/test/t/test_firefox.py @@ -6,7 +6,12 @@ class TestFirefox: def test_1(self, completion): assert completion - @pytest.mark.complete("firefox -", require_cmd=True) + # --help test: running as root in GH actions container croaks: + # Running Firefox as root in a regular user's session is not supported. + # ($HOME is /github/home which is owned by uid 1001.) + @pytest.mark.complete( + "firefox -", require_cmd=True, xfail="! firefox --help &>/dev/null" + ) def test_2(self, completion): assert completion assert not completion.endswith(" ") diff --git a/test/t/test_function.py b/test/t/test_function.py index 4401f02..ce74780 100644 --- a/test/t/test_function.py +++ b/test/t/test_function.py @@ -1,7 +1,19 @@ import pytest +from conftest import assert_bash_exec, assert_complete + +@pytest.mark.bashcomp(ignore_env=r"^\+declare -f fn$") class TestFunction: @pytest.mark.complete("function _parse_") def test_1(self, completion): assert completion + + @pytest.mark.complete("function non_existent_function ") + def test_2(self, completion): + assert completion == "()" + + def test_3(self, bash): + assert_bash_exec(bash, "fn() { echo; }") + completion = assert_complete(bash, "function fn ") + assert completion == "() { ^J echo^J}" diff --git a/test/t/test_gdb.py b/test/t/test_gdb.py index 2ad12c4..9f038f8 100644 --- a/test/t/test_gdb.py +++ b/test/t/test_gdb.py @@ -12,3 +12,13 @@ class TestGdb: "core core.12345 " "core.weston.1000.deadbeef.5308.1555362132000000".split() ) + + @pytest.mark.complete("gdb aw") + def test_3(self, completion): + """Check that the completion can generate command names""" + assert completion == ["k"] or "awk" in completion + + @pytest.mark.complete("gdb built") + def test_4(self, completion): + """Check that the completion does not generate builtin names""" + assert not (completion == ["in"] or "builtin" in completion) diff --git a/test/t/test_getconf.py b/test/t/test_getconf.py index c80c803..e9578e7 100644 --- a/test/t/test_getconf.py +++ b/test/t/test_getconf.py @@ -2,7 +2,7 @@ import pytest class TestGetconf: - @pytest.mark.complete("getconf P") + @pytest.mark.complete("getconf P", skipif="! getconf -a &>/dev/null") def test_1(self, completion): assert completion diff --git a/test/t/test_grep.py b/test/t/test_grep.py index a249122..ded7fb5 100644 --- a/test/t/test_grep.py +++ b/test/t/test_grep.py @@ -11,6 +11,16 @@ class TestGrep: """ Test --no-*dir isn't restricted to dirs only. - Not really a grep option, but tests _longopt. + Not really a grep option, but tests _comp_complete_longopt. """ assert completion == "foo foo.d/".split() + + @pytest.mark.complete("grep TZ ", cwd="shared/default") + def test_no_variable_assignment_confusion(self, completion): + """ + Test TZ doesn't trigger known variable value assignment completion. + + Not really a grep specific, but good to test somewhere. + Refs https://github.com/scop/bash-completion/issues/457 + """ + assert "foo" in completion diff --git a/test/t/test_gssdp_device_sniffer.py b/test/t/test_gssdp_device_sniffer.py new file mode 100644 index 0000000..be05d0d --- /dev/null +++ b/test/t/test_gssdp_device_sniffer.py @@ -0,0 +1,14 @@ +import pytest + + +@pytest.mark.bashcomp( + cmd="gssdp-device-sniffer", +) +class TestGssdpDeviceSniffer: + @pytest.mark.complete("gssdp-device-sniffer ") + def test_basic(self, completion): + assert not completion + + @pytest.mark.complete("gssdp-device-sniffer -", require_cmd=True) + def test_options(self, completion): + assert "--help" in completion diff --git a/test/t/test_hash.py b/test/t/test_hash.py new file mode 100644 index 0000000..2a1bf0c --- /dev/null +++ b/test/t/test_hash.py @@ -0,0 +1,11 @@ +import pytest + + +class TestHash: + @pytest.mark.complete("hash ", require_cmd=True) + def test_basic(self, completion): + assert completion + + @pytest.mark.complete("hash -", require_cmd=True) + def test_options(self, completion): + assert completion diff --git a/test/t/test_help.py b/test/t/test_help.py new file mode 100644 index 0000000..b056c55 --- /dev/null +++ b/test/t/test_help.py @@ -0,0 +1,21 @@ +import pytest + + +class TestHelp: + @pytest.mark.complete("help ") + def test_basic(self, completion): + assert "help" in completion + + @pytest.mark.complete("help -") + def test_options(self, completion): + assert completion + + @pytest.mark.complete( + r"help \(", + skipif="! compgen -A helptopic | grep -qxF '(( ... ))'", # bash 4.2 + ) + def test_parens(self, completion): + # Assumption: an item like "(( ... ))" exists in the output + assert any( + x.startswith(r"\(") and x.endswith(r"\)\)") for x in completion + ) diff --git a/test/t/test_info.py b/test/t/test_info.py index e12d900..2fdbcca 100644 --- a/test/t/test_info.py +++ b/test/t/test_info.py @@ -10,3 +10,11 @@ class TestInfo: @pytest.mark.complete("info -", require_cmd=True) def test_2(self, completion): assert completion + + @pytest.mark.complete( + "info nonexistent-na", + env=dict(INFOPATH="'$(echo malicious code >/dev/tty)'"), + ) + def test_infopath_code_injection(self, completion): + # no completion, no space appended + assert not completion diff --git a/test/t/test_installpkg.py b/test/t/test_installpkg.py index e665f52..0ae4840 100644 --- a/test/t/test_installpkg.py +++ b/test/t/test_installpkg.py @@ -12,7 +12,8 @@ class TestInstallpkg: @pytest.mark.complete("installpkg --") def test_2(self, completion): assert ( - completion == "--ask --infobox --md5sum --menu " + completion + == "--ask --infobox --md5sum --menu " "--priority --root --tagfile --terse --warn".split() ) diff --git a/test/t/test_invoke_rc_d.py b/test/t/test_invoke_rc_d.py index 61e2987..5ef768c 100644 --- a/test/t/test_invoke_rc_d.py +++ b/test/t/test_invoke_rc_d.py @@ -1,8 +1,14 @@ +import sys + import pytest @pytest.mark.bashcomp(cmd="invoke-rc.d") class TestInvokeRcD: + @pytest.mark.xfail( + sys.platform == "darwin", + reason="Service completion not available on macOS", + ) @pytest.mark.complete("invoke-rc.d ") def test_1(self, completion): assert completion @@ -10,5 +16,6 @@ class TestInvokeRcD: @pytest.mark.complete("invoke-rc.d --no-fallback --") def test_2(self, completion): """Test already specified option is not offered.""" - assert completion + if sys.platform != "darwin": # no service completion + assert completion assert "--no-fallback" not in completion diff --git a/test/t/test_ip.py b/test/t/test_ip.py index 320647f..0be088c 100644 --- a/test/t/test_ip.py +++ b/test/t/test_ip.py @@ -10,6 +10,19 @@ class TestIp: def test_2(self, completion): assert completion - @pytest.mark.complete("ip route replace ") + @pytest.mark.complete("ip route replace ", require_cmd=True) def test_r_r(self, completion): assert completion + + @pytest.mark.complete( + "ip monitor ", + require_cmd=True, + skipif="ip monitor help 2>/dev/null; (( $? != 255 ))", + ) + def test_monitor(self, completion): + assert "neigh" in completion + assert "all" in completion + + @pytest.mark.complete("ip -", require_cmd=True) + def test_options(self, completion): + assert "-family" in completion diff --git a/test/t/test_iperf.py b/test/t/test_iperf.py index c38e954..0b67001 100644 --- a/test/t/test_iperf.py +++ b/test/t/test_iperf.py @@ -22,3 +22,8 @@ class TestIperf: @pytest.mark.complete("iperf -", require_cmd=True) def test_5(self, completion): assert completion + + @pytest.mark.complete("iperf --format ", require_cmd=True) + def test_format(self, completion): + # 2.0.5 has only up to m/M, later may have g/G, t/T, ... + assert all(x in completion for x in "k m K M".split()) diff --git a/test/t/test_iperf3.py b/test/t/test_iperf3.py index 15f3a03..35070c1 100644 --- a/test/t/test_iperf3.py +++ b/test/t/test_iperf3.py @@ -18,3 +18,8 @@ class TestIperf3: @pytest.mark.complete("iperf3 --server --", require_cmd=True) def test_4(self, completion): assert "--daemon" in completion + + @pytest.mark.complete("iperf3 --format ", require_cmd=True) + def test_format(self, completion): + # 3.0.7 has only up to m/M, later may have g/G, t/T, ... + assert all(x in completion for x in "k m K M".split()) diff --git a/test/t/test_isort.py b/test/t/test_isort.py index b142d1c..1c86b84 100644 --- a/test/t/test_isort.py +++ b/test/t/test_isort.py @@ -6,6 +6,6 @@ class TestIsort: def test_1(self, completion): assert completion - @pytest.mark.complete("isort -", require_cmd=True, require_longopt=True) + @pytest.mark.complete("isort -", require_longopt=True) def test_2(self, completion): assert completion diff --git a/test/t/test_java.py b/test/t/test_java.py index ce0f773..03f1520 100644 --- a/test/t/test_java.py +++ b/test/t/test_java.py @@ -1,6 +1,6 @@ import pytest -from conftest import is_bash_type +from conftest import is_bash_type, assert_bash_exec, bash_env_saved @pytest.mark.bashcomp( @@ -47,3 +47,46 @@ class TestJava: @pytest.mark.complete("java -jar java/") def test_6(self, completion): assert completion == "a/ bashcomp.jar bashcomp.war".split() + + @pytest.mark.complete("javadoc -sourcepath java/a:java/a/c ") + def test_sourcepath_1(self, completion): + """sourcepath should be split by `:`""" + assert completion == "c" + + @pytest.mark.complete("javadoc -sourcepath java/?:java/x ") + def test_sourcepath_2(self, completion): + """pathname expansion should not happen after splitting the argument by + `:`""" + assert not completion + + @pytest.mark.complete("javadoc -sourcepath java/a ") + def test_packages_1(self, completion): + assert completion == "c" + + @pytest.mark.complete("javadoc -sourcepath java/a x") + def test_packages_2(self, completion): + assert not completion + + @pytest.mark.complete( + "javadoc -sourcepath java/a x", shopt=dict(failglob=True) + ) + def test_packages_3(self, completion): + assert not completion + + @pytest.mark.complete("javadoc -sourcepath java/a ", env=dict(IFS="a")) + def test_packages_4(self, completion): + assert completion == "c" + + def test_packages_5(self, bash): + """_comp_cmd_java__packages should not modify the outerscope `cur`""" + with bash_env_saved(bash) as bash_env: + bash_env.write_variable("cur", "a.b.c") + assert_bash_exec( + bash, + "_comp_test_f() { local cword=3 words=(javadoc -sourcepath java/a a.b.c); COMPREPLY+=(); _comp_cmd_java__packages; }; _comp_test_f", + ) + + @pytest.mark.complete("javadoc -sourcepath java a.") + def test_packages_6(self, completion): + """A period in package names should not be converted to slash.""" + assert completion == "c" diff --git a/test/t/test_javaws.py b/test/t/test_javaws.py index 596c735..4d033c3 100644 --- a/test/t/test_javaws.py +++ b/test/t/test_javaws.py @@ -6,6 +6,12 @@ class TestJavaws: def test_1(self, completion): assert completion - @pytest.mark.complete("javaws -", require_cmd=True) + @pytest.mark.complete( + "javaws -", + require_cmd=True, + xfail=( + "! (javaws -help 2>&1 || :) | " "command grep -q -- '[[:space:]]-'" + ), + ) def test_2(self, completion): assert completion diff --git a/test/t/test_jq.py b/test/t/test_jq.py index 4701414..ca4d0e0 100644 --- a/test/t/test_jq.py +++ b/test/t/test_jq.py @@ -28,3 +28,11 @@ class TestJq: @pytest.mark.complete("jq --slurpfile foo ") def test_6(self, completion): assert completion + + @pytest.mark.complete("jq --args ") + def test_no_completion_after_args(self, completion): + assert not completion + + @pytest.mark.complete("jq --jsonargs foo ") + def test_no_completion_after_jsonargs(self, completion): + assert not completion diff --git a/test/t/test_jsonschema.py b/test/t/test_jsonschema.py index 6027f5d..5f43435 100644 --- a/test/t/test_jsonschema.py +++ b/test/t/test_jsonschema.py @@ -6,8 +6,6 @@ class TestJsonschema: def test_1(self, completion): assert completion - @pytest.mark.complete( - "jsonschema -", require_cmd=True, require_longopt=True - ) + @pytest.mark.complete("jsonschema -", require_longopt=True) def test_2(self, completion): assert completion diff --git a/test/t/test_kdvi.py b/test/t/test_kdvi.py index c2ab011..114e024 100644 --- a/test/t/test_kdvi.py +++ b/test/t/test_kdvi.py @@ -1,10 +1,22 @@ import pytest +from conftest import assert_complete, create_dummy_filedirs + +@pytest.mark.bashcomp(temp_cwd=True) class TestKdvi: - @pytest.mark.complete("kdvi ", cwd="kdvi") - def test_1(self, completion): - assert completion == sorted( - "foo/ .dvi .DVI .dvi.bz2 .DVI.bz2 .dvi.gz " - ".DVI.gz .dvi.Z .DVI.Z".split() + 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(), ) + + completion = assert_complete(bash, "kdvi ") + assert completion == [ + x + for x in sorted(files + ["%s/" % d for d in dirs]) + if x.lower() != ".txt" + ] diff --git a/test/t/test_kill.py b/test/t/test_kill.py index 9699435..9233c00 100644 --- a/test/t/test_kill.py +++ b/test/t/test_kill.py @@ -13,3 +13,7 @@ class TestKill: @pytest.mark.complete("kill -") def test_3(self, completion): assert all("-%s" % x in completion for x in "l s ABRT USR1".split()) + + @pytest.mark.complete("kill %", pre_cmds=("bash -c 'sleep 5' &",)) + def test_jobs(self, bash, completion): + assert "bash" in completion diff --git a/test/t/test_killall.py b/test/t/test_killall.py index 4b67d96..96eab8a 100644 --- a/test/t/test_killall.py +++ b/test/t/test_killall.py @@ -2,7 +2,6 @@ import pytest class TestKillall: - # "p": Assume our process name completion runs ps and at least it is shown @pytest.mark.complete("killall p") def test_1(self, completion): diff --git a/test/t/test_kpdf.py b/test/t/test_kpdf.py index 68b36fe..b7e658f 100644 --- a/test/t/test_kpdf.py +++ b/test/t/test_kpdf.py @@ -1,7 +1,19 @@ import pytest +from conftest import assert_complete, create_dummy_filedirs + +@pytest.mark.bashcomp(temp_cwd=True) class TestKpdf: - @pytest.mark.complete("kpdf ", cwd="kpdf") - def test_1(self, completion): - assert completion == sorted("foo/ .eps .ps .EPS .PS .pdf .PDF".split()) + def test_1(self, bash): + files, dirs = create_dummy_filedirs( + ".eps .EPS .pdf .PDF .ps .PS .txt".split(), + "foo".split(), + ) + + completion = assert_complete(bash, "kpdf ") + assert completion == [ + x + for x in sorted(files + ["%s/" % d for d in dirs]) + if x.lower() != ".txt" + ] diff --git a/test/t/test_ld.py b/test/t/test_ld.py index f6a16bb..004692e 100644 --- a/test/t/test_ld.py +++ b/test/t/test_ld.py @@ -6,6 +6,6 @@ class TestLd: def test_1(self, completion): assert completion - @pytest.mark.complete("ld -", require_cmd=True) + @pytest.mark.complete("ld -", require_longopt=True) def test_options(self, completion): assert completion diff --git a/test/t/test_lrzip.py b/test/t/test_lrzip.py index d61ee9d..dbc3939 100644 --- a/test/t/test_lrzip.py +++ b/test/t/test_lrzip.py @@ -13,3 +13,12 @@ class TestLrzip: @pytest.mark.complete("lrzip -", require_cmd=True) def test_3(self, completion): assert completion + + @pytest.mark.complete( + "lrzip --", + # require_longopt not applicable, useful --help may give nonzero exit + require_cmd=True, + xfail=("! { lrzip --help 2>&1 || :; } | command grep -qF -- --help"), + ) + def test_longopt(self, completion): + assert completion diff --git a/test/t/test_ls.py b/test/t/test_ls.py index 8abcb59..f91ee6b 100644 --- a/test/t/test_ls.py +++ b/test/t/test_ls.py @@ -24,7 +24,7 @@ class TestLs: assert_bash_exec( bash, "for u in $(compgen -u); do " - "eval test -d ~$u || echo $u; unset u; done", + "eval test -d ~$u || echo $u; unset -v u; done", want_output=True, ) .strip() diff --git a/test/t/test_lvm.py b/test/t/test_lvm.py index ea25b97..82f1aa4 100644 --- a/test/t/test_lvm.py +++ b/test/t/test_lvm.py @@ -5,3 +5,11 @@ class TestLvm: @pytest.mark.complete("lvm pv") def test_1(self, completion): assert completion + + @pytest.mark.complete( + "lvm lvcreate --", + require_cmd=True, + xfail="! lvm lvcreate --help &>/dev/null", + ) + def test_subcommand_options(self, completion): + assert completion diff --git a/test/t/test_make.py b/test/t/test_make.py index 19861b0..34fc7e5 100644 --- a/test/t/test_make.py +++ b/test/t/test_make.py @@ -2,6 +2,8 @@ import os import pytest +from conftest import assert_complete + class TestMake: @pytest.mark.complete("make -f Ma", cwd="make") @@ -12,12 +14,12 @@ class TestMake: def test_2(self, bash, completion): """Hidden targets.""" assert completion == ".cache/ .test_passes".split() - os.remove("%s/make/%s" % (bash.cwd, "extra_makefile")) + os.remove(f"{bash.cwd}/make/extra_makefile") @pytest.mark.complete("make .cache/", cwd="make", require_cmd=True) def test_3(self, bash, completion): - assert completion == "1 2".split() - os.remove("%s/make/%s" % (bash.cwd, "extra_makefile")) + assert completion == ".cache/1 .cache/2".split() + os.remove(f"{bash.cwd}/make/extra_makefile") @pytest.mark.complete("make ", cwd="shared/empty_dir") def test_4(self, completion): @@ -30,18 +32,58 @@ class TestMake: @pytest.mark.complete("make ", cwd="make", require_cmd=True) def test_6(self, bash, completion): assert completion == "all clean extra_makefile install sample".split() - os.remove("%s/make/%s" % (bash.cwd, "extra_makefile")) + os.remove(f"{bash.cwd}/make/extra_makefile") @pytest.mark.complete("make .cache/.", cwd="make", require_cmd=True) def test_7(self, bash, completion): - assert completion == ".1 .2".split() - os.remove("%s/make/%s" % (bash.cwd, "extra_makefile")) + assert completion == ".cache/.1 .cache/.2".split() + os.remove(f"{bash.cwd}/make/extra_makefile") @pytest.mark.complete("make -C make ", require_cmd=True) def test_8(self, bash, completion): assert completion == "all clean extra_makefile install sample".split() - os.remove("%s/make/%s" % (bash.cwd, "extra_makefile")) + os.remove(f"{bash.cwd}/make/extra_makefile") + + @pytest.mark.complete("make -nC make ", require_cmd=True) + def test_8n(self, bash, completion): + assert completion == "all clean extra_makefile install sample".split() + os.remove(f"{bash.cwd}/make/extra_makefile") @pytest.mark.complete("make -", require_cmd=True) def test_9(self, completion): assert completion + + +@pytest.mark.bashcomp(require_cmd=True, cwd="make/test2") +class TestMake2: + def test_github_issue_544_1(self, bash): + completion = assert_complete(bash, "make ab") + assert completion == "c/xyz" + + def test_github_issue_544_2(self, bash): + completion = assert_complete(bash, "make 1") + assert completion == "23/" + + def test_github_issue_544_3(self, bash): + completion = assert_complete(bash, "make 123/") + assert completion == ["123/xaa", "123/xbb"] + + def test_github_issue_544_4(self, bash): + completion = assert_complete(bash, "make 123/xa") + assert completion == "a" + + def test_subdir_1(self, bash): + completion = assert_complete(bash, "make sub1") + assert completion == "test/bar/" + + def test_subdir_2(self, bash): + completion = assert_complete(bash, "make sub2") + assert completion == "test/bar/alpha" + + def test_subdir_3(self, bash): + completion = assert_complete(bash, "make sub3") + assert completion == "test/" + + def test_subdir_4(self, bash): + completion = assert_complete(bash, "make sub4") + assert completion == "sub4test/bar/ sub4test2/foo/gamma".split() diff --git a/test/t/test_makepkg.py b/test/t/test_makepkg.py index f643a29..ad37762 100644 --- a/test/t/test_makepkg.py +++ b/test/t/test_makepkg.py @@ -2,7 +2,7 @@ import pytest @pytest.mark.bashcomp( - ignore_env=r"^-declare -f _makepkg_bootstrap$", + ignore_env=r"^-declare -f _comp_cmd_makepkg__bootstrap$", xfail="! makepkg --help 2>&1 | command grep -qiF slackware", ) class TestMakepkg: diff --git a/test/t/test_man.py b/test/t/test_man.py index 1ff9f84..081b8fc 100644 --- a/test/t/test_man.py +++ b/test/t/test_man.py @@ -1,13 +1,17 @@ import pytest -from conftest import assert_bash_exec +from conftest import ( + assert_bash_exec, + assert_complete, + is_bash_type, + prepare_fixture_dir, +) @pytest.mark.bashcomp( - ignore_env=r"^[+-]((BASHOPTS|MANPATH)=|shopt -. failglob)" + ignore_env=r"^[+-]((BASHOPTS|MANPATH|manpath)=|shopt -. failglob)" ) class TestMan: - manpath = "$PWD/man" assumed_present = "man" @@ -20,13 +24,12 @@ class TestMan: else: pytest.skip("Cygwin doesn't like paths with colons") return - assert_bash_exec(bash, "mkdir -p $TESTDIR/../tmp/man/man3") - assert_bash_exec( - bash, "touch $TESTDIR/../tmp/man/man3/Bash::Completion.3pm.gz" - ) - request.addfinalizer( - lambda: assert_bash_exec(bash, "rm -r $TESTDIR/../tmp/man") + tmpdir, _, _ = prepare_fixture_dir( + request, + files=["man/man3/Bash::Completion.3pm.gz"], + dirs=["man", "man/man3"], ) + return tmpdir @pytest.mark.complete( "man bash-completion-testcas", @@ -96,20 +99,60 @@ class TestMan: "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( - "man Bash::C", - require_cmd=True, - env=dict(MANPATH="%s:../tmp/man" % manpath), - ) - def test_10(self, bash, colonpath, completion): + def test_10(self, request, bash, colonpath): + if not is_bash_type(bash, "man"): + pytest.skip("Command not found") + completion = assert_complete( + bash, + "man Bash::C", + env=dict(MANPATH="%s:%s/man" % (TestMan.manpath, colonpath)), + ) assert completion == "ompletion" @pytest.mark.complete("man -", require_cmd=True) def test_11(self, completion): assert completion + + @pytest.mark.complete("man -S 1", require_cmd=True) + def test_delimited_first(self, completion): + # just appends space + assert not completion + assert completion.endswith(" ") + + @pytest.mark.complete("man -S 1:", require_cmd=True) + def test_delimited_after_delimiter(self, completion): + assert completion + assert "1" not in completion + + @pytest.mark.complete("man -S 1:2", require_cmd=True) + def test_delimited_later(self, completion): + # just appends space + assert not completion + assert completion.endswith(" ") + + @pytest.mark.complete("man -S 1:1", require_cmd=True) + def test_delimited_deduplication(self, completion): + # no completion, no space appended + assert not completion + assert not completion.endswith(" ") + + @pytest.mark.complete( + "man bash-completion-zstd-testcas", + env=dict(MANPATH=manpath), + require_cmd=True, + ) + def test_zstd_arbitrary_sectsuffix(self, completion): + assert completion == "e" + + @pytest.mark.complete( + "man bash-completion-testcas", + env=dict(MANPATH="'$(echo malicious code >/dev/tty)'"), + ) + def test_manpath_code_injection(self, completion): + # no completion, no space appended + assert not completion diff --git a/test/t/test_md5sum.py b/test/t/test_md5sum.py index 0a3286a..e1568a1 100644 --- a/test/t/test_md5sum.py +++ b/test/t/test_md5sum.py @@ -6,6 +6,6 @@ class TestMd5sum: def test_1(self, completion): assert completion - @pytest.mark.complete("md5sum -", require_longopt=True) + @pytest.mark.complete("md5sum --", require_longopt=True) def test_options(self, completion): assert completion diff --git a/test/t/test_munindoc.py b/test/t/test_munindoc.py index eea13ca..38c7d52 100644 --- a/test/t/test_munindoc.py +++ b/test/t/test_munindoc.py @@ -2,7 +2,6 @@ import pytest class TestMunindoc: - # Assume at least munin* available # require_cmd is not strictly correct here, but... @pytest.mark.complete("munindoc m", require_cmd=True) diff --git a/test/t/test_mutt.py b/test/t/test_mutt.py index 0c4074f..e5b6387 100644 --- a/test/t/test_mutt.py +++ b/test/t/test_mutt.py @@ -5,6 +5,13 @@ from conftest import assert_bash_exec @pytest.mark.bashcomp(pre_cmds=("HOME=$PWD/mutt",)) class TestMutt: + @pytest.fixture(scope="class") + def functions(self, bash): + assert_bash_exec( + bash, + '_comp_test__muttconffiles() { local REPLY; _comp_cmd_mutt__get_conffiles "$@" && printf "%s\\n" "${REPLY[@]}"; }', + ) + @pytest.mark.complete("mutt -") def test_1(self, completion): assert completion @@ -17,17 +24,17 @@ class TestMutt: def test_3(self, completion): assert completion == "a1 a2".split() - def test_4(self, bash): + def test_4(self, bash, functions): got = ( assert_bash_exec( bash, - '_muttconffiles "$HOME/muttrc" "$HOME/muttrc"', + '_comp_test__muttconffiles "$HOME/muttrc" "$HOME/muttrc"', want_output=True, ) .strip() .split() ) assert got == [ - "%s/mutt/%s" % (bash.cwd, x) + f"{bash.cwd}/mutt/{x}" for x in ("muttrc", "bar/muttrc_b", "foo/muttrc_f") ] diff --git a/test/t/test_mypy.py b/test/t/test_mypy.py index 11628c1..9d5f16b 100644 --- a/test/t/test_mypy.py +++ b/test/t/test_mypy.py @@ -6,7 +6,7 @@ class TestMypy: def test_1(self, completion): assert completion - @pytest.mark.complete("mypy --", require_cmd=True, require_longopt=True) + @pytest.mark.complete("mypy --", require_longopt=True) def test_2(self, completion): assert completion diff --git a/test/t/test_neomutt.py b/test/t/test_neomutt.py new file mode 100644 index 0000000..36657fe --- /dev/null +++ b/test/t/test_neomutt.py @@ -0,0 +1,7 @@ +import pytest + + +class TestNeomutt: + @pytest.mark.complete("neomutt -") + def test_1(self, completion): + assert completion diff --git a/test/t/test_nmap.py b/test/t/test_nmap.py index 9aff8b2..76aa989 100644 --- a/test/t/test_nmap.py +++ b/test/t/test_nmap.py @@ -7,7 +7,7 @@ class TestNmap: @pytest.fixture(scope="class") def functions(self, request, bash): assert_bash_exec(bash, "_mock_nmap() { cat nmap/nmap-h.txt; }") - assert_bash_exec(bash, "complete -F _nmap _mock_nmap") + assert_bash_exec(bash, "complete -F _comp_cmd_nmap _mock_nmap") @pytest.mark.complete("nmap --v", require_cmd=True) def test_live_options(self, completion): diff --git a/test/t/test_nproc.py b/test/t/test_nproc.py index 66a49ac..29f208b 100644 --- a/test/t/test_nproc.py +++ b/test/t/test_nproc.py @@ -6,12 +6,6 @@ class TestNproc: def test_1(self, completion): assert not completion - @pytest.mark.complete( - "nproc --", - xfail=( - "! nproc --help &>/dev/null || " - "! nproc --help 2>&1 | command grep -qF -- --help" - ), - ) + @pytest.mark.complete("nproc --", require_longopt=True) def test_2(self, completion): assert completion diff --git a/test/t/test_op.py b/test/t/test_op.py deleted file mode 100644 index 662cde5..0000000 --- a/test/t/test_op.py +++ /dev/null @@ -1,11 +0,0 @@ -import pytest - - -class TestOp: - @pytest.mark.complete("op ", require_cmd=True) - def test_1(self, completion): - assert completion - - @pytest.mark.complete("op --", require_cmd=True) - def test_2(self, completion): - assert completion diff --git a/test/t/test_pdftoppm.py b/test/t/test_pdftoppm.py new file mode 100644 index 0000000..2a1a4c6 --- /dev/null +++ b/test/t/test_pdftoppm.py @@ -0,0 +1,11 @@ +import pytest + + +class TestPdftoppm: + @pytest.mark.complete("pdftoppm ") + def test_files(self, completion): + assert completion + + @pytest.mark.complete("pdftoppm -", require_cmd=True) + def test_options(self, completion): + assert completion diff --git a/test/t/test_pgrep.py b/test/t/test_pgrep.py index 9a998ed..db3dfdd 100644 --- a/test/t/test_pgrep.py +++ b/test/t/test_pgrep.py @@ -2,7 +2,6 @@ import pytest class TestPgrep: - # "p": Assume that our process name completion runs ps @pytest.mark.complete("pgrep p") def test_1(self, completion): @@ -32,4 +31,3 @@ class TestPgrep: ) def test_nslist_after_comma(self, completion): assert completion - assert not any("," in x for x in completion) diff --git a/test/t/test_pidof.py b/test/t/test_pidof.py index c33a4d3..de77298 100644 --- a/test/t/test_pidof.py +++ b/test/t/test_pidof.py @@ -2,7 +2,6 @@ import pytest class TestPidof: - # "p": Assume that our process name completion runs ps @pytest.mark.complete("pidof p") def test_1(self, completion): diff --git a/test/t/test_pkg_config.py b/test/t/test_pkg_config.py index 81e02ca..14f9c86 100644 --- a/test/t/test_pkg_config.py +++ b/test/t/test_pkg_config.py @@ -1,3 +1,5 @@ +import os + import pytest @@ -10,3 +12,11 @@ class TestPkgConfig: @pytest.mark.complete("pkg-config -", require_cmd=True) def test_2(self, completion): assert completion + + @pytest.mark.complete( + "pkg-config %s/bash-completion.pc --variable=" + % os.getenv("ABS_TOP_BUILDDIR", "../.."), + require_cmd=True, + ) + def test_variable(self, completion): + assert "completionsdir" in completion diff --git a/test/t/test_pkgconf.py b/test/t/test_pkgconf.py new file mode 100644 index 0000000..1466a89 --- /dev/null +++ b/test/t/test_pkgconf.py @@ -0,0 +1,22 @@ +import os + +import pytest + + +@pytest.mark.bashcomp(cmd="pkgconf") +class TestPkgconf: + @pytest.mark.complete("pkgconf ") + def test_1(self, completion): + assert completion + + @pytest.mark.complete("pkgconf -", require_cmd=True) + def test_2(self, completion): + assert completion + + @pytest.mark.complete( + "pkgconf %s/bash-completion.pc --variable=" + % os.getenv("ABS_TOP_BUILDDIR", "../.."), + require_cmd=True, + ) + def test_variable(self, completion): + assert "completionsdir" in completion diff --git a/test/t/test_portinstall.py b/test/t/test_portinstall.py index eb2118e..73a21be 100644 --- a/test/t/test_portinstall.py +++ b/test/t/test_portinstall.py @@ -3,22 +3,19 @@ import pytest from conftest import assert_bash_exec -@pytest.mark.bashcomp(ignore_env=r"^[+-]PORTSDIR=") +@pytest.mark.bashcomp(ignore_env=r"^[+-]PORTSDIR=", temp_cwd=True) class TestPortinstall: @pytest.fixture(scope="class") def portsdir(self, request, bash): - assert_bash_exec(bash, "PORTSDIR=$PWD/../tmp") + assert_bash_exec(bash, "PORTSDIR=$PWD") assert_bash_exec( bash, "command sed -e s,PORTSDIR,$PORTSDIR,g " - "pkgtools/ports/INDEX.dist >$PORTSDIR/INDEX", - ) - assert_bash_exec(bash, "cp $PORTSDIR/INDEX $PORTSDIR/INDEX-5") - request.addfinalizer( - lambda: assert_bash_exec(bash, "rm $PORTSDIR/INDEX{,-5}") + '"$SRCDIRABS"/fixtures/pkgtools/ports/INDEX.dist >INDEX', ) + assert_bash_exec(bash, "cp INDEX INDEX-5") - @pytest.mark.complete("portinstall ", env=dict(PORTSDIR="$PWD/../tmp")) + @pytest.mark.complete("portinstall ", env=dict(PORTSDIR="$PWD")) def test_1(self, completion, portsdir): assert ( completion diff --git a/test/t/test_pr.py b/test/t/test_pr.py index c790a86..23fa767 100644 --- a/test/t/test_pr.py +++ b/test/t/test_pr.py @@ -6,6 +6,6 @@ class TestPr: def test_1(self, completion): assert completion - @pytest.mark.complete("pr -", require_cmd=True) + @pytest.mark.complete("pr -", require_longopt=True) def test_options(self, completion): assert completion diff --git a/test/t/test_printenv.py b/test/t/test_printenv.py index 540c4f6..8384ec0 100644 --- a/test/t/test_printenv.py +++ b/test/t/test_printenv.py @@ -13,7 +13,7 @@ class TestPrintenv: @pytest.mark.complete( "printenv -", require_cmd=True, - xfail="! printenv --help 2>&1 | command grep -qF -- ' -'", + xfail="! printenv --help 2>&1 | command grep -q '^[[:space:]]*-'", ) def test_options(self, completion): assert completion diff --git a/test/t/test_ps.py b/test/t/test_ps.py new file mode 100644 index 0000000..a6bfec1 --- /dev/null +++ b/test/t/test_ps.py @@ -0,0 +1,50 @@ +import pytest + + +def is_int(s): + try: + int(s) + except ValueError: + return False + else: + return True + + +class TestPs: + @pytest.mark.complete("ps -", require_cmd=True) + def test_1(self, completion): + assert completion + + @pytest.mark.complete("ps --help ") + def test_2(self, completion): + assert completion == [ + "all", + "list", + "misc", + "output", + "simple", + "threads", + ] + + @pytest.mark.complete("ps --help all ") + def test_3(self, completion): + assert not completion + + @pytest.mark.complete("ps --version ") + def test_4(self, completion): + assert not completion + + @pytest.mark.complete("ps --pid ") + def test_5(self, completion): + assert completion + assert all(map(is_int, completion)) + + @pytest.mark.complete("ps --format ", require_cmd=True) + def test_6(self, completion): + assert completion + assert all(map(lambda c: not c.startswith(("-", ",")), completion)) + + @pytest.mark.complete("ps --format user,", require_cmd=True) + def test_7(self, completion): + assert completion + assert all(map(lambda c: not c.startswith(("-", ",")), completion)) diff --git a/test/t/test_psql.py b/test/t/test_psql.py index ffd6c05..73abc4b 100644 --- a/test/t/test_psql.py +++ b/test/t/test_psql.py @@ -2,7 +2,6 @@ import pytest class TestPsql: - # --help can fail due to missing package dependencies, e.g. on Ubuntu 14 @pytest.mark.complete( "psql -", require_cmd=True, xfail="! psql --help &>/dev/null" diff --git a/test/t/test_pushd.py b/test/t/test_pushd.py index 290e1d1..5b115d3 100644 --- a/test/t/test_pushd.py +++ b/test/t/test_pushd.py @@ -5,3 +5,7 @@ class TestPushd: @pytest.mark.complete("pushd ") def test_1(self, completion): assert completion + + @pytest.mark.complete("pushd -") + def test_options(self, completion): + assert completion diff --git a/test/t/test_pydocstyle.py b/test/t/test_pydocstyle.py index 1f44320..028a3c0 100644 --- a/test/t/test_pydocstyle.py +++ b/test/t/test_pydocstyle.py @@ -6,8 +6,6 @@ class TestPydocstyle: def test_1(self, completion): assert completion - @pytest.mark.complete( - "pydocstyle -", require_cmd=True, require_longopt=True - ) + @pytest.mark.complete("pydocstyle -", require_longopt=True) def test_2(self, completion): assert completion diff --git a/test/t/test_pylint.py b/test/t/test_pylint.py index 43a4c43..76c7778 100644 --- a/test/t/test_pylint.py +++ b/test/t/test_pylint.py @@ -2,10 +2,22 @@ import pytest class TestPylint: - @pytest.mark.complete("pylint --v", require_cmd=True, require_longopt=True) + @pytest.mark.complete("pylint --v", require_longopt=True) def test_1(self, completion): assert completion @pytest.mark.complete("pylint --confidence=HIGH,") def test_2(self, completion): assert completion + + @pytest.mark.complete("pylint --help-msg=", require_longopt=True) + def test_all_message_ids(self, completion): + assert any("-" in x for x in completion) + + @pytest.mark.complete("pylint --disable=", require_longopt=True) + def test_enabled_message_ids(self, completion): + assert any("-" in x for x in completion) + + @pytest.mark.complete("pylint --enable=foo,", require_longopt=True) + def test_disabled_message_ids(self, completion): + assert any("-" in x for x in completion) diff --git a/test/t/test_pyston.py b/test/t/test_pyston.py new file mode 100644 index 0000000..3c23f31 --- /dev/null +++ b/test/t/test_pyston.py @@ -0,0 +1,19 @@ +import pytest + + +class TestPyston: + @pytest.mark.complete("pyston ") + def test_basic(self, completion): + assert completion + + @pytest.mark.complete("pyston -", require_cmd=True) + def test_options(self, completion): + assert completion + + @pytest.mark.complete( + "pyston -b", + require_cmd=True, + skipif="! pyston -h | command grep -qwF -- -bb", + ) + def test_bb(self, completion): + assert "-bb" in completion diff --git a/test/t/test_pytest.py b/test/t/test_pytest.py index e70c7a5..dededc2 100644 --- a/test/t/test_pytest.py +++ b/test/t/test_pytest.py @@ -8,7 +8,7 @@ class TestPytest: def test_1(self, completion): assert completion - @pytest.mark.complete("pytest -") + @pytest.mark.complete("pytest -", require_cmd=True) def test_2(self, completion): assert completion diff --git a/test/t/test_python.py b/test/t/test_python.py index 5308dcb..bcc566b 100644 --- a/test/t/test_python.py +++ b/test/t/test_python.py @@ -37,3 +37,50 @@ class TestPython: @pytest.mark.complete("python -m json.", require_cmd=True) def test_9(self, completion): assert "json.tool" in completion + + @pytest.mark.complete( + "python -b", + require_cmd=True, + skipif="! python -h | command grep -qwF -- -bb", + ) + def test_bb(self, completion): + assert "-bb" in completion + + @pytest.mark.complete("python foo ", cwd="python") + def test_script_arg(self, completion): + assert "bar.txt" in completion + + @pytest.mark.complete("python -- foo ", cwd="python") + def test_script_arg_with_double_hyphen(self, completion): + assert "bar.txt" in completion + + @pytest.mark.complete("python -m foo bar -p ", cwd="python") + def test_module_arg(self, completion): + assert "bar.txt" in completion + + @pytest.mark.complete("python foo bar -p ", cwd="python") + def test_script_arg_after_option(self, completion): + assert "bar.txt" in completion + + @pytest.mark.complete("python -- foo bar -p ", cwd="python") + def test_script_arg_after_option_with_double_hyphen(self, completion): + assert "bar.txt" in completion + + @pytest.mark.complete("python -m foo bar -p ", cwd="python") + def test_module_arg_after_option(self, completion): + assert "bar.txt" in completion + + @pytest.mark.complete("python -mfoo bar -p ", cwd="python") + def test_module_arg_after_option_with_connected_m_arg(self, completion): + assert "bar.txt" in completion + + @pytest.mark.complete("python -- ", cwd="python") + def test_script_name(self, completion): + assert "bar.txt" not in completion + + @pytest.mark.complete("python -W -mfoo ", cwd="python") + def test_script_name_with_fake_m_arg(self, completion): + """In this case, -mfoo looks like an option to specify the module, but + it should not be treated as the module name because it is an option + argument to -W.""" + assert "bar.txt" not in completion diff --git a/test/t/test_python3.py b/test/t/test_python3.py index a4f6d96..179f90c 100644 --- a/test/t/test_python3.py +++ b/test/t/test_python3.py @@ -34,6 +34,10 @@ class TestPython3: def test_8(self, completion): assert completion - @pytest.mark.complete("python3 -m json.", require_cmd=True) - def test_9(self, completion): - assert "json.tool" in completion + @pytest.mark.complete( + "python3 -b", + require_cmd=True, + skipif="! python3 -h | command grep -qwF -- -bb", + ) + def test_bb(self, completion): + assert "-bb" in completion diff --git a/test/t/test_qemu.py b/test/t/test_qemu.py index 129c0b4..0dd0a2a 100644 --- a/test/t/test_qemu.py +++ b/test/t/test_qemu.py @@ -9,3 +9,7 @@ class TestQemu: @pytest.mark.complete("qemu -", require_cmd=True) def test_2(self, completion): assert completion + + @pytest.mark.complete("qemu -k ", require_cmd=True) + def test_keymaps(self, completion): + assert any(x.lower().startswith("en") for x in completion) diff --git a/test/t/test_qemu_system_x86_64.py b/test/t/test_qemu_system_x86_64.py new file mode 100644 index 0000000..c9e8052 --- /dev/null +++ b/test/t/test_qemu_system_x86_64.py @@ -0,0 +1,16 @@ +import pytest + + +@pytest.mark.bashcomp(cmd="qemu-system-x86_64") +class TestQemuSystemX8664: + @pytest.mark.complete("qemu-system-x86_64 ") + def test_basic(self, completion): + assert completion + + @pytest.mark.complete("qemu-system-x86_64 -", require_cmd=True) + def test_options(self, completion): + assert completion + + @pytest.mark.complete("qemu-system-x86_64 -k ", require_cmd=True) + def test_keymaps(self, completion): + assert any(x.lower().startswith("en") for x in completion) diff --git a/test/t/test_reportbug.py b/test/t/test_reportbug.py index 2c57b56..9347adf 100644 --- a/test/t/test_reportbug.py +++ b/test/t/test_reportbug.py @@ -5,3 +5,7 @@ class TestReportbug: @pytest.mark.complete("reportbug --m", require_cmd=True) def test_1(self, completion): assert completion + + @pytest.mark.complete("reportbug --bts=", require_cmd=True) + def test_bts(self, completion): + assert "default" in completion diff --git a/test/t/test_rmdir.py b/test/t/test_rmdir.py index b981677..19b5ea4 100644 --- a/test/t/test_rmdir.py +++ b/test/t/test_rmdir.py @@ -11,6 +11,6 @@ class TestRmdir: """Should complete dirs only, also when invoked using full path.""" assert completion == ["bar bar.d/", "foo.d/"] - @pytest.mark.complete("rmdir -", require_cmd=True) + @pytest.mark.complete("rmdir -", require_longopt=True) def test_options(self, completion): assert completion diff --git a/test/t/test_rpm.py b/test/t/test_rpm.py index e6f7198..2f1567b 100644 --- a/test/t/test_rpm.py +++ b/test/t/test_rpm.py @@ -6,7 +6,7 @@ class TestRpm: def test_1(self, completion): assert completion - @pytest.mark.complete("rpm -q ", skipif='test -z "$(rpm -qa 2>/dev/null)"') + @pytest.mark.complete("rpm -q ", skipif='test ! "$(rpm -qa 2>/dev/null)"') def test_2(self, completion): assert completion diff --git a/test/t/test_rsync.py b/test/t/test_rsync.py index d54ce6f..0f98dcc 100644 --- a/test/t/test_rsync.py +++ b/test/t/test_rsync.py @@ -1,7 +1,7 @@ import pytest -@pytest.mark.bashcomp(ignore_env=r"^[+-]_scp_path_esc=") +@pytest.mark.bashcomp(ignore_env=r"^[+-]_comp_cmd_scp__path_esc=") class TestRsync: @pytest.mark.complete("rsync ") def test_1(self, completion): @@ -14,3 +14,7 @@ class TestRsync: @pytest.mark.complete("rsync --rsh=") def test_3(self, completion): assert completion == "rsh ssh".split() + + @pytest.mark.complete("rsync --", require_cmd=True) + def test_4(self, completion): + assert "--help" in completion diff --git a/test/t/test_scp.py b/test/t/test_scp.py index 66b8da2..3bd06ee 100644 --- a/test/t/test_scp.py +++ b/test/t/test_scp.py @@ -76,4 +76,22 @@ class TestScp: Connection to it must open sufficiently quickly for the ConnectTimeout and sleep_after_tab settings. """ - assert completion == "%s:%s/" % (LIVE_HOST, live_pwd) + assert completion == f"{LIVE_HOST}:{live_pwd}/" + + @pytest.mark.complete("scp -o Foo=") + def test_option_arg(self, completion): + assert not completion # and no errors either + + @pytest.mark.complete( + "scp hostname-not-expected-to-exist-in-known-hosts:", + shopt=dict(nullglob=True), + ) + def test_remote_path_with_nullglob(self, completion): + assert not completion + + @pytest.mark.complete( + "scp hostname-not-expected-to-exist-in-known-hosts:", + shopt=dict(failglob=True), + ) + def test_remote_path_with_failglob(self, completion): + assert not completion diff --git a/test/t/test_secret_tool.py b/test/t/test_secret_tool.py index cbfc0cb..7791446 100644 --- a/test/t/test_secret_tool.py +++ b/test/t/test_secret_tool.py @@ -1,7 +1,7 @@ import pytest -@pytest.mark.bashcomp(cmd="secret-tool",) +@pytest.mark.bashcomp(cmd="secret-tool") class TestSecretTool: @pytest.mark.complete("secret-tool ", require_cmd=True) def test_modes(self, completion): @@ -9,4 +9,4 @@ class TestSecretTool: @pytest.mark.complete("secret-tool search ") def test_no_complete(self, completion): - assert not completion + assert completion == "--all --unlock".split() diff --git a/test/t/test_service.py b/test/t/test_service.py index 7ce4312..8e7fb05 100644 --- a/test/t/test_service.py +++ b/test/t/test_service.py @@ -1,7 +1,13 @@ +import sys + import pytest class TestService: + @pytest.mark.xfail( + sys.platform == "darwin", + reason="Service completion not available on macOS", + ) @pytest.mark.complete("service ") def test_1(self, completion): assert completion diff --git a/test/t/test_sha224sum.py b/test/t/test_sha224sum.py new file mode 100644 index 0000000..c6b6138 --- /dev/null +++ b/test/t/test_sha224sum.py @@ -0,0 +1,7 @@ +import pytest + + +class TestSha224sum: + @pytest.mark.complete("sha224sum --", require_longopt=True) + def test_options(self, completion): + assert completion diff --git a/test/t/test_sha256sum.py b/test/t/test_sha256sum.py new file mode 100644 index 0000000..c8df94c --- /dev/null +++ b/test/t/test_sha256sum.py @@ -0,0 +1,15 @@ +import pytest + + +class TestSha256sum: + @pytest.mark.complete("sha256sum --", require_longopt=True) + def test_options(self, completion): + assert completion + + @pytest.mark.complete("sha256sum ", cwd="sha256sum") + def test_summing(self, completion): + assert completion == "foo" + + @pytest.mark.complete("sha256sum -c ", cwd="sha256sum") + def test_checking(self, completion): + assert completion == "foo.sha256" diff --git a/test/t/test_sha384sum.py b/test/t/test_sha384sum.py new file mode 100644 index 0000000..903ee8c --- /dev/null +++ b/test/t/test_sha384sum.py @@ -0,0 +1,7 @@ +import pytest + + +class TestSha384sum: + @pytest.mark.complete("sha384sum --", require_longopt=True) + def test_options(self, completion): + assert completion diff --git a/test/t/test_sha512sum.py b/test/t/test_sha512sum.py new file mode 100644 index 0000000..746e64e --- /dev/null +++ b/test/t/test_sha512sum.py @@ -0,0 +1,7 @@ +import pytest + + +class TestSha512sum: + @pytest.mark.complete("sha512sum --", require_longopt=True) + def test_options(self, completion): + assert completion diff --git a/test/t/test_ssh.py b/test/t/test_ssh.py index 8e95819..f714d99 100644 --- a/test/t/test_ssh.py +++ b/test/t/test_ssh.py @@ -10,8 +10,12 @@ class TestSsh: @pytest.mark.complete("ssh -F config ls", cwd="ssh") def test_2(self, completion): - """Should complete both commands and hostname.""" - assert all(x in completion for x in "ls ls_known_host".split()) + """ + Should not complete commands when host is not specified. + + Test sanity assumes there are commands starting with `ls`. + """ + assert completion == "_known_host" @pytest.mark.complete("ssh bash", cwd="ssh") def test_3(self, completion): @@ -58,3 +62,8 @@ class TestSsh: def test_protocol_option_bundling(self, bash, protocol): completion = assert_complete(bash, "ssh -%sF ssh/" % protocol) assert "config" in completion + + @pytest.mark.complete("ssh -F config -o ForwardX11=yes ls", cwd="ssh") + def test_options_with_args_and_arg_counting(self, completion): + """Options with arguments should not confuse arg counting.""" + assert completion == "_known_host" diff --git a/test/t/test_ssh_add.py b/test/t/test_ssh_add.py index 7e49372..cec12c7 100644 --- a/test/t/test_ssh_add.py +++ b/test/t/test_ssh_add.py @@ -15,3 +15,23 @@ class TestSshAdd: ) def test_2(self, completion): assert completion + + @pytest.mark.complete( + "ssh-add -", + require_cmd=True, + xfail="ssh-add --help 2>&1 | " + "command grep -qiF 'Could not open a connection'", + shopt=dict(failglob=True), + ) + def test_2_failglob(self, completion): + assert completion + + @pytest.mark.complete( + "ssh-add -", + require_cmd=True, + xfail="ssh-add --help 2>&1 | " + "command grep -qiF 'Could not open a connection'", + shopt=dict(nullglob=True), + ) + def test_2_nullglob(self, completion): + assert completion diff --git a/test/t/test_ssh_copy_id.py b/test/t/test_ssh_copy_id.py index e38e901..327ee7c 100644 --- a/test/t/test_ssh_copy_id.py +++ b/test/t/test_ssh_copy_id.py @@ -8,7 +8,7 @@ import pytest # identities are found. Try to make sure there is at least one. "HOME=$PWD/ssh-copy-id", ), - ignore_env=r"^[+-]_scp_path_esc=", + ignore_env=r"^[+-]_comp_cmd_scp__path_esc=", ) class TestSshCopyId: @pytest.mark.complete("ssh-copy-id -", require_cmd=True) diff --git a/test/t/test_ssh_keygen.py b/test/t/test_ssh_keygen.py index b773ab4..cc6ff4e 100644 --- a/test/t/test_ssh_keygen.py +++ b/test/t/test_ssh_keygen.py @@ -7,6 +7,18 @@ class TestSshKeygen: def test_1(self, completion): assert completion + @pytest.mark.complete( + "ssh-keygen -", require_cmd=True, shopt=dict(failglob=True) + ) + def test_1_failglob(self, completion): + assert completion + + @pytest.mark.complete( + "ssh-keygen -", require_cmd=True, shopt=dict(nullglob=True) + ) + def test_1_nullglob(self, completion): + assert completion + @pytest.mark.complete("ssh-keygen -s foo_key ssh-copy-id/.ssh/") def test_filedir_pub_at_end_of_s(self, completion): assert completion @@ -15,13 +27,11 @@ class TestSshKeygen: @pytest.mark.complete("ssh-keygen -s foo_key -n foo,") def test_usernames_for_n(self, completion): assert completion - assert not any("," in x for x in completion) # TODO check that these are usernames @pytest.mark.complete("ssh-keygen -s foo_key -h -n foo,") def test_host_for_h_n(self, completion): assert completion - assert not any("," in x for x in completion) # TODO check that these are hostnames @pytest.mark.complete("ssh-keygen -Y foo -n ") @@ -57,3 +67,19 @@ class TestSshKeygen: @pytest.mark.complete("ssh-keygen -O unknown=") def test_O_unknown(self, completion): assert not completion + + @pytest.mark.complete("ssh-keygen -O application=") + def test_O_application(self, completion): + assert completion == "ssh:" + + @pytest.mark.complete("ssh-keygen -O application=s") + def test_O_application_s(self, completion): + assert completion == "sh:" + + @pytest.mark.complete("ssh-keygen -O application=ssh:") + def test_O_application_ssh_colon(self, completion): + assert not completion + + @pytest.mark.complete("ssh-keygen -O application=nonexistent") + def test_O_application_nonexistent(self, completion): + assert not completion diff --git a/test/t/test_ssh_keyscan.py b/test/t/test_ssh_keyscan.py new file mode 100644 index 0000000..ee65832 --- /dev/null +++ b/test/t/test_ssh_keyscan.py @@ -0,0 +1,19 @@ +import pytest + + +@pytest.mark.bashcomp( + cmd="ssh-keyscan", +) +class TestSshKeyscan: + @pytest.mark.complete("ssh-keyscan ") + def test_basic(self, completion): + assert completion + + @pytest.mark.complete("ssh-keyscan -", require_cmd=True) + def test_options(self, completion): + assert completion + + @pytest.mark.complete("ssh-keyscan -t rsa,", require_cmd=True) + def test_type_delimited(self, completion): + assert completion + assert "rsa" not in completion diff --git a/test/t/test_sshfs.py b/test/t/test_sshfs.py index 44daed3..cb4189b 100644 --- a/test/t/test_sshfs.py +++ b/test/t/test_sshfs.py @@ -1,7 +1,7 @@ import pytest -@pytest.mark.bashcomp(ignore_env=r"^[+-]_scp_path_esc=") +@pytest.mark.bashcomp(ignore_env=r"^[+-]_comp_cmd_scp__path_esc=") class TestSshfs: @pytest.mark.complete("sshfs ./") def test_1(self, completion): diff --git a/test/t/test_su.py b/test/t/test_su.py index 9aa064d..d71d6e1 100644 --- a/test/t/test_su.py +++ b/test/t/test_su.py @@ -6,6 +6,8 @@ class TestSu: def test_1(self, completion): assert completion - @pytest.mark.complete("su -", require_cmd=True) + @pytest.mark.complete( + "su -", require_cmd=True, skipif="[[ $OSTYPE != *linux* ]]" + ) def test_2(self, completion): assert completion diff --git a/test/t/test_sudo.py b/test/t/test_sudo.py index a349466..a7d67de 100644 --- a/test/t/test_sudo.py +++ b/test/t/test_sudo.py @@ -1,6 +1,6 @@ import pytest -from conftest import assert_complete +from conftest import assert_bash_exec, assert_complete class TestSudo: @@ -8,9 +8,9 @@ class TestSudo: def test_1(self, completion): assert completion - @pytest.mark.complete("sudo cd fo", cwd="shared/default") + @pytest.mark.complete("sudo cd foo", cwd="shared/default") def test_2(self, completion): - assert completion == "o.d/" + assert completion == ".d/" assert not completion.endswith(" ") @pytest.mark.complete("sudo sh share") @@ -81,3 +81,7 @@ class TestSudo: part, _ = part_full_group completion = assert_complete(bash, "sudo chown foo:bar:%s" % part) assert not completion + + def test_12(self, bash): + assert_complete(bash, 'sudo "/tmp/aaa bbb" ') + assert_bash_exec(bash, "! complete -p aaa", want_output=None) diff --git a/test/t/test_sum.py b/test/t/test_sum.py index bfb2cf4..2060370 100644 --- a/test/t/test_sum.py +++ b/test/t/test_sum.py @@ -6,6 +6,6 @@ class TestSum: def test_1(self, completion): assert completion - @pytest.mark.complete("sum -", require_longopt=True) + @pytest.mark.complete("sum --", require_longopt=True) def test_options(self, completion): assert completion diff --git a/test/t/test_synclient.py b/test/t/test_synclient.py index 8a31a65..8ce66f6 100644 --- a/test/t/test_synclient.py +++ b/test/t/test_synclient.py @@ -2,7 +2,6 @@ import pytest class TestSynclient: - # synclient -l may error out with e.g. # "Couldn't find synaptics properties. No synaptics driver loaded?" @pytest.mark.complete( diff --git a/test/t/test_tar.py b/test/t/test_tar.py index 4518d0b..2616446 100644 --- a/test/t/test_tar.py +++ b/test/t/test_tar.py @@ -5,7 +5,7 @@ import pytest from conftest import assert_bash_exec -@pytest.mark.bashcomp(ignore_env=r"^-declare -f _tar$") +@pytest.mark.bashcomp(ignore_env=r"^-declare -f _comp_cmd_tar$") class TestTar: @pytest.fixture(scope="class") def gnu_tar(self, bash): @@ -13,8 +13,8 @@ class TestTar: if not re.search(r"\bGNU ", got): pytest.skip("Not GNU tar") - @pytest.mark.complete("tar ") - def test_1(self, completion): + @pytest.mark.complete("tar ", shopt=dict(failglob=True)) + def test_1(self, bash, completion): assert completion # Test "f" when mode is not as first option diff --git a/test/t/test_time.py b/test/t/test_time.py index 231f14e..f2049d5 100644 --- a/test/t/test_time.py +++ b/test/t/test_time.py @@ -4,9 +4,15 @@ import pytest class TestTime: - @pytest.mark.complete("time set") - def test_1(self, completion): - assert completion + @pytest.mark.complete("time _comp_delimite", cwd="shared/empty_dir") + def test_command(self, completion): + """ + Test completion of commands. + + We use a function of ours as the test subject, as that's guaranteed + to be available, and do not rely on anything in particular in $PATH. + """ + assert completion == "d" or "_comp_delimited" in completion @pytest.mark.complete("time -p find -typ") def test_2(self, completion): diff --git a/test/t/test_tox.py b/test/t/test_tox.py index f012a03..b101b9b 100644 --- a/test/t/test_tox.py +++ b/test/t/test_tox.py @@ -2,7 +2,7 @@ import pytest class TestTox: - @pytest.mark.complete("tox -") + @pytest.mark.complete("tox -", require_cmd=True) def test_1(self, completion): assert completion @@ -12,7 +12,7 @@ class TestTox: @pytest.mark.complete("tox -e foo,", cwd="tox") def test_3(self, completion): - assert all(x in completion for x in "py37 ALL".split()) + assert all("foo," + x in completion for x in "py37 ALL".split()) @pytest.mark.complete("tox -e foo -- ", cwd="tox") def test_default_after_dashdash(self, completion): diff --git a/test/t/test_tree.py b/test/t/test_tree.py new file mode 100644 index 0000000..214e415 --- /dev/null +++ b/test/t/test_tree.py @@ -0,0 +1,23 @@ +import pytest + + +class TestTree: + @pytest.mark.complete("tree ", cwd="shared/default") + def test_basic(self, completion): + assert completion == ["bar bar.d/", "foo.d/"] + + @pytest.mark.complete("tree --fromfile ", cwd="shared/default") + def test_fromfile(self, completion): + assert completion == ["bar", "bar bar.d/", "foo", "foo.d/"] + + @pytest.mark.complete( + "tree -", + require_cmd=True, + xfail="! tree --help 2>&1 | command grep -qF -- ' -'", + ) + def test_options(self, completion): + assert completion + + @pytest.mark.complete("tree --sort=", require_cmd=True) + def test_equals_sign_split(self, completion): + assert completion diff --git a/test/t/test_truncate.py b/test/t/test_truncate.py new file mode 100644 index 0000000..b50baac --- /dev/null +++ b/test/t/test_truncate.py @@ -0,0 +1,11 @@ +import pytest + + +class TestTruncate: + @pytest.mark.complete("truncate ") + def test_basic(self, completion): + assert completion + + @pytest.mark.complete("truncate -", require_cmd=True) + def test_options(self, completion): + assert completion diff --git a/test/t/test_tshark.py b/test/t/test_tshark.py index f49533e..d44e62d 100644 --- a/test/t/test_tshark.py +++ b/test/t/test_tshark.py @@ -1,7 +1,7 @@ import pytest -@pytest.mark.bashcomp(ignore_env=r"^\+_tshark_pr(ef|otocol)s=") +@pytest.mark.bashcomp(ignore_env=r"^\+_comp_cmd_tshark__pr(ef|otocol)s=") class TestTshark: @pytest.mark.complete("tshark -", require_cmd=True) def test_1(self, completion): @@ -14,7 +14,7 @@ class TestTshark: @pytest.mark.complete("tshark -O foo,htt", require_cmd=True) def test_3(self, completion): # p: one completion only; http: e.g. http and http2 - assert completion == "p" or "http" in completion + assert completion == "p" or "foo,http" in completion @pytest.mark.complete("tshark -o tcp", require_cmd=True) def test_4(self, completion): diff --git a/test/t/test_ulimit.py b/test/t/test_ulimit.py index 3ab974c..271b6ce 100644 --- a/test/t/test_ulimit.py +++ b/test/t/test_ulimit.py @@ -1,10 +1,12 @@ import pytest +from conftest import assert_complete + class TestUlimit: @pytest.mark.complete("ulimit ") def test_1(self, completion): - assert completion + assert not completion @pytest.mark.complete("ulimit -", require_cmd=True) def test_2(self, completion): @@ -33,3 +35,13 @@ class TestUlimit: def test_7(self, completion): """Test modes are NOT completed with -a given somewhere.""" assert not completion + + @pytest.mark.parametrize("flag", ["-S", "-H"]) + def test_no_special_values_after_soft_or_hard(self, bash, flag): + completion = assert_complete(bash, "ulimit %s " % flag) + assert not completion + + @pytest.mark.complete("ulimit -c 0 -n ") + def test_special_not_just_first(self, completion): + """Test we offer limit values not only for the first option.""" + assert completion diff --git a/test/t/test_umount.py b/test/t/test_umount.py index 2baf0da..de18835 100644 --- a/test/t/test_umount.py +++ b/test/t/test_umount.py @@ -1,3 +1,5 @@ +import sys + import pytest from conftest import assert_bash_exec @@ -11,12 +13,14 @@ class TestUmount: (correctly) uses absolute paths. So we create a custom completion which reads from a file in our text fixture instead. """ + if sys.platform != "linux": + pytest.skip("Linux specific") assert_bash_exec(bash, "unset COMPREPLY cur; unset -f _mnt_completion") assert_bash_exec( bash, "_mnt_completion() { " - "local cur=$(_get_cword); " - "_linux_fstab $(_get_pword) < mount/test-fstab; " + "local cur prev;_comp_get_words cur prev; " + '_comp_cmd_umount__linux_fstab "$prev" < mount/test-fstab; ' "} && complete -F _mnt_completion _mnt", ) request.addfinalizer( @@ -75,9 +79,10 @@ class TestUmount: def test_mnt_label_quote(self, completion, dummy_mnt): assert completion == r"ian-it\'s\ awesome" + @pytest.mark.skipif(sys.platform != "linux", reason="Linux specific") def test_linux_fstab_unescape(self, bash): assert_bash_exec(bash, r"var=one\'two\\040three\\") - assert_bash_exec(bash, "__linux_fstab_unescape var") + assert_bash_exec(bash, "_comp_cmd_umount__linux_fstab_unescape var") output = assert_bash_exec( bash, r'printf "%s\n" "$var"', want_output=True ) diff --git a/test/t/test_upgradepkg.py b/test/t/test_upgradepkg.py index 87fe8e4..8411333 100644 --- a/test/t/test_upgradepkg.py +++ b/test/t/test_upgradepkg.py @@ -12,8 +12,8 @@ class TestUpgradepkg: @pytest.mark.complete("upgradepkg --") def test_2(self, completion): assert ( - completion == "--dry-run --install-new --reinstall " - "--verbose".split() + completion + == "--dry-run --install-new --reinstall --verbose".split() ) @pytest.mark.complete("upgradepkg ", cwd="slackware/home") diff --git a/test/t/test_useradd.py b/test/t/test_useradd.py index 5cbf6ce..2eddede 100644 --- a/test/t/test_useradd.py +++ b/test/t/test_useradd.py @@ -9,3 +9,7 @@ class TestUseradd: @pytest.mark.complete("useradd -", require_cmd=True) def test_2(self, completion): assert completion + + @pytest.mark.complete("useradd -R shells --shell=") + def test_chroot_shells(self, completion): + assert completion == "/bash/completion/canary" diff --git a/test/t/test_usermod.py b/test/t/test_usermod.py index ef3dd5a..61c99a0 100644 --- a/test/t/test_usermod.py +++ b/test/t/test_usermod.py @@ -9,3 +9,7 @@ class TestUsermod: @pytest.mark.complete("usermod -", require_cmd=True) def test_2(self, completion): assert completion + + @pytest.mark.complete("useradd --root shells -s ") + def test_chroot_shells(self, completion): + assert completion == "/bash/completion/canary" diff --git a/test/t/test_valgrind.py b/test/t/test_valgrind.py index 0553b55..dbe7028 100644 --- a/test/t/test_valgrind.py +++ b/test/t/test_valgrind.py @@ -4,7 +4,6 @@ import pytest class TestValgrind: - # b: Assume we have at least bash that starts with b in PATH @pytest.mark.complete("valgrind b") def test_1(self, completion): diff --git a/test/t/test_vipw.py b/test/t/test_vipw.py index 07b454b..b78fcbd 100644 --- a/test/t/test_vipw.py +++ b/test/t/test_vipw.py @@ -1,7 +1,12 @@ +import sys + import pytest class TestVipw: @pytest.mark.complete("vipw -", require_cmd=True) def test_1(self, completion): - assert completion + if sys.platform == "darwin": + assert not completion # takes no options + else: + assert completion diff --git a/test/t/test_vncviewer.py b/test/t/test_vncviewer.py index 9e2f148..643d98d 100644 --- a/test/t/test_vncviewer.py +++ b/test/t/test_vncviewer.py @@ -1,7 +1,9 @@ import pytest -@pytest.mark.bashcomp(ignore_env=r"^-declare -f _vncviewer_bootstrap$") +@pytest.mark.bashcomp( + ignore_env=r"^-declare -f _comp_cmd_vncviewer__bootstrap$" +) class TestVncviewer: @pytest.mark.complete("vncviewer ") def test_1(self, completion): diff --git a/test/t/test_who.py b/test/t/test_who.py index 9131ac7..69b93c9 100644 --- a/test/t/test_who.py +++ b/test/t/test_who.py @@ -2,8 +2,6 @@ import pytest class TestWho: - @pytest.mark.complete( - "who --", require_cmd=True, xfail="! who --help &>/dev/null" - ) + @pytest.mark.complete("who --", require_longopt=True) def test_1(self, completion): assert completion diff --git a/test/t/test_wine.py b/test/t/test_wine.py index d0e5698..e18ea1c 100644 --- a/test/t/test_wine.py +++ b/test/t/test_wine.py @@ -9,3 +9,7 @@ class TestWine: @pytest.mark.complete("wine notepad ", cwd="shared/default") def test_2(self, completion): assert completion == ["bar", "bar bar.d/", "foo", "foo.d/"] + + @pytest.mark.complete("wine --help ") + def test_no_complete_after_help(self, completion): + assert not completion diff --git a/test/t/test_xdg_mime.py b/test/t/test_xdg_mime.py index 432be06..b91e75a 100644 --- a/test/t/test_xdg_mime.py +++ b/test/t/test_xdg_mime.py @@ -19,10 +19,14 @@ class TestXdgMime: def test_4(self, completion): assert completion - @pytest.mark.complete("xdg-mime default foo.desktop ") + @pytest.mark.complete("xdg-mime default foo.desktop ", require_cmd=True) def test_5(self, completion): assert completion @pytest.mark.complete("xdg-mime install --mode ") def test_6(self, completion): assert completion + + @pytest.mark.complete("xdg-mime query filetype foo ") + def test_filetype_one_arg(self, completion): + assert not completion diff --git a/test/t/test_xev.py b/test/t/test_xev.py new file mode 100644 index 0000000..3ced4e0 --- /dev/null +++ b/test/t/test_xev.py @@ -0,0 +1,11 @@ +import pytest + + +class TestXev: + @pytest.mark.complete("xev ") + def test_basic(self, completion): + assert not completion + + @pytest.mark.complete("xev -", require_cmd=True) + def test_options(self, completion): + assert completion diff --git a/test/t/test_xfreerdp.py b/test/t/test_xfreerdp.py index 5616271..3dce4ad 100644 --- a/test/t/test_xfreerdp.py +++ b/test/t/test_xfreerdp.py @@ -42,7 +42,7 @@ class TestXfreerdp: @pytest.mark.complete( "xfreerdp /kbd:", require_cmd=True, - skipif='test -z "$(xfreerdp /kbd-list 2>/dev/null)"', + skipif='test ! "$(xfreerdp /kbd-list 2>/dev/null)"', ) def test_4(self, bash, completion, help_success, slash_syntax): assert completion @@ -58,3 +58,7 @@ class TestXfreerdp: @pytest.mark.complete("xfreerdp --help ", require_cmd=True) def test_7(self, completion): assert not completion + + @pytest.mark.complete("xfreerdp ./") + def test_rdp_files(self, completion): + assert completion # just dirs for now in the fixture, but that'll do diff --git a/test/t/test_xhost.py b/test/t/test_xhost.py index bb2df82..b704b9c 100644 --- a/test/t/test_xhost.py +++ b/test/t/test_xhost.py @@ -8,15 +8,13 @@ class TestXhost: @pytest.mark.parametrize("prefix", ["+", "-", ""]) def test_hosts(self, bash, hosts, prefix): completion = assert_complete(bash, "xhost %s" % prefix) - assert completion == ["%s%s" % (prefix, x) for x in hosts] + assert completion == [f"{prefix}{x}" for x in hosts] @pytest.mark.parametrize("prefix", ["+", "-", ""]) def test_partial_hosts(self, bash, hosts, prefix): first_char, partial_hosts = partialize(bash, hosts) - completion = assert_complete(bash, "xhost %s%s" % (prefix, first_char)) + completion = assert_complete(bash, f"xhost {prefix}{first_char}") if len(completion) == 1: assert completion == partial_hosts[0][1:] else: - assert completion == sorted( - "%s%s" % (prefix, x) for x in partial_hosts - ) + assert completion == sorted(f"{prefix}{x}" for x in partial_hosts) diff --git a/test/t/test_xmlwf.py b/test/t/test_xmlwf.py index 901f78a..eab9894 100644 --- a/test/t/test_xmlwf.py +++ b/test/t/test_xmlwf.py @@ -9,3 +9,7 @@ class TestXmlwf: @pytest.mark.complete("xmlwf -", require_cmd=True) def test_2(self, completion): assert completion + + @pytest.mark.complete("xmlwf -sa ") + def test_no_arg_to_a(self, completion): + assert not completion diff --git a/test/t/test_xrandr.py b/test/t/test_xrandr.py index e766922..8d8a6bc 100644 --- a/test/t/test_xrandr.py +++ b/test/t/test_xrandr.py @@ -1,15 +1,122 @@ import pytest +ENV = dict(PATH="$PWD/xrandr:$PATH") +OUTPUTS = sorted("DP-0 DP-1 DP-2 DP-3 eDP-1-1 HDMI-0".split()) + +@pytest.mark.bashcomp(pre_cmds=("PATH=$PATH:$PWD/xrandr",)) class TestXrandr: @pytest.mark.complete("xrandr ", require_cmd=True) - def test_1(self, completion): + def test_no_args(self, completion): assert completion - @pytest.mark.complete("xrandr --mode ") - def test_2(self, completion): - assert not completion - @pytest.mark.complete("xrandr -", require_cmd=True) - def test_3(self, completion): + def test_single_dash(self, completion): assert completion + + @pytest.mark.complete("xrandr --output ", env=ENV) + def test_output(self, completion): + assert completion == OUTPUTS + + @pytest.mark.complete("xrandr --output HDMI-0 --left-of ", env=ENV) + def test_output_left_of(self, completion): + assert completion == OUTPUTS + + @pytest.mark.complete("xrandr --output HDMI-0 --reflect ", env=ENV) + def test_output_reflect(self, completion): + assert completion == sorted("normal x y xy".split()) + + @pytest.mark.complete("xrandr --reflect ", require_cmd=True) + def test_output_reflect_nooutput(self, completion): + assert not completion + + @pytest.mark.complete("xrandr --output HDMI-0 --rotate ", env=ENV) + def test_output_rotate(self, completion): + assert completion == sorted("normal inverted left right".split()) + + @pytest.mark.complete("xrandr --rotate ", require_cmd=True) + def test_output_rotate_nooutput(self, completion): + assert not completion + + @pytest.mark.complete("xrandr --output HDMI-0 --filter ", env=ENV) + def test_output_filter(self, completion): + assert completion == sorted("bilinear nearest".split()) + + @pytest.mark.complete("xrandr --output HDMI-0 --mode ", env=ENV) + def test_output_mode(self, completion): + assert completion == sorted( + "1024x768 1280x1024 1280x800 1600x900 1920x1080 720x480 " + "800x600 1152x864 1280x720 1440x900 1680x1050 640x480 720x576".split() + ) + + @pytest.mark.complete("xrandr --mode ", require_cmd=True) + def test_output_mode_nooutput(self, completion): + assert not completion + + @pytest.mark.complete("xrandr --addmode ", env=ENV) + def test_addmode_first(self, completion): + assert completion == OUTPUTS + + @pytest.mark.complete("xrandr --addmode HDMI-0 ", env=ENV) + def test_addmode_second(self, completion): + assert completion == sorted( + "1024x576 1280x800 1440x900 320x200 432x243 640x350 700x450 800x450 928x696 " + "1024x768 1280x960 1600x900 320x240 480x270 640x360 700x525 800x600 960x540 " + "1024x768i 1368x768 1680x1050 360x200 512x288 640x400 720x400 832x624 960x600 " + "1152x864 1400x1050 1920x1080 360x202 512x384 640x480 720x405 840x525 960x720 " + "1280x1024 1400x900 320x175 400x300 512x384i 640x512 720x480 864x486 " + "1280x720 1440x810 320x180 416x312 576x432 684x384 720x576 896x672".split() + ) + + @pytest.mark.complete("xrandr --delmode ", env=ENV) + def test_delmode_first(self, completion): + assert completion == OUTPUTS + + @pytest.mark.complete("xrandr --delmode HDMI-0 ", env=ENV) + def test_delmode_second(self, completion): + assert completion == sorted( + "1024x768 1280x1024 1280x800 1600x900 1920x1080 720x480 " + "800x600 1152x864 1280x720 1440x900 1680x1050 640x480 720x576".split() + ) + + @pytest.mark.complete("xrandr --dpi ", env=ENV) + def test_dpi(self, completion): + assert completion == OUTPUTS + + @pytest.mark.complete("xrandr -o ", env=ENV) + def test_orientation(self, completion): + assert completion == sorted( + "normal inverted left right 0 1 2 3".split() + ) + + @pytest.mark.complete("xrandr --setmonitor testmonitor ", env=ENV) + def test_setmonitor_second(self, completion): + assert completion == sorted("auto".split()) + + @pytest.mark.complete("xrandr --setmonitor testmonitor auto ", env=ENV) + def test_setmonitor_third(self, completion): + assert completion == OUTPUTS + ["none"] + + @pytest.mark.complete("xrandr --delmonitor ", env=ENV) + def test_delmonitor(self, completion): + assert completion == sorted("eDP-1-1 HDMI-0".split()) + + @pytest.mark.complete("xrandr --setprovideroutputsource ", env=ENV) + def test_setprovideroutputsource_first(self, completion): + assert completion == sorted("modesetting".split()) + + @pytest.mark.complete( + "xrandr --setprovideroutputsource modesetting ", env=ENV + ) + def test_setprovideroutputsource_second(self, completion): + assert completion == sorted("0x0 modesetting NVIDIA-0".split()) + + @pytest.mark.complete("xrandr --setprovideroffloadsink ", env=ENV) + def test_setprovideroffloadsink_first(self, completion): + assert completion == sorted("modesetting".split()) + + @pytest.mark.complete( + "xrandr --setprovideroffloadsink modesetting ", env=ENV + ) + def test_setprovideroffloadsink_second(self, completion): + assert completion == sorted("0x0 modesetting".split()) diff --git a/test/t/test_xz.py b/test/t/test_xz.py index f226d02..8ecb1a0 100644 --- a/test/t/test_xz.py +++ b/test/t/test_xz.py @@ -9,8 +9,9 @@ class TestXz: @pytest.mark.complete("xz -d xz/") def test_2(self, completion): assert ( - completion == "a/ bashcomp.lzma bashcomp.tar.xz " - "bashcomp.tlz bashcomp.xz".split() + completion + == "a/ bashcomp.lzma bashcomp.tar.xz bashcomp.tlz " + "bashcomp.xz".split() ) @pytest.mark.complete("xz xz/") diff --git a/test/t/unit/Makefile.am b/test/t/unit/Makefile.am index 3eb652a..54722de 100644 --- a/test/t/unit/Makefile.am +++ b/test/t/unit/Makefile.am @@ -1,21 +1,38 @@ EXTRA_DIST = \ + test_unit_abspath.py \ + test_unit_command_offset.py \ + test_unit_compgen.py \ + test_unit_compgen_commands.py \ test_unit_count_args.py \ + test_unit_delimited.py \ + test_unit_deprecate_func.py \ + test_unit_dequote.py \ test_unit_expand.py \ - test_unit_expand_tilde_by_ref.py \ + test_unit_expand_glob.py \ + test_unit_expand_tilde.py \ test_unit_filedir.py \ test_unit_find_unique_completion_pair.py \ - test_unit_get_comp_words_by_ref.py \ + test_unit_get_first_arg.py \ test_unit_get_cword.py \ - test_unit_init_completion.py \ + test_unit_get_words.py \ + test_unit_initialize.py \ test_unit_ip_addresses.py \ - test_unit_known_hosts_real.py \ + test_unit_known_hosts.py \ test_unit_longopt.py \ + test_unit_looks_like_path.py \ test_unit_parse_help.py \ test_unit_parse_usage.py \ + test_unit_pgids.py \ + test_unit_pids.py \ + test_unit_pnames.py \ test_unit_quote.py \ - test_unit_quote_readline.py \ + test_unit_quote_compgen.py \ + test_unit_realcommand.py \ + test_unit_split.py \ test_unit_tilde.py \ + test_unit_unlocal.py \ test_unit_variables.py \ + test_unit_xfunc.py \ test_unit_xinetd_services.py all: diff --git a/test/t/unit/test_unit_abspath.py b/test/t/unit/test_unit_abspath.py new file mode 100644 index 0000000..97d506c --- /dev/null +++ b/test/t/unit/test_unit_abspath.py @@ -0,0 +1,67 @@ +import pytest + +from conftest import assert_bash_exec + + +@pytest.mark.bashcomp( + cmd=None, cwd="shared", ignore_env=r"^\+declare -f __tester$" +) +class TestUnitAbsPath: + @pytest.fixture + def functions(self, bash): + assert_bash_exec( + bash, + ( + "__tester() { " + "local REPLY; " + '_comp_abspath "$1"; ' + 'printf %s "$REPLY"; ' + "}" + ), + ) + + def test_non_pollution(self, bash): + """Test environment non-pollution, detected at teardown.""" + assert_bash_exec( + bash, + "foo() { local REPLY=; _comp_abspath bar; }; foo; unset -f foo", + want_output=None, + ) + + def test_absolute(self, bash, functions): + output = assert_bash_exec( + bash, + "__tester /foo/bar", + want_output=True, + want_newline=False, + ) + assert output.strip() == "/foo/bar" + + def test_relative(self, bash, functions): + output = assert_bash_exec( + bash, + "__tester foo/bar", + want_output=True, + want_newline=False, + ) + assert output.strip().endswith("/shared/foo/bar") + + def test_cwd(self, bash, functions): + output = assert_bash_exec( + bash, + "__tester ./foo/./bar", + want_output=True, + want_newline=False, + ) + assert output.strip().endswith("/shared/foo/bar") + + def test_parent(self, bash, functions): + output = assert_bash_exec( + bash, + "__tester ../shared/foo/bar", + want_output=True, + want_newline=False, + ) + assert output.strip().endswith( + "/shared/foo/bar" + ) and not output.strip().endswith("../shared/foo/bar") diff --git a/test/t/unit/test_unit_command_offset.py b/test/t/unit/test_unit_command_offset.py new file mode 100644 index 0000000..0e32c1f --- /dev/null +++ b/test/t/unit/test_unit_command_offset.py @@ -0,0 +1,144 @@ +from shlex import quote + +import pytest + +from conftest import assert_bash_exec, assert_complete, bash_env_saved + + +def join(words): + """Return a shell-escaped string from *words*.""" + return " ".join(quote(word) for word in words) + + +@pytest.mark.bashcomp( + cmd=None, + cwd="_command_offset", + ignore_env=r"^[+-](COMPREPLY|REPLY)=", +) +class TestUnitCommandOffset: + wordlist = sorted(["foo", "bar"]) + + @pytest.fixture(scope="class") + def functions(self, bash): + assert_bash_exec( + bash, + "_cmd1() { _comp_command_offset 1; }; complete -F _cmd1 cmd1; " + "complete -F _comp_command meta; " + "_compfunc() { COMPREPLY=(%s); }" % join(self.wordlist), + ) + + completions = [ + 'complete -F _compfunc "${COMP_WORDS[0]}"', + 'complete -W %s "${COMP_WORDS[0]}"' % quote(join(self.wordlist)), + 'COMPREPLY=(dummy); complete -r "${COMP_WORDS[0]}"', + "COMPREPLY+=(${#COMPREPLY[@]})", + ] + for idx, comp in enumerate(completions, 2): + assert_bash_exec( + bash, + "_cmd%(idx)s() { %(comp)s && return 124; }; " + "complete -F _cmd%(idx)s cmd%(idx)s" + % {"idx": idx, "comp": comp}, + ) + + assert_bash_exec( + bash, "complete -W %s 'cmd!'" % quote(join(self.wordlist)) + ) + assert_bash_exec(bash, 'complete -W \'"$word1" "$word2"\' cmd6') + + assert_bash_exec(bash, "complete -C ./completer cmd7") + + def test_1(self, bash, functions): + assert_complete(bash, 'cmd1 "/tmp/aaa bbb" ') + assert_bash_exec(bash, "! complete -p aaa", want_output=None) + + @pytest.mark.parametrize( + "cmd,expected_completion", + [ + ("cmd2", wordlist), + ("cmd3", wordlist), + ("cmd4", []), + ("cmd5", ["0"]), + ], + ) + def test_2(self, bash, functions, cmd, expected_completion): + """Test meta-completion for completion functions that signal that + completion should be retried (i.e. change compspec and return 124). + + cmd2: The case when the completion spec is overwritten by the one that + contains "-F func" + + cmd3: The case when the completion spec is overwritten by the one + without "-F func". + + cmd4: The case when the completion spec is removed, in which we expect + no completions. This mimics the behavior of Bash's progcomp for the + exit status 124. + + cmd5: The case when the completion spec is unchanged. The retry should + be attempted at most once to avoid infinite loops. COMPREPLY should be + cleared before the retry. + """ + assert assert_complete(bash, "meta %s " % cmd) == expected_completion + + @pytest.mark.parametrize( + "cmd,expected_completion", + [ + ("cmd7 ", wordlist), + ("cmd7 l", ["line\\^Jtwo", "long"]), + ("cmd7 lo", ["ng"]), + ("cmd7 line", ["\\^Jtwo"]), + ("cmd7 cont1", ["cont10", "cont11\\"]), + ], + ) + def test_3(self, bash, functions, cmd, expected_completion): + got = assert_complete(bash, f"cmd1 {cmd}") + assert got == assert_complete(bash, cmd) + assert got == expected_completion + + def test_cmd_quoted(self, bash, functions): + assert assert_complete(bash, "meta 'cmd2' ") == self.wordlist + + def test_cmd_specialchar(self, bash, functions): + assert assert_complete(bash, "meta 'cmd!' ") == self.wordlist + + def test_space(self, bash, functions): + with bash_env_saved(bash) as bash_env: + bash_env.write_variable("word1", "a b c") + bash_env.write_variable("word2", "d e f") + assert assert_complete(bash, "meta cmd6 ") == ["a b c", "d e f"] + + @pytest.fixture(scope="class") + def find_original_word_functions(self, bash): + assert_bash_exec( + bash, + "_comp_test_reassemble() {" + " local IFS=$' \\t\\n' REPLY;" + ' COMP_LINE=$1; _comp_split COMP_WORDS "$2"; COMP_CWORD=$((${#COMP_WORDS[@]}-1));' + " _comp__reassemble_words = words cword;" + "}", + ) + assert_bash_exec( + bash, + "_comp_test_1() {" + ' local COMP_WORDS COMP_LINE COMP_CWORD words cword REPLY; _comp_test_reassemble "$1" "$2";' + ' _comp__find_original_word "$3";' + ' echo "$REPLY";' + "}", + ) + + def test_find_original_word_1(self, bash, find_original_word_functions): + result = assert_bash_exec( + bash, + '_comp_test_1 "sudo su do su do abc" "sudo su do su do abc" 3', + want_output=True, + ).strip() + assert result == "3" + + def test_find_original_word_2(self, bash, find_original_word_functions): + result = assert_bash_exec( + bash, + '_comp_test_1 "sudo --prefix=su su do abc" "sudo --prefix = su su do abc" 2', + want_output=True, + ).strip() + assert result == "4" diff --git a/test/t/unit/test_unit_compgen.py b/test/t/unit/test_unit_compgen.py new file mode 100644 index 0000000..cfdec8e --- /dev/null +++ b/test/t/unit/test_unit_compgen.py @@ -0,0 +1,173 @@ +import pytest +import re + +from conftest import assert_bash_exec, bash_env_saved, assert_complete + + +@pytest.mark.bashcomp(cmd=None) +class TestUtilCompgen: + @pytest.fixture + def functions(self, bash): + assert_bash_exec( + bash, + "_comp__test_dump() { ((${#arr[@]})) && printf '<%s>' \"${arr[@]}\"; echo; }", + ) + assert_bash_exec( + bash, + '_comp__test_compgen() { local -a arr=(00); _comp_compgen -v arr "$@"; _comp__test_dump; }', + ) + assert_bash_exec( + bash, + '_comp__test_words() { local -a input=("${@:1:$#-1}"); _comp__test_compgen -c "${@:$#}" -- -W \'${input[@]+"${input[@]}"}\'; }', + ) + assert_bash_exec( + bash, + '_comp__test_words_ifs() { local input=$2; _comp__test_compgen -F "$1" -c "${@:$#}" -- -W \'$input\'; }', + ) + + assert_bash_exec( + bash, + '_comp_cmd_fc() { _comp_compgen -c "$(_get_cword)" -C _filedir filedir; }; ' + "complete -F _comp_cmd_fc fc; " + "complete -F _comp_cmd_fc -o filenames fc2", + ) + assert_bash_exec( + bash, + '_comp_cmd_fcd() { _comp_compgen -c "$(_get_cword)" -C _filedir filedir -d; }; ' + "complete -F _comp_cmd_fcd fcd", + ) + + # test_8_option_U + assert_bash_exec( + bash, + "_comp_compgen_gen8() { local -a arr=(x y z); _comp_compgen -U arr -- -W '\"${arr[@]}\"'; }", + ) + + # test_9_inherit_a + assert_bash_exec( + bash, + '_comp_compgen_gen9sub() { local -a gen=(00); _comp_compgen -v gen -- -W 11; _comp_compgen_set "${gen[@]}"; }; ' + "_comp_compgen_gen9() { _comp_compgen_gen9sub; _comp_compgen -a gen9sub; }", + ) + + def test_1_basic(self, bash, functions): + output = assert_bash_exec( + bash, "_comp__test_words 12 34 56 ''", want_output=True + ) + assert output.strip() == "<12><34><56>" + + def test_2_space(self, bash, functions): + output = assert_bash_exec( + bash, + "_comp__test_words $'a b' $'c d\\t' ' e ' $'\\tf\\t' ''", + want_output=True, + ) + assert output.strip() == "<a b><c d\t>< e ><\tf\t>" + + def test_2_IFS(self, bash, functions): + with bash_env_saved(bash) as bash_env: + bash_env.write_variable("IFS", "34") + output = assert_bash_exec( + bash, "_comp__test_words 12 34 56 ''", want_output=True + ) + assert output.strip() == "<12><34><56>" + + def test_3_glob(self, bash, functions): + output = assert_bash_exec( + bash, + "_comp__test_words '*' '[a-z]*' '[a][b][c]' ''", + want_output=True, + ) + assert output.strip() == "<*><[a-z]*><[a][b][c]>" + + def test_3_failglob(self, bash, functions): + with bash_env_saved(bash) as bash_env: + bash_env.shopt("failglob", True) + output = assert_bash_exec( + bash, + "_comp__test_words '*' '[a-z]*' '[a][b][c]' ''", + want_output=True, + ) + assert output.strip() == "<*><[a-z]*><[a][b][c]>" + + def test_3_nullglob(self, bash, functions): + with bash_env_saved(bash) as bash_env: + bash_env.shopt("nullglob", True) + output = assert_bash_exec( + bash, + "_comp__test_words '*' '[a-z]*' '[a][b][c]' ''", + want_output=True, + ) + assert output.strip() == "<*><[a-z]*><[a][b][c]>" + + def test_4_empty(self, bash, functions): + output = assert_bash_exec( + bash, "_comp__test_words ''", want_output=True + ) + assert output.strip() == "" + + def test_5_option_F(self, bash, functions): + output = assert_bash_exec( + bash, + "_comp__test_words_ifs '25' ' 123 456 555 ' ''", + want_output=True, + ) + assert output.strip() == "< 1><3 4><6 >< >" + + def test_6_option_C_1(self, bash, functions): + output = assert_bash_exec( + bash, + "_comp__test_compgen -c a -C _filedir filedir", + want_output=True, + ) + set1 = set(re.findall(r"<[^<>]*>", output.strip())) + assert set1 == {"<a b>", "<a$b>", "<a&b>", "<a'b>", "<ab>", "<aé>"} + + def test_6_option_C_2(self, bash, functions): + output = assert_bash_exec( + bash, + "_comp__test_compgen -c b -C _filedir -- -d", + want_output=True, + ) + assert output.strip() == "<brackets>" + + @pytest.mark.parametrize("funcname", "fc fc2".split()) + def test_6_option_C_3(self, bash, functions, funcname): + completion = assert_complete(bash, "%s _filedir ab/" % funcname) + assert completion == "e" + + @pytest.mark.complete(r"fcd a\ ") + def test_6_option_C_4(self, functions, completion): + # Note: we are not in the original directory that "b" exists, so Bash + # will not suffix a slash to the directory name. + assert completion == "b" + + def test_7_icmd(self, bash, functions): + with bash_env_saved(bash) as bash_env: + bash_env.write_variable( + "BASH_COMPLETION_USER_DIR", "$PWD/_comp_compgen", quote=False + ) + + completions = assert_complete(bash, "compgen-cmd1 '") + assert completions == ["012", "123", "234", "5abc", "6def", "7ghi"] + + def test_7_xcmd(self, bash, functions): + with bash_env_saved(bash) as bash_env: + bash_env.write_variable( + "BASH_COMPLETION_USER_DIR", "$PWD/_comp_compgen", quote=False + ) + + completions = assert_complete(bash, "compgen-cmd2 '") + assert completions == ["012", "123", "234", "5foo", "6bar", "7baz"] + + def test_8_option_U(self, bash, functions): + output = assert_bash_exec( + bash, "_comp__test_compgen gen8", want_output=True + ) + assert output.strip() == "<x><y><z>" + + def test_9_inherit_a(self, bash, functions): + output = assert_bash_exec( + bash, "_comp__test_compgen gen9", want_output=True + ) + assert output.strip() == "<11><11>" diff --git a/test/t/unit/test_unit_compgen_commands.py b/test/t/unit/test_unit_compgen_commands.py new file mode 100644 index 0000000..d866239 --- /dev/null +++ b/test/t/unit/test_unit_compgen_commands.py @@ -0,0 +1,47 @@ +import pytest + +from conftest import assert_bash_exec, assert_complete, bash_env_saved + + +@pytest.mark.bashcomp(cmd=None, ignore_env=r"^\+COMPREPLY=") +class TestUtilCompgenCommands: + @pytest.fixture(scope="class") + def functions(self, request, bash): + assert_bash_exec( + bash, + r"_comp_compgen_commands__test() {" + r" local COMPREPLY=() cur=${1-};" + r" _comp_compgen_commands;" + r' printf "%s\n" "${COMPREPLY[@]-}";' + r"}", + ) + assert_bash_exec( + bash, + "_comp_cmd_ccc() {" + " local cur;" + " _comp_get_words cur;" + " unset -v COMPREPLY;" + " _comp_compgen_commands;" + "}; complete -F _comp_cmd_ccc ccc", + ) + + def test_basic(self, bash, functions): + output = assert_bash_exec( + bash, "_comp_compgen_commands__test sh", want_output=True + ) + assert output.strip() + + @pytest.mark.parametrize( + "shopt_no_empty,result_empty", ((True, True), (False, False)) + ) + def test_empty(self, bash, functions, shopt_no_empty, result_empty): + with bash_env_saved(bash) as bash_env: + bash_env.shopt("no_empty_cmd_completion", shopt_no_empty) + output = assert_bash_exec( + bash, "_comp_compgen_commands__test", want_output=True + ) + assert (output.strip() == "") == result_empty + + def test_spaces(self, bash, functions): + completion = assert_complete(bash, "ccc shared/default/bar") + assert completion == r"\ bar.d/" diff --git a/test/t/unit/test_unit_compgen_split.py b/test/t/unit/test_unit_compgen_split.py new file mode 100644 index 0000000..935ea2a --- /dev/null +++ b/test/t/unit/test_unit_compgen_split.py @@ -0,0 +1,102 @@ +import pytest + +from conftest import assert_bash_exec + + +@pytest.mark.bashcomp(cmd=None) +class TestUtilCompgenSplit: + @pytest.fixture + def functions(self, bash): + assert_bash_exec( + bash, + "_comp__test_dump() { ((${#arr[@]})) && printf '<%s>' \"${arr[@]}\"; echo; }", + ) + assert_bash_exec( + bash, + '_comp__test_compgen() { local -a arr=(00); _comp_compgen -v arr "$@"; _comp__test_dump; }', + ) + + assert_bash_exec( + bash, + "_comp__test_cmd1() { echo foo bar; echo baz; }", + ) + assert_bash_exec( + bash, + '_comp__test_attack() { echo "\\$(echo should_not_run >&2)"; }', + ) + + def test_1_basic(self, bash, functions): + output = assert_bash_exec( + bash, + '_comp__test_compgen split -- "$(_comp__test_cmd1)"', + want_output=True, + ) + assert output.strip() == "<foo><bar><baz>" + + def test_2_attack(self, bash, functions): + output = assert_bash_exec( + bash, + '_comp__test_compgen split -- "$(_comp__test_attack)"', + want_output=True, + ) + assert output.strip() == "<$(echo><should_not_run><>&2)>" + + def test_3_sep1(self, bash, functions): + output = assert_bash_exec( + bash, + '_comp__test_compgen split -l -- "$(_comp__test_cmd1)"', + want_output=True, + ) + assert output.strip() == "<foo bar><baz>" + + def test_3_sep2(self, bash, functions): + output = assert_bash_exec( + bash, + "_comp__test_compgen split -F $'b\\n' -- \"$(_comp__test_cmd1)\"", + want_output=True, + ) + assert output.strip() == "<foo ><ar><az>" + + def test_4_optionX(self, bash, functions): + output = assert_bash_exec( + bash, + '_comp__test_compgen split -X bar -- "$(_comp__test_cmd1)"', + want_output=True, + ) + assert output.strip() == "<foo><baz>" + + def test_4_optionS(self, bash, functions): + output = assert_bash_exec( + bash, + '_comp__test_compgen split -S .txt -- "$(_comp__test_cmd1)"', + want_output=True, + ) + assert output.strip() == "<foo.txt><bar.txt><baz.txt>" + + def test_4_optionP(self, bash, functions): + output = assert_bash_exec( + bash, + '_comp__test_compgen split -P /tmp/ -- "$(_comp__test_cmd1)"', + want_output=True, + ) + assert output.strip() == "</tmp/foo></tmp/bar></tmp/baz>" + + def test_4_optionPS(self, bash, functions): + output = assert_bash_exec( + bash, + '_comp__test_compgen split -P [ -S ] -- "$(_comp__test_cmd1)"', + want_output=True, + ) + assert output.strip() == "<[foo]><[bar]><[baz]>" + + def test_5_empty(self, bash, functions): + output = assert_bash_exec( + bash, '_comp__test_compgen split -- ""', want_output=True + ) + assert output.strip() == "" + + def test_5_empty2(self, bash, functions): + output = assert_bash_exec( + bash, '_comp__test_compgen split -- " "', want_output=True + ) + assert output.strip() == "" diff --git a/test/t/unit/test_unit_count_args.py b/test/t/unit/test_unit_count_args.py index 56bce2c..7b018e4 100644 --- a/test/t/unit/test_unit_count_args.py +++ b/test/t/unit/test_unit_count_args.py @@ -4,63 +4,167 @@ from conftest import TestUnitBase, assert_bash_exec @pytest.mark.bashcomp( - cmd=None, ignore_env=r"^[+-](args|COMP_(WORDS|CWORD|LINE|POINT))=" + cmd=None, + ignore_env=r"^[+-](REPLY|cword|words|COMP_(WORDS|CWORD|LINE|POINT))=", ) class TestUnitCountArgs(TestUnitBase): + @pytest.fixture + def functions(self, bash): + assert_bash_exec( + bash, + '_comp__test_unit() { local -a words=(); local cword REPLY=""; _comp__reassemble_words "<>&" words cword; _comp_count_args "$@"; echo "$REPLY"; }', + ) + def _test(self, *args, **kwargs): - return self._test_unit("_count_args %s; echo $args", *args, **kwargs) + return self._test_unit("_comp__test_unit %s", *args, **kwargs) def test_1(self, bash): - assert_bash_exec(bash, "COMP_CWORD= _count_args >/dev/null") + assert_bash_exec( + bash, + 'COMP_LINE= COMP_POINT=0 COMP_WORDS=() COMP_CWORD=; _comp_count_args -n ""', + ) - def test_2(self, bash): + def test_2(self, bash, functions): """a b| should set args to 1""" output = self._test(bash, "(a b)", 1, "a b", 3) assert output == "1" - def test_3(self, bash): + def test_3(self, bash, functions): """a b|c should set args to 1""" output = self._test(bash, "(a bc)", 1, "a bc", 3) assert output == "1" - def test_4(self, bash): + def test_4(self, bash, functions): """a b c| should set args to 2""" output = self._test(bash, "(a b c)", 2, "a b c", 4) assert output == "2" - def test_5(self, bash): + def test_5(self, bash, functions): """a b| c should set args to 1""" output = self._test(bash, "(a b c)", 1, "a b c", 3) assert output == "1" - def test_6(self, bash): + def test_6(self, bash, functions): """a b -c| d should set args to 2""" output = self._test(bash, "(a b -c d)", 2, "a b -c d", 6) assert output == "2" - def test_7(self, bash): + def test_7(self, bash, functions): """a b -c d e| with -c arg excluded should set args to 2""" output = self._test( - bash, "(a b -c d e)", 4, "a b -c d e", 10, arg='"" "@(-c|--foo)"' + bash, "(a b -c d e)", 4, "a b -c d e", 10, arg='-a "@(-c|--foo)"' ) assert output == "2" - def test_8(self, bash): + def test_8(self, bash, functions): """a -b -c d e| with -c arg excluded - and -b included should set args to 1""" + and -b included should set args to 1""" output = self._test( bash, "(a -b -c d e)", 4, "a -b -c d e", 11, - arg='"" "@(-c|--foo)" "-[b]"', + arg='-a "@(-c|--foo)" -i "-[b]"', ) assert output == "2" - def test_9(self, bash): + def test_9(self, bash, functions): """a -b -c d e| with -b included should set args to 3""" output = self._test( - bash, "(a -b -c d e)", 4, "a -b -c d e", 11, arg='"" "" "-b"' + bash, "(a -b -c d e)", 4, "a -b -c d e", 11, arg='-i "-b"' + ) + assert output == "3" + + def test_10_single_hyphen_1(self, bash): + """- should be counted as an argument representing stdout/stdin""" + output = self._test(bash, "(a -b - c -d e)", 5, "a -b - c -d e", 12) + assert output == "3" + + def test_10_single_hyphen_2(self, bash): + """- in an option argument should be skipped""" + output = self._test( + bash, "(a -b - c - e)", 5, "a -b - c - e", 11, arg='-a "-b"' + ) + assert output == "3" + + def test_11_double_hyphen_1(self, bash): + """all the words after -- should be counted""" + output = self._test( + bash, "(a -b -- -c -d e)", 5, "a -b -- -c -d e", 14 ) assert output == "3" + + def test_11_double_hyphen_2(self, bash): + """all the words after -- should be counted""" + output = self._test(bash, "(a b -- -c -d e)", 5, "a b -- -c -d e", 13) + assert output == "4" + + def test_12_exclude_optarg_1(self, bash): + """an option argument should be skipped even if it matches the argument pattern""" + output = self._test( + bash, "(a -o -x b c)", 4, "a -o -x b c", 10, arg='-a "-o" -i "-x"' + ) + assert output == "2" + + def test_12_exclude_optarg_2(self, bash): + """an option argument should be skipped even if it matches the argument pattern""" + output = self._test( + bash, + "(a -o -x -x c)", + 4, + "a -o -x -x c", + 11, + arg='-a "-o" -i "-x"', + ) + assert output == "2" + + def test_12_exclude_optarg_3(self, bash): + """an option argument should be skipped even if it matches the argument pattern""" + output = self._test( + bash, + "(a -o -x -y c)", + 4, + "a -o -x -y c", + 11, + arg='-a "-o" -i "-x"', + ) + assert output == "1" + + def test_13_plus_option_optarg(self, bash): + """When +o is specified to be an option taking an option argument, it should not be counted as an argument""" + output = self._test( + bash, "(a +o b c)", 3, "a +o b c", 7, arg='-a "+o"' + ) + assert output == "1" + + def test_14_no_optarg_chain_1(self, bash): + """an option argument should not take another option argument""" + output = self._test( + bash, "(a -o -o -o -o c)", 5, "a -o -o -o -o c", 14, arg='-a "-o"' + ) + assert output == "1" + + def test_14_no_optarg_chain_2(self, bash): + """an option argument should not take another option argument""" + output = self._test( + bash, + "(a -o -o b -o -o c)", + 6, + "a -o -o b -o -o c", + 16, + arg='-a "-o"', + ) + assert output == "2" + + def test_15_double_hyphen_optarg(self, bash): + """-- should lose its meaning when it is an option argument""" + output = self._test( + bash, "(a -o -- -b -c d)", 5, "a -o -- -b -c d", 14, arg='-a "-o"' + ) + assert output == "1" + + def test_16_empty_word(self, bash): + """An empty word should not take an option argument""" + output = self._test(bash, "(a '' x '' y d)", 5, "a x y d", 8) + assert output == "5" diff --git a/test/t/unit/test_unit_delimited.py b/test/t/unit/test_unit_delimited.py new file mode 100644 index 0000000..e20dcd1 --- /dev/null +++ b/test/t/unit/test_unit_delimited.py @@ -0,0 +1,42 @@ +import pytest + +from conftest import assert_bash_exec + + +@pytest.mark.bashcomp(cmd=None) +class TestUnitDelimited: + @pytest.fixture(scope="class") + def functions(self, request, bash): + assert_bash_exec( + bash, + "_comp_cmd_test_delim() {" + " local cur prev words cword comp_args;" + " _comp_get_words cur;" + " _comp_delimited , -W 'alpha beta bravo';" + "};" + "complete -F _comp_cmd_test_delim test_delim", + ) + + @pytest.mark.complete("test_delim --opt=a") + def test_1(self, functions, completion): + assert completion == ["lpha"] + + @pytest.mark.complete("test_delim --opt=b") + def test_2(self, functions, completion): + assert completion == ["beta", "bravo"] + + @pytest.mark.complete("test_delim --opt=alpha,b") + def test_3(self, functions, completion): + assert completion == ["alpha,beta", "alpha,bravo"] + + @pytest.mark.complete("test_delim --opt=alpha,be") + def test_4(self, functions, completion): + assert completion == ["ta"] + + @pytest.mark.complete("test_delim --opt=beta,a") + def test_5(self, functions, completion): + assert completion == ["lpha"] + + @pytest.mark.complete("test_delim --opt=c") + def test_6(self, functions, completion): + assert not completion diff --git a/test/t/unit/test_unit_deprecate_func.py b/test/t/unit/test_unit_deprecate_func.py new file mode 100644 index 0000000..c825e8c --- /dev/null +++ b/test/t/unit/test_unit_deprecate_func.py @@ -0,0 +1,15 @@ +import pytest + +from conftest import assert_bash_exec + + +@pytest.mark.bashcomp(cmd=None, ignore_env=r"^\+declare -f func[12]$") +class TestUnitDeprecateFunc: + def test_1(self, bash): + assert_bash_exec( + bash, + 'func1() { echo "func1($*)"; }; ' + "_comp_deprecate_func 2.12 func2 func1", + ) + output = assert_bash_exec(bash, "func2 1 2 3", want_output=True) + assert output.strip() == "func1(1 2 3)" diff --git a/test/t/unit/test_unit_dequote.py b/test/t/unit/test_unit_dequote.py new file mode 100644 index 0000000..0ac814d --- /dev/null +++ b/test/t/unit/test_unit_dequote.py @@ -0,0 +1,161 @@ +import pytest + +from conftest import assert_bash_exec, bash_env_saved + + +@pytest.mark.bashcomp( + cmd=None, + cwd="_filedir", + ignore_env=r"^\+declare -f __tester$", +) +class TestDequote: + def test_1_char(self, bash): + assert_bash_exec( + bash, + '__tester() { local REPLY=dummy v=var;_comp_dequote "$1";local ext=$?;((${#REPLY[@]}))&&printf \'<%s>\' "${REPLY[@]}";echo;return $ext;}', + ) + output = assert_bash_exec(bash, "__tester a", want_output=True) + assert output.strip() == "<a>" + + def test_2_str(self, bash): + output = assert_bash_exec(bash, "__tester abc", want_output=True) + assert output.strip() == "<abc>" + + def test_3_null(self, bash): + output = assert_bash_exec(bash, "__tester ''", want_output=True) + assert output.strip() == "" + + def test_4_empty(self, bash): + output = assert_bash_exec(bash, "__tester \"''\"", want_output=True) + assert output.strip() == "<>" + + def test_5_brace(self, bash): + output = assert_bash_exec(bash, "__tester 'a{1..3}'", want_output=True) + assert output.strip() == "<a1><a2><a3>" + + def test_6_glob(self, bash): + output = assert_bash_exec(bash, "__tester 'a?b'", want_output=True) + assert output.strip() == "<a b><a$b><a&b><a'b>" + + def test_7_quote_1(self, bash): + output = assert_bash_exec( + bash, "__tester '\"a\"'\\'b\\'\\$\\'c\\'", want_output=True + ) + assert output.strip() == "<abc>" + + def test_7_quote_2(self, bash): + output = assert_bash_exec( + bash, "__tester '\\\"\\'\\''\\$\\`'", want_output=True + ) + assert output.strip() == "<\"'$`>" + + def test_7_quote_3(self, bash): + output = assert_bash_exec( + bash, "__tester \\$\\'a\\\\tb\\'", want_output=True + ) + assert output.strip() == "<a\tb>" + + def test_7_quote_4(self, bash): + output = assert_bash_exec( + bash, '__tester \'"abc\\"def"\'', want_output=True + ) + assert output.strip() == '<abc"def>' + + def test_7_quote_5(self, bash): + output = assert_bash_exec( + bash, "__tester \\'abc\\'\\\\\\'\\'def\\'", want_output=True + ) + assert output.strip() == "<abc'def>" + + def test_8_param_1(self, bash): + output = assert_bash_exec(bash, "__tester '$v'", want_output=True) + assert output.strip() == "<var>" + + def test_8_param_2(self, bash): + output = assert_bash_exec(bash, "__tester '${v}'", want_output=True) + assert output.strip() == "<var>" + + def test_8_param_3(self, bash): + output = assert_bash_exec(bash, "__tester '${#v}'", want_output=True) + assert output.strip() == "<3>" + + def test_8_param_4(self, bash): + output = assert_bash_exec(bash, "__tester '${v[0]}'", want_output=True) + assert output.strip() == "<var>" + + def test_9_qparam_1(self, bash): + output = assert_bash_exec(bash, "__tester '\"$v\"'", want_output=True) + assert output.strip() == "<var>" + + def test_9_qparam_2(self, bash): + output = assert_bash_exec( + bash, "__tester '\"${v[@]}\"'", want_output=True + ) + assert output.strip() == "<var>" + + def test_10_pparam_1(self, bash): + output = assert_bash_exec(bash, "__tester '$?'", want_output=True) + assert output.strip() == "<0>" + + def test_10_pparam_2(self, bash): + output = assert_bash_exec(bash, "__tester '${#1}'", want_output=True) + assert output.strip() == "<5>" # The string `${#1}` is five characters + + def test_unsafe_1(self, bash): + output = assert_bash_exec( + bash, "! __tester '$(echo hello >&2)'", want_output=True + ) + assert output.strip() == "" + + def test_unsafe_2(self, bash): + output = assert_bash_exec( + bash, "! __tester '|echo hello >&2'", want_output=True + ) + assert output.strip() == "" + + def test_unsafe_3(self, bash): + output = assert_bash_exec( + bash, "! __tester '>| important_file.txt'", want_output=True + ) + assert output.strip() == "" + + def test_unsafe_4(self, bash): + output = assert_bash_exec( + bash, "! __tester '`echo hello >&2`'", want_output=True + ) + assert output.strip() == "" + + def test_glob_default(self, bash): + with bash_env_saved(bash) as bash_env: + bash_env.shopt("failglob", False) + bash_env.shopt("nullglob", False) + output = assert_bash_exec( + bash, "__tester 'non-existent-*.txt'", want_output=True + ) + assert output.strip() == "<non-existent-*.txt>" + + def test_glob_noglob(self, bash): + with bash_env_saved(bash) as bash_env: + bash_env.set("noglob", True) + output = assert_bash_exec( + bash, + "__tester 'non-existent-*.txt'", + want_output=True, + ) + assert output.strip() == "<non-existent-*.txt>" + + def test_glob_failglob(self, bash): + with bash_env_saved(bash) as bash_env: + bash_env.shopt("failglob", True) + output = assert_bash_exec( + bash, "! __tester 'non-existent-*.txt'", want_output=True + ) + assert output.strip() == "" + + def test_glob_nullglob(self, bash): + with bash_env_saved(bash) as bash_env: + bash_env.shopt("nullglob", True) + output = assert_bash_exec( + bash, "__tester 'non-existent-*.txt'", want_output=True + ) + assert output.strip() == "" diff --git a/test/t/unit/test_unit_expand.py b/test/t/unit/test_unit_expand.py index d2a3ebc..0be6c52 100644 --- a/test/t/unit/test_unit_expand.py +++ b/test/t/unit/test_unit_expand.py @@ -1,31 +1,42 @@ import pytest -from conftest import assert_bash_exec +from conftest import assert_bash_exec, bash_env_saved @pytest.mark.bashcomp(cmd=None, ignore_env=r"^[+-](cur|COMPREPLY)=") class TestUnitExpand: def test_1(self, bash): - assert_bash_exec(bash, "_expand >/dev/null") + assert_bash_exec(bash, "_comp_expand >/dev/null") def test_2(self, bash): """Test environment non-pollution, detected at teardown.""" - assert_bash_exec(bash, "foo() { _expand; }; foo; unset foo") + assert_bash_exec(bash, "foo() { _comp_expand; }; foo; unset -f foo") def test_user_home_compreply(self, bash, user_home): user, home = user_home output = assert_bash_exec( bash, - r'cur="~%s"; _expand; printf "%%s\n" "$COMPREPLY"' % user, + r'cur="~%s"; _comp_expand; printf "%%s\n" "$COMPREPLY"' % user, want_output=True, ) assert output.strip() == home + def test_user_home_compreply_failglob(self, bash, user_home): + user, home = user_home + with bash_env_saved(bash) as bash_env: + bash_env.shopt("failglob", True) + output = assert_bash_exec( + bash, + r'cur="~%s"; _comp_expand; printf "%%s\n" "$COMPREPLY"' % user, + want_output=True, + ) + assert output.strip() == home + def test_user_home_cur(self, bash, user_home): user, home = user_home output = assert_bash_exec( bash, - r'cur="~%s/a"; _expand; printf "%%s\n" "$cur"' % user, + r'cur="~%s/a"; _comp_expand; printf "%%s\n" "$cur"' % user, want_output=True, ) assert output.strip() == "%s/a" % home diff --git a/test/t/unit/test_unit_expand_glob.py b/test/t/unit/test_unit_expand_glob.py new file mode 100644 index 0000000..64d04a7 --- /dev/null +++ b/test/t/unit/test_unit_expand_glob.py @@ -0,0 +1,83 @@ +import pytest + +from conftest import assert_bash_exec, bash_env_saved + + +@pytest.mark.bashcomp( + cmd=None, + cwd="_filedir", + ignore_env=r"^\+declare -f (dump_array|__tester)$", +) +class TestExpandGlob: + @pytest.fixture(scope="class") + def functions(self, bash): + assert_bash_exec( + bash, + "dump_array() { ((${#arr[@]})) && printf '<%s>' \"${arr[@]}\"; echo; }", + ) + assert_bash_exec( + bash, + '__tester() { local LC_ALL= LC_COLLATE=C arr; _comp_expand_glob arr "$@";dump_array; }', + ) + + def test_match_all(self, bash, functions): + output = assert_bash_exec(bash, "__tester '*'", want_output=True) + assert output.strip() == "<a b><a$b><a&b><a'b><ab><aé><brackets><ext>" + + def test_match_pattern(self, bash, functions): + output = assert_bash_exec(bash, "__tester 'a*'", want_output=True) + assert output.strip() == "<a b><a$b><a&b><a'b><ab><aé>" + + def test_match_unmatched(self, bash, functions): + output = assert_bash_exec( + bash, "__tester 'unmatched-*'", want_output=True + ) + assert output.strip() == "" + + def test_match_multiple_words(self, bash, functions): + output = assert_bash_exec(bash, "__tester 'b* e*'", want_output=True) + assert output.strip() == "<brackets><ext>" + + def test_match_brace_expansion(self, bash, functions): + output = assert_bash_exec( + bash, "__tester 'brac{ket,unmatched}*'", want_output=True + ) + assert output.strip() == "<brackets>" + + def test_protect_from_noglob(self, bash, functions): + with bash_env_saved(bash, functions) as bash_env: + bash_env.set("noglob", True) + output = assert_bash_exec(bash, "__tester 'a*'", want_output=True) + assert output.strip() == "<a b><a$b><a&b><a'b><ab><aé>" + + def test_protect_from_failglob(self, bash, functions): + with bash_env_saved(bash) as bash_env: + bash_env.shopt("failglob", True) + output = assert_bash_exec( + bash, "__tester 'unmatched-*'", want_output=True + ) + assert output.strip() == "" + + def test_protect_from_nullglob(self, bash, functions): + with bash_env_saved(bash) as bash_env: + bash_env.shopt("nullglob", False) + output = assert_bash_exec( + bash, "__tester 'unmatched-*'", want_output=True + ) + assert output.strip() == "" + + def test_protect_from_dotglob(self, bash, functions): + with bash_env_saved(bash) as bash_env: + bash_env.shopt("dotglob", True) + output = assert_bash_exec( + bash, "__tester 'ext/foo/*'", want_output=True + ) + assert output.strip() == "" + + def test_protect_from_GLOBIGNORE(self, bash, functions): + with bash_env_saved(bash) as bash_env: + # Note: dotglob is changed by GLOBIGNORE + bash_env.save_shopt("dotglob") + bash_env.write_variable("GLOBIGNORE", "*") + output = assert_bash_exec(bash, "__tester 'a*'", want_output=True) + assert output.strip() == "<a b><a$b><a&b><a'b><ab><aé>" diff --git a/test/t/unit/test_unit_expand_tilde_by_ref.py b/test/t/unit/test_unit_expand_tilde.py index 17bdedf..3552fd6 100644 --- a/test/t/unit/test_unit_expand_tilde_by_ref.py +++ b/test/t/unit/test_unit_expand_tilde.py @@ -3,16 +3,27 @@ import pytest from conftest import assert_bash_exec -@pytest.mark.bashcomp(cmd=None, ignore_env=r"^[+-]var=") -class TestUnitExpandTildeByRef: +@pytest.mark.bashcomp(cmd=None) +class TestUnitExpandTilde: def test_1(self, bash): + """The old interface `__expand_tilde_by_ref` should not fail when it is + called without arguments""" assert_bash_exec(bash, "__expand_tilde_by_ref >/dev/null") def test_2(self, bash): """Test environment non-pollution, detected at teardown.""" assert_bash_exec( bash, - '_x() { local aa="~"; __expand_tilde_by_ref aa; }; _x; unset _x', + '_x() { local REPLY; _comp_expand_tilde "~"; }; _x; unset -f _x', + ) + + @pytest.fixture(scope="class") + def functions(self, bash): + # $HOME tinkering: protect against $HOME != ~user; our "home" is the + # latter but plain_tilde follows $HOME + assert_bash_exec( + bash, + '_comp__test_unit() { local REPLY HOME=$1; _comp_expand_tilde "$2"; printf "%s\\n" "$REPLY"; }', ) @pytest.mark.parametrize("plain_tilde", (True, False)) @@ -28,19 +39,21 @@ class TestUnitExpandTildeByRef: ("/a;echo hello", True), ), ) - def test_expand(self, bash, user_home, plain_tilde, suffix_expanded): + def test_expand( + self, bash, user_home, plain_tilde, suffix_expanded, functions + ): user, home = user_home suffix, expanded = suffix_expanded + home2 = home if plain_tilde: user = "" if not suffix or not expanded: - home = "~" + home2 = "~" elif not expanded: - home = "~%s" % user + home2 = "~%s" % user output = assert_bash_exec( bash, - r'var="~%s%s"; __expand_tilde_by_ref var; printf "%%s\n" "$var"' - % (user, suffix), + r'_comp__test_unit "%s" "~%s%s"' % (home, user, suffix), want_output=True, ) - assert output.strip() == "%s%s" % (home, suffix.replace(r"\$", "$"),) + assert output.strip() == "%s%s" % (home2, suffix.replace(r"\$", "$")) diff --git a/test/t/unit/test_unit_filedir.py b/test/t/unit/test_unit_filedir.py index b847efc..07ae2f3 100644 --- a/test/t/unit/test_unit_filedir.py +++ b/test/t/unit/test_unit_filedir.py @@ -15,18 +15,18 @@ class TestUnitFiledir: def functions(self, request, bash): assert_bash_exec( bash, - "_f() { local cur=$(_get_cword); unset COMPREPLY; _filedir; }; " + "_f() { local cur;_comp_get_words cur; unset -v COMPREPLY; _comp_compgen_filedir; }; " "complete -F _f f; " "complete -F _f -o filenames f2", ) assert_bash_exec( bash, - "_g() { local cur=$(_get_cword); unset COMPREPLY; _filedir e1; }; " + "_g() { local cur;_comp_get_words cur; unset -v COMPREPLY; _comp_compgen_filedir e1; }; " "complete -F _g g", ) assert_bash_exec( bash, - "_fd() { local cur=$(_get_cword); unset COMPREPLY; _filedir -d; };" + "_fd() { local cur;_comp_get_words cur; unset -v COMPREPLY; _comp_compgen_filedir -d; };" "complete -F _fd fd", ) @@ -59,7 +59,7 @@ class TestUnitFiledir: return lc_ctype def test_1(self, bash): - assert_bash_exec(bash, "_filedir >/dev/null") + assert_bash_exec(bash, "_comp_compgen_filedir >/dev/null") @pytest.mark.parametrize("funcname", "f f2".split()) def test_2(self, bash, functions, funcname): @@ -196,7 +196,7 @@ class TestUnitFiledir: @pytest.mark.parametrize("funcname", "f f2".split()) def test_22(self, bash, functions, funcname, non_windows_testdir): completion = assert_complete( - bash, r"%s '%s/a\b/" % (funcname, non_windows_testdir) + bash, rf"{funcname} '{non_windows_testdir}/a\b/" ) assert completion == "g'" diff --git a/test/t/unit/test_unit_get_cword.py b/test/t/unit/test_unit_get_cword.py index 0b56d16..d2bb526 100644 --- a/test/t/unit/test_unit_get_cword.py +++ b/test/t/unit/test_unit_get_cword.py @@ -1,11 +1,12 @@ -import pexpect +import pexpect # type: ignore[import] import pytest from conftest import PS1, TestUnitBase, assert_bash_exec @pytest.mark.bashcomp( - cmd=None, ignore_env=r"^[+-](COMP_(WORDS|CWORD|LINE|POINT)|_scp_path_esc)=" + cmd=None, + ignore_env=r"^[+-](COMP_(WORDS|CWORD|LINE|POINT)|_comp_cmd_scp__path_esc)=", ) class TestUnitGetCword(TestUnitBase): def _test(self, *args, **kwargs): @@ -49,12 +50,12 @@ class TestUnitGetCword(TestUnitBase): assert output == r"b\ c" def test_8(self, bash): - r"""a b\| c should return b\ """ + r"""a b\| c should return b\ """ # fmt: skip output = self._test(bash, r"(a 'b\ c')", 1, r"a b\ c", 4) assert output == "b\\" def test_9(self, bash): - r"""a "b\| should return "b\ """ + r"""a "b\| should return "b\ """ # fmt: skip output = self._test(bash, "(a '\"b\\')", 1, r"a \"b\\", 5) assert output == '"b\\' @@ -103,7 +104,7 @@ class TestUnitGetCword(TestUnitBase): a -n| should return -n This test makes sure `_get_cword' doesn't use `echo' to return its - value, because -n might be interpreted by `echo' and thus woud not + value, because -n might be interpreted by `echo' and thus would not be returned. """ output = self._test(bash, "(a -n)", 1, "a -n", 4) @@ -152,3 +153,12 @@ class TestUnitGetCword(TestUnitBase): ] ) assert got == 1 + + def test_25(self, bash): + """ + a b c:| with trailing whitespace after the caret (no more words) and + with WORDBREAKS -= : should return c: + """ + assert_bash_exec(bash, "add_comp_wordbreak_char :") + output = self._test(bash, "(a b c :)", 3, "a b c: ", 6, arg=":") + assert output == "c:" diff --git a/test/t/unit/test_unit_get_first_arg.py b/test/t/unit/test_unit_get_first_arg.py new file mode 100644 index 0000000..415e217 --- /dev/null +++ b/test/t/unit/test_unit_get_first_arg.py @@ -0,0 +1,90 @@ +import pytest + +from conftest import assert_bash_exec + + +@pytest.mark.bashcomp(cmd=None) +class TestUnitGetFirstArg: + @pytest.fixture(scope="class") + def functions(self, bash): + assert_bash_exec( + bash, + '_comp__test_unit() { local -a "words=$1"; local cword=$2 REPLY=; shift 2; _comp_get_first_arg "$@" && printf "%s\\n" "$REPLY"; return 0; }', + ) + + def _test(self, bash, words, cword, args=""): + return assert_bash_exec( + bash, + '_comp__test_unit "%s" %d %s' % (words, cword, args), + want_output=None, + ).strip() + + def test_1(self, bash, functions): + assert_bash_exec(bash, "_comp__test_unit '()' 0") + + def test_2(self, bash, functions): + output = self._test(bash, "(a b)", 2) + assert output == "b" + + def test_3(self, bash, functions): + output = self._test(bash, "(a bc)", 2) + assert output == "bc" + + def test_4(self, bash, functions): + output = self._test(bash, "(a b c)", 2) + assert output == "b" + + def test_5(self, bash, functions): + """Neither of the current word and the command name should be picked + as the first argument""" + output = self._test(bash, "(a b c)", 1) + assert output == "" + + def test_6(self, bash, functions): + """Options starting with - should not be picked as the first + argument""" + output = self._test(bash, "(a -b -c d e)", 4) + assert output == "d" + + def test_7_single_hyphen(self, bash, functions): + """- should be counted as an argument representing stdout/stdin""" + output = self._test(bash, "(a -b - c -d e)", 5) + assert output == "-" + + def test_8_double_hyphen_1(self, bash, functions): + """any word after -- should be picked""" + output = self._test(bash, "(a -b -- -c -d e)", 5) + assert output == "-c" + + def test_8_double_hyphen_2(self, bash, functions): + """any word after -- should be picked only without any preceding argument""" + output = self._test(bash, "(a b -- -c -d e)", 5) + assert output == "b" + + def test_9_skip_optarg_1(self, bash, functions): + output = self._test(bash, "(a -b -c d e f)", 5, '-a "@(-c|--foo)"') + assert output == "e" + + def test_9_skip_optarg_2(self, bash, functions): + output = self._test(bash, "(a -b --foo d e f)", 5, '-a "@(-c|--foo)"') + assert output == "e" + + def test_9_skip_optarg_3(self, bash): + output = self._test(bash, "(a -b - c d e)", 5, '-a "-b"') + assert output == "c" + + def test_9_skip_optarg_4(self, bash): + output = self._test(bash, "(a -b -c d e f)", 5, '-a "-[bc]"') + assert output == "d" + + def test_9_skip_optarg_5(self, bash): + output = self._test(bash, "(a +o b c d)", 4, '-a "+o"') + assert output == "c" + + def test_9_skip_optarg_6(self, bash): + output = self._test(bash, "(a -o -o -o -o b c)", 6, '-a "-o"') + assert output == "b" + + def test_9_skip_optarg_7(self, bash): + output = self._test(bash, "(a -o -- -b -c d e)", 6, '-a "-o"') + assert output == "d" diff --git a/test/t/unit/test_unit_get_comp_words_by_ref.py b/test/t/unit/test_unit_get_words.py index b6498fa..63c4034 100644 --- a/test/t/unit/test_unit_get_comp_words_by_ref.py +++ b/test/t/unit/test_unit_get_words.py @@ -11,10 +11,10 @@ class TestUnitGetCompWordsByRef(TestUnitBase): def _test(self, bash, *args, **kwargs): assert_bash_exec(bash, "unset cur prev") output = self._test_unit( - "_get_comp_words_by_ref %s cur prev; echo $cur,${prev-}", + "_comp_get_words %s cur prev; echo $cur,${prev-}", bash, *args, - **kwargs + **kwargs, ) return output.strip() @@ -22,7 +22,7 @@ class TestUnitGetCompWordsByRef(TestUnitBase): assert_bash_exec( bash, "COMP_WORDS=() COMP_CWORD= COMP_POINT= COMP_LINE= " - "_get_comp_words_by_ref cur >/dev/null", + "_comp_get_words cur >/dev/null", ) def test_2(self, bash): @@ -41,12 +41,12 @@ class TestUnitGetCompWordsByRef(TestUnitBase): assert output == "," def test_5(self, bash): - """|a """ + """|a """ # fmt: skip output = self._test(bash, "(a)", 0, "a ", 0) assert output == "," def test_6(self, bash): - """ | a """ + """ | a """ # fmt: skip output = self._test(bash, "(a)", 0, " a ", 1) assert output.strip() == "," @@ -134,9 +134,9 @@ class TestUnitGetCompWordsByRef(TestUnitBase): def test_23(self, bash): """a -n| - This test makes sure `_get_cword' doesn't use `echo' to return its - value, because -n might be interpreted by `echo' and thus woud not - be returned. + This test makes sure `_comp_get_words' doesn't use `echo' to + return its value, because -n might be interpreted by `echo' + and thus would not be returned. """ output = self._test(bash, "(a -n)", 1, "a -n", 4) assert output == "-n,a" @@ -175,7 +175,7 @@ class TestUnitGetCompWordsByRef(TestUnitBase): """a b| to all vars""" assert_bash_exec(bash, "unset words cword cur prev") output = self._test_unit( - "_get_comp_words_by_ref words cword cur prev%s; " + "_comp_get_words words cword cur prev%s; " 'echo "${words[@]}",$cword,$cur,$prev', bash, "(a b)", @@ -189,7 +189,7 @@ class TestUnitGetCompWordsByRef(TestUnitBase): """a b| to alternate vars""" assert_bash_exec(bash, "unset words2 cword2 cur2 prev2") output = self._test_unit( - "_get_comp_words_by_ref -w words2 -i cword2 -c cur2 -p prev2%s; " + "_comp_get_words -w words2 -i cword2 -c cur2 -p prev2%s; " 'echo $cur2,$prev2,"${words2[@]}",$cword2', bash, "(a b)", @@ -204,7 +204,7 @@ class TestUnitGetCompWordsByRef(TestUnitBase): """a b : c| with wordbreaks -= :""" assert_bash_exec(bash, "unset words") output = self._test_unit( - '_get_comp_words_by_ref -n : words%s; echo "${words[@]}"', + '_comp_get_words -n : words%s; echo "${words[@]}"', bash, "(a b : c)", 3, @@ -217,7 +217,7 @@ class TestUnitGetCompWordsByRef(TestUnitBase): """a b: c| with wordbreaks -= :""" assert_bash_exec(bash, "unset words") output = self._test_unit( - '_get_comp_words_by_ref -n : words%s; echo "${words[@]}"', + '_comp_get_words -n : words%s; echo "${words[@]}"', bash, "(a b : c)", 3, @@ -230,7 +230,7 @@ class TestUnitGetCompWordsByRef(TestUnitBase): """a b :c| with wordbreaks -= :""" assert_bash_exec(bash, "unset words") output = self._test_unit( - '_get_comp_words_by_ref -n : words%s; echo "${words[@]}"', + '_comp_get_words -n : words%s; echo "${words[@]}"', bash, "(a b : c)", 3, @@ -243,7 +243,7 @@ class TestUnitGetCompWordsByRef(TestUnitBase): r"""a b\ :c| with wordbreaks -= :""" assert_bash_exec(bash, "unset words") output = self._test_unit( - '_get_comp_words_by_ref -n : words%s; echo "${words[@]}"', + '_comp_get_words -n : words%s; echo "${words[@]}"', bash, "(a 'b ' : c)", 3, @@ -255,6 +255,6 @@ class TestUnitGetCompWordsByRef(TestUnitBase): def test_unknown_arg_error(self, bash): with pytest.raises(AssertionError) as ex: _ = assert_bash_exec( - bash, "_get_comp_words_by_ref dummy", want_output=True + bash, "_comp_get_words dummy", want_output=True ) ex.match("dummy.* unknown argument") diff --git a/test/t/unit/test_unit_init_completion.py b/test/t/unit/test_unit_init_completion.py deleted file mode 100644 index 64a5a79..0000000 --- a/test/t/unit/test_unit_init_completion.py +++ /dev/null @@ -1,34 +0,0 @@ -import pytest - -from conftest import TestUnitBase, assert_bash_exec, assert_complete - - -@pytest.mark.bashcomp( - cmd=None, - ignore_env=r"^[+-](COMP(_(WORDS|CWORD|LINE|POINT)|REPLY)|" - r"cur|cword|words)=", -) -class TestUnitInitCompletion(TestUnitBase): - def test_1(self, bash): - """Test environment non-pollution, detected at teardown.""" - assert_bash_exec( - bash, - "foo() { " - "local cur prev words cword " - "COMP_WORDS=() COMP_CWORD=0 COMP_LINE= COMP_POINT=0; " - "_init_completion; }; " - "foo; unset foo", - ) - - def test_2(self, bash): - output = self._test_unit( - "_init_completion %s; echo $cur,${prev-}", bash, "(a)", 0, "a", 0 - ) - assert output == "," - - @pytest.mark.parametrize("redirect", "> >> 2> < &>".split()) - def test_redirect(self, bash, redirect): - completion = assert_complete( - bash, "%s " % redirect, cwd="shared/default" - ) - assert all(x in completion for x in "foo bar".split()) diff --git a/test/t/unit/test_unit_initialize.py b/test/t/unit/test_unit_initialize.py new file mode 100644 index 0000000..63fddee --- /dev/null +++ b/test/t/unit/test_unit_initialize.py @@ -0,0 +1,66 @@ +import pytest + +from conftest import TestUnitBase, assert_bash_exec, assert_complete + + +@pytest.mark.bashcomp( + cmd=None, + ignore_env=r"^[+-](COMP(_(WORDS|CWORD|LINE|POINT)|REPLY)|" + r"cur|prev|cword|words)=|^\+declare -f _cmd1$", +) +class TestUnitInitCompletion(TestUnitBase): + def test_1(self, bash): + """Test environment non-pollution, detected at teardown.""" + assert_bash_exec( + bash, + "foo() { " + "local cur prev words cword comp_args " + "COMP_WORDS=() COMP_CWORD=0 COMP_LINE= COMP_POINT=0; " + "_comp_initialize; }; " + "foo; unset -f foo", + ) + + def test_2(self, bash): + output = self._test_unit( + "_comp_initialize %s; echo $cur,${prev-}", bash, "(a)", 0, "a", 0 + ) + assert output == "," + + @pytest.mark.parametrize("redirect", "> >> 2> < &>".split()) + def test_redirect(self, bash, redirect): + completion = assert_complete( + bash, "%s " % redirect, cwd="shared/default" + ) + assert all(x in completion for x in "foo bar".split()) + + @pytest.fixture(scope="class") + def cmd1_empty_completion_setup(self, bash): + assert_bash_exec( + bash, + '_cmd1() { local cur prev words cword comp_args; _comp_initialize -- "$@"; } && ' + "complete -F _cmd1 cmd1", + ) + + @pytest.mark.parametrize("redirect", "> >> 2> {fd1}> < &> &>> >|".split()) + def test_redirect_2(self, bash, cmd1_empty_completion_setup, redirect): + # Note: Bash 4.3 and below cannot properly extract the redirection ">|" + if redirect == ">|": + skipif = "((BASH_VERSINFO[0] * 100 + BASH_VERSINFO[1] < 404))" + try: + assert_bash_exec(bash, skipif, want_output=None) + except AssertionError: + pass + else: + pytest.skip(skipif) + + completion = assert_complete( + bash, "cmd1 %s f" % redirect, cwd="shared/default" + ) + assert "foo" in completion + + @pytest.mark.parametrize("redirect", "> >> 2> < &>".split()) + def test_redirect_3(self, bash, redirect): + completion = assert_complete( + bash, "cmd1 %sf" % redirect, cwd="shared/default" + ) + assert "foo" in completion diff --git a/test/t/unit/test_unit_ip_addresses.py b/test/t/unit/test_unit_ip_addresses.py index 8120c88..37f3b0e 100644 --- a/test/t/unit/test_unit_ip_addresses.py +++ b/test/t/unit/test_unit_ip_addresses.py @@ -9,41 +9,41 @@ class TestUnitIpAddresses: def functions(self, request, bash): assert_bash_exec( bash, - "_ia() { local cur=$(_get_cword);unset COMPREPLY;" - "_ip_addresses; }", + "_ia() { local cur;_comp_get_words cur;" + "unset -v COMPREPLY;_comp_compgen_ip_addresses; }", ) assert_bash_exec(bash, "complete -F _ia ia") assert_bash_exec( bash, - "_iaa() { local cur=$(_get_cword);unset COMPREPLY;" - "_ip_addresses -a; }", + "_iaa() { local cur;_comp_get_words cur;" + "unset -v COMPREPLY;_comp_compgen_ip_addresses -a; }", ) assert_bash_exec(bash, "complete -F _iaa iaa") assert_bash_exec( bash, - " _ia6() { local cur=$(_get_cword);unset COMPREPLY;" - "_ip_addresses -6; }", + " _ia6() { local cur;_comp_get_words cur;" + "unset -v COMPREPLY;_comp_compgen_ip_addresses -6; }", ) assert_bash_exec(bash, "complete -F _ia6 ia6") def test_1(self, bash): - assert_bash_exec(bash, "_ip_addresses") + assert_bash_exec(bash, "_comp_compgen_ip_addresses") @pytest.mark.complete("iaa ") def test_2(self, functions, completion): - """_ip_addresses -a should complete ip addresses.""" + """_comp_compgen_ip_addresses -a should complete ip addresses.""" assert completion assert all("." in x or ":" in x for x in completion) @pytest.mark.complete("ia ") def test_3(self, functions, completion): - """_ip_addresses should complete ipv4 addresses.""" + """_comp_compgen_ip_addresses should complete ipv4 addresses.""" assert completion assert all("." in x for x in completion) @pytest.mark.xfail(in_container(), reason="Probably fails in a container") @pytest.mark.complete("ia6 ") def test_4(self, functions, completion): - """_ip_addresses -6 should complete ipv6 addresses.""" + """_comp_compgen_ip_addresses -6 should complete ipv6 addresses.""" assert completion assert all(":" in x for x in completion) diff --git a/test/t/unit/test_unit_known_hosts_real.py b/test/t/unit/test_unit_known_hosts.py index ac5205e..b0f715b 100644 --- a/test/t/unit/test_unit_known_hosts_real.py +++ b/test/t/unit/test_unit_known_hosts.py @@ -2,14 +2,14 @@ from itertools import chain import pytest -from conftest import assert_bash_exec +from conftest import assert_bash_exec, bash_env_saved @pytest.mark.bashcomp( cmd=None, - ignore_env="^[+-](COMP(REPLY|_KNOWN_HOSTS_WITH_HOSTFILE)|OLDHOME)=", + ignore_env="^[+-](COMPREPLY|BASH_COMPLETION_KNOWN_HOSTS_WITH_HOSTFILE)=", ) -class TestUnitKnownHostsReal: +class TestUnitCompgenKnownHosts: @pytest.mark.parametrize( "prefix,colon_flag,hostfile", [("", "", True), ("", "", False), ("user@", "c", True)], @@ -21,9 +21,9 @@ class TestUnitKnownHostsReal: "%s%s%s" % (prefix, x, ":" if colon_flag else "") for x in chain( hosts if hostfile else avahi_hosts, - # fixtures/_known_hosts_real/config + # fixtures/_known_hosts/config "gee hus jar #not-a-comment".split(), - # fixtures/_known_hosts_real/known_hosts + # fixtures/_known_hosts/known_hosts ( "doo", "ike", @@ -43,14 +43,14 @@ class TestUnitKnownHostsReal: ) assert_bash_exec( bash, - "unset -v COMP_KNOWN_HOSTS_WITH_HOSTFILE" + "unset -v BASH_COMPLETION_KNOWN_HOSTS_WITH_HOSTFILE" if hostfile - else "COMP_KNOWN_HOSTS_WITH_HOSTFILE=", + else "BASH_COMPLETION_KNOWN_HOSTS_WITH_HOSTFILE=", ) output = assert_bash_exec( bash, - "_known_hosts_real -a%sF _known_hosts_real/config '%s'; " - r'printf "%%s\n" "${COMPREPLY[@]}"; unset COMPREPLY' + "_comp_compgen_known_hosts -a%sF _known_hosts/config '%s'; " + r'printf "%%s\n" "${COMPREPLY[@]}"; unset -v COMPREPLY' % (colon_flag, prefix), want_output=True, ) @@ -66,12 +66,13 @@ class TestUnitKnownHostsReal: ) def test_ip_filtering(self, bash, family, result): assert_bash_exec( - bash, "unset -v COMPREPLY COMP_KNOWN_HOSTS_WITH_HOSTFILE" + bash, + "unset -v COMPREPLY BASH_COMPLETION_KNOWN_HOSTS_WITH_HOSTFILE", ) output = assert_bash_exec( bash, - "COMP_KNOWN_HOSTS_WITH_HOSTFILE= " - "_known_hosts_real -%sF _known_hosts_real/localhost_config ''; " + "BASH_COMPLETION_KNOWN_HOSTS_WITH_HOSTFILE= " + "_comp_compgen_known_hosts -%sF _known_hosts/localhost_config ''; " r'printf "%%s\n" "${COMPREPLY[@]}"' % family, want_output=True, ) @@ -79,17 +80,17 @@ class TestUnitKnownHostsReal: def test_consecutive_spaces(self, bash, hosts): expected = hosts.copy() - # fixtures/_known_hosts_real/spaced conf + # fixtures/_known_hosts/spaced conf expected.extend("gee hus #not-a-comment".split()) - # fixtures/_known_hosts_real/known_hosts2 + # fixtures/_known_hosts/known_hosts2 expected.extend("two two2 two3 two4".split()) # fixtures/_known_hosts_/spaced known_hosts expected.extend("doo ike".split()) output = assert_bash_exec( bash, - "unset -v COMPREPLY COMP_KNOWN_HOSTS_WITH_HOSTFILE; " - "_known_hosts_real -aF '_known_hosts_real/spaced conf' ''; " + "unset -v COMPREPLY BASH_COMPLETION_KNOWN_HOSTS_WITH_HOSTFILE; " + "_comp_compgen_known_hosts -aF '_known_hosts/spaced conf' ''; " r'printf "%s\n" "${COMPREPLY[@]}"', want_output=True, ) @@ -97,62 +98,58 @@ class TestUnitKnownHostsReal: def test_files_starting_with_tilde(self, bash, hosts): expected = hosts.copy() - # fixtures/_known_hosts_real/known_hosts2 + # fixtures/_known_hosts/known_hosts2 expected.extend("two two2 two3 two4".split()) - # fixtures/_known_hosts_real/known_hosts3 + # fixtures/_known_hosts/known_hosts3 expected.append("three") - # fixtures/_known_hosts_real/known_hosts4 + # fixtures/_known_hosts/known_hosts4 expected.append("four") - assert_bash_exec(bash, 'OLDHOME="$HOME"; HOME="%s"' % bash.cwd) - output = assert_bash_exec( - bash, - "unset -v COMPREPLY COMP_KNOWN_HOSTS_WITH_HOSTFILE; " - "_known_hosts_real -aF _known_hosts_real/config_tilde ''; " - 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", bash.cwd) + output = assert_bash_exec( + bash, + "unset -v COMPREPLY BASH_COMPLETION_KNOWN_HOSTS_WITH_HOSTFILE;" + " _comp_compgen_known_hosts -aF _known_hosts/config_tilde ''; " + r'printf "%s\n" "${COMPREPLY[@]}"', + want_output=True, + ) + assert sorted(set(output.strip().split())) == sorted(expected) def test_included_configs(self, bash, hosts): expected = hosts.copy() - # fixtures/_known_hosts_real/config_include_recursion + # fixtures/_known_hosts/config_include_recursion expected.append("recursion") - # fixtures/_known_hosts_real/.ssh/config_relative_path + # fixtures/_known_hosts/.ssh/config_relative_path expected.append("relative_path") - # fixtures/_known_hosts_real/.ssh/config_asterisk_* + # fixtures/_known_hosts/.ssh/config_asterisk_* expected.extend("asterisk_1 asterisk_2".split()) - # fixtures/_known_hosts_real/.ssh/config_question_mark + # fixtures/_known_hosts/.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" % bash.cwd) + output = assert_bash_exec( + bash, + "unset -v COMPREPLY BASH_COMPLETION_KNOWN_HOSTS_WITH_HOSTFILE;" + " _comp_compgen_known_hosts -aF _known_hosts/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" % bash.cwd) + bash_env.chdir("_known_hosts") + output = assert_bash_exec( + bash, + "unset -v COMPREPLY BASH_COMPLETION_KNOWN_HOSTS_WITH_HOSTFILE;" + " _comp_compgen_known_hosts -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_load_completion.py b/test/t/unit/test_unit_load_completion.py new file mode 100644 index 0000000..6272e5b --- /dev/null +++ b/test/t/unit/test_unit_load_completion.py @@ -0,0 +1,94 @@ +import pytest + +from conftest import assert_bash_exec, bash_env_saved + + +@pytest.mark.bashcomp(cmd=None, cwd="_comp_load") +class TestLoadCompletion: + def test_userdir_1(self, bash): + with bash_env_saved(bash) as bash_env: + bash_env.write_variable( + "BASH_COMPLETION_USER_DIR", + "$PWD/userdir1:$PWD/userdir2:$BASH_COMPLETION_USER_DIR", + quote=False, + ) + bash_env.write_variable( + "PATH", "$PWD/prefix1/bin:$PWD/prefix1/sbin", quote=False + ) + output = assert_bash_exec( + bash, "_comp_load cmd1", want_output=True + ) + assert output.strip() == "cmd1: sourced from userdir1" + output = assert_bash_exec( + bash, "_comp_load cmd2", want_output=True + ) + assert output.strip() == "cmd2: sourced from userdir2" + + def test_PATH_1(self, bash): + with bash_env_saved(bash) as bash_env: + bash_env.write_variable( + "PATH", "$PWD/prefix1/bin:$PWD/prefix1/sbin", quote=False + ) + output = assert_bash_exec( + bash, "_comp_load cmd1", want_output=True + ) + assert output.strip() == "cmd1: sourced from prefix1" + output = assert_bash_exec( + bash, "_comp_load cmd2", want_output=True + ) + assert output.strip() == "cmd2: sourced from prefix1" + output = assert_bash_exec( + bash, "complete -p cmd2", want_output=True + ) + assert " cmd2" in output + output = assert_bash_exec( + bash, 'complete -p "$PWD/prefix1/sbin/cmd2"', want_output=True + ) + assert "/prefix1/sbin/cmd2" in output + + def test_cmd_path_1(self, bash): + assert_bash_exec(bash, "complete -r cmd1 || :", want_output=None) + output = assert_bash_exec( + bash, "_comp_load prefix1/bin/cmd1", want_output=True + ) + assert output.strip() == "cmd1: sourced from prefix1" + output = assert_bash_exec( + bash, 'complete -p "$PWD/prefix1/bin/cmd1"', want_output=True + ) + assert "/prefix1/bin/cmd1" in output + assert_bash_exec(bash, "! complete -p cmd1", want_output=None) + output = assert_bash_exec( + bash, "_comp_load prefix1/sbin/cmd2", want_output=True + ) + assert output.strip() == "cmd2: sourced from prefix1" + output = assert_bash_exec( + bash, "_comp_load bin/cmd1", want_output=True + ) + assert output.strip() == "cmd1: sourced from prefix1" + output = assert_bash_exec( + bash, "_comp_load bin/cmd2", want_output=True + ) + assert output.strip() == "cmd2: sourced from prefix1" + + def test_cmd_path_2(self, bash): + with bash_env_saved(bash) as bash_env: + bash_env.write_variable("PATH", "$PWD/bin:$PATH", quote=False) + output = assert_bash_exec( + bash, "_comp_load cmd1", want_output=True + ) + assert output.strip() == "cmd1: sourced from prefix1" + output = assert_bash_exec( + bash, "_comp_load cmd2", want_output=True + ) + assert output.strip() == "cmd2: sourced from prefix1" + + def test_cmd_intree_precedence(self, bash): + """ + Test in-tree, i.e. completions/$cmd relative to the main script + has precedence over location derived from PATH. + """ + with bash_env_saved(bash) as bash_env: + bash_env.write_variable("PATH", "$PWD/prefix1/bin", quote=False) + # The in-tree `sh` completion should be loaded here, + # and cause no output, unlike our `$PWD/prefix1/bin/sh` canary. + assert_bash_exec(bash, "_comp_load sh", want_output=False) diff --git a/test/t/unit/test_unit_longopt.py b/test/t/unit/test_unit_longopt.py index c5488e3..ab823d4 100644 --- a/test/t/unit/test_unit_longopt.py +++ b/test/t/unit/test_unit_longopt.py @@ -10,9 +10,9 @@ class TestUnitLongopt: @pytest.fixture(scope="class") def functions(self, request, bash): assert_bash_exec(bash, "_grephelp() { cat _longopt/grep--help.txt; }") - assert_bash_exec(bash, "complete -F _longopt _grephelp") + assert_bash_exec(bash, "complete -F _comp_complete_longopt _grephelp") assert_bash_exec(bash, "_various() { cat _longopt/various.txt; }") - assert_bash_exec(bash, "complete -F _longopt _various") + assert_bash_exec(bash, "complete -F _comp_complete_longopt _various") @pytest.mark.complete("_grephelp --") def test_1(self, functions, completion): diff --git a/test/t/unit/test_unit_looks_like_path.py b/test/t/unit/test_unit_looks_like_path.py new file mode 100644 index 0000000..3e86b48 --- /dev/null +++ b/test/t/unit/test_unit_looks_like_path.py @@ -0,0 +1,33 @@ +import shlex + +import pytest + +from conftest import TestUnitBase, assert_bash_exec + + +@pytest.mark.bashcomp(cmd=None) +class TestUnitQuote(TestUnitBase): + @pytest.mark.parametrize( + "thing_looks_like", + ( + ("", False), + ("foo", False), + ("/foo", True), + ("foo/", True), + ("foo/bar", True), + (".", True), + ("../", True), + ("~", True), + ("~foo", True), + ), + ) + def test_1(self, bash, thing_looks_like): + thing, looks_like = thing_looks_like + output = assert_bash_exec( + bash, + f"_comp_looks_like_path {shlex.quote(thing)}; printf %s $?", + want_output=True, + want_newline=False, + ) + is_zero = output.strip() == "0" + assert (looks_like and is_zero) or (not looks_like and not is_zero) diff --git a/test/t/unit/test_unit_parse_help.py b/test/t/unit/test_unit_parse_help.py index 4a02155..1a46f3f 100644 --- a/test/t/unit/test_unit_parse_help.py +++ b/test/t/unit/test_unit_parse_help.py @@ -2,29 +2,29 @@ import pytest -from conftest import assert_bash_exec +from conftest import assert_bash_exec, bash_env_saved @pytest.mark.bashcomp(cmd=None, ignore_env=r"^\+declare -f fn$") class TestUnitParseHelp: def test_1(self, bash): assert_bash_exec(bash, "fn() { echo; }") - output = assert_bash_exec(bash, "_parse_help fn") + output = assert_bash_exec(bash, "_parse_help fn; (($? == 1))") assert not output def test_2(self, bash): assert_bash_exec(bash, "fn() { echo 'no dashes here'; }") - output = assert_bash_exec(bash, "_parse_help fn") + output = assert_bash_exec(bash, "_parse_help fn; (($? == 1))") assert not output def test_3(self, bash): assert_bash_exec(bash, "fn() { echo 'internal-dash'; }") - output = assert_bash_exec(bash, "_parse_help fn") + output = assert_bash_exec(bash, "_parse_help fn; (($? == 1))") assert not output def test_4(self, bash): assert_bash_exec(bash, "fn() { echo 'no -leading-dashes'; }") - output = assert_bash_exec(bash, "_parse_help fn") + output = assert_bash_exec(bash, "_parse_help fn; (($? == 1))") assert not output def test_5(self, bash): @@ -94,6 +94,20 @@ class TestUnitParseHelp: output = assert_bash_exec(bash, "_parse_help fn", want_output=True) assert output.split() == "--foo".split() + def test_17_failglob(self, bash): + assert_bash_exec(bash, "fn() { echo '--foo[=bar]'; }") + with bash_env_saved(bash) as bash_env: + bash_env.shopt("failglob", True) + output = assert_bash_exec(bash, "_parse_help fn", want_output=True) + assert output.split() == "--foo".split() + + def test_17_nullglob(self, bash): + assert_bash_exec(bash, "fn() { echo '--foo[=bar]'; }") + with bash_env_saved(bash) as bash_env: + bash_env.shopt("nullglob", True) + output = assert_bash_exec(bash, "_parse_help fn", want_output=True) + assert output.split() == "--foo".split() + def test_18(self, bash): assert_bash_exec(bash, "fn() { echo '--foo=<bar>'; }") output = assert_bash_exec(bash, "_parse_help fn", want_output=True) @@ -144,6 +158,13 @@ class TestUnitParseHelp: output = assert_bash_exec(bash, "_parse_help fn", want_output=True) assert output.split() == "--foo".split() + def test_27_middle_dot(self, bash): + """We do not want to include the period at the end of the sentence but + want to include dots connecting names.""" + assert_bash_exec(bash, "fn() { echo '--foo.bar.'; }") + output = assert_bash_exec(bash, "_parse_help fn", want_output=True) + assert output.split() == "--foo.bar".split() + def test_28(self, bash): assert_bash_exec(bash, "fn() { echo '-f or --foo'; }") output = assert_bash_exec(bash, "_parse_help fn", want_output=True) @@ -161,7 +182,7 @@ class TestUnitParseHelp: assert_bash_exec( bash, r"fn() { printf '%s\n' $'----\n---foo\n----- bar'; }" ) - output = assert_bash_exec(bash, "_parse_help fn") + output = assert_bash_exec(bash, "_parse_help fn; (($? == 1))") assert not output def test_31(self, bash): @@ -181,3 +202,33 @@ class TestUnitParseHelp: ) output = assert_bash_exec(bash, "_parse_help fn", want_output=True) assert output.split() == "--exclude=".split() + + def test_custom_helpopt1(self, bash): + assert_bash_exec(bash, "fn() { [[ $1 == -h ]] && echo '-option'; }") + output = assert_bash_exec(bash, "_parse_help fn -h", want_output=True) + assert output.split() == "-option".split() + + def test_custom_helpopt2(self, bash): + assert_bash_exec(bash, "fn() { [[ $1 == '-?' ]] && echo '-option'; }") + output = assert_bash_exec( + bash, "_parse_help fn '-?'", want_output=True + ) + assert output.split() == "-option".split() + + def test_custom_helpopt2_failglob(self, bash): + assert_bash_exec(bash, "fn() { [[ $1 == '-?' ]] && echo '-option'; }") + with bash_env_saved(bash) as bash_env: + bash_env.shopt("failglob", True) + output = assert_bash_exec( + bash, "_parse_help fn '-?'", want_output=True + ) + assert output.split() == "-option".split() + + def test_custom_helpopt2_nullglob(self, bash): + assert_bash_exec(bash, "fn() { [[ $1 == '-?' ]] && echo '-option'; }") + with bash_env_saved(bash) as bash_env: + bash_env.shopt("nullglob", True) + output = assert_bash_exec( + bash, "_parse_help fn '-?'", want_output=True + ) + assert output.split() == "-option".split() diff --git a/test/t/unit/test_unit_parse_usage.py b/test/t/unit/test_unit_parse_usage.py index f0cb711..0106922 100644 --- a/test/t/unit/test_unit_parse_usage.py +++ b/test/t/unit/test_unit_parse_usage.py @@ -1,18 +1,18 @@ import pytest -from conftest import assert_bash_exec +from conftest import assert_bash_exec, bash_env_saved @pytest.mark.bashcomp(cmd=None, ignore_env=r"^\+declare -f fn$") class TestUnitParseUsage: def test_1(self, bash): assert_bash_exec(bash, "fn() { echo; }") - output = assert_bash_exec(bash, "_parse_usage fn") + output = assert_bash_exec(bash, "_parse_usage fn; (($? == 1))") assert not output def test_2(self, bash): assert_bash_exec(bash, "fn() { echo 'no dashes here'; }") - output = assert_bash_exec(bash, "_parse_usage fn") + output = assert_bash_exec(bash, "_parse_usage fn; (($? == 1))") assert not output def test_3(self, bash): @@ -59,7 +59,7 @@ class TestUnitParseUsage: assert_bash_exec( bash, "fn() { echo ----; echo ---foo; echo '----- bar'; }" ) - output = assert_bash_exec(bash, "_parse_usage fn") + output = assert_bash_exec(bash, "_parse_usage fn; (($? == 1))") assert not output def test_12(self, bash): @@ -67,3 +67,41 @@ class TestUnitParseUsage: bash, "echo '[-duh]' | _parse_usage -", want_output=True ) assert output.split() == "-d -u -h".split() + + def test_custom_helpopt1(self, bash): + assert_bash_exec( + bash, "fn() { [[ $1 == -h ]] && echo 'fn [-option]'; true; }" + ) + output = assert_bash_exec(bash, "_parse_usage fn -h", want_output=True) + assert output.split() == "-o -p -t -i -o -n".split() + + def test_custom_helpopt2(self, bash): + assert_bash_exec( + bash, "fn() { [[ $1 == '-?' ]] && echo 'fn [-option]'; }" + ) + output = assert_bash_exec( + bash, "_parse_usage fn '-?'", want_output=True + ) + assert output.split() == "-o -p -t -i -o -n".split() + + def test_custom_helpopt2_failglob(self, bash): + assert_bash_exec( + bash, "fn() { [[ $1 == '-?' ]] && echo 'fn [-option]'; }" + ) + with bash_env_saved(bash) as bash_env: + bash_env.shopt("failglob", True) + output = assert_bash_exec( + bash, "_parse_usage fn '-?'", want_output=True + ) + assert output.split() == "-o -p -t -i -o -n".split() + + def test_custom_helpopt2_nullglob(self, bash): + assert_bash_exec( + bash, "fn() { [[ $1 == '-?' ]] && echo 'fn [-option]'; }" + ) + with bash_env_saved(bash) as bash_env: + bash_env.shopt("nullglob", True) + output = assert_bash_exec( + bash, "_parse_usage fn '-?'", want_output=True + ) + assert output.split() == "-o -p -t -i -o -n".split() diff --git a/test/t/unit/test_unit_pgids.py b/test/t/unit/test_unit_pgids.py new file mode 100644 index 0000000..05019ec --- /dev/null +++ b/test/t/unit/test_unit_pgids.py @@ -0,0 +1,34 @@ +import os + +import pytest + +from conftest import assert_bash_exec, bash_env_saved + + +@pytest.mark.bashcomp(cmd=None, ignore_env=r"^\+(COMPREPLY|cur)=") +class TestUnitPgids: + def test_smoke(self, bash): + with bash_env_saved(bash) as bash_env: + bash_env.write_variable("cur", "") + assert_bash_exec(bash, "_comp_compgen_pgids >/dev/null") + + def test_non_pollution(self, bash): + """Test environment non-pollution, detected at teardown.""" + assert_bash_exec( + bash, + "foo() { local cur=; _comp_compgen_pgids; }; foo; unset -f foo", + ) + + def test_ints(self, bash): + """Test that we get something sensible, and only int'y strings.""" + with bash_env_saved(bash) as bash_env: + bash_env.write_variable("cur", "") + completion = assert_bash_exec( + bash, + r'_comp_compgen_pgids; printf "%s\n" "${COMPREPLY[@]}"', + want_output=True, + ).split() + assert completion + if hasattr(os, "getpgid"): + assert str(os.getpgid(0)) in completion + assert all(x.isdigit() for x in completion) diff --git a/test/t/unit/test_unit_pids.py b/test/t/unit/test_unit_pids.py new file mode 100644 index 0000000..3681c8c --- /dev/null +++ b/test/t/unit/test_unit_pids.py @@ -0,0 +1,34 @@ +import os + +import pytest + +from conftest import assert_bash_exec, bash_env_saved + + +@pytest.mark.bashcomp(cmd=None, ignore_env=r"^\+COMPREPLY=") +class TestUnitPids: + def test_smoke(self, bash): + with bash_env_saved(bash) as bash_env: + bash_env.write_variable("cur", "") + assert_bash_exec(bash, "_comp_compgen_pids >/dev/null") + + def test_non_pollution(self, bash): + """Test environment non-pollution, detected at teardown.""" + assert_bash_exec( + bash, + "foo() { local cur=; _comp_compgen_pids; }; foo; unset -f foo", + ) + + def test_ints(self, bash): + """Test that we get something sensible, and only int'y strings.""" + with bash_env_saved(bash) as bash_env: + bash_env.write_variable("cur", "") + completion = assert_bash_exec( + bash, + r'_comp_compgen_pids; printf "%s\n" "${COMPREPLY[@]}"', + want_output=True, + ).split() + assert completion + if hasattr(os, "getpid"): + assert str(os.getpid()) in completion + assert all(x.isdigit() for x in completion) diff --git a/test/t/unit/test_unit_pnames.py b/test/t/unit/test_unit_pnames.py new file mode 100644 index 0000000..e0819e5 --- /dev/null +++ b/test/t/unit/test_unit_pnames.py @@ -0,0 +1,29 @@ +import pytest + +from conftest import assert_bash_exec, bash_env_saved + + +@pytest.mark.bashcomp(cmd=None, ignore_env=r"^\+COMPREPLY=") +class TestUnitPnames: + def test_smoke(self, bash): + with bash_env_saved(bash) as bash_env: + bash_env.write_variable("cur", "") + assert_bash_exec(bash, "_comp_compgen_pnames >/dev/null") + + def test_non_pollution(self, bash): + """Test environment non-pollution, detected at teardown.""" + assert_bash_exec( + bash, + "foo() { local cur=; _comp_compgen_pnames; }; foo; unset -f foo", + ) + + def test_something(self, bash): + """Test that we get something.""" + with bash_env_saved(bash) as bash_env: + bash_env.write_variable("cur", "") + completion = assert_bash_exec( + bash, + r'_comp_compgen_pids; printf "%s\n" "${COMPREPLY[@]}"', + want_output=True, + ).split() + assert completion diff --git a/test/t/unit/test_unit_quote.py b/test/t/unit/test_unit_quote.py index b280bd6..3508782 100644 --- a/test/t/unit/test_unit_quote.py +++ b/test/t/unit/test_unit_quote.py @@ -3,34 +3,41 @@ import pytest from conftest import TestUnitBase, assert_bash_exec -@pytest.mark.bashcomp(cmd=None) +@pytest.mark.bashcomp( + cmd=None, + ignore_env=r"^\+declare -f __tester$", +) class TestUnitQuote(TestUnitBase): def test_1(self, bash): + assert_bash_exec( + bash, + '__tester() { local REPLY; _comp_quote "$1"; printf %s "$REPLY"; }', + ) output = assert_bash_exec( - bash, 'quote "a b"', want_output=True, want_newline=False + bash, '__tester "a b"', want_output=True, want_newline=False ) assert output.strip() == "'a b'" def test_2(self, bash): output = assert_bash_exec( - bash, 'quote "a b"', want_output=True, want_newline=False + bash, '__tester "a b"', want_output=True, want_newline=False ) assert output.strip() == "'a b'" def test_3(self, bash): output = assert_bash_exec( - bash, 'quote " a "', want_output=True, want_newline=False + bash, '__tester " a "', want_output=True, want_newline=False ) assert output.strip() == "' a '" def test_4(self, bash): output = assert_bash_exec( - bash, "quote \"a'b'c\"", want_output=True, want_newline=False + bash, "__tester \"a'b'c\"", want_output=True, want_newline=False ) assert output.strip() == r"'a'\''b'\''c'" def test_5(self, bash): output = assert_bash_exec( - bash, 'quote "a\'"', want_output=True, want_newline=False + bash, '__tester "a\'"', want_output=True, want_newline=False ) assert output.strip() == r"'a'\'''" diff --git a/test/t/unit/test_unit_quote_compgen.py b/test/t/unit/test_unit_quote_compgen.py new file mode 100644 index 0000000..faf23fe --- /dev/null +++ b/test/t/unit/test_unit_quote_compgen.py @@ -0,0 +1,173 @@ +import os + +import pytest + +from conftest import assert_bash_exec, assert_complete, bash_env_saved + + +@pytest.mark.bashcomp(cmd=None, temp_cwd=True) +class TestUnitQuoteCompgen: + @pytest.fixture(scope="class") + def functions(self, bash): + assert_bash_exec( + bash, + '_comp__test_quote_compgen() { local REPLY; _comp_quote_compgen "$1"; printf %s "$REPLY"; }', + ) + + @pytest.mark.parametrize( + "funcname", "_comp__test_quote_compgen quote_readline".split() + ) + def test_exec(self, bash, functions, funcname): + assert_bash_exec(bash, "%s '' >/dev/null" % funcname) + + @pytest.mark.parametrize( + "funcname", "_comp__test_quote_compgen quote_readline".split() + ) + def test_env_non_pollution(self, bash, functions, funcname): + """Test environment non-pollution, detected at teardown.""" + assert_bash_exec( + bash, "foo() { %s meh >/dev/null; }; foo; unset -f foo" % funcname + ) + + @pytest.mark.parametrize( + "funcname", "_comp__test_quote_compgen quote_readline".split() + ) + def test_1(self, bash, functions, funcname): + output = assert_bash_exec( + bash, "%s '';echo" % funcname, want_output=True + ) + assert output.strip() == "''" + + @pytest.mark.parametrize( + "funcname", "_comp__test_quote_compgen quote_readline".split() + ) + def test_2(self, bash, functions, funcname): + output = assert_bash_exec( + bash, "%s foo;echo" % funcname, want_output=True + ) + assert output.strip() == "foo" + + @pytest.mark.parametrize( + "funcname", "_comp__test_quote_compgen quote_readline".split() + ) + def test_3(self, bash, functions, funcname): + output = assert_bash_exec( + bash, '%s foo\\"bar;echo' % funcname, want_output=True + ) + assert output.strip() == 'foo\\"bar' + + @pytest.mark.parametrize( + "funcname", "_comp__test_quote_compgen quote_readline".split() + ) + def test_4(self, bash, functions, funcname): + output = assert_bash_exec( + bash, "%s '$(echo x >&2)';echo" % funcname, want_output=True + ) + assert output.strip() == "\\$\\(echo\\ x\\ \\>\\&2\\)" + + def test_github_issue_189_1(self, bash, functions): + """Test error messages on a certain command line + + Reported at https://github.com/scop/bash-completion/issues/189 + + Syntax error messages should not be shown by completion on the + following line: + + $ ls -- '${[TAB] + $ rm -- '${[TAB] + + """ + assert_bash_exec(bash, "_comp__test_quote_compgen $'\\'${' >/dev/null") + + def test_github_issue_492_1(self, bash, functions): + """Test unintended code execution on a certain command line + + Reported at https://github.com/scop/bash-completion/pull/492 + + Arbitrary commands could be unintendedly executed by + _comp_quote_compgen. In the following example, the command "touch + 1.txt" would be unintendedly created before the fix. The file "1.txt" + should not be created by completion on the following line: + + $ echo '$(touch file.txt)[TAB] + + """ + assert_bash_exec( + bash, "_comp__test_quote_compgen $'\\'$(touch 1.txt)' >/dev/null" + ) + assert not os.path.exists("./1.txt") + + def test_github_issue_492_2(self, bash, functions): + """Test the file clear by unintended redirection on a certain command line + + Reported at https://github.com/scop/bash-completion/pull/492 + + The file "1.0" should not be created by completion on the following + line: + + $ awk '$1 > 1.0[TAB] + + """ + assert_bash_exec( + bash, "_comp__test_quote_compgen $'\\'$1 > 1.0' >/dev/null" + ) + assert not os.path.exists("./1.0") + + def test_github_issue_492_3(self, bash, functions): + """Test code execution through unintended pathname expansions + + When there is a file named "quote=$(COMMAND)" (for + _comp_compgen_filedir) or "REPLY=$(COMMAND)" (for _comp_quote_compgen), + the completion of the word '$* results in the execution of COMMAND. + + $ echo '$*[TAB] + + """ + os.mkdir("./REPLY=$(echo injected >&2)") + assert_bash_exec(bash, "_comp__test_quote_compgen $'\\'$*' >/dev/null") + + def test_github_issue_492_4(self, bash, functions): + """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, "_comp__test_quote_compgen $'a\\\\\\tb*' >/dev/null" + ) + + def test_github_issue_526_1(self, bash): + r"""Regression tests for unprocessed escape sequences after quotes + + Ref [1] https://github.com/scop/bash-completion/pull/492#discussion_r637213822 + Ref [2] https://github.com/scop/bash-completion/pull/526 + + The escape sequences in the local variable of "value" in + "_comp_quote_compgen" needs to be unescaped by passing it to printf as + the format string. This causes a problem in the following case [where + the spaces after "alpha\" is a TAB character inserted in the command + string by "C-v TAB"]: + + $ echo alpha\ b[TAB] + + """ + os.mkdir("./alpha\tbeta") + assert ( + assert_complete( + # Remark on "rendered_cmd": Bash aligns the last character 'b' + # in the rendered cmd to an "8 x n" boundary using spaces. + # Here, the command string is assumed to start from column 2 + # because the width of PS1 (conftest.PS1 = '/@') is 2, + bash, + "echo alpha\\\026\tb", + rendered_cmd="echo alpha\\ b", + ) + == "eta/" + ) diff --git a/test/t/unit/test_unit_quote_readline.py b/test/t/unit/test_unit_quote_readline.py deleted file mode 100644 index e2b437e..0000000 --- a/test/t/unit/test_unit_quote_readline.py +++ /dev/null @@ -1,15 +0,0 @@ -import pytest - -from conftest import assert_bash_exec - - -@pytest.mark.bashcomp(cmd=None) -class TestUnitQuoteReadline: - def test_exec(self, bash): - assert_bash_exec(bash, "quote_readline '' >/dev/null") - - def test_env_non_pollution(self, bash): - """Test environment non-pollution, detected at teardown.""" - assert_bash_exec( - bash, "foo() { quote_readline meh >/dev/null; }; foo; unset foo" - ) diff --git a/test/t/unit/test_unit_realcommand.py b/test/t/unit/test_unit_realcommand.py new file mode 100644 index 0000000..4eb7b73 --- /dev/null +++ b/test/t/unit/test_unit_realcommand.py @@ -0,0 +1,90 @@ +import pytest + +from conftest import assert_bash_exec, bash_env_saved + + +@pytest.mark.bashcomp( + cmd=None, cwd="shared", ignore_env=r"^\+declare -f __tester$" +) +class TestUnitRealCommand: + @pytest.fixture + def functions(self, bash): + assert_bash_exec( + bash, + ( + "__tester() { " + "local REPLY rc; " + '_comp_realcommand "$1"; ' + "rc=$?; " + 'printf %s "$REPLY"; ' + "return $rc; " + "}" + ), + ) + + def test_non_pollution(self, bash): + """Test environment non-pollution, detected at teardown.""" + assert_bash_exec( + bash, + "foo() { local REPLY=; _comp_realcommand bar; }; foo; unset -f foo", + want_output=None, + ) + + def test_basename(self, bash, functions): + with bash_env_saved(bash) as bash_env: + bash_env.write_variable("PATH", "$PWD/bin:$PATH", quote=False) + output = assert_bash_exec( + bash, + "__tester arp", + want_output=True, + want_newline=False, + ) + assert output.strip().endswith("/shared/bin/arp") + + def test_basename_nonexistent(self, bash, functions): + filename = "non-existent-file-for-bash-completion-tests" + skipif = "! type -P %s" % filename + try: + assert_bash_exec(bash, skipif, want_output=None) + except AssertionError: + pytest.skipif(skipif) + output = assert_bash_exec( + bash, + "! __tester %s" % filename, + want_output=False, + ) + assert output.strip() == "" + + def test_relative(self, bash, functions): + output = assert_bash_exec( + bash, + "__tester bin/arp", + want_output=True, + want_newline=False, + ) + assert output.strip().endswith("/shared/bin/arp") + + def test_relative_nonexistent(self, bash, functions): + output = assert_bash_exec( + bash, + "! __tester bin/non-existent", + want_output=False, + ) + assert output.strip() == "" + + def test_absolute(self, bash, functions): + output = assert_bash_exec( + bash, + '__tester "$PWD/bin/arp"', + want_output=True, + want_newline=False, + ) + assert output.strip().endswith("/shared/bin/arp") + + def test_absolute_nonexistent(self, bash, functions): + output = assert_bash_exec( + bash, + '! __tester "$PWD/bin/non-existent"', + want_output=False, + ) + assert output.strip() == "" diff --git a/test/t/unit/test_unit_split.py b/test/t/unit/test_unit_split.py new file mode 100644 index 0000000..d1f228e --- /dev/null +++ b/test/t/unit/test_unit_split.py @@ -0,0 +1,90 @@ +import pytest + +from conftest import assert_bash_exec, bash_env_saved + + +@pytest.mark.bashcomp( + cmd=None, ignore_env=r"^\+declare -f (dump_array|__tester)$" +) +class TestUtilSplit: + @pytest.fixture + def functions(self, bash): + assert_bash_exec( + bash, "dump_array() { printf '<%s>' \"${arr[@]}\"; echo; }" + ) + assert_bash_exec( + bash, + '__tester() { local -a arr=(00); _comp_split "${@:1:$#-1}" arr "${@:$#}"; dump_array; }', + ) + + def test_1(self, bash, functions): + output = assert_bash_exec( + bash, "__tester '12 34 56'", want_output=True + ) + assert output.strip() == "<12><34><56>" + + def test_2(self, bash, functions): + output = assert_bash_exec( + bash, "__tester $'12\\n34\\n56'", want_output=True + ) + assert output.strip() == "<12><34><56>" + + def test_3(self, bash, functions): + output = assert_bash_exec( + bash, "__tester '12:34:56'", want_output=True + ) + assert output.strip() == "<12:34:56>" + + def test_option_F_1(self, bash, functions): + output = assert_bash_exec( + bash, "__tester -F : '12:34:56'", want_output=True + ) + assert output.strip() == "<12><34><56>" + + def test_option_F_2(self, bash, functions): + output = assert_bash_exec( + bash, "__tester -F : '12 34 56'", want_output=True + ) + assert output.strip() == "<12 34 56>" + + def test_option_l_1(self, bash, functions): + output = assert_bash_exec( + bash, "__tester -l $'12\\n34\\n56'", want_output=True + ) + assert output.strip() == "<12><34><56>" + + def test_option_l_2(self, bash, functions): + output = assert_bash_exec( + bash, "__tester -l '12 34 56'", want_output=True + ) + assert output.strip() == "<12 34 56>" + + def test_option_a_1(self, bash, functions): + output = assert_bash_exec( + bash, "__tester -aF : '12:34:56'", want_output=True + ) + assert output.strip() == "<00><12><34><56>" + + def test_protect_from_failglob(self, bash, functions): + with bash_env_saved(bash) as bash_env: + bash_env.shopt("failglob", True) + output = assert_bash_exec( + bash, "__tester -F '*' '12*34*56'", want_output=True + ) + assert output.strip() == "<12><34><56>" + + def test_protect_from_nullglob(self, bash, functions): + with bash_env_saved(bash) as bash_env: + bash_env.shopt("nullglob", True) + output = assert_bash_exec( + bash, "__tester -F '*' '12*34*56'", want_output=True + ) + assert output.strip() == "<12><34><56>" + + def test_protect_from_IFS(self, bash, functions): + with bash_env_saved(bash) as bash_env: + bash_env.write_variable("IFS", "34") + output = assert_bash_exec( + bash, "__tester '12 34 56'", want_output=True + ) + assert output.strip() == "<12><34><56>" diff --git a/test/t/unit/test_unit_tilde.py b/test/t/unit/test_unit_tilde.py index 35a4e4c..fae0dd6 100644 --- a/test/t/unit/test_unit_tilde.py +++ b/test/t/unit/test_unit_tilde.py @@ -6,23 +6,24 @@ from conftest import assert_bash_exec @pytest.mark.bashcomp(cmd=None, ignore_env=r"^\+COMPREPLY=") class TestUnitTilde: def test_1(self, bash): - assert_bash_exec(bash, "_tilde >/dev/null") + assert_bash_exec(bash, "! _comp_compgen_tilde >/dev/null") def test_2(self, bash): """Test environment non-pollution, detected at teardown.""" assert_bash_exec( - bash, 'foo() { local aa="~"; _tilde "$aa"; }; foo; unset foo' + bash, + 'foo() { local aa="~"; _comp_compgen -c "$aa" tilde; }; foo; unset -f foo', ) def test_3(self, bash): """Test for https://bugs.debian.org/766163""" - assert_bash_exec(bash, "_tilde ~-o") + assert_bash_exec(bash, "! _comp_compgen -c ~-o tilde") def _test_part_full(self, bash, part, full): res = ( assert_bash_exec( bash, - '_tilde "~%s"; echo "${COMPREPLY[@]}"' % part, + '_comp_compgen -c "~%s" tilde; echo "${COMPREPLY[@]}"' % part, want_output=True, ) .strip() diff --git a/test/t/unit/test_unit_unlocal.py b/test/t/unit/test_unit_unlocal.py new file mode 100644 index 0000000..be5ec56 --- /dev/null +++ b/test/t/unit/test_unit_unlocal.py @@ -0,0 +1,18 @@ +import pytest + +from conftest import assert_bash_exec + + +@pytest.mark.bashcomp(cmd=None, ignore_env=r"^\+(VAR=|declare -f foo)") +class TestUnlocal: + def test_1(self, bash): + cmd = ( + "foo() { " + "local VAR=inner; " + "_comp_unlocal VAR; " + "echo $VAR; " + "}; " + "VAR=outer; foo; " + ) + res = assert_bash_exec(bash, cmd, want_output=True).strip() + assert res == "outer" diff --git a/test/t/unit/test_unit_variables.py b/test/t/unit/test_unit_variables.py index d62bc4a..f15d249 100644 --- a/test/t/unit/test_unit_variables.py +++ b/test/t/unit/test_unit_variables.py @@ -11,7 +11,7 @@ class TestUnitVariables: assert_bash_exec( bash, "unset assoc2 && declare -A assoc2=([idx1]=1 [idx2]=2)" ) - assert_bash_exec(bash, "unset ${!___v*} && declare ___var=''") + assert_bash_exec(bash, 'unset ${!___v*} && declare ___var=""') request.addfinalizer( lambda: assert_bash_exec(bash, "unset ___var assoc1 assoc2") ) @@ -28,6 +28,10 @@ class TestUnitVariables: def test_multiple_array_indexes(self, functions, completion): assert completion == "${assoc2[idx1]} ${assoc2[idx2]}".split() + @pytest.mark.complete(": ${assoc2[", shopt=dict(failglob=True)) + def test_multiple_array_indexes_failglob(self, functions, completion): + assert completion == "${assoc2[idx1]} ${assoc2[idx2]}".split() + @pytest.mark.complete(": ${assoc1[bogus]") def test_closing_curly_after_square(self, functions, completion): assert completion == "}" diff --git a/test/t/unit/test_unit_xfunc.py b/test/t/unit/test_unit_xfunc.py new file mode 100644 index 0000000..3fb3a72 --- /dev/null +++ b/test/t/unit/test_unit_xfunc.py @@ -0,0 +1,71 @@ +import pytest + +from conftest import assert_bash_exec, bash_env_saved + + +@pytest.mark.bashcomp(cmd=None) +class TestUnitXfunc: + def test_1(self, bash): + with bash_env_saved(bash) as bash_env: + bash_env.write_variable( + "BASH_COMPLETION_USER_DIR", "$PWD/_comp_xfunc", quote=False + ) + + # test precondition + assert_bash_exec( + bash, + "! declare -F _comp_xfunc_xfunc_test1_utility1 &>/dev/null", + ) + + # first invocation (completion/xfunc-test1 is sourced) + output = assert_bash_exec( + bash, + "_comp_xfunc xfunc-test1 utility1 'a b' cde fgh", + want_output=True, + ) + assert output.strip() == "util1[<a b><cde><fgh>]" + + # test precondition + assert_bash_exec( + bash, "declare -F _comp_xfunc_xfunc_test1_utility1 &>/dev/null" + ) + + # second invocation (completion/xfunc-test1 is not sourced) + output = assert_bash_exec( + bash, + "_comp_xfunc xfunc-test1 utility1 'a b' cde fgh", + want_output=True, + ) + assert output.strip() == "util1[<a b><cde><fgh>]" + + def test_2(self, bash): + with bash_env_saved(bash) as bash_env: + bash_env.write_variable( + "BASH_COMPLETION_USER_DIR", "$PWD/_comp_xfunc", quote=False + ) + + # test precondition + assert_bash_exec( + bash, "! declare -F _comp_xfunc_non_standard_name &>/dev/null" + ) + + # first invocation (completion/xfunc-test2 is sourced) + output = assert_bash_exec( + bash, + "_comp_xfunc xfunc-test2 _comp_xfunc_non_standard_name 'a b' cde fgh", + want_output=True, + ) + assert output.strip() == "util2[<a b><cde><fgh>]" + + # test precondition + assert_bash_exec( + bash, "declare -F _comp_xfunc_non_standard_name &>/dev/null" + ) + + # second invocation (completion/xfunc-test2 is not sourced) + output = assert_bash_exec( + bash, + "_comp_xfunc xfunc-test2 _comp_xfunc_non_standard_name 'a b' cde fgh", + want_output=True, + ) + assert output.strip() == "util2[<a b><cde><fgh>]" diff --git a/test/t/unit/test_unit_xinetd_services.py b/test/t/unit/test_unit_xinetd_services.py index 7a90cb7..a9e33a9 100644 --- a/test/t/unit/test_unit_xinetd_services.py +++ b/test/t/unit/test_unit_xinetd_services.py @@ -6,17 +6,19 @@ from conftest import assert_bash_exec @pytest.mark.bashcomp(cmd=None, ignore_env=r"^\+COMPREPLY=") class TestUnitXinetdServices: def test_direct(self, bash): - assert_bash_exec(bash, "_xinetd_services >/dev/null") + assert_bash_exec(bash, "_comp_compgen_xinetd_services >/dev/null") def test_env_non_pollution(self, bash): """Test environment non-pollution, detected at teardown.""" - assert_bash_exec(bash, "foo() { _xinetd_services; }; foo; unset foo") + assert_bash_exec( + bash, "foo() { _comp_compgen_xinetd_services; }; foo; unset -f foo" + ) def test_basic(self, bash): output = assert_bash_exec( bash, - "foo() { local BASHCOMP_XINETDDIR=$PWD/shared/bin;unset COMPREPLY; " - '_xinetd_services; printf "%s\\n" "${COMPREPLY[@]}"; }; foo; unset foo', + "foo() { local _comp__test_xinetd_dir=$PWD/shared/bin; unset -v COMPREPLY; " + '_comp_compgen_xinetd_services; printf "%s\\n" "${COMPREPLY[@]}"; }; foo; unset -f foo', want_output=True, ) assert sorted(output.split()) == ["arp", "ifconfig"] |