Commits

Brodie Rao committed ec6eeb9

Refreshing queue

Comments (0)

Files changed (6)

+# HG changeset patch
+# Parent fb00d4c4faf7a3f0f531a0dd1d7627d8276ccf81
+
+diff --git a/cram.py b/cram.py
+--- a/cram.py
++++ b/cram.py
+@@ -116,37 +116,29 @@ def unified_diff(a, b, fromfile='', tofi
+                 for line in b[j1:j2]:
+                     yield '+' + line
+ 
+-def load_test(s):
+-    """Load test from string representation.
++def test_token(command, output, ret, indent):
++    return 'test', (''.join(command), ''.join(output), ret, indent)
+ 
+-    The result is an iterable over (type, value) pairs. If type is "comment"
+-    then value is a comment string. If type is "test" then value is a tuple of
+-    (command, output, retcode, indent).
+-    """
+-    def mktest(retcode):
+-        return 'test', (''.join(command), ''.join(output), retcode, indent)
++def comment_token(comment):
++    return 'comment', ''.join(comment)
+ 
+-    def mkcomment():
+-        return 'comment', ''.join(comment)
++def format_output(s):
++    if s.endswith('%'):
++        return s[:-1]
++    return s + '\n'
+ 
+-    def outnl(s):
+-        if s.endswith('%'):
+-            return s[:-1]
+-        else:
+-            return s + '\n'
++def tokenize(s):
++    """Parse iterable of lines as a test"""
+ 
+-    regexps = [re.compile(x) for x in [
+-        '(?P<sp>  +)\$ (?P<cmd>.*)',
+-        '(?P<sp>  +)> (?P<cmdcont>.*)',
+-        '(?P<sp>  +)\[(?P<ret>[0-9]+)\]',
+-        '(?P<sp>  +)(?P<out>.*)',
+-    ]]
+-
++    regexps = [re.compile(r) for r in [r'(?P<sp>  +)\$ (?P<cmd>.*)',
++                                       r'(?P<sp>  +)> (?P<cmdcont>.*)',
++                                       r'(?P<sp>  +)\[(?P<ret>\d+)\]',
++                                       r'(?P<sp>  +)(?P<out>.*)']]
+     state = 'comment'
+     comment, command, output = [], [], []
+     indent = 0
+ 
+-    for line in s.splitlines(True):
++    for line in s:
+         matched = {}
+         for r in regexps:
+             m = r.match(line)
+@@ -155,80 +147,47 @@ def load_test(s):
+                 break
+         if state == 'comment':
+             if 'cmd' in matched:
+-                if len(comment) > 0:
+-                    yield mkcomment()
++                if comment:
++                    yield comment_token(comment)
+                 state = 'command'
+                 indent = len(matched['sp'])
+                 command = [matched['cmd'] + '\n']
+                 output = []
+             else:
+                 comment.append(line)
+-        elif state == 'command':
++        else:
+             sp = len(matched.get('sp', ''))
+             if sp > indent:
+                 line = line[indent:].rstrip('\n')
+-                output.append(outnl(line))
++                output.append(format_output(line))
+             elif sp < indent:
+-                yield mktest(0)
++                yield test_token(command, output, 0, indent)
+                 state = 'comment'
+                 comment = [line]
+             elif 'cmd' in matched:
+-                yield mktest(0)
++                yield test_token(command, output, 0, indent)
+                 command = [matched['cmd'] + '\n']
+                 output = []
+             elif 'cmdcont' in matched:
+                 line = matched['cmdcont']
+-                if len(output) == 0:
++                if not output:
+                     command.append(line + '\n')
+                 else:
+-                    output.append(outnl(line))
++                    output.append(format_output(line))
+             elif 'ret' in matched:
+-                yield mktest(int(matched['ret']))
++                yield test_token(command, output, matched['ret'], indent)
+                 state = 'comment'
+                 comment = []
+-            elif 'out' in matched:
+-                output.append(outnl(matched['out']))
+             else:
+-                raise Exception('unreachable')
+-        else:
+-            raise Exception('unreachable')
++                output.append(format_output(matched['out']))
+ 
+-    if state == 'comment' and len(comment) > 0:
+-        yield mkcomment()
++    if state == 'comment' and comment:
++        yield comment_token(comment)
+     elif state == 'command':
+-        yield mktest(0)
++        yield test_token(command, output, 0, indent)
+ 
+-def dump_test(test):
+-    """Dump test to string representation.
+-
+-    Inverse of load_test.
+-    """
+-    buf = []
+-    for t, v in test:
+-        if t == 'comment':
+-            buf.append(v)
+-        elif t == 'test':
+-            cmd, out, ret, indent = v
+-            sp = ' ' * indent
+-            for i, line in enumerate(cmd.splitlines(True)):
+-                if i == 0:
+-                    c = '$ '
+-                else:
+-                    c = '> '
+-                buf.append(sp + c + line)
+-            outlines = out.splitlines(True)
+-            buf.append(''.join([sp + line for line in outlines]))
+-            if len(outlines) > 0 and not outlines[-1].endswith('\n'):
+-                buf.append('%\n')
+-            if ret != 0:
+-                buf.append(sp + '[%d]\n' % ret)
+-        else:
+-            chunk = (t, v)
+-            raise Exception('unknown test chunk type: %r' % (chunk,))
+-    return ''.join(buf)
+-
+-def parse_test_output(output, salt, test):
+-    """Convert test output string into an iterable over test pairs."""
++def parse_output(output, salt, test):
++    """Convert test output string into an iterable over test pairs"""
+     parsed, buf = [], []
+     for line in output.splitlines(True):
+         if line.startswith(salt):
+@@ -240,17 +199,38 @@ def parse_test_output(output, salt, test
+ 
+     parsed = iter(parsed)
+     for chunk in test:
+-        t, v = chunk
+-        if t == 'comment':
++        token, block = chunk
++        if token == 'comment':
+             yield chunk
+-        elif t == 'test':
+-            command, _, _, indent = v
++        else:
++            command, output, retcode, indent = block
+             output, retcode = parsed.next()
+             if output.endswith('\n'):
+                 output = output[:-1]
+             yield 'test', (command, output, retcode, indent)
+-        else:
+-            raise Exception('unknown test chunk type: %r' % (chunk,))
++
++def serialize(output, salt, test):
++    """Serialize output returned by tokenize(s)"""
++    postout = []
++    for t, v in parse_output(output, salt, test):
++        if t == 'comment':
++            postout.append(v)
++        elif t == 'test':
++            cmd, out, ret, indent = v
++            sp = ' ' * indent
++            for i, line in enumerate(cmd.splitlines(True)):
++                if i == 0:
++                    c = '$ '
++                else:
++                    c = '> '
++                postout.append(sp + c + line)
++            outlines = out.splitlines(True)
++            postout.append(''.join([sp + line for line in outlines]))
++            if outlines and not outlines[-1].endswith('\n'):
++                postout.append('%\n')
++            if ret != 0:
++                postout.append(sp + '[%d]\n' % ret)
++    return ''.join(postout)
+ 
+ def test(path):
+     """Run test at path and return input, output, and diff.
+@@ -264,14 +244,8 @@ def test(path):
+     If a test exits with return code 80, the actual output is set to
+     None and diff is set to [].
+     """
+-    fd = open(path)
+-    try:
+-        ref = fd.read()
+-    finally:
+-        fd.close()
+-    refout = ref.splitlines(True)
+-    refparsed = list(load_test(ref))
+-    tests = [v for t, v in refparsed if t == 'test']
++    refout = list(open(path))
++    tokens = list(tokenize(refout))
+ 
+     abspath = os.path.abspath(path)
+     env = os.environ.copy()
+@@ -281,18 +255,16 @@ def test(path):
+                          universal_newlines=True, env=env,
+                          close_fds=os.name == 'posix')
+     salt = 'CRAM%s' % time.time()
+-    try:
+-        for cmd, _, _, _ in tests:
+-            p.stdin.write(cmd)
++
++    for token, block in tokens:
++        if token == 'test':
++            p.stdin.write(block[0])
+             p.stdin.write('echo "\n%s $?"\n' % salt)
+-        output = p.communicate()[0]
+-        if p.returncode == 80:
+-            return refout, None, []
+-    finally:
+-        p.wait()
+ 
+-    postparsed = parse_test_output(output, salt, refparsed)
+-    postout = dump_test(postparsed).splitlines(True)
++    output = p.communicate()[0]
++    if p.returncode == 80:
++        return refout, None, []
++    postout = serialize(output, salt, tokens).splitlines(True)
+ 
+     diff = unified_diff(refout, postout, abspath, abspath + '.err')
+     for firstline in diff:
 # HG changeset patch
-# Parent ceeaaf87a3d38b86c499615f2d9c46ae342134c6
+# Parent fcc8af25df11ec2a4a4c2a6be22f6691fe81419e
 Add contrib/cram-mode.el
 
 diff --git a/contrib/cram-mode.el b/contrib/cram-mode.el
 new file mode 100644
 --- /dev/null
 +++ b/contrib/cram-mode.el
-@@ -0,0 +1,29 @@
-+;;; cram-mode.el - Major mode for Cram tests (derived from shell-script-mode)
+@@ -0,0 +1,27 @@
++;;; cram-mode.el - Major mode for Cram tests
 +
-+;(autoload 'shell-script-mode "sh-script")
-+(require 'sh-script)
++(require 'mmm-auto)
 +
 +(add-to-list 'auto-mode-alist '("\\.t\\'" . cram-mode)) ;; FIXME
 +
-+(defconst cram-font-lock-syntactic-keywords
-+  ;; Any line that doesn't begin with two spaces is considered a doc comment.
-+  (append '(("^[^ \n]\\{2\\}.+?\n$" 0 "!"))
-+          sh-font-lock-syntactic-keywords)
-+  "Additional `font-lock-syntactic-keywords' for Cram mode.")
-+
-+(defun cram-syntax-begin-function ()
-+  ;; Start syntax highlighting after "  $ " or "  > ".
-+  (if (re-search-backward "^  [\\$>] " nil t)
-+      (+ (point) 4)
-+    (point-min)))
-+
 +;;;###autoload
-+(define-derived-mode cram-mode shell-script-mode "Cram"
-+  "Major mode for editing Cram tests (derived from shell-script-mode)"
-+  (set (make-local-variable 'comment-start) "# \\|^[^ ]{2}")
-+  (set (make-local-variable 'syntax-begin-function)
-+       'cram-syntax-begin-function)
-+  (set (make-local-variable 'sh-font-lock-syntactic-keywords)
-+       cram-font-lock-syntactic-keywords))
++(define-derived-mode cram-mode mmm-mode "Cram"
++  "Major mode for editing Cram tests"
++  (mmm-add-group
++   'cram-test
++   '((comment
++      :submode text-mode
++      :face mmm-comment-submode-face
++      :front "^\n\\|^[^ ]\\{2\\}"
++      :include-front t)
++     (command
++      :submode shell-script-mode
++      :face mmm-code-submode-face
++      :front "^  [\\$>] ")
++     (output
++      :submode text-mode
++      :face mmm-output-submode-face
++      :front "^  [^\\$>].+\n"
++      :include-front t))))
 +
 +(provide 'cram-mode)

rst

-# HG changeset patch
-# Parent b6068c163092fd8f6da048c190f485e62b89ad68
-
-diff --git a/examples/bare.t b/examples/bare.t
---- a/examples/bare.t
-+++ b/examples/bare.t
-@@ -1,1 +1,3 @@
-+::
-+
-   $ true
-diff --git a/examples/env.t b/examples/env.t
---- a/examples/env.t
-+++ b/examples/env.t
-@@ -1,4 +1,4 @@
--Check environment variables:
-+Check environment variables::
- 
-   $ echo "$LANG"
-   C
-diff --git a/examples/fail.t b/examples/fail.t
---- a/examples/fail.t
-+++ b/examples/fail.t
-@@ -1,4 +1,4 @@
--Wrong output and bad regexes:
-+Wrong output and bad regexes::
- 
-   $ echo 1
-   2
-@@ -7,7 +7,7 @@ Wrong output and bad regexes:
-   foo\ (re)
-    (re)
- 
--Offset regular expression:
-+Offset regular expression::
- 
-   $ printf 'foo\n\n1\n'
-   
-diff --git a/examples/skip.t b/examples/skip.t
---- a/examples/skip.t
-+++ b/examples/skip.t
-@@ -1,5 +1,5 @@
- This test is considered "skipped" because it exits with return code
- 80. This is useful for skipping tests that only work on certain
--platforms or in certain settings.
-+platforms or in certain settings::
- 
-   $ exit 80
-diff --git a/examples/test.t b/examples/test.t
---- a/examples/test.t
-+++ b/examples/test.t
-@@ -1,4 +1,4 @@
--Simple commands:
-+Simple commands::
- 
-   $ echo foo
-   foo
-@@ -6,7 +6,7 @@ Simple commands:
-   bar
-   baz
- 
--Multi-line command:
-+Multi-line command::
- 
-   $ foo() {
-   >     echo bar
-@@ -14,34 +14,34 @@ Multi-line command:
-   $ foo
-   bar
- 
--Regular expression:
-+Regular expression::
- 
-   $ echo foobarbaz
-   foobar.* (re)
- 
--Glob:
-+Glob::
- 
-   $ printf '* \\foobarbaz {10}\n'
-   \* \\fo?bar* {10} (glob)
- 
--Literal match ending in (re) and (glob):
-+Literal match ending in (re) and (glob)::
- 
-   $ echo 'foo\Z\Z\Z bar (re)'
-   foo\Z\Z\Z bar (re)
-   $ echo 'baz??? quux (glob)'
-   baz??? quux (glob)
- 
--Exit code:
-+Exit code::
- 
-   $ (exit 1)
-   [1]
- 
--Write to stderr:
-+Write to stderr::
- 
-   $ echo foo >&2
-   foo
- 
--No newline:
-+No newline::
- 
-   $ python -c 'import sys; sys.stdout.write("foo"); sys.stdout.flush()'
-   foo%
-diff --git a/tests/cram.t b/tests/cram.t
---- a/tests/cram.t
-+++ b/tests/cram.t
-@@ -1,5 +1,5 @@
- The $PYTHON environment variable should be set when running this test
--from Python.
-+from Python::
- 
-   $ [ -n "$PYTHON" ] || PYTHON="`which python`"
-   $ if [ -n "$COVERAGE" ]; then
-@@ -10,7 +10,7 @@ from Python.
-   > fi
-   $ command -v md5 > /dev/null || alias md5=md5sum
- 
--Usage:
-+Usage::
- 
-   $ cram -h
-   [Uu]sage: cram \[OPTIONS\] TESTS\.\.\. (re)
-@@ -41,12 +41,12 @@ Usage:
-   no such file: non-existent
-   [2]
- 
--Copy in example tests:
-+Copy in example tests::
- 
-   $ cp -R "$TESTDIR"/../examples .
-   $ find . -name '*.err' -exec rm '{}' \;
- 
--Run cram examples:
-+Run cram examples::
- 
-   $ cram -q examples examples/fail.t examples/.hidden.t
-   .s.!s.
-@@ -57,7 +57,7 @@ Run cram examples:
-   .*\b114b031c5361553b32d9337a31f39ce5\b.* (re)
-   $ rm examples/fail.t.err
- 
--Verbose mode:
-+Verbose mode::
- 
-   $ cram -q -v examples examples/fail.t examples/.hidden.t
-   examples/bare.t: passed
-@@ -73,7 +73,7 @@ Verbose mode:
-   .*\b114b031c5361553b32d9337a31f39ce5\b.* (re)
-   $ rm examples/fail.t.err
- 
--Interactive mode (don't merge):
-+Interactive mode (don't merge)::
- 
-   $ cram -n -i examples/fail.t
-   !
-@@ -107,7 +107,7 @@ Interactive mode (don't merge):
-   .*\bec9a94814a64428cd2327580164a01b9\b.* (re)
-   .*\b114b031c5361553b32d9337a31f39ce5\b.* (re)
- 
--Interactive mode (merge):
-+Interactive mode (merge)::
- 
-   $ cp examples/fail.t examples/fail.t.orig
-   $ cram -y -i examples/fail.t
-@@ -143,7 +143,7 @@ Interactive mode (merge):
-   .*\b44b27872ea5380df986e19ba23aed934\b.* (re)
-   $ mv examples/fail.t.orig examples/fail.t
- 
--Verbose interactive mode (answer manually and don't merge):
-+Verbose interactive mode (answer manually and don't merge)::
- 
-   $ printf 'bad\nn\n' | cram -v -i examples/fail.t
-   examples/fail.t: failed
-@@ -204,7 +204,7 @@ Verbose interactive mode (answer manuall
-   .*\bec9a94814a64428cd2327580164a01b9\b.* (re)
-   .*\b114b031c5361553b32d9337a31f39ce5\b.* (re)
- 
--Verbose interactive mode (answer manually and merge):
-+Verbose interactive mode (answer manually and merge)::
- 
-   $ cp examples/fail.t examples/fail.t.orig
-   $ printf 'bad\ny\n' | cram -v -i examples/fail.t
-@@ -239,7 +239,7 @@ Verbose interactive mode (answer manuall
-   .*\b44b27872ea5380df986e19ba23aed934\b.* (re)
-   $ mv examples/fail.t.orig examples/fail.t
- 
--Test missing patch(1) and patch(1) error:
-+Test missing patch(1) and patch(1) error::
- 
-   $ PATH=. cram -i examples/fail.t
-   patch(1) required for -i
-@@ -285,7 +285,7 @@ Test missing patch(1) and patch(1) error
-   .*\b114b031c5361553b32d9337a31f39ce5\b.* (re)
-   $ rm patch examples/fail.t.err
- 
--Test that a fixed .err file is deleted:
-+Test that a fixed .err file is deleted::
- 
-   $ echo "  $ echo 1" > fixed.t
-   $ cram fixed.t
-@@ -305,10 +305,7 @@ Test that a fixed .err file is deleted:
-   $ test \! -f fixed.t.err
-   $ rm fixed.t
- 
--Don't sterilize environment:
--
--Note: We can't set the locale to foo because some shells will issue
--warnings for invalid locales.
-+Don't sterilize environment::
- 
-   $ TZ=foo; export TZ
-   $ CDPATH=foo; export CDPATH
-@@ -340,3 +337,6 @@ warnings for invalid locales.
-   # Ran 1 tests, 0 skipped, 1 failed.
-   [1]
-   $ rm examples/env.t.err
-+
-+Note: We can't set the locale to foo because some shells will issue
-+warnings for invalid locales.
-rst
-runone
-nicer-api
-stdin
-cram-mode
+spaces
+cleanup
+whitespace #+whitespace
+runone #+api
+nicer-api #+api
+stdin #+api
+cram-mode #+cram-mode
 jobs
+# HG changeset patch
+# User Andrey Vlasovskikh <andrey.vlasovskikh@gmail.com>
+# Date 1286046293 -14400
+# Node ID fb00d4c4faf7a3f0f531a0dd1d7627d8276ccf81
+# Parent  fcc8af25df11ec2a4a4c2a6be22f6691fe81419e
+Two or more spaces for indenting tests
+
+It may be useful for embedding tests into documentation. For example, using 4
+spaces is natural to Markdown and some other simple markup languages.
+
+In order to implement arbitrary indentation I wrote parser and serializer
+functions for the test files format. They are quite large unfortunately, but
+may be helpful in the future.
+
+diff --git a/cram.py b/cram.py
+--- a/cram.py
++++ b/cram.py
+@@ -116,6 +116,142 @@ def unified_diff(a, b, fromfile='', tofi
+                 for line in b[j1:j2]:
+                     yield '+' + line
+ 
++def load_test(s):
++    """Load test from string representation.
++
++    The result is an iterable over (type, value) pairs. If type is "comment"
++    then value is a comment string. If type is "test" then value is a tuple of
++    (command, output, retcode, indent).
++    """
++    def mktest(retcode):
++        return 'test', (''.join(command), ''.join(output), retcode, indent)
++
++    def mkcomment():
++        return 'comment', ''.join(comment)
++
++    def outnl(s):
++        if s.endswith('%'):
++            return s[:-1]
++        else:
++            return s + '\n'
++
++    regexps = [re.compile(x) for x in [
++        '(?P<sp>  +)\$ (?P<cmd>.*)',
++        '(?P<sp>  +)> (?P<cmdcont>.*)',
++        '(?P<sp>  +)\[(?P<ret>[0-9]+)\]',
++        '(?P<sp>  +)(?P<out>.*)',
++    ]]
++
++    state = 'comment'
++    comment, command, output = [], [], []
++    indent = 0
++
++    for line in s.splitlines(True):
++        matched = {}
++        for r in regexps:
++            m = r.match(line)
++            if m:
++                matched = m.groupdict()
++                break
++        if state == 'comment':
++            if 'cmd' in matched:
++                if len(comment) > 0:
++                    yield mkcomment()
++                state = 'command'
++                indent = len(matched['sp'])
++                command = [matched['cmd'] + '\n']
++                output = []
++            else:
++                comment.append(line)
++        elif state == 'command':
++            sp = len(matched.get('sp', ''))
++            if sp > indent:
++                line = line[indent:].rstrip('\n')
++                output.append(outnl(line))
++            elif sp < indent:
++                yield mktest(0)
++                state = 'comment'
++                comment = [line]
++            elif 'cmd' in matched:
++                yield mktest(0)
++                command = [matched['cmd'] + '\n']
++                output = []
++            elif 'cmdcont' in matched:
++                line = matched['cmdcont']
++                if len(output) == 0:
++                    command.append(line + '\n')
++                else:
++                    output.append(outnl(line))
++            elif 'ret' in matched:
++                yield mktest(int(matched['ret']))
++                state = 'comment'
++                comment = []
++            elif 'out' in matched:
++                output.append(outnl(matched['out']))
++            else:
++                raise Exception('unreachable')
++        else:
++            raise Exception('unreachable')
++
++    if state == 'comment' and len(comment) > 0:
++        yield mkcomment()
++    elif state == 'command':
++        yield mktest(0)
++
++def dump_test(test):
++    """Dump test to string representation.
++
++    Inverse of load_test.
++    """
++    buf = []
++    for t, v in test:
++        if t == 'comment':
++            buf.append(v)
++        elif t == 'test':
++            cmd, out, ret, indent = v
++            sp = ' ' * indent
++            for i, line in enumerate(cmd.splitlines(True)):
++                if i == 0:
++                    c = '$ '
++                else:
++                    c = '> '
++                buf.append(sp + c + line)
++            outlines = out.splitlines(True)
++            buf.append(''.join([sp + line for line in outlines]))
++            if len(outlines) > 0 and not outlines[-1].endswith('\n'):
++                buf.append('%\n')
++            if ret != 0:
++                buf.append(sp + '[%d]\n' % ret)
++        else:
++            chunk = (t, v)
++            raise Exception('unknown test chunk type: %r' % (chunk,))
++    return ''.join(buf)
++
++def parse_test_output(output, salt, test):
++    """Convert test output string into an iterable over test pairs."""
++    parsed, buf = [], []
++    for line in output.splitlines(True):
++        if line.startswith(salt):
++            retcode = int(line.split()[1])
++            parsed.append((''.join(buf), retcode))
++            buf = []
++        else:
++            buf.append(line)
++
++    parsed = iter(parsed)
++    for chunk in test:
++        t, v = chunk
++        if t == 'comment':
++            yield chunk
++        elif t == 'test':
++            command, _, _, indent = v
++            output, retcode = parsed.next()
++            if output.endswith('\n'):
++                output = output[:-1]
++            yield 'test', (command, output, retcode, indent)
++        else:
++            raise Exception('unknown test chunk type: %r' % (chunk,))
++
+ def test(path):
+     """Run test at path and return input, output, and diff.
+ 
+@@ -128,7 +264,15 @@ def test(path):
+     If a test exits with return code 80, the actual output is set to
+     None and diff is set to [].
+     """
+-    f = open(path)
++    fd = open(path)
++    try:
++        ref = fd.read()
++    finally:
++        fd.close()
++    refout = ref.splitlines(True)
++    refparsed = list(load_test(ref))
++    tests = [v for t, v in refparsed if t == 'test']
++
+     abspath = os.path.abspath(path)
+     env = os.environ.copy()
+     env['TESTDIR'] = os.path.dirname(abspath)
+@@ -137,44 +281,18 @@ def test(path):
+                          universal_newlines=True, env=env,
+                          close_fds=os.name == 'posix')
+     salt = 'CRAM%s' % time.time()
++    try:
++        for cmd, _, _, _ in tests:
++            p.stdin.write(cmd)
++            p.stdin.write('echo "\n%s $?"\n' % salt)
++        output = p.communicate()[0]
++        if p.returncode == 80:
++            return refout, None, []
++    finally:
++        p.wait()
+ 
+-    after = {}
+-    refout, postout = [], []
+-    i = pos = prepos = -1
+-    for i, line in enumerate(f):
+-        refout.append(line)
+-        if line.startswith('  $ '):
+-            after.setdefault(pos, []).append(line)
+-            prepos = pos
+-            pos = i
+-            p.stdin.write('echo "\n%s %s $?"\n' % (salt, i))
+-            p.stdin.write(line[4:])
+-        elif line.startswith('  > '):
+-            after.setdefault(prepos, []).append(line)
+-            p.stdin.write(line[4:])
+-        elif not line.startswith('  '):
+-            after.setdefault(pos, []).append(line)
+-    p.stdin.write('echo "\n%s %s $?"\n' % (salt, i + 1))
+-
+-    output = p.communicate()[0]
+-    if p.returncode == 80:
+-        return (refout, None, [])
+-
+-    pos = -1
+-    ret = 0
+-    for i, line in enumerate(output.splitlines(True)):
+-        if line.startswith(salt):
+-            presalt = postout.pop()
+-            if presalt != '  \n':
+-                postout.append(presalt[:-1] + '%\n')
+-            ret = int(line.split()[2])
+-            if ret != 0:
+-                postout.append('  [%s]\n' % ret)
+-            postout += after.pop(pos, [])
+-            pos = int(line.split()[1])
+-        else:
+-            postout.append('  ' + line)
+-    postout += after.pop(pos, [])
++    postparsed = parse_test_output(output, salt, refparsed)
++    postout = dump_test(postparsed).splitlines(True)
+ 
+     diff = unified_diff(refout, postout, abspath, abspath + '.err')
+     for firstline in diff:
+diff --git a/examples/test.t b/examples/test.t
+--- a/examples/test.t
++++ b/examples/test.t
+@@ -55,3 +55,53 @@ No newline:
+     %
+   $ echo foo
+   foo
++
++Lines indented with less than 2 spaces are just comments:
++
++$ echo 0
++
++ $ echo 1
++
++Allow arbitrary indent (2 or more spaces):
++
++  $ echo 2
++  2
++
++   $ echo 3
++   3
++
++    $ echo 4
++    4
++
++Consistent indent in commands:
++
++    $ echo '
++    > 6
++    > 7'
++    
++    6
++    7
++
++Inconsistent indent in commands:
++
++    $ echo ' > 8'
++     > 8
++
++    $ echo -n
++   > not a command line
++
++Inconsistent indent in output lines:
++
++    $ echo ' 9'
++     9
++
++    $ echo 13
++    13
++   not an output line
++
++    $ echo ' 14
++    > 15'
++     14
++    15
++   not an output line
++
+# HG changeset patch
+# Parent 0171f5b9e4c7c4e0c6849b2b35cc01bdd8a45236
+diff --git a/cram.py b/cram.py
+--- a/cram.py
++++ b/cram.py
+@@ -137,24 +137,27 @@ def test(path):
+                          universal_newlines=True, env=env,
+                          close_fds=os.name == 'posix')
+     salt = 'CRAM%s' % time.time()
++    presalt = 'PRE' + salt
+ 
+     after = {}
+     refout, postout = [], []
+     i = pos = prepos = -1
+     for i, line in enumerate(f):
+         refout.append(line)
+-        if line.startswith('  $ '):
++        sl = line.lstrip()
++        indent = line[:-len(sl)]
++        if not indent:
++            after.setdefault(pos, []).append(line)
++        elif sl.startswith('$ '):
+             after.setdefault(pos, []).append(line)
+             prepos = pos
+             pos = i
+-            p.stdin.write('echo "\n%s %s $?"\n' % (salt, i))
+-            p.stdin.write(line[4:])
+-        elif line.startswith('  > '):
++            p.stdin.write('echo "PRE%s\n%s %s $?"\n' % (salt, salt, i))
++            p.stdin.write(sl[2:])
++        elif sl.startswith('> '):
+             after.setdefault(prepos, []).append(line)
+-            p.stdin.write(line[4:])
+-        elif not line.startswith('  '):
+-            after.setdefault(pos, []).append(line)
+-    p.stdin.write('echo "\n%s %s $?"\n' % (salt, i + 1))
++            p.stdin.write(sl[2:])
++    p.stdin.write('echo "PRE%s\n%s %s $?"\n' % (salt, salt, i + 1))
+ 
+     output = p.communicate()[0]
+     if p.returncode == 80:
+@@ -164,9 +167,11 @@ def test(path):
+     ret = 0
+     for i, line in enumerate(output.splitlines(True)):
+         if line.startswith(salt):
+-            presalt = postout.pop()
+-            if presalt != '  \n':
+-                postout.append(presalt[:-1] + '%\n')
++            preline = postout.pop()
++            print repr(preline), repr(presalt)
++            if preline[:-1].endswith(presalt):
++                print repr(preline[:-len(presalt) - 1])
++                postout.append(preline[1:-len(presalt) - 1] + '%\n')
+             ret = int(line.split()[2])
+             if ret != 0:
+                 postout.append('  [%s]\n' % ret)