Skip to content

Commit 726e4c0

Browse files
committed
test: simplify completion parsing
Use completion-display-width 0 in inputrc to get completions listed one per line, return only completed suffix for single completions on the command line to avoid various weird side effects with escaping etc. Contains a few known test suite failures that will be addressed later.
1 parent d176aaf commit 726e4c0

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

43 files changed

+147
-191
lines changed

test/config/inputrc

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,9 @@ set print-completions-horizontally on
1616
# Don't use pager when showing completions
1717
set page-completions off
1818

19+
# Pirnt each completion on its own line
20+
set completion-display-width 0
21+
1922
# Local variables:
2023
# mode: shell-script
2124
# End:

test/t/conftest.py

Lines changed: 14 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -393,28 +393,21 @@ class CompletionResult(Iterable[str]):
393393
Class to hold completion results.
394394
"""
395395

396-
def __init__(self, output: str, items: Optional[Iterable[str]] = None):
396+
def __init__(self, output: Optional[str] = None):
397397
"""
398-
When items are specified, they are used as the base for comparisons
399-
provided by this class. When not, regular expressions are used instead.
400-
This is because it is not always possible to unambiguously split a
401-
completion output string into individual items, for example when the
402-
items contain whitespace.
403-
404398
:param output: All completion output as-is.
405-
:param items: Completions as individual items. Should be specified
406-
only in cases where the completions are robustly known to be
407-
exactly the specified ones.
408399
"""
409-
self.output = output
410-
self._items = None if items is None else sorted(items)
400+
self.output = output or ""
411401

412402
def endswith(self, suffix: str) -> bool:
413403
return self.output.endswith(suffix)
414404

415405
def startswith(self, prefix: str) -> bool:
416406
return self.output.startswith(prefix)
417407

408+
def _items(self) -> List[str]:
409+
return [x.strip() for x in self.output.strip().splitlines()]
410+
418411
def __eq__(self, expected: object) -> bool:
419412
"""
420413
Returns True if completion contains expected items, and no others.
@@ -428,44 +421,19 @@ def __eq__(self, expected: object) -> bool:
428421
return False
429422
else:
430423
expiter = expected
431-
if self._items is not None:
432-
return self._items == expiter
433-
return bool(
434-
re.match(
435-
r"^\s*" + r"\s+".join(re.escape(x) for x in expiter) + r"\s*$",
436-
self.output,
437-
)
438-
)
424+
return self._items() == expiter
439425

440426
def __contains__(self, item: str) -> bool:
441-
if self._items is not None:
442-
return item in self._items
443-
return bool(
444-
re.search(r"(^|\s)%s(\s|$)" % re.escape(item), self.output)
445-
)
427+
return item in self._items()
446428

447429
def __iter__(self) -> Iterator[str]:
448-
"""
449-
Note that iteration over items may not be accurate when items were not
450-
specified to the constructor, if individual items in the output contain
451-
whitespace. In those cases, it errs on the side of possibly returning
452-
more items than there actually are, and intends to never return fewer.
453-
"""
454-
return iter(
455-
self._items
456-
if self._items is not None
457-
else re.split(r" {2,}|\r\n", self.output.strip())
458-
)
430+
return iter(self._items())
459431

460432
def __len__(self) -> int:
461-
"""
462-
Uses __iter__, see caveat in it. While possibly inaccurate, this is
463-
good enough for truthiness checks.
464-
"""
465-
return len(list(iter(self)))
433+
return len(self._items())
466434

467435
def __repr__(self) -> str:
468-
return "<CompletionResult %s>" % list(self)
436+
return "<CompletionResult %s>" % self._items()
469437

470438

471439
def assert_complete(
@@ -527,14 +495,10 @@ def assert_complete(
527495
result = CompletionResult(output)
528496
elif got == 2:
529497
output = bash.match.group(1)
530-
result = CompletionResult(
531-
output,
532-
# Note that this causes returning the sole completion *unescaped*
533-
[shlex.split(cmd + output)[-1]],
534-
)
498+
result = CompletionResult(output)
535499
else:
536500
# TODO: warn about EOF/TIMEOUT?
537-
result = CompletionResult("", [])
501+
result = CompletionResult()
538502
finally:
539503
bash.sendintr()
540504
bash.expect_exact(PS1)
@@ -564,7 +528,7 @@ def assert_complete(
564528
def completion(request, bash: pexpect.spawn) -> CompletionResult:
565529
marker = request.node.get_closest_marker("complete")
566530
if not marker:
567-
return CompletionResult("", [])
531+
return CompletionResult()
568532
for pre_cmd in marker.kwargs.get("pre_cmds", []):
569533
assert_bash_exec(bash, pre_cmd)
570534
cmd = getattr(request.cls, "cmd", None)
@@ -630,7 +594,7 @@ def assert_complete_at_point(
630594
bash.expect(fullexpected)
631595
else:
632596
# TODO: warn about EOF/TIMEOUT?
633-
result = CompletionResult("", [])
597+
result = CompletionResult()
634598

635599
return result
636600

test/t/test_7z.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,11 @@ def test_1(self, completion):
88

99
@pytest.mark.complete("7z a ar -tzi")
1010
def test_2(self, completion):
11-
assert completion == "-tzip"
11+
assert completion == "p"
1212

1313
@pytest.mark.complete(r"7z x -wa\ ", cwd="_filedir")
1414
def test_3(self, completion):
15-
assert completion == r"-wa b/"
15+
assert completion == "b/"
1616
assert not completion.endswith(" ")
1717

1818
@pytest.mark.complete("7z x ", cwd="7z")

test/t/test_apt_get.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ def test_1(self, completion):
99

1010
@pytest.mark.complete("apt-get install ./", cwd="dpkg")
1111
def test_2(self, completion):
12-
assert completion == "./bash-completion-test-subject.deb"
12+
assert completion == "bash-completion-test-subject.deb"
1313

1414
@pytest.mark.complete("apt-get build-dep ")
1515
def test_build_dep_dirs(self, completion):

test/t/test_ccache.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,15 +12,15 @@ def test_2(self, completion):
1212

1313
@pytest.mark.complete("ccache stt")
1414
def test_3(self, completion):
15-
assert "stty" in completion
15+
assert completion == "y" or "stty" in completion
1616

1717
@pytest.mark.complete("ccache --zero-stats stt")
1818
def test_4(self, completion):
19-
assert "stty" in completion
19+
assert completion == "y" or "stty" in completion
2020

2121
@pytest.mark.complete("ccache --hel", require_cmd=True)
2222
def test_5(self, completion):
23-
assert "--help" in completion
23+
assert completion == "p" or "--help" in completion
2424

2525
@pytest.mark.complete("ccache --zero-stats sh +")
2626
def test_6(self, completion):

test/t/test_cd.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ def test_1(self, completion):
99

1010
@pytest.mark.complete("cd fo", env=dict(CDPATH="shared/default"))
1111
def test_2(self, completion):
12-
assert completion == "foo.d/"
12+
assert completion == "o.d/"
1313

1414
@pytest.mark.complete("cd fo")
1515
def test_3(self, completion):

test/t/test_chown.py

Lines changed: 9 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -31,39 +31,38 @@ def test_3(self, completion):
3131
def test_4(self, bash, part_full_user):
3232
part, full = part_full_user
3333
completion = assert_complete(bash, "chown %s" % part)
34-
assert completion == full
34+
assert completion == full[len(part) :]
3535
assert completion.endswith(" ")
3636

3737
def test_5(self, bash, part_full_user, part_full_group):
3838
_, user = part_full_user
3939
partgroup, fullgroup = part_full_group
4040
completion = assert_complete(bash, "chown %s:%s" % (user, partgroup))
41-
assert completion == "%s:%s" % (user, fullgroup)
41+
assert completion == fullgroup[len(partgroup) :]
4242
assert completion.output.endswith(" ")
4343

4444
def test_6(self, bash, part_full_group):
4545
part, full = part_full_group
4646
completion = assert_complete(bash, "chown dot.user:%s" % part)
47-
assert completion == "dot.user:%s" % full
47+
assert completion == full[len(part) :]
4848
assert completion.output.endswith(" ")
4949

5050
@pytest.mark.parametrize(
5151
"prefix",
5252
[
53-
# TODO(xfails): check escaping, whitespace
54-
pytest.param(r"funky\ user:", marks=pytest.mark.xfail),
53+
r"funky\ user:",
5554
"funky.user:",
56-
pytest.param(r"funky\.user:", marks=pytest.mark.xfail),
57-
pytest.param(r"fu\ nky.user:", marks=pytest.mark.xfail),
58-
pytest.param(r"f\ o\ o\.\bar:", marks=pytest.mark.xfail),
59-
pytest.param(r"foo\_b\ a\.r\ :", marks=pytest.mark.xfail),
55+
r"funky\.user:",
56+
r"fu\ nky.user:",
57+
r"f\ o\ o\.\bar:",
58+
r"foo\_b\ a\.r\ :",
6059
],
6160
)
6261
def test_7(self, bash, part_full_group, prefix):
6362
"""Test preserving special chars in $prefix$partgroup<TAB>."""
6463
part, full = part_full_group
6564
completion = assert_complete(bash, "chown %s%s" % (prefix, part))
66-
assert completion == "%s%s" % (prefix, full)
65+
assert completion == full[len(part) :]
6766
assert completion.output.endswith(" ")
6867

6968
def test_8(self, bash, part_full_user, part_full_group):

test/t/test_cppcheck.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,12 +20,12 @@ def test_4(self, completion):
2020

2121
@pytest.mark.complete("cppcheck --enable=al")
2222
def test_5(self, completion):
23-
assert completion == "--enable=all"
23+
assert completion == "l"
2424

2525
@pytest.mark.complete("cppcheck --enable=xx,styl")
2626
def test_6(self, completion):
27-
assert completion == "--enable=xx,style"
27+
assert completion == "e"
2828

2929
@pytest.mark.complete("cppcheck --enable=xx,yy,styl")
3030
def test_7(self, completion):
31-
assert completion == "--enable=xx,yy,style"
31+
assert completion == "e"

test/t/test_crontab.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ def test_1(self, completion):
88

99
@pytest.mark.complete("crontab -l -")
1010
def test_only_u_with_l(self, completion):
11-
assert completion == "-u"
11+
assert completion == "u"
1212

1313
@pytest.mark.complete("crontab -r -")
1414
def test_no_l_with_r(self, completion):

test/t/test_curl.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,21 +8,21 @@ def test_1(self, completion):
88

99
@pytest.mark.complete("curl -o f", cwd="shared/default/foo.d")
1010
def test_2(self, completion):
11-
assert completion == "foo"
11+
assert completion == "oo"
1212

1313
@pytest.mark.complete("curl -LRo f", cwd="shared/default/foo.d")
1414
def test_3(self, completion):
15-
assert completion == "foo"
15+
assert completion == "oo"
1616

1717
@pytest.mark.complete("curl --o f")
1818
def test_4(self, completion):
1919
assert not completion
2020

2121
@pytest.mark.complete("curl --data @", cwd="shared/default/foo.d")
2222
def test_data_atfile(self, completion):
23-
assert completion == "@foo"
23+
assert completion == "foo"
2424

2525
@pytest.mark.complete("curl --data @foo.", cwd="shared/default")
2626
def test_data_atfile_dir(self, completion):
27-
assert completion == "@foo.d/"
27+
assert completion == "d/"
2828
assert not completion.endswith(" ")

0 commit comments

Comments
 (0)