Commits

Steve Losh committed 8fc2f25 Merge

Merge.

Comments (0)

Files changed (9)

+syntax: glob
+
+*.orig
+*.rej
+*~
+*.mergebackup
+*.o
+*.so
+*.dll
+*.py[cdo]
+*$py.class
+__pycache__
+*.swp
+*.prof
+\#*\#
+.\#*
+.coverage
+*,cover
+htmlcov
+tests/*.err
+examples/*.err
+build
+dist
+MANIFEST
+cram.egg-info
+.DS_Store
+tags
+cscope.*
+
+syntax: regexp
+^\.pc/
+^\.(pydev)?project
 995a287114b0a2a0bcd79b9c5ce8ff98765e7c8a 0.2
 924d14e0636a7ff5815c2412409115a69dfc63f0 0.3
 3ba61fadf306c63ec4bc3254522f286a27ac974a 0.4
+112e96e43892344954a98b0f05a32819f2b6c20d 0.5
 
 tests:
 ifeq ($(PYTHON),all)
-	python2.4 setup.py -q test
-	python2.5 setup.py -q test
-	python2.6 setup.py -q test
-	python2.7 setup.py -q test
-	python3.1 setup.py -q test
-	python3.2 setup.py -q test
+	python2.4 -tt setup.py -q test
+	python2.5 -tt setup.py -q test
+	python2.6 -tt -3 setup.py -q test
+	python2.7 -tt -3 setup.py -q test
+	python3.1 -tt -bb setup.py -q test
+	python3.2 -tt -bb setup.py -q test
 else
-	$(PYTHON) setup.py -q test
+	$(PYTHON) -tt setup.py -q test
 endif
 
 coverage:
 
 Here's a snippet from `Cram's own test suite`_::
 
-    The $PYTHON environment variable should be set when running this
-    test from Python.
+    The $PYTHON environment variable should be set when running this test
+    from Python.
 
       $ [ -n "$PYTHON" ] || PYTHON="`which python`"
       $ if [ -n "$COVERAGE" ]; then
       >   coverage erase
-      >   alias cram="`which coverage` run -a $TESTDIR/../cram.py"
+      >   alias cram="`which coverage` run --branch -a $TESTDIR/../cram.py"
       > else
       >   alias cram="$PYTHON $TESTDIR/../cram.py"
       > fi
       [Uu]sage: cram \[OPTIONS\] TESTS\.\.\. (re)
 
       [Oo]ptions: (re)
-        -h, --help         show this help message and exit
-        -V, --version      show version information and exit
-        -q, --quiet        don't print diffs
-        -v, --verbose      show filenames and test status
-        -i, --interactive  interactively merge changed test output
-        -y, --yes          answer yes to all questions
-        -n, --no           answer no to all questions
-        --keep-tmpdir      keep temporary directories
-        -E                 don't reset common environment variables
+        -h, --help          show this help message and exit
+        -V, --version       show version information and exit
+        -q, --quiet         don't print diffs
+        -v, --verbose       show filenames and test status
+        -i, --interactive   interactively merge changed test output
+        -y, --yes           answer yes to all questions
+        -n, --no            answer no to all questions
+        -E, --preserve-env  don't reset common environment variables
+        --keep-tmpdir       keep temporary directories
+        --shell=PATH        shell to use for running tests
+        --indent=NUM        number of spaces to use for indentation
 
 The format in a nutshell:
 
 * Lines ending with a space and the keyword ``(no-eol)`` will match
   actual output that doesn't end in a newline.
 
-* Actual output containing unprintable characters are escaped and
-  suffixed with a space and the keyword ``(esc)``. Lines matching
+* Actual output lines containing unprintable characters are escaped
+  and suffixed with a space and the keyword ``(esc)``. Lines matching
   unprintable output must also contain the keyword.
 
 * Anything else is a comment.
 ``--interactive``, you'll be prompted to merge the actual output back
 into the test. This makes it easy to quickly prototype new tests.
 
+You can specify a default set of options by creating a ``.cramrc``
+file. For example::
+
+    [cram]
+    verbose = True
+    indent = 4
+
+Is the same as invoking Cram with ``--verbose`` and ``--indent=4``.
+
+To change what configuration file Cram loads, you can set the
+``CRAMRC`` environment variable. You can also specify command line
+options in the ``CRAM`` environment variable.
+
 Note that the following environment variables are reset before tests
 are run:
 
 News
 ----
 
+Version 0.6
+```````````
+* Added support for specifying options in ``.cramrc`` (configurable
+  with the ``CRAMRC`` environment variable).
+
+* Added a ``--shell`` option to change the shell tests are run
+  with. Contributed by `Kamil Kisiel`_.
+
+* Added the long option ``--preserve-env`` for ``-E``.
+
+.. _Kamil Kisiel: http://kamilkisiel.net/
+
 Version 0.5 (Jan. 8, 2011)
 ``````````````````````````
 * **The test format has changed:** Matching output not ending in a
-    newline now requires the ``(no-eol)`` keyword instead of ending
-    the line in ``%``.
+  newline now requires the ``(no-eol)`` keyword instead of ending the
+  line in ``%``.
 
 * Matching output containing unprintable characters now requires the
   ``(esc)`` keyword. Real output containing unprintable characters
 
 * Tests are no longer required to end in a trailing newline.
 
-
 Version 0.4 (Sep. 28, 2010)
 ```````````````````````````
 * **The test format has changed:** Output lines containing regular
+# Maintainer: Andrey Vlasovskikh <andrey.vlasovskikh@gmail.com>
+
+pkgname=cram
+pkgver=0.5
+pkgrel=1
+pkgdesc="A simple testing framework for command line applications"
+arch=(any)
+url="http://bitheap.org/cram/"
+license=('GPL')
+depends=('python2')
+source=("http://pypi.python.org/packages/source/c/cram/cram-$pkgver.tar.gz")
+md5sums=('3b484aa4569e479a267c45841a0d429d')
+
+build() {
+    cd "$srcdir/$pkgname-$pkgver"
+    python2 setup.py build || return 1
+    python2 setup.py install --root=$pkgdir --optimize=1 || return 1
+}
 
 import difflib
 import itertools
+import locale
+import optparse
 import os
 import re
+import signal
 import subprocess
 import sys
 import shutil
 import time
 import tempfile
 
+try:
+    import configparser
+except ImportError:
+    import ConfigParser as configparser
+
 __all__ = ['main', 'test']
 
 def findtests(paths):
     """Like the string-escape codec, but doesn't escape quotes"""
     return escapesub(lambda m: escapemap[m.group(0)], s[:-1]) + ' (esc)\n'
 
-def test(path, indent=2):
+def makeresetsigpipe():
+    """Make a function to reset SIGPIPE to SIG_DFL (for use in subprocesses).
+
+    Doing subprocess.Popen(..., preexec_fn=makeresetsigpipe()) will prevent
+    Python's SIGPIPE handler (SIG_IGN) from being inherited by the
+    child process.
+    """
+    if sys.platform == 'win32' or getattr(signal, 'SIGPIPE', None) is None:
+        return None
+    return lambda: signal.signal(signal.SIGPIPE, signal.SIG_DFL)
+
+def encodeinput(s):
+    """Encode s so it can be used as subprocess input.
+
+    For Python 2, this returns the string as-is. For Python 3, it encodes it
+    based on the locale (to match open() decoding in the same manner).
+    """
+    # This is absurd. Surely there's a better way?!
+    if sys.platform == 'win32' or sys.version_info == 2:
+        return s
+    return s.encode(locale.getpreferredencoding())
+
+def test(path, shell, indent=2):
     """Run test at path and return input, output, and diff.
 
     This returns a 3-tuple containing the following:
     abspath = os.path.abspath(path)
     env = os.environ.copy()
     env['TESTDIR'] = os.path.dirname(abspath)
-    p = subprocess.Popen(['/bin/sh', '-'], bufsize=-1, stdin=subprocess.PIPE,
+    p = subprocess.Popen([shell, '-'], bufsize=-1, stdin=subprocess.PIPE,
                          stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
                          universal_newlines=True, env=env,
+                         preexec_fn=makeresetsigpipe(),
                          close_fds=os.name == 'posix')
     salt = 'CRAM%s' % time.time()
 
     after = {}
     refout, postout = [], []
     i = pos = prepos = -1
+    stdin = []
     for i, line in enumerate(f):
         refout.append(line)
         if line.startswith(cmdline):
             after.setdefault(pos, []).append(line)
             prepos = pos
             pos = i
-            p.stdin.write('echo "\n%s %s $?"\n' % (salt, i))
-            p.stdin.write(line[len(cmdline):])
+            stdin.append('echo "\n%s %s $?"\n' % (salt, i))
+            stdin.append(line[len(cmdline):])
         elif line.startswith(conline):
             after.setdefault(prepos, []).append(line)
-            p.stdin.write(line[len(conline):])
+            stdin.append(line[len(conline):])
         elif not line.startswith(indent):
             after.setdefault(pos, []).append(line)
-    p.stdin.write('echo "\n%s %s $?"\n' % (salt, i + 1))
+    stdin.append('echo "\n%s %s $?"\n' % (salt, i + 1))
 
-    output = p.communicate()[0]
+    output = p.communicate(input=encodeinput(''.join(stdin)))[0]
     if p.returncode == 80:
         return (refout, None, [])
 
 def patch(cmd, diff):
     """Run echo [lines from diff] | cmd -p0"""
     p = subprocess.Popen([cmd, '-p0'], bufsize=-1, stdin=subprocess.PIPE,
-                         universal_newlines=True, close_fds=os.name == 'posix')
-    for line in diff:
-        p.stdin.write(line)
-    p.stdin.close()
-    p.wait()
+                         universal_newlines=True,
+                         preexec_fn=makeresetsigpipe(),
+                         close_fds=os.name == 'posix')
+    p.communicate(''.join(diff))
     return p.returncode == 0
 
-def run(paths, tmpdir, quiet=False, verbose=False, patchcmd=None, answer=None,
-        indent=2, color=False):
+def run(paths, tmpdir, shell, quiet=False, verbose=False, patchcmd=None,
+        answer=None, indent=2, color=False):
     """Run tests in paths in tmpdir.
 
     If quiet is True, diffs aren't printed. If verbose is True,
             os.mkdir(testdir)
             try:
                 os.chdir(testdir)
-                refout, postout, diff = test(abspath, indent)
+                refout, postout, diff = test(abspath, shell, indent)
             finally:
                 os.chdir(cwd)
 
     """Return the patch to cmd or None if not found"""
     for p in os.environ['PATH'].split(os.pathsep):
         path = os.path.join(p, cmd)
-        if os.path.exists(path) and os.access(path, os.X_OK):
+        if os.access(path, os.X_OK):
             return path
     return None
 
+def expandpath(path):
+    """Expands ~ and environment variables in path"""
+    return os.path.expanduser(os.path.expandvars(path))
+
+class OptionParser(optparse.OptionParser):
+    """Like optparse.OptionParser, but supports setting values through
+    CRAM= and .cramrc."""
+
+    def __init__(self, *args, **kwargs):
+        self._config_opts = {}
+        optparse.OptionParser.__init__(self, *args, **kwargs)
+
+    def add_option(self, *args, **kwargs):
+        option = optparse.OptionParser.add_option(self, *args, **kwargs)
+        if option.dest and option.dest != 'version':
+            key = option.dest.replace('_', '-')
+            self._config_opts[key] = option.action == 'store_true'
+        return option
+
+    def parse_args(self, args=None, values=None):
+        config = configparser.RawConfigParser()
+        config.read(expandpath(os.environ.get('CRAMRC', '.cramrc')))
+        defaults = {}
+        for key, isbool in self._config_opts.items():
+            try:
+                if isbool:
+                    try:
+                        value = config.getboolean('cram', key)
+                    except ValueError:
+                        value = config.get('cram', key)
+                        self.error('--%s: invalid boolean value: %r'
+                                   % (key, value))
+                else:
+                    value = config.get('cram', key)
+            except (configparser.NoSectionError, configparser.NoOptionError):
+                pass
+            else:
+                defaults[key] = value
+        self.set_defaults(**defaults)
+
+        eargs = os.environ.get('CRAM', '').strip()
+        if eargs:
+            import shlex
+            args = args or []
+            args += shlex.split(eargs)
+
+        try:
+            return optparse.OptionParser.parse_args(self, args, values)
+        except optparse.OptionValueError:
+            self.error(str(sys.exc_info()[1]))
+
 def main(args):
     """Main entry point.
 
     args should not contain the script name.
     """
-    from optparse import OptionParser
+    if sys.platform == 'win32':
+        shell = 'cmd'
+    else:
+        shell = '/bin/sh'
 
-    eargs = os.environ.get('CRAM', '').strip()
-    if eargs:
-        import shlex
-        args += shlex.split(eargs)
-
-    p = OptionParser(usage='cram [OPTIONS] TESTS...')
+    p = OptionParser(usage='cram [OPTIONS] TESTS...', prog='cram')
     p.add_option('-V', '--version', action='store_true',
                  help='show version information and exit')
     p.add_option('-q', '--quiet', action='store_true',
                  help='answer yes to all questions')
     p.add_option('-n', '--no', action='store_true',
                  help='answer no to all questions')
+    p.add_option('-E', '--preserve-env', action='store_true',
+                 help="don't reset common environment variables")
     p.add_option('--keep-tmpdir', action='store_true',
                  help='keep temporary directories')
+    p.add_option('--shell', action='store', default=shell, metavar='PATH',
+                 help='shell to use for running tests')
     p.add_option('--indent', action='store', default=2, metavar='NUM',
                  type='int', help='number of spaces to use for indentation')
-    p.add_option('-E', action='store_false', dest='sterilize', default=True,
-                 help="don't reset common environment variables")
     p.add_option('--color', dest='color', action='store_true', default=False,
                  help="colorize output (requires tput)")
     p.add_option('--no-color', dest='color', action='store_false',
     for s in ('TMPDIR', 'TEMP', 'TMP'):
         os.environ[s] = proctmp
 
-    if opts.sterilize:
+    if not opts.preserve_env:
         for s in ('LANG', 'LC_ALL', 'LANGUAGE'):
             os.environ[s] = 'C'
         os.environ['TZ'] = 'GMT'
         answer = None
 
     try:
-        return run(paths, tmpdir, opts.quiet, opts.verbose, patchcmd, answer,
-                   opts.indent, opts.color)
+        return run(paths, tmpdir, opts.shell, opts.quiet, opts.verbose,
+                   patchcmd, answer, opts.indent, opts.color)
     finally:
         if opts.keep_tmpdir:
             log('# Kept temporary directory: %s\n' % tmpdir)
   \x00\x01\x02\x03\x04\x05\x06\x07\x08\t\x0b\x0c\x0e\x0f\x10\x11\x12 (esc)
   $ printf '\023\024\025\026\027\030\031\032\033\034\035\036\037\040\047\n'
   \x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f ' (esc)
+
+Command that closes a pipe:
+
+  $ cat /dev/urandom | head -1 > /dev/null
+
+If Cram let Python's SIGPIPE handler get inherited by this script, we
+might see broken pipe messages.
         'License :: OSI Approved :: GNU General Public License (GPL)',
         'Natural Language :: English',
         'Operating System :: OS Independent',
+        'Programming Language :: Python',
+        'Programming Language :: Python :: 3',
         'Programming Language :: Unix Shell',
         'Topic :: Software Development :: Testing',
     ],
 The $PYTHON environment variable should be set when running this test
 from Python.
 
+  $ [ "$0" != "/bin/bash" ] || shopt -s expand_aliases
   $ [ -n "$PYTHON" ] || PYTHON="`which python`"
   $ if [ -n "$COVERAGE" ]; then
   >   coverage erase
   > fi
   $ command -v md5 > /dev/null || alias md5=md5sum
 
+Note: Bash doesn't expand aliases by default in non-interactive mode,
+so we enable it manually if the test is run with --shell=/bin/bash.
+
 Usage:
 
   $ cram -h
   [Uu]sage: cram \[OPTIONS\] TESTS\.\.\. (re)
   
   [Oo]ptions: (re)
-    -h, --help         show this help message and exit
-    -V, --version      show version information and exit
-    -q, --quiet        don't print diffs
-    -v, --verbose      show filenames and test status
-    -i, --interactive  interactively merge changed test output
-    -y, --yes          answer yes to all questions
-    -n, --no           answer no to all questions
-    --keep-tmpdir      keep temporary directories
-    --indent=NUM       number of spaces to use for indentation
-    -E                 don't reset common environment variables
-    --color            colorize output (requires tput)
-    --no-color         don't colorize output
+    -h, --help          show this help message and exit
+    -V, --version       show version information and exit
+    -q, --quiet         don't print diffs
+    -v, --verbose       show filenames and test status
+    -i, --interactive   interactively merge changed test output
+    -y, --yes           answer yes to all questions
+    -n, --no            answer no to all questions
+    -E, --preserve-env  don't reset common environment variables
+    --keep-tmpdir       keep temporary directories
+    --shell=PATH        shell to use for running tests
+    --indent=NUM        number of spaces to use for indentation
+    --color             colorize output (requires tput)
+    --no-color          don't colorize output
   $ cram -V
   Cram CLI testing framework (version 0.5)
   
   no such file: non-existent
   [2]
 
+Options in .cramrc:
+
+  $ cat > .cramrc <<EOF
+  > [cram]
+  > yes = True
+  > no = 1
+  > indent = 4
+  > EOF
+  $ cram
+  options -y and -n are mutually exclusive
+  [2]
+  $ mv .cramrc config
+  $ CRAMRC=config cram
+  options -y and -n are mutually exclusive
+  [2]
+  $ rm config
+
+Invalid option in .cramrc:
+
+  $ cat > .cramrc <<EOF
+  > [cram]
+  > indent = hmm
+  > EOF
+  $ cram
+  [Uu]sage: cram \[OPTIONS\] TESTS\.\.\. (re)
+  
+  cram: error: option --indent: invalid integer value: 'hmm'
+  [2]
+  $ rm .cramrc
+  $ cat > .cramrc <<EOF
+  > [cram]
+  > verbose = hmm
+  > EOF
+  $ cram
+  [Uu]sage: cram \[OPTIONS\] TESTS\.\.\. (re)
+  
+  cram: error: --verbose: invalid boolean value: 'hmm'
+  [2]
+  $ rm .cramrc
+
 Options in an environment variable:
 
   $ CRAM='-y -n' cram
   .*\b7a23dfa85773c77648f619ad0f9df554\b.* (re)
   $ rm examples/fail.t.err
 
+Run examples with bash:
+
+  $ cram --shell=/bin/bash -q examples examples/fail.t
+  .s.!.s.
+  # Ran 7 tests, 2 skipped, 1 failed.
+  [1]
+  $ md5 examples/fail.t examples/fail.t.err
+  .*\b0f598c2b7b8ca5bcb8880e492ff6b452\b.* (re)
+  .*\b7a23dfa85773c77648f619ad0f9df554\b.* (re)
+  $ rm examples/fail.t.err
+
 Verbose mode:
 
   $ cram -q -v examples examples/fail.t
   [1]
   $ rm examples/env.t.err
 
+Note: We can't set the locale to foo because some shells will issue
+warnings for invalid locales.
+
 Test --keep-tmpdir:
 
   $ cram -q --keep-tmpdir examples/test.t | while read line; do
   test.t
   tmp
 
-Note: We can't set the locale to foo because some shells will issue
-warnings for invalid locales.
-
 Custom indentation:
 
   $ cat > indent.t <<EOF