15

I would like to write a Unit Test for a (rather complex) Bash completion script, preferrably with Python - just something that gets the values of a Bash completion programmatically. The test should look like this:

def test_completion():
  # trigger_completion should return what a user should get on triggering 
  # Bash completion like this: 'pbt createkvm<TAB>' 
  assert trigger_completion('pbt createkvm') == "module1 module2 module3" 

How can I simulate Bash completion programmatically to check the completion values inside a testsuite for my tool?

Wolkenarchitekt
  • 20,170
  • 29
  • 111
  • 174
  • 2
    Does [this](http://stackoverflow.com/questions/3520936/accesssing-bash-completions-for-specific-commands-programmatically) helps? I haven't tried it myself though. – Vikas Feb 04 '12 at 01:35
  • 1
    If you want to simulate user interaction, `expect` is your friend - do your fs/env setup in python and then run bash from within an expect script with subprocess. The escaping can be tricky, but otherwise it's pretty straightforward. – je4d Feb 10 '12 at 21:04
  • @je4d - What is expect in this context? – Lou Dec 10 '20 at 16:49

2 Answers2

9

Say you have a bash-completion script in a file called asdf-completion, containing:

_asdf() {
COMPREPLY=()
local cur prev
cur=$(_get_cword)
COMPREPLY=( $( compgen -W "one two three four five six" -- "$cur") )
return 0
}    
complete -F _asdf asdf

This uses the shell function _asdf to provide completions for the fictional asdf command. If we set the right environment variables (from the bash man page), then we can get the same result, which is the placement of the potential expansions into the COMPREPLY variable. Here's an example of doing that in a unittest:

import subprocess
import unittest

class BashTestCase(unittest.TestCase):
    def test_complete(self):
        completion_file="asdf-completion"
        partial_word="f"
        cmd=["asdf", "other", "arguments", partial_word]
        cmdline = ' '.join(cmd)

        out = subprocess.Popen(['bash', '-i', '-c',
            r'source {compfile}; COMP_LINE="{cmdline}" COMP_WORDS=({cmdline}) COMP_CWORD={cword} COMP_POINT={cmdlen} $(complete -p {cmd} | sed "s/.*-F \\([^ ]*\\) .*/\\1/") && echo ${{COMPREPLY[*]}}'.format(
                compfile=completion_file, cmdline=cmdline, cmdlen=len(cmdline), cmd=cmd[0], cword=cmd.index(partial_word)
                )],
            stdout=subprocess.PIPE)
        stdout, stderr = out.communicate()
        self.assertEqual(stdout, "four five\n")

if (__name__=='__main__'):
    unittest.main()

This should work for any completions that use -F, but may work for others as well.

je4d's comment to use expect is a good one for a more complete test.

bonsaiviking
  • 5,825
  • 1
  • 20
  • 35
  • So if I'm reading your sed correctly (and I doubt it) ... you're grabbing the output of `complete -p {cmd}` and stripping everything up to `complete -F`, preserving the name of the function without a leading space? So if your `complete -p {cmd}` outputted `complete -F _comp_func somecmd` then your sed would get `_comp_func` ... am I close? – Lou Dec 10 '20 at 17:27
  • 1
    @Lou That's right. The Python string formatting places the parameter strings into environment variables in the shell command. The command itself reads in the file with the completion functions, then invokes the completion function (extracted as you described) in the same way that Bash would internally. Finally, it echos out the COMPREPLY variable so that `subprocess.communicate()` can read it on STDOUT. – bonsaiviking Dec 10 '20 at 20:37
  • Ah great, thanks for the clarification! Seds can be hard to read if you haven't written them I think :) – Lou Dec 11 '20 at 09:44
1

bonsaiviking's solution almost worked for me. I had to change the bash string script. I added an extra ';' separator to the executed bash script otherwise the execution wouldn't work on Mac OS X. Not really sure why.

I also generalized the initialization of the various COMP_ arguments a bit to handle the various cases I ended up with.

The final solution is a helper class to test bash completion from python so that the above test would be written as:

from completion import BashCompletionTest

class AdsfTestCase(BashCompletionTest):
    def test_orig(self):
        self.run_complete("other arguments f", "four five")

    def run_complete(self, command, expected):
        completion_file="adsf-completion"
        program="asdf"
        super(AdsfTestCase, self).run_complete(completion_file, program, command, expected)


if (__name__=='__main__'):
    unittest.main()

The completion lib is located under https://github.com/lacostej/unity3d-bash-completion/blob/master/lib/completion.py

coffeebreaks
  • 3,787
  • 1
  • 26
  • 25