88import tempfile
99import time
1010from pathlib import Path
11- from typing import Callable , Iterable , Iterator , List , Optional , Tuple
11+ from types import TracebackType
12+ from typing import (
13+ Callable ,
14+ Dict ,
15+ Iterable ,
16+ Iterator ,
17+ List ,
18+ Optional ,
19+ Tuple ,
20+ Type ,
21+ )
1222
1323import pexpect # type: ignore[import]
1424import pytest
1525
1626PS1 = "/@"
1727MAGIC_MARK = "__MaGiC-maRKz!__"
28+ MAGIC_MARK2 = "Re8SCgEdfN"
1829
1930
2031def find_unique_completion_pair (
@@ -387,26 +398,139 @@ def assert_bash_exec(
387398 return output
388399
389400
390- def _bash_copy_variable (bash : pexpect .spawn , src_var : str , dst_var : str ):
391- assert_bash_exec (
392- bash ,
393- "if [[ ${%s+set} ]]; then %s=${%s}; else unset -v %s; fi"
394- % (src_var , dst_var , src_var , dst_var ),
395- )
401+ class bash_env_saved :
402+ def __init__ (self , bash : pexpect .spawn , sendintr : bool = False ):
403+ self .bash = bash
404+ self .cwd : Optional [str ] = None
405+ self .saved_shopt : Dict [str , int ] = {}
406+ self .saved_variables : Dict [str , int ] = {}
407+ self .sendintr = sendintr
408+
409+ def __enter__ (self ):
410+ return self
396411
412+ def __exit__ (
413+ self ,
414+ exc_type : Optional [Type [BaseException ]],
415+ exc_value : Optional [BaseException ],
416+ exc_traceback : Optional [TracebackType ],
417+ ) -> None :
418+ self ._restore_env ()
419+ return None
397420
398- def bash_save_variable (
399- bash : pexpect .spawn , varname : str , new_value : Optional [str ] = None
400- ):
401- _bash_copy_variable (bash , varname , "_bash_completion_test_" + varname )
402- if new_value :
421+ def _copy_variable (self , src_var : str , dst_var : str ):
403422 assert_bash_exec (
404- bash , "%s=%s" % (varname , shlex .quote (str (new_value )))
423+ self .bash ,
424+ "if [[ ${%s+set} ]]; then %s=${%s}; else unset -v %s; fi"
425+ % (src_var , dst_var , src_var , dst_var ),
405426 )
406427
428+ def _unset_variable (self , varname : str ):
429+ assert_bash_exec (self .bash , "unset -v %s" % varname )
430+
431+ def _save_cwd (self ):
432+ if not self .cwd :
433+ self .cwd = self .bash .cwd
434+
435+ def _check_shopt (self , name : str ):
436+ assert_bash_exec (
437+ self .bash ,
438+ '[[ $(shopt -p %s) == "${_BASHCOMP_TEST_NEWSHOPT_%s}" ]]'
439+ % (name , name ),
440+ )
441+
442+ def _unprotect_shopt (self , name : str ):
443+ if name not in self .saved_shopt :
444+ self .saved_shopt [name ] = 1
445+ assert_bash_exec (
446+ self .bash ,
447+ "_BASHCOMP_TEST_OLDSHOPT_%s=$(shopt -p %s; true)"
448+ % (name , name ),
449+ )
450+ else :
451+ self ._check_shopt (name )
407452
408- def bash_restore_variable (bash : pexpect .spawn , varname : str ):
409- _bash_copy_variable (bash , "_bash_completion_test_" + varname , varname )
453+ def _protect_shopt (self , name : str ):
454+ assert_bash_exec (
455+ self .bash ,
456+ "_BASHCOMP_TEST_NEWSHOPT_%s=$(shopt -p %s; true)" % (name , name ),
457+ )
458+
459+ 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+ )
465+
466+ def _unprotect_variable (self , varname : str ):
467+ if varname not in self .saved_variables :
468+ self .saved_variables [varname ] = 1
469+ self ._copy_variable (varname , "_BASHCOMP_TEST_OLDVAR_" + varname )
470+ else :
471+ self ._check_variable (varname )
472+
473+ def _protect_variable (self , varname : str ):
474+ self ._copy_variable (varname , "_BASHCOMP_TEST_NEWVAR_" + varname )
475+
476+ def _restore_env (self ):
477+ if self .sendintr :
478+ self .bash .sendintr ()
479+ self .bash .expect_exact (PS1 )
480+
481+ # We first go back to the original directory before restoring
482+ # variables because "cd" affects "OLDPWD".
483+ if self .cwd :
484+ self ._unprotect_variable ("OLDPWD" )
485+ assert_bash_exec (self .bash , "cd %s" % shlex .quote (str (self .cwd )))
486+ self ._protect_variable ("OLDPWD" )
487+ self .cwd = None
488+
489+ for name in self .saved_shopt :
490+ self ._check_shopt (name )
491+ assert_bash_exec (
492+ self .bash , 'eval "$_BASHCOMP_TEST_OLDSHOPT_%s"' % name
493+ )
494+ self ._unset_variable ("_BASHCOMP_TEST_OLDSHOPT_" + name )
495+ self ._unset_variable ("_BASHCOMP_TEST_NEWSHOPT_" + name )
496+ self .saved_shopt = {}
497+
498+ for varname in self .saved_variables :
499+ self ._check_variable (varname )
500+ self ._copy_variable ("_BASHCOMP_TEST_OLDVAR_" + varname , varname )
501+ self ._unset_variable ("_BASHCOMP_TEST_OLDVAR_" + varname )
502+ self ._unset_variable ("_BASHCOMP_TEST_NEWVAR_" + varname )
503+ self .saved_variables = {}
504+
505+ def chdir (self , path : str ):
506+ self ._save_cwd ()
507+ self ._unprotect_variable ("OLDPWD" )
508+ assert_bash_exec (self .bash , "cd %s" % shlex .quote (path ))
509+ self ._protect_variable ("OLDPWD" )
510+
511+ def shopt (self , name : str , value : bool ):
512+ self ._unprotect_shopt (name )
513+ if value :
514+ assert_bash_exec (self .bash , "shopt -s %s" % name )
515+ else :
516+ assert_bash_exec (self .bash , "shopt -u %s" % name )
517+ self ._protect_shopt (name )
518+
519+ def write_variable (self , varname : str , new_value : str , quote : bool = True ):
520+ if quote :
521+ new_value = shlex .quote (new_value )
522+ self ._unprotect_variable (varname )
523+ assert_bash_exec (self .bash , "%s=%s" % (varname , new_value ))
524+ self ._protect_variable (varname )
525+
526+ # TODO: We may restore the "export" attribute as well though it is
527+ # not currently tested in "diff_env"
528+ def write_env (self , envname : str , new_value : str , quote : bool = True ):
529+ if quote :
530+ new_value = shlex .quote (new_value )
531+ self ._unprotect_variable (envname )
532+ assert_bash_exec (self .bash , "export %s=%s" % (envname , new_value ))
533+ self ._protect_variable (envname )
410534
411535
412536def get_env (bash : pexpect .spawn ) -> List [str ]:
@@ -433,7 +557,7 @@ def diff_env(before: List[str], after: List[str], ignore: str):
433557 if not re .search (r"^(---|\+\+\+|@@ )" , x )
434558 # Ignore variables expected to change:
435559 and not re .search (
436- r"^[-+](_|PPID|BASH_REMATCH|_bash_completion_test_ \w+)=" ,
560+ r"^[-+](_|PPID|BASH_REMATCH|_BASHCOMP_TEST_ \w+)=" ,
437561 x ,
438562 re .ASCII ,
439563 )
@@ -520,23 +644,19 @@ def assert_complete(
520644 pass
521645 else :
522646 pytest .xfail (xfail )
523- cwd = kwargs .get ("cwd" )
524- if cwd :
525- bash_save_variable (bash , "OLDPWD" )
526- assert_bash_exec (bash , "cd '%s'" % cwd )
527- env_prefix = "_BASHCOMP_TEST_"
528- env = kwargs .get ("env" , {})
529- if env :
530- # Back up environment and apply new one
531- assert_bash_exec (
532- bash ,
533- " " .join ('%s%s="${%s-}"' % (env_prefix , k , k ) for k in env .keys ()),
534- )
535- assert_bash_exec (
536- bash ,
537- "export %s" % " " .join ("%s=%s" % (k , v ) for k , v in env .items ()),
538- )
539- try :
647+
648+ with bash_env_saved (bash , sendintr = True ) as bash_env :
649+
650+ cwd = kwargs .get ("cwd" )
651+ if cwd :
652+ bash_env .chdir (str (cwd ))
653+
654+ for k , v in kwargs .get ("env" , {}).items ():
655+ bash_env .write_env (k , v , quote = False )
656+
657+ for k , v in kwargs .get ("shopt" , {}).items ():
658+ bash_env .shopt (k , v )
659+
540660 bash .send (cmd + "\t " )
541661 # Sleep a bit if requested, to avoid `.*` matching too early
542662 time .sleep (kwargs .get ("sleep_after_tab" , 0 ))
@@ -558,37 +678,13 @@ def assert_complete(
558678 output = bash .before
559679 if output .endswith (MAGIC_MARK ):
560680 output = bash .before [: - len (MAGIC_MARK )]
561- result = CompletionResult (output )
681+ return CompletionResult (output )
562682 elif got == 2 :
563683 output = bash .match .group (1 )
564- result = CompletionResult (output )
684+ return CompletionResult (output )
565685 else :
566686 # TODO: warn about EOF/TIMEOUT?
567- result = CompletionResult ()
568- finally :
569- bash .sendintr ()
570- bash .expect_exact (PS1 )
571- if env :
572- # Restore environment, and clean up backup
573- # TODO: Test with declare -p if a var was set, backup only if yes, and
574- # similarly restore only backed up vars. Should remove some need
575- # for ignore_env.
576- assert_bash_exec (
577- bash ,
578- "export %s"
579- % " " .join (
580- '%s="$%s%s"' % (k , env_prefix , k ) for k in env .keys ()
581- ),
582- )
583- assert_bash_exec (
584- bash ,
585- "unset -v %s"
586- % " " .join ("%s%s" % (env_prefix , k ) for k in env .keys ()),
587- )
588- if cwd :
589- assert_bash_exec (bash , "cd - >/dev/null" )
590- bash_restore_variable (bash , "OLDPWD" )
591- return result
687+ return CompletionResult ()
592688
593689
594690@pytest .fixture
0 commit comments