summaryrefslogtreecommitdiff
path: root/test/t/conftest.py
diff options
context:
space:
mode:
Diffstat (limited to 'test/t/conftest.py')
-rw-r--r--test/t/conftest.py367
1 files changed, 248 insertions, 119 deletions
diff --git a/test/t/conftest.py b/test/t/conftest.py
index 20942e87..5c1603d5 100644
--- a/test/t/conftest.py
+++ b/test/t/conftest.py
@@ -3,18 +3,18 @@ import os
import re
import shlex
import subprocess
-from typing import Iterable, List, Optional, Tuple, Union
+import time
+from typing import Callable, Iterable, Iterator, List, Optional, Tuple
import pexpect
import pytest
-
PS1 = "/@"
MAGIC_MARK = "__MaGiC-maRKz!__"
def find_unique_completion_pair(
- items: Iterable[str]
+ items: Iterable[str],
) -> Optional[Tuple[str, str]]:
result = None
bestscore = 0
@@ -56,10 +56,22 @@ def find_unique_completion_pair(
@pytest.fixture(scope="class")
-def part_full_user(bash: pexpect.spawn) -> Optional[Tuple[str, str]]:
- res = (
- assert_bash_exec(bash, "compgen -u", want_output=True).strip().split()
- )
+def output_sort_uniq(bash: pexpect.spawn) -> Callable[[str], List[str]]:
+ def _output_sort_uniq(command: str) -> List[str]:
+ return sorted(
+ set( # weed out possible duplicates
+ assert_bash_exec(bash, command, want_output=True).split()
+ )
+ )
+
+ return _output_sort_uniq
+
+
+@pytest.fixture(scope="class")
+def part_full_user(
+ bash: pexpect.spawn, output_sort_uniq: Callable[[str], List[str]]
+) -> Optional[Tuple[str, str]]:
+ res = output_sort_uniq("compgen -u")
pair = find_unique_completion_pair(res)
if not pair:
pytest.skip("No suitable test user found")
@@ -67,10 +79,10 @@ def part_full_user(bash: pexpect.spawn) -> Optional[Tuple[str, str]]:
@pytest.fixture(scope="class")
-def part_full_group(bash: pexpect.spawn) -> Optional[Tuple[str, str]]:
- res = (
- assert_bash_exec(bash, "compgen -g", want_output=True).strip().split()
- )
+def part_full_group(
+ bash: pexpect.spawn, output_sort_uniq: Callable[[str], List[str]]
+) -> Optional[Tuple[str, str]]:
+ res = output_sort_uniq("compgen -g")
pair = find_unique_completion_pair(res)
if not pair:
pytest.skip("No suitable test user found")
@@ -78,6 +90,82 @@ def part_full_group(bash: pexpect.spawn) -> Optional[Tuple[str, str]]:
@pytest.fixture(scope="class")
+def hosts(bash: pexpect.spawn) -> List[str]:
+ output = assert_bash_exec(bash, "compgen -A hostname", want_output=True)
+ return sorted(set(output.split() + _avahi_hosts(bash)))
+
+
+@pytest.fixture(scope="class")
+def avahi_hosts(bash: pexpect.spawn) -> List[str]:
+ return _avahi_hosts(bash)
+
+
+def _avahi_hosts(bash: pexpect.spawn) -> List[str]:
+ output = assert_bash_exec(
+ bash,
+ "! type avahi-browse &>/dev/null || "
+ "avahi-browse -cpr _workstation._tcp 2>/dev/null "
+ "| command grep ^= | cut -d';' -f7",
+ want_output=None,
+ )
+ return sorted(set(output.split()))
+
+
+@pytest.fixture(scope="class")
+def known_hosts(bash: pexpect.spawn) -> List[str]:
+ output = assert_bash_exec(
+ bash,
+ '_known_hosts_real ""; '
+ r'printf "%s\n" "${COMPREPLY[@]}"; unset COMPREPLY',
+ want_output=True,
+ )
+ return sorted(set(output.split()))
+
+
+@pytest.fixture(scope="class")
+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()
+ return (user, home)
+
+
+def partialize(
+ bash: pexpect.spawn, items: Iterable[str]
+) -> Tuple[str, List[str]]:
+ """
+ Get list of items starting with the first char of first of items.
+
+ Disregard items starting with a COMP_WORDBREAKS character
+ (e.g. a colon ~ IPv6 address), they are special cases requiring
+ special tests.
+ """
+ first_char = None
+ comp_wordbreaks = assert_bash_exec(
+ bash,
+ 'printf "%s" "$COMP_WORDBREAKS"',
+ want_output=True,
+ want_newline=False,
+ )
+ partial_items = []
+ for item in sorted(items):
+ if first_char is None:
+ if item[0] not in comp_wordbreaks:
+ first_char = item[0]
+ partial_items.append(item)
+ elif item.startswith(first_char):
+ partial_items.append(item)
+ else:
+ break
+ if first_char is None:
+ pytest.skip("Could not generate partial items list from %s" % items)
+ # superfluous/dead code to assist mypy; pytest.skip always raises
+ assert first_char is not None
+ return first_char, partial_items
+
+
+@pytest.fixture(scope="class")
def bash(request) -> pexpect.spawn:
logfile = None
@@ -135,7 +223,7 @@ def bash(request) -> pexpect.spawn:
skipif = marker.kwargs.get("skipif")
if skipif:
try:
- assert_bash_exec(bash, skipif)
+ assert_bash_exec(bash, skipif, want_output=None)
except AssertionError:
pass
else:
@@ -144,7 +232,7 @@ def bash(request) -> pexpect.spawn:
xfail = marker.kwargs.get("xfail")
if xfail:
try:
- assert_bash_exec(bash, xfail)
+ assert_bash_exec(bash, xfail, want_output=None)
except AssertionError:
pass
else:
@@ -182,7 +270,7 @@ def bash(request) -> pexpect.spawn:
logfile.close()
-def is_testable(bash: pexpect.spawn, cmd: str) -> bool:
+def is_testable(bash: pexpect.spawn, cmd: Optional[str]) -> bool:
if not cmd:
pytest.fail("Could not resolve name of command to test")
return False
@@ -214,8 +302,14 @@ def load_completion_for(bash: pexpect.spawn, cmd: str) -> bool:
def assert_bash_exec(
- bash: pexpect.spawn, cmd: str, want_output: bool = False, want_newline=True
+ bash: pexpect.spawn,
+ cmd: str,
+ want_output: Optional[bool] = False,
+ want_newline=True,
) -> str:
+ """
+ :param want_output: if None, don't care if got output or not
+ """
# Send command
bash.sendline(cmd)
@@ -243,16 +337,17 @@ def assert_bash_exec(
status,
output,
)
- if output:
- assert want_output, (
- 'Unexpected output from "%s": exit status=%s, output="%s"'
- % (cmd, status, output)
- )
- else:
- assert not want_output, (
- 'Expected output from "%s": exit status=%s, output="%s"'
- % (cmd, status, output)
- )
+ if want_output is not None:
+ if output:
+ assert want_output, (
+ 'Unexpected output from "%s": exit status=%s, output="%s"'
+ % (cmd, status, output)
+ )
+ else:
+ assert not want_output, (
+ 'Expected output from "%s": exit status=%s, output="%s"'
+ % (cmd, status, output)
+ )
return output
@@ -293,76 +388,52 @@ def diff_env(before: List[str], after: List[str], ignore: str):
assert not diff, "Environment should not be modified"
-class CompletionResult:
+class CompletionResult(Iterable[str]):
"""
Class to hold completion results.
"""
- def __init__(self, output: str, items: Optional[Iterable[str]] = None):
+ def __init__(self, output: Optional[str] = None):
"""
- When items are specified, they are used as the base for comparisons
- provided by this class. When not, regular expressions are used instead.
- This is because it is not always possible to unambiguously split a
- completion output string into individual items, for example when the
- items contain whitespace.
-
:param output: All completion output as-is.
- :param items: Completions as individual items. Should be specified
- only in cases where the completions are robustly known to be
- exactly the specified ones.
"""
- self.output = output
- self._items = None if items is None else sorted(items)
+ self.output = output or ""
def endswith(self, suffix: str) -> bool:
return self.output.endswith(suffix)
- def __eq__(self, expected: Union[str, Iterable[str]]) -> bool:
+ def startswith(self, prefix: str) -> bool:
+ return self.output.startswith(prefix)
+
+ def _items(self) -> List[str]:
+ return [x.strip() for x in self.output.strip().splitlines()]
+
+ def __eq__(self, expected: object) -> bool:
"""
Returns True if completion contains expected items, and no others.
Defining __eq__ this way is quite ugly, but facilitates concise
testing code.
"""
- expiter = [expected] if isinstance(expected, str) else expected
- if self._items is not None:
- return self._items == expiter
- return bool(
- re.match(
- r"^\s*" + r"\s+".join(re.escape(x) for x in expiter) + r"\s*$",
- self.output,
- )
- )
+ if isinstance(expected, str):
+ expiter = [expected] # type: Iterable
+ elif not isinstance(expected, Iterable):
+ return False
+ else:
+ expiter = expected
+ return self._items() == expiter
def __contains__(self, item: str) -> bool:
- if self._items is not None:
- return item in self._items
- return bool(
- re.search(r"(^|\s)%s(\s|$)" % re.escape(item), self.output)
- )
+ return item in self._items()
- def __iter__(self) -> Iterable[str]:
- """
- Note that iteration over items may not be accurate when items were not
- specified to the constructor, if individual items in the output contain
- whitespace. In those cases, it errs on the side of possibly returning
- more items than there actually are, and intends to never return fewer.
- """
- return iter(
- self._items
- if self._items is not None
- else re.split(r" {2,}|\r\n", self.output.strip())
- )
+ def __iter__(self) -> Iterator[str]:
+ return iter(self._items())
def __len__(self) -> int:
- """
- Uses __iter__, see caveat in it. While possibly inaccurate, this is
- good enough for truthiness checks.
- """
- return len(list(iter(self)))
+ return len(self._items())
def __repr__(self) -> str:
- return "<CompletionResult %s>" % list(self)
+ return "<CompletionResult %s>" % self._items()
def assert_complete(
@@ -371,7 +442,7 @@ def assert_complete(
skipif = kwargs.get("skipif")
if skipif:
try:
- assert_bash_exec(bash, skipif)
+ assert_bash_exec(bash, skipif, want_output=None)
except AssertionError:
pass
else:
@@ -379,7 +450,7 @@ def assert_complete(
xfail = kwargs.get("xfail")
if xfail:
try:
- assert_bash_exec(bash, xfail)
+ assert_bash_exec(bash, xfail, want_output=None)
except AssertionError:
pass
else:
@@ -393,57 +464,63 @@ def assert_complete(
# Back up environment and apply new one
assert_bash_exec(
bash,
- " ".join('%s%s="$%s"' % (env_prefix, k, k) for k in env.keys()),
+ " ".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()),
)
- bash.send(cmd + "\t")
- bash.expect_exact(cmd)
- bash.send(MAGIC_MARK)
- got = bash.expect(
- [
- # 0: multiple lines, result in .before
- r"\r\n" + re.escape(PS1 + cmd) + ".*" + MAGIC_MARK,
- # 1: no completion
- r"^" + MAGIC_MARK,
- # 2: on same line, result in .match
- r"^([^\r]+)%s$" % 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)
- elif got == 2:
- output = bash.match.group(1)
- result = CompletionResult(output, [shlex.split(cmd + output)[-1]])
- else:
- # TODO: warn about EOF/TIMEOUT?
- result = CompletionResult("", [])
- 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()),
+ try:
+ bash.send(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.send(MAGIC_MARK)
+ got = bash.expect(
+ [
+ # 0: multiple lines, result in .before
+ r"\r\n" + re.escape(PS1 + cmd) + ".*" + re.escape(MAGIC_MARK),
+ # 1: no completion
+ r"^" + re.escape(MAGIC_MARK),
+ # 2: on same line, result in .match
+ r"^([^\r]+)%s$" % re.escape(MAGIC_MARK),
+ pexpect.EOF,
+ pexpect.TIMEOUT,
+ ]
)
- if cwd:
- assert_bash_exec(bash, "cd - >/dev/null")
+ if got == 0:
+ output = bash.before
+ if output.endswith(MAGIC_MARK):
+ output = bash.before[: -len(MAGIC_MARK)]
+ result = CompletionResult(output)
+ elif got == 2:
+ output = bash.match.group(1)
+ result = CompletionResult(output)
+ else:
+ # TODO: warn about EOF/TIMEOUT?
+ result = CompletionResult()
+ finally:
+ bash.sendintr()
+ bash.expect_exact(PS1)
+ if env:
+ # Restore environment, and clean up backup
+ # TODO: Test with declare -p if a var was set, backup only if yes, and
+ # similarly restore only backed up vars. Should remove some need
+ # for ignore_env.
+ assert_bash_exec(
+ bash,
+ "export %s"
+ % " ".join(
+ '%s="$%s%s"' % (k, env_prefix, k) for k in env.keys()
+ ),
+ )
+ assert_bash_exec(
+ bash,
+ "unset -v %s"
+ % " ".join("%s%s" % (env_prefix, k) for k in env.keys()),
+ )
+ if cwd:
+ assert_bash_exec(bash, "cd - >/dev/null")
return result
@@ -451,7 +528,7 @@ def assert_complete(
def completion(request, bash: pexpect.spawn) -> CompletionResult:
marker = request.node.get_closest_marker("complete")
if not marker:
- return CompletionResult("", [])
+ return CompletionResult()
for pre_cmd in marker.kwargs.get("pre_cmds", []):
assert_bash_exec(bash, pre_cmd)
cmd = getattr(request.cls, "cmd", None)
@@ -467,9 +544,61 @@ def completion(request, bash: pexpect.spawn) -> CompletionResult:
) % ((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(