summaryrefslogtreecommitdiffstats
path: root/third_party/python/cram/cram/_test.py
diff options
context:
space:
mode:
Diffstat (limited to 'third_party/python/cram/cram/_test.py')
-rw-r--r--third_party/python/cram/cram/_test.py230
1 files changed, 230 insertions, 0 deletions
diff --git a/third_party/python/cram/cram/_test.py b/third_party/python/cram/cram/_test.py
new file mode 100644
index 0000000000..27ef99c597
--- /dev/null
+++ b/third_party/python/cram/cram/_test.py
@@ -0,0 +1,230 @@
+"""Utilities for running individual tests"""
+
+import itertools
+import os
+import re
+import time
+
+from cram._encoding import b, bchr, bytestype, envencode, unicodetype
+from cram._diff import esc, glob, regex, unified_diff
+from cram._process import PIPE, STDOUT, execute
+
+__all__ = ['test', 'testfile']
+
+_needescape = re.compile(b(r'[\x00-\x09\x0b-\x1f\x7f-\xff]')).search
+_escapesub = re.compile(b(r'[\x00-\x09\x0b-\x1f\\\x7f-\xff]')).sub
+_escapemap = dict((bchr(i), b(r'\x%02x' % i)) for i in range(256))
+_escapemap.update({b('\\'): b('\\\\'), b('\r'): b(r'\r'), b('\t'): b(r'\t')})
+
+def _escape(s):
+ """Like the string-escape codec, but doesn't escape quotes"""
+ return (_escapesub(lambda m: _escapemap[m.group(0)], s[:-1]) +
+ b(' (esc)\n'))
+
+def test(lines, shell='/bin/sh', indent=2, testname=None, env=None,
+ cleanenv=True, debug=False):
+ r"""Run test lines and return input, output, and diff.
+
+ This returns a 3-tuple containing the following:
+
+ (list of lines in test, same list with actual output, diff)
+
+ diff is a generator that yields the diff between the two lists.
+
+ If a test exits with return code 80, the actual output is set to
+ None and diff is set to [].
+
+ Note that the TESTSHELL environment variable is available in the
+ test (set to the specified shell). However, the TESTDIR and
+ TESTFILE environment variables are not available. To run actual
+ test files, see testfile().
+
+ Example usage:
+
+ >>> from cram._encoding import b
+ >>> refout, postout, diff = test([b(' $ echo hi\n'),
+ ... b(' [a-z]{2} (re)\n')])
+ >>> refout == [b(' $ echo hi\n'), b(' [a-z]{2} (re)\n')]
+ True
+ >>> postout == [b(' $ echo hi\n'), b(' hi\n')]
+ True
+ >>> bool(diff)
+ False
+
+ lines may also be a single bytes string:
+
+ >>> refout, postout, diff = test(b(' $ echo hi\n bye\n'))
+ >>> refout == [b(' $ echo hi\n'), b(' bye\n')]
+ True
+ >>> postout == [b(' $ echo hi\n'), b(' hi\n')]
+ True
+ >>> bool(diff)
+ True
+ >>> (b('').join(diff) ==
+ ... b('--- \n+++ \n@@ -1,2 +1,2 @@\n $ echo hi\n- bye\n+ hi\n'))
+ True
+
+ Note that the b() function is internal to Cram. If you're using Python 2,
+ use normal string literals instead. If you're using Python 3, use bytes
+ literals.
+
+ :param lines: Test input
+ :type lines: bytes or collections.Iterable[bytes]
+ :param shell: Shell to run test in
+ :type shell: bytes or str or list[bytes] or list[str]
+ :param indent: Amount of indentation to use for shell commands
+ :type indent: int
+ :param testname: Optional test file name (used in diff output)
+ :type testname: bytes or None
+ :param env: Optional environment variables for the test shell
+ :type env: dict or None
+ :param cleanenv: Whether or not to sanitize the environment
+ :type cleanenv: bool
+ :param debug: Whether or not to run in debug mode (don't capture stdout)
+ :type debug: bool
+ :return: Input, output, and diff iterables
+ :rtype: (list[bytes], list[bytes], collections.Iterable[bytes])
+ """
+ indent = b(' ') * indent
+ cmdline = indent + b('$ ')
+ conline = indent + b('> ')
+ usalt = 'CRAM%s' % time.time()
+ salt = b(usalt)
+
+ if env is None:
+ env = os.environ.copy()
+
+ if cleanenv:
+ for s in ('LANG', 'LC_ALL', 'LANGUAGE'):
+ env[s] = 'C'
+ env['TZ'] = 'GMT'
+ env['CDPATH'] = ''
+ env['COLUMNS'] = '80'
+ env['GREP_OPTIONS'] = ''
+
+ if isinstance(lines, bytestype):
+ lines = lines.splitlines(True)
+
+ if isinstance(shell, (bytestype, unicodetype)):
+ shell = [shell]
+ env['TESTSHELL'] = shell[0]
+
+ if debug:
+ stdin = []
+ for line in lines:
+ if not line.endswith(b('\n')):
+ line += b('\n')
+ if line.startswith(cmdline):
+ stdin.append(line[len(cmdline):])
+ elif line.startswith(conline):
+ stdin.append(line[len(conline):])
+
+ execute(shell + ['-'], stdin=b('').join(stdin), env=env)
+ return ([], [], [])
+
+ after = {}
+ refout, postout = [], []
+ i = pos = prepos = -1
+ stdin = []
+ for i, line in enumerate(lines):
+ if not line.endswith(b('\n')):
+ line += b('\n')
+ refout.append(line)
+ if line.startswith(cmdline):
+ after.setdefault(pos, []).append(line)
+ prepos = pos
+ pos = i
+ stdin.append(b('echo %s %s $?\n' % (usalt, i)))
+ stdin.append(line[len(cmdline):])
+ elif line.startswith(conline):
+ after.setdefault(prepos, []).append(line)
+ stdin.append(line[len(conline):])
+ elif not line.startswith(indent):
+ after.setdefault(pos, []).append(line)
+ stdin.append(b('echo %s %s $?\n' % (usalt, i + 1)))
+
+ output, retcode = execute(shell + ['-'], stdin=b('').join(stdin),
+ stdout=PIPE, stderr=STDOUT, env=env)
+ if retcode == 80:
+ return (refout, None, [])
+
+ pos = -1
+ ret = 0
+ for i, line in enumerate(output[:-1].splitlines(True)):
+ out, cmd = line, None
+ if salt in line:
+ out, cmd = line.split(salt, 1)
+
+ if out:
+ if not out.endswith(b('\n')):
+ out += b(' (no-eol)\n')
+
+ if _needescape(out):
+ out = _escape(out)
+ postout.append(indent + out)
+
+ if cmd:
+ ret = int(cmd.split()[1])
+ if ret != 0:
+ postout.append(indent + b('[%s]\n' % (ret)))
+ postout += after.pop(pos, [])
+ pos = int(cmd.split()[0])
+
+ postout += after.pop(pos, [])
+
+ if testname:
+ diffpath = testname
+ errpath = diffpath + b('.err')
+ else:
+ diffpath = errpath = b('')
+ diff = unified_diff(refout, postout, diffpath, errpath,
+ matchers=[esc, glob, regex])
+ for firstline in diff:
+ return refout, postout, itertools.chain([firstline], diff)
+ return refout, postout, []
+
+def testfile(path, shell='/bin/sh', indent=2, env=None, cleanenv=True,
+ debug=False, testname=None):
+ """Run test at path and return input, output, and diff.
+
+ This returns a 3-tuple containing the following:
+
+ (list of lines in test, same list with actual output, diff)
+
+ diff is a generator that yields the diff between the two lists.
+
+ If a test exits with return code 80, the actual output is set to
+ None and diff is set to [].
+
+ Note that the TESTDIR, TESTFILE, and TESTSHELL environment
+ variables are available to use in the test.
+
+ :param path: Path to test file
+ :type path: bytes or str
+ :param shell: Shell to run test in
+ :type shell: bytes or str or list[bytes] or list[str]
+ :param indent: Amount of indentation to use for shell commands
+ :type indent: int
+ :param env: Optional environment variables for the test shell
+ :type env: dict or None
+ :param cleanenv: Whether or not to sanitize the environment
+ :type cleanenv: bool
+ :param debug: Whether or not to run in debug mode (don't capture stdout)
+ :type debug: bool
+ :param testname: Optional test file name (used in diff output)
+ :type testname: bytes or None
+ :return: Input, output, and diff iterables
+ :rtype: (list[bytes], list[bytes], collections.Iterable[bytes])
+ """
+ f = open(path, 'rb')
+ try:
+ abspath = os.path.abspath(path)
+ env = env or os.environ.copy()
+ env['TESTDIR'] = envencode(os.path.dirname(abspath))
+ env['TESTFILE'] = envencode(os.path.basename(abspath))
+ if testname is None: # pragma: nocover
+ testname = os.path.basename(abspath)
+ return test(f, shell, indent=indent, testname=testname, env=env,
+ cleanenv=cleanenv, debug=debug)
+ finally:
+ f.close()