def esc(el, l): """Apply an escape match to a line annotated with '(esc)'""" ann = b(' (esc)\n') if el.endswith(ann): el = codecs.escape_decode(el[:-len(ann)])[0] + b('\n') if l.endswith(ann): l = codecs.escape_decode(l[:-len(ann)])[0] + b('\n') return el == l
def testwrapper(): """Test function that adds CLI output""" total[0] += 1 _log(None, path + b(': '), verbose) refout, postout, diff = test() if refout is None: skipped[0] += 1 _log('s', 'empty\n', verbose) return refout, postout, diff abspath = os.path.abspath(path) if error_dir is not None: errpath = os.path.join(error_dir.encode(), os.path.basename(path) + b'.err') else: errpath = abspath + b('.err') if postout is None: skipped[0] += 1 _log('s', 'skipped\n', verbose) elif not diff: _log('.', 'passed\n', verbose) if os.path.exists(errpath): os.remove(errpath) else: failed[0] += 1 _log('!', 'failed\n', verbose) if not quiet: _log('\n', None, verbose) errfile = open(errpath, 'wb') try: for line in postout: errfile.write(line) finally: errfile.close() if not quiet: origdiff = diff diff = [] for line in origdiff: stdoutb.write(line) diff.append(line) if (patchcmd and _prompt('Accept this change?', 'yN', answer) == 'y'): if _patch(patchcmd, diff): _log(None, path + b(': merged output\n'), verbose) os.remove(errpath) else: _log(path + b(': merge failed\n')) return refout, postout, diff
def _findtests(paths): """Yield tests in paths in sorted order""" for p in paths: if os.path.isdir(p): for root, dirs, files in os.walk(p): if os.path.basename(root).startswith(b('.')): continue for f in sorted(files): if not f.startswith(b('.')) and f.endswith(b('.t')): yield os.path.normpath(os.path.join(root, f)) else: yield os.path.normpath(p)
def _findtests(paths): """Yield tests in paths in sorted order""" for p in paths: if os.path.isdir(p): for root, dirs, files in _walk(p): if os.path.basename(root).startswith(b('.')): continue for f in sorted(files): if not f.startswith(b('.')) and f.endswith(b('.t')): yield os.path.normpath(os.path.join(root, f)) else: yield os.path.normpath(p)
def esc(el, line): """Apply an escape match to a line annotated with '(esc)'""" ann = b(' (esc)\n') if el.endswith(ann): el = codecs.escape_decode(el[:-len(ann)])[0] + b('\n') if el == line: return True if line.endswith(ann): line = codecs.escape_decode(line[:-len(ann)])[0] + b('\n') return el == line
def testwrapper(): """Test function that adds CLI output""" total[0] += 1 _log(None, path + b(': '), verbose) refout, postout, diff = test() if refout is None: skipped[0] += 1 _log('s', 'empty\n', verbose) return refout, postout, diff abspath = os.path.abspath(path) errpath = abspath + b('.err') if postout is None: skipped[0] += 1 _log('s', 'skipped\n', verbose) elif not diff: _log('.', 'passed\n', verbose) if os.path.exists(errpath): os.remove(errpath) else: failed[0] += 1 _log('!', 'failed\n', verbose) if not quiet: _log('\n', None, verbose) if not noerrfiles: errfile = open(errpath, 'wb') try: for line in postout: errfile.write(line) finally: errfile.close() if not quiet: origdiff = diff diff = [] for line in origdiff: stdoutb.write(line) diff.append(line) if (patchcmd and _prompt('Accept this change?', 'yN', answer) == 'y'): if _patch(patchcmd, diff): _log(None, path + b(': merged output\n'), verbose) if not noerrfiles: os.remove(errpath) else: _log(path + b(': merge failed\n')) return refout, postout, diff
def _glob(el, l): r"""Match a glob-like pattern. The only supported special characters are * and ?. Escaping is supported. >>> from cram._encoding import b >>> bool(_glob(b(r'\* \\ \? fo?b*'), b('* \\ ? foobar'))) True """ i, n = 0, len(el) res = b('') while i < n: c = el[i:i + 1] i += 1 if c == b('\\') and el[i] in b('*?\\'): res += el[i - 1:i + 1] i += 1 elif c == b('*'): res += b('.*') elif c == b('?'): res += b('.') else: res += re.escape(c) return _regex(res, l)
def runtests(paths, tmpdir, shell, indent=2, cleanenv=True, debug=False, debug_script=False, noescape=False): """Run tests and yield results. This yields a sequence of 2-tuples containing the following: (test path, test function) The test function, when called, runs the test in a temporary directory and returns a 3-tuple: (list of lines in the test, same list with actual output, diff) """ cwd = os.getcwd() seen = set() basenames = set() for i, path in enumerate(_findtests(paths)): abspath = os.path.abspath(path) if abspath in seen: continue seen.add(abspath) if not os.stat(path).st_size: yield (path, lambda: (None, None, None)) continue basename = os.path.basename(path) if basename in basenames: basename = basename + b('-%s' % i) else: basenames.add(basename) def test(): """Run test file""" testdir = os.path.join(tmpdir, basename) os.mkdir(testdir) try: os.chdir(testdir) return testfile(abspath, shell, indent=indent, cleanenv=cleanenv, debug=debug, debug_script=debug_script, testname=path, noescape=noescape) finally: os.chdir(cwd) yield (path, test)
def _regex(pattern, s): """Match a regular expression or return False if invalid. >>> from cram._encoding import b >>> [bool(_regex(r, b('foobar'))) for r in (b('foo.*'), b('***'))] [True, False] """ try: return re.match(pattern + b(r'\Z'), s) except re.error: return False
def runtests(paths, tmpdir, shell, indent=2, cleanenv=True, debug=False): """Run tests and yield results. This yields a sequence of 2-tuples containing the following: (test path, test function) The test function, when called, runs the test in a temporary directory and returns a 3-tuple: (list of lines in the test, same list with actual output, diff) """ cwd = os.getcwd() seen = set() basenames = set() for i, path in enumerate(_findtests(paths)): abspath = os.path.abspath(path) if abspath in seen: continue seen.add(abspath) if not os.stat(path).st_size: yield (path, lambda: (None, None, None)) continue basename = os.path.basename(path) if basename in basenames: basename = basename + b('-%s' % i) else: basenames.add(basename) def test(): """Run test file""" testdir = os.path.join(tmpdir, basename) os.mkdir(testdir) try: os.chdir(testdir) return testfile(abspath, shell, indent=indent, cleanenv=cleanenv, debug=debug, testname=path) finally: os.chdir(cwd) yield (path, test)
def main(args): """Main entry point. If you're thinking of using Cram in other Python code (e.g., unit tests), consider using the test() or testfile() functions instead. :param args: Script arguments (excluding script name) :type args: str :return: Exit code (non-zero on failure) :rtype: int """ opts, paths, getusage = _parseopts(args) if opts.version: sys.stdout.write("""Cram CLI testing framework (version 0.7) Copyright (C) 2010-2016 Brodie Rao <*****@*****.**> and others This is free software; see the source for copying conditions. There is NO warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. """) return conflicts = [('--yes', opts.yes, '--no', opts.no), ('--quiet', opts.quiet, '--interactive', opts.interactive), ('--debug', opts.debug, '--quiet', opts.quiet), ('--debug', opts.debug, '--interactive', opts.interactive), ('--debug', opts.debug, '--verbose', opts.verbose), ('--debug', opts.debug, '--xunit-file', opts.xunit_file)] for s1, o1, s2, o2 in conflicts: if o1 and o2: sys.stderr.write('options %s and %s are mutually exclusive\n' % (s1, s2)) return 2 shellcmd = _which(opts.shell) if not shellcmd: stderrb.write(b('shell not found: ') + fsencode(opts.shell) + b('\n')) return 2 shell = [shellcmd] if opts.shell_opts: shell += shlex.split(opts.shell_opts) patchcmd = None if opts.interactive: patchcmd = _which('patch') if not patchcmd: sys.stderr.write('patch(1) required for -i\n') return 2 if not paths: sys.stdout.write(getusage()) return 2 badpaths = [path for path in paths if not os.path.exists(path)] if badpaths: stderrb.write(b('no such file: ') + badpaths[0] + b('\n')) return 2 if opts.yes: answer = 'y' elif opts.no: answer = 'n' else: answer = None tmpdir = os.environ['CRAMTMP'] = tempfile.mkdtemp('', 'cramtests-') tmpdirb = fsencode(tmpdir) proctmp = os.path.join(tmpdir, 'tmp') for s in ('TMPDIR', 'TEMP', 'TMP'): os.environ[s] = proctmp os.mkdir(proctmp) try: tests = runtests(paths, tmpdirb, shell, indent=opts.indent, cleanenv=not opts.preserve_env, debug=opts.debug) if not opts.debug: tests = runcli(tests, quiet=opts.quiet, verbose=opts.verbose, patchcmd=patchcmd, answer=answer) if opts.xunit_file is not None: tests = runxunit(tests, opts.xunit_file) hastests = False failed = False for path, test in tests: hastests = True refout, postout, diff = test() if diff: failed = True if not hastests: sys.stderr.write('no tests found\n') return 2 return int(failed) finally: if opts.keep_tmpdir: stdoutb.write(b('# Kept temporary directory: ') + tmpdirb + b('\n')) else: shutil.rmtree(tmpdir)
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 _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'))
"""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,
def _patch(cmd, diff, path): """Run echo [lines from diff] | cmd -p0""" out, retcode = execute([cmd, '-p0'], stdin=b('').join(diff), cwd=path) return retcode == 0
def _matchannotation(keyword, matchfunc, el, l): """Apply match function based on annotation keyword""" ann = b(' (%s)\n' % keyword) return el.endswith(ann) and matchfunc(el[:-len(ann)], l[:-1])
def unified_diff(l1, l2, fromfile=b(''), tofile=b(''), fromfiledate=b(''), tofiledate=b(''), n=3, lineterm=b('\n'), matchers=None): r"""Compare two sequences of lines; generate the delta as a unified diff. This is like difflib.unified_diff(), but allows custom matchers. >>> from cram._encoding import b >>> l1 = [b('a\n'), b('? (glob)\n')] >>> l2 = [b('a\n'), b('b\n')] >>> (list(unified_diff(l1, l2, b('f1'), b('f2'), b('1970-01-01'), ... b('1970-01-02'))) == ... [b('--- f1\t1970-01-01\n'), b('+++ f2\t1970-01-02\n'), ... b('@@ -1,2 +1,2 @@\n'), b(' a\n'), b('-? (glob)\n'), b('+b\n')]) True >>> from cram._diff import glob >>> list(unified_diff(l1, l2, matchers=[glob])) [] """ if matchers is None: matchers = [] started = False matcher = _SequenceMatcher(None, l1, l2, matchers=matchers) for group in matcher.get_grouped_opcodes(n): if not started: if fromfiledate: fromdate = b('\t') + fromfiledate else: fromdate = b('') if tofiledate: todate = b('\t') + tofiledate else: todate = b('') yield b('--- ') + fromfile + fromdate + lineterm yield b('+++ ') + tofile + todate + lineterm started = True i1, i2, j1, j2 = group[0][1], group[-1][2], group[0][3], group[-1][4] yield (b("@@ -%d,%d +%d,%d @@" % (i1 + 1, i2 - i1, j1 + 1, j2 - j1)) + lineterm) for tag, i1, i2, j1, j2 in group: if tag == 'equal': for line in l1[i1:i2]: yield b(' ') + line continue if tag == 'replace' or tag == 'delete': for line in l1[i1:i2]: yield b('-') + line if tag == 'replace' or tag == 'insert': for line in l2[j1:j2]: yield b('+') + line
def _patch(cmd, diff): """Run echo [lines from diff] | cmd -p0""" out, retcode = execute([cmd, '-p0'], stdin=b('').join(diff)) return retcode == 0
"""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, noerrfile=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)
def main(args): """Main entry point. If you're thinking of using Cram in other Python code (e.g., unit tests), consider using the test() or testfile() functions instead. :param args: Script arguments (excluding script name) :type args: str :return: Exit code (non-zero on failure) :rtype: int """ opts, paths, getusage = _parseopts(args) if opts.version: sys.stdout.write("""Cram CLI testing framework (version 0.7) Copyright (C) 2010-2016 Brodie Rao <*****@*****.**> and others This is free software; see the source for copying conditions. There is NO warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. """) return conflicts = [('--yes', opts.yes, '--no', opts.no), ('--quiet', opts.quiet, '--interactive', opts.interactive), ('--debug', opts.debug, '--quiet', opts.quiet), ('--debug', opts.debug, '--interactive', opts.interactive), ('--debug', opts.debug, '--verbose', opts.verbose), ('--debug', opts.debug, '--xunit-file', opts.xunit_file)] for s1, o1, s2, o2 in conflicts: if o1 and o2: sys.stderr.write('options %s and %s are mutually exclusive\n' % (s1, s2)) return 2 shellcmd = _which(opts.shell) if not shellcmd: stderrb.write(b('shell not found: ') + fsencode(opts.shell) + b('\n')) return 2 shell = [shellcmd] if opts.shell_opts: shell += shlex.split(opts.shell_opts) patchcmd = None if opts.interactive: patchcmd = _which('patch') if not patchcmd: sys.stderr.write('patch(1) required for -i\n') return 2 if not paths: sys.stdout.write(getusage()) return 2 badpaths = [path for path in paths if not os.path.exists(path)] if badpaths: stderrb.write(b('no such file: ') + badpaths[0] + b('\n')) return 2 if opts.yes: answer = 'y' elif opts.no: answer = 'n' else: answer = None tmpdir = os.environ['CRAMTMP'] = tempfile.mkdtemp('', 'cramtests-') tmpdirb = fsencode(tmpdir) proctmp = os.path.join(tmpdir, 'tmp') for s in ('TMPDIR', 'TEMP', 'TMP'): os.environ[s] = proctmp os.mkdir(proctmp) try: tests = runtests(paths, tmpdirb, shell, indent=opts.indent, cleanenv=not opts.preserve_env, debug=opts.debug, noerrfiles=opts.no_err_files) if not opts.debug: tests = runcli(tests, quiet=opts.quiet, verbose=opts.verbose, patchcmd=patchcmd, answer=answer, noerrfiles=opts.no_err_files) if opts.xunit_file is not None: tests = runxunit(tests, opts.xunit_file) hastests = False failed = False for path, test in tests: hastests = True refout, postout, diff = test() if diff: failed = True if not hastests: sys.stderr.write('no tests found\n') return 2 return int(failed) finally: if opts.keep_tmpdir: stdoutb.write( b('# Kept temporary directory: ') + tmpdirb + b('\n')) else: shutil.rmtree(tmpdir)
def test(lines, shell='/bin/sh', indent=2, testname=None, env=None, cleanenv=True, debug=False, noerrfile=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 unified_diff(l1, l2, fromfile=b(''), tofile=b(''), fromfiledate=b(''), tofiledate=b(''), n=3, lineterm=b('\n'), matchers=None): """Compare two sequences of lines; generate the delta as a unified diff. This is like difflib.unified_diff(), but allows custom matchers. """ if matchers is None: matchers = [] started = False matcher = _SequenceMatcher(None, l1, l2, matchers=matchers) for group in matcher.get_grouped_opcodes(n): if not started: if fromfiledate: fromdate = b('\t') + fromfiledate else: fromdate = b('') if tofiledate: todate = b('\t') + tofiledate else: todate = b('') yield b('--- ') + fromfile + fromdate + lineterm yield b('+++ ') + tofile + todate + lineterm started = True i1, i2, j1, j2 = group[0][1], group[-1][2], group[0][3], group[-1][4] yield (b("@@ -%d,%d +%d,%d @@" % (i1 + 1, i2 - i1, j1 + 1, j2 - j1)) + lineterm) for tag, i1, i2, j1, j2 in group: if tag == 'equal': for line in l1[i1:i2]: yield b(' ') + line continue if tag == 'replace' or tag == 'delete': for line in l1[i1:i2]: yield b('-') + line if tag == 'replace' or tag == 'insert': for line in l2[j1:j2]: yield b('+') + line