Skip to content

Commit d1c13da

Browse files
authored
Merge pull request #529 from akinomyoga/fix-test_man
2 parents a7aa054 + 7e65989 commit d1c13da

File tree

3 files changed

+152
-60
lines changed

3 files changed

+152
-60
lines changed

bash_completion

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2369,8 +2369,12 @@ fi
23692369
unset compat_dir i _blacklist_glob
23702370
23712371
# source user completion file
2372+
#
2373+
# Remark: We explicitly check that $user_completion is not '/dev/null' since
2374+
# /dev/null may be a regular file in broken systems and can contain arbitrary
2375+
# garbages of suppressed command outputs.
23722376
user_completion=${BASH_COMPLETION_USER_FILE:-~/.bash_completion}
2373-
[[ ${BASH_SOURCE[0]} != "$user_completion" && -r $user_completion && -f $user_completion ]] &&
2377+
[[ $user_completion != "${BASH_SOURCE[0]}" && $user_completion != /dev/null && -r $user_completion && -f $user_completion ]] &&
23742378
. $user_completion
23752379
unset user_completion
23762380

test/t/conftest.py

Lines changed: 144 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -186,29 +186,55 @@ def partialize(
186186
def bash(request) -> pexpect.spawn:
187187

188188
logfile = None
189+
histfile = None
190+
tmpdir = None
191+
bash = None
192+
189193
if os.environ.get("BASHCOMP_TEST_LOGFILE"):
190194
logfile = open(os.environ["BASHCOMP_TEST_LOGFILE"], "w")
191195
elif os.environ.get("CI"):
192196
logfile = sys.stdout
197+
193198
testdir = os.path.abspath(
194199
os.path.join(os.path.dirname(__file__), os.pardir)
195200
)
196-
env = os.environ.copy()
197-
env.update(
198-
dict(
199-
SRCDIR=testdir, # TODO needed at least by bashrc
200-
SRCDIRABS=testdir,
201-
PS1=PS1,
202-
INPUTRC="%s/config/inputrc" % testdir,
203-
TERM="dumb",
204-
LC_COLLATE="C", # to match Python's default locale unaware sort
205-
HISTFILE="/dev/null", # to leave user's history file alone
206-
)
201+
202+
# Create an empty temporary file for HISTFILE.
203+
#
204+
# To prevent the tested Bash processes from writing to the user's
205+
# history file or any other files, we prepare an empty temporary
206+
# file for each test.
207+
#
208+
# - Note that HISTFILE=/dev/null may not work. It results in the
209+
# removal of the device /dev/null and the creation of a regular
210+
# file at /dev/null when the number of commands reach
211+
# HISTFILESIZE by Bash-4.3's bug. This causes the execution of
212+
# garbages through BASH_COMPLETION_USER_FILE=/dev/null. - Note
213+
# also that "unset -v HISTFILE" in "test/config/bashrc" was not
214+
# adopted because "test/config/bashrc" is loaded after the
215+
# history is read from the history file.
216+
#
217+
histfile = tempfile.NamedTemporaryFile(
218+
prefix="bash-completion-test_", delete=False
207219
)
208220

209-
tmpdir = None
210-
bash = None
211221
try:
222+
# release the file handle so that Bash can open the file.
223+
histfile.close()
224+
225+
env = os.environ.copy()
226+
env.update(
227+
dict(
228+
SRCDIR=testdir, # TODO needed at least by bashrc
229+
SRCDIRABS=testdir,
230+
PS1=PS1,
231+
INPUTRC="%s/config/inputrc" % testdir,
232+
TERM="dumb",
233+
LC_COLLATE="C", # to match Python's default locale unaware sort
234+
HISTFILE=histfile.name,
235+
)
236+
)
237+
212238
marker = request.node.get_closest_marker("bashcomp")
213239

214240
# Set up the current working directory
@@ -306,6 +332,11 @@ def bash(request) -> pexpect.spawn:
306332
bash.close()
307333
if tmpdir:
308334
tmpdir.cleanup()
335+
if histfile:
336+
try:
337+
os.remove(histfile.name)
338+
except OSError:
339+
pass
309340
if logfile and logfile != sys.stdout:
310341
logfile.close()
311342

@@ -399,13 +430,21 @@ def assert_bash_exec(
399430

400431

401432
class bash_env_saved:
433+
counter: int = 0
434+
402435
def __init__(self, bash: pexpect.spawn, sendintr: bool = False):
436+
bash_env_saved.counter += 1
437+
self.prefix: str = "_BASHCOMP_TEST%d" % bash_env_saved.counter
438+
403439
self.bash = bash
404-
self.cwd: Optional[str] = None
440+
self.cwd_changed: bool = False
405441
self.saved_shopt: Dict[str, int] = {}
406442
self.saved_variables: Dict[str, int] = {}
407443
self.sendintr = sendintr
408444

445+
self.noexcept: bool = False
446+
self.captured_error: Optional[Exception] = None
447+
409448
def __enter__(self):
410449
return self
411450

@@ -418,111 +457,158 @@ def __exit__(
418457
self._restore_env()
419458
return None
420459

460+
def _safe_sendintr(self):
461+
try:
462+
self.bash.sendintr()
463+
self.bash.expect_exact(PS1)
464+
except Exception as e:
465+
if self.noexcept:
466+
self.captured_error = e
467+
else:
468+
raise
469+
470+
def _safe_exec(self, cmd: str):
471+
try:
472+
self.bash.sendline(cmd)
473+
self.bash.expect_exact(cmd)
474+
self.bash.expect_exact("\r\n" + PS1)
475+
except Exception as e:
476+
if self.noexcept:
477+
self._safe_sendintr()
478+
self.captured_error = e
479+
else:
480+
raise
481+
482+
def _safe_assert(self, cmd: str):
483+
try:
484+
assert_bash_exec(self.bash, cmd, want_output=None)
485+
except Exception as e:
486+
if self.noexcept:
487+
self._safe_sendintr()
488+
self.captured_error = e
489+
else:
490+
raise
491+
421492
def _copy_variable(self, src_var: str, dst_var: str):
422-
assert_bash_exec(
423-
self.bash,
493+
self._safe_exec(
424494
"if [[ ${%s+set} ]]; then %s=${%s}; else unset -v %s; fi"
425495
% (src_var, dst_var, src_var, dst_var),
426496
)
427497

428498
def _unset_variable(self, varname: str):
429-
assert_bash_exec(self.bash, "unset -v %s" % varname)
499+
self._safe_exec("unset -v %s" % varname)
430500

431501
def _save_cwd(self):
432-
if not self.cwd:
433-
self.cwd = self.bash.cwd
502+
if not self.cwd_changed:
503+
self.cwd_changed = True
504+
self._copy_variable("PWD", "%s_OLDPWD" % self.prefix)
434505

435506
def _check_shopt(self, name: str):
436-
assert_bash_exec(
437-
self.bash,
438-
'[[ $(shopt -p %s) == "${_BASHCOMP_TEST_NEWSHOPT_%s}" ]]'
439-
% (name, name),
507+
self._safe_assert(
508+
'[[ $(shopt -p %s) == "${%s_NEWSHOPT_%s}" ]]'
509+
% (name, self.prefix, name),
440510
)
441511

442512
def _unprotect_shopt(self, name: str):
443513
if name not in self.saved_shopt:
444514
self.saved_shopt[name] = 1
445-
assert_bash_exec(
446-
self.bash,
447-
"_BASHCOMP_TEST_OLDSHOPT_%s=$(shopt -p %s; true)"
448-
% (name, name),
515+
self._safe_exec(
516+
"%s_OLDSHOPT_%s=$(shopt -p %s || true)"
517+
% (self.prefix, name, name),
449518
)
450519
else:
451520
self._check_shopt(name)
452521

453522
def _protect_shopt(self, name: str):
454-
assert_bash_exec(
455-
self.bash,
456-
"_BASHCOMP_TEST_NEWSHOPT_%s=$(shopt -p %s; true)" % (name, name),
523+
self._safe_exec(
524+
"%s_NEWSHOPT_%s=$(shopt -p %s || true)"
525+
% (self.prefix, name, name),
457526
)
458527

459528
def _check_variable(self, varname: str):
460-
assert_bash_exec(
461-
self.bash,
462-
'[[ ${%s-%s} == "${_BASHCOMP_TEST_NEWVAR_%s-%s}" ]]'
463-
% (varname, MAGIC_MARK2, varname, MAGIC_MARK2),
464-
)
529+
try:
530+
self._safe_assert(
531+
'[[ ${%s-%s} == "${%s_NEWVAR_%s-%s}" ]]'
532+
% (varname, MAGIC_MARK2, self.prefix, varname, MAGIC_MARK2),
533+
)
534+
except Exception:
535+
self._copy_variable(
536+
"%s_NEWVAR_%s" % (self.prefix, varname), varname
537+
)
538+
raise
539+
else:
540+
if self.noexcept and self.captured_error:
541+
self._copy_variable(
542+
"%s_NEWVAR_%s" % (self.prefix, varname), varname
543+
)
465544

466545
def _unprotect_variable(self, varname: str):
467546
if varname not in self.saved_variables:
468547
self.saved_variables[varname] = 1
469-
self._copy_variable(varname, "_BASHCOMP_TEST_OLDVAR_" + varname)
548+
self._copy_variable(
549+
varname, "%s_OLDVAR_%s" % (self.prefix, varname)
550+
)
470551
else:
471552
self._check_variable(varname)
472553

473554
def _protect_variable(self, varname: str):
474-
self._copy_variable(varname, "_BASHCOMP_TEST_NEWVAR_" + varname)
555+
self._copy_variable(varname, "%s_NEWVAR_%s" % (self.prefix, varname))
475556

476557
def _restore_env(self):
558+
self.noexcept = True
559+
477560
if self.sendintr:
478-
self.bash.sendintr()
479-
self.bash.expect_exact(PS1)
561+
self._safe_sendintr()
480562

481563
# We first go back to the original directory before restoring
482564
# variables because "cd" affects "OLDPWD".
483-
if self.cwd:
565+
if self.cwd_changed:
484566
self._unprotect_variable("OLDPWD")
485-
assert_bash_exec(
486-
self.bash, "command cd -- %s" % shlex.quote(str(self.cwd))
487-
)
567+
self._safe_exec('command cd -- "$%s_OLDPWD"' % self.prefix)
488568
self._protect_variable("OLDPWD")
489-
self.cwd = None
569+
self._unset_variable("%s_OLDPWD" % self.prefix)
570+
self.cwd_changed = False
490571

491572
for name in self.saved_shopt:
492573
self._check_shopt(name)
493-
assert_bash_exec(
494-
self.bash, 'eval "$_BASHCOMP_TEST_OLDSHOPT_%s"' % name
495-
)
496-
self._unset_variable("_BASHCOMP_TEST_OLDSHOPT_" + name)
497-
self._unset_variable("_BASHCOMP_TEST_NEWSHOPT_" + name)
574+
self._safe_exec('eval "$%s_OLDSHOPT_%s"' % (self.prefix, name))
575+
self._unset_variable("%s_OLDSHOPT_%s" % (self.prefix, name))
576+
self._unset_variable("%s_NEWSHOPT_%s" % (self.prefix, name))
498577
self.saved_shopt = {}
499578

500579
for varname in self.saved_variables:
501580
self._check_variable(varname)
502-
self._copy_variable("_BASHCOMP_TEST_OLDVAR_" + varname, varname)
503-
self._unset_variable("_BASHCOMP_TEST_OLDVAR_" + varname)
504-
self._unset_variable("_BASHCOMP_TEST_NEWVAR_" + varname)
581+
self._copy_variable(
582+
"%s_OLDVAR_%s" % (self.prefix, varname), varname
583+
)
584+
self._unset_variable("%s_OLDVAR_%s" % (self.prefix, varname))
585+
self._unset_variable("%s_NEWVAR_%s" % (self.prefix, varname))
505586
self.saved_variables = {}
506587

588+
self.noexcept = False
589+
if self.captured_error:
590+
raise self.captured_error
591+
507592
def chdir(self, path: str):
508593
self._save_cwd()
594+
self.cwd_changed = True
509595
self._unprotect_variable("OLDPWD")
510-
assert_bash_exec(self.bash, "command cd -- %s" % shlex.quote(path))
596+
self._safe_exec("command cd -- %s" % shlex.quote(path))
511597
self._protect_variable("OLDPWD")
512598

513599
def shopt(self, name: str, value: bool):
514600
self._unprotect_shopt(name)
515601
if value:
516-
assert_bash_exec(self.bash, "shopt -s %s" % name)
602+
self._safe_exec("shopt -s %s" % name)
517603
else:
518-
assert_bash_exec(self.bash, "shopt -u %s" % name)
604+
self._safe_exec("shopt -u %s" % name)
519605
self._protect_shopt(name)
520606

521607
def write_variable(self, varname: str, new_value: str, quote: bool = True):
522608
if quote:
523609
new_value = shlex.quote(new_value)
524610
self._unprotect_variable(varname)
525-
assert_bash_exec(self.bash, "%s=%s" % (varname, new_value))
611+
self._safe_exec("%s=%s" % (varname, new_value))
526612
self._protect_variable(varname)
527613

528614
# TODO: We may restore the "export" attribute as well though it is
@@ -531,7 +617,7 @@ def write_env(self, envname: str, new_value: str, quote: bool = True):
531617
if quote:
532618
new_value = shlex.quote(new_value)
533619
self._unprotect_variable(envname)
534-
assert_bash_exec(self.bash, "export %s=%s" % (envname, new_value))
620+
self._safe_exec("export %s=%s" % (envname, new_value))
535621
self._protect_variable(envname)
536622

537623

@@ -559,7 +645,7 @@ def diff_env(before: List[str], after: List[str], ignore: str):
559645
if not re.search(r"^(---|\+\+\+|@@ )", x)
560646
# Ignore variables expected to change:
561647
and not re.search(
562-
r"^[-+](_|PPID|BASH_REMATCH|_BASHCOMP_TEST_\w+)=",
648+
r"^[-+](_|PPID|BASH_REMATCH|_BASHCOMP_TEST\w+)=",
563649
x,
564650
re.ASCII,
565651
)

test/t/test_man.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
assert_bash_exec,
55
assert_complete,
66
bash_env_saved,
7+
is_bash_type,
78
prepare_fixture_dir,
89
)
910

@@ -105,8 +106,9 @@ def test_8(self, completion):
105106
def test_9(self, bash, completion):
106107
assert self.assumed_present in completion
107108

108-
@pytest.mark.complete(require_cmd=True)
109109
def test_10(self, request, bash, colonpath):
110+
if not is_bash_type(bash, "man"):
111+
pytest.skip("Command not found")
110112
with bash_env_saved(bash) as bash_env:
111113
bash_env.write_env(
112114
"MANPATH",

0 commit comments

Comments
 (0)