Minority Opinions

Not everyone can be mainstream, after all.

Bash Completion for Nosetests

leave a comment »

I won’t claim nosetests as the epitome of testing in Python, but it does have a few good features.  However, I’ve found myself unreasonably annoyed that the output uses a completely different format for test names than the command line.  If they were the same, then I could copy and paste them (in any of four different ways, but that’s another story) to quickly run a failing test on its own.

Eventually, I realized that a decent second option would be to have the test cases included in the shell’s command-line completion.  However, my operating system doesn’t include any completion at all for it.  A quick web search also failed to find anything that did exactly what I want.  So, working from examples in /etc/bash_completion.d/, I wrote my own:

shopt -s extglob

__nosewords()
{
  local file base pattern words
  fname="$1"
  base="$2"
  cname=."$fname".nwc

  if [ "$cname" -ot "$fname" ]
  then
    nosetests -v --collect-only "$fname" 2> "$cname"
  fi

  case "$base" in
    *.*)
      pattern='s/^\(\w\+\) (\w\+\.\(\w\+\)).*$/\2.\1/;s/^\w\+\.\(\w\+\) .*/\1 /';;
    *)
      pattern='s/^\(\w\+\) (\w\+\.\(\w\+\)).*$/\2/;s/^\w\+\.\(\w\+\) .*/\1 /';;
  esac
  words=$(grep 'ok$' "$cname" | sed "$pattern" | uniq)
  compgen -W "${words}" -- "$base"
}

_nosetests()
{
  COMPREPLY=()
  local cur fname base
  compopt -o nospace
  cur="${COMP_WORDS[COMP_CWORD]}"
  case "$cur" in
    :)
      # Look at the previous COMP_WORD for the file name.
      fname="${COMP_WORDS[COMP_CWORD-1]}"
      COMPREPLY=( $(__nosewords $fname "") )
      return 0;;
    *:*)
      # For when the colon doesn't separate words.
      fname=${cur%%:*}
      base=${cur#*:}
      COMPREPLY=( $(__nosewords "$fname" "$base" | sed "s|^|$fname:|") )
      return 0;;
    *.py)
      # The filename was already complete, so we want a test case or suite.
      COMPREPLY=( "$cur:" )
      #COMPREPLY=( $(__nosewords "$cur" "" | sed "s|^|$fname:|") )
      return 0;;
    *)
      if [[ "${COMP_WORDS[COMP_CWORD-1]}" == ":" ]]
      then
        # Complete the test part, with the file earlier.
        fname="${COMP_WORDS[COMP_CWORD-2]}"
        COMPREPLY=( $(__nosewords "$fname" "$cur") )
        if [[ "${COMPREPLY[@]}" == "$cur" ]]
        then
          # Add the dot automatically, and any known prefix beyond that.
          COMPREPLY=( $(__nosewords "$fname" "$cur.") )
        fi
      else
        # Match the filename, limiting to test case modules.
        COMPREPLY=( $(compgen -f -X '!test*.py' -- "$cur") )
      fi
      return 0;;
  esac
}

complete -F _nosetests nosetests

To be honest, I’m not sure if the extglob setting is still necessary; it might be left over from before I knew about the -f and -X flags of compgen. It doesn’t hurt anything for me, but feel free to remove it.

There are a few tricks here to work with both TestCase class and simple function test cases, including inheritance in the former; that’s the primary reason it relies on nosetests itself for case collection, instead of parsing the files directly. For speed, it caches the result of that call, for as long as the file is untouched; considering that tests almost always have a common prefix, it’s a big help even in a single test name. Unfortunately, it still has issues with generated test cases, and the caching could be a problem if the file is in a subdirectory.

I might sometime enhance it with flag completion, and test the case where colon doesn’t split words, but I may already have spent more time on this script than it’s saved.

Advertisements

Written by eswald

13 Mar 2012 at 10:04 pm

Posted in Python, Technology

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s