Commits

Brodie Rao  committed ac291a4

Use patch(1) to merge output

  • Participants
  • Parent commits 3c2cb65

Comments (0)

Files changed (4)

   ``.t`` file, and ``$CRAMTMP`` is set to the test runner's temporary
   directory.
 
+* ``-i``/``--interactive`` now requires ``patch(1)``. Instead of the
+  ``.err`` replacing the original test file when answering yes to a
+  merge, the diff output is ran through ``patch(1)`` this prevents
+  matching regular expressions and globs from getting clobbered.
+
 * Previous ``.err`` files are removed when tests pass.
 
-* Added ``-q/--quiet`` to suppress diff output.
+* Added ``-q``/``--quiet`` to suppress diff output.
 
 * The number of tests, the number of skipped tests, and the number of
   failed tests are now printed after all tests are finished.
-* Use patch(1) or something similar to merge diff output back in when
-  using -i. This would have the benefit of not clobbering regexes and
-  globs that matched fine.
-
 * Implement -j flag, most likely using spawnv().
 
 * Using spawnv() machinery, implement distributed testing.
         sys.stdout.flush()
         if auto is not None:
             sys.stdout.write(auto + '\n')
+            sys.stdout.flush()
             return auto
 
         answer = sys.stdin.readline().strip().lower()
         sys.stdout.write(msg)
         sys.stdout.flush()
 
-def run(paths, quiet=False, verbose=False, interactive=False,
-        basetmp=None, keeptmp=False, answer=None):
+def patch(cmd, diff):
+    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.flush()
+    p.stdin.close()
+    p.wait()
+    return p.returncode == 0
+
+def run(paths, quiet=False, verbose=False, basetmp=None, keeptmp=False,
+        patchcmd=None, answer=None):
     """Run tests in paths.
 
-    If quiet is True, diffs aren't printed.
-
-    If verbose is True, filenames and status information are printed.
+    If quiet is True, diffs aren't printed. If verbose is True,
+    filenames and status information are printed.
 
     If basetmp is set, each test is run in a random temporary
-    directory inside basetmp.
+    directory inside basetmp. If keeptmp is also True, temporary
+    directories are preserved after use.
 
-    If basetmp is set and keeptmp is True, temporary directories are
-    preserved after use.
-
-    If interactive is True, a prompt is written to stdout asking if
+    If patchcmd is set, a prompt is written to stdout asking if
     changed output should be merged back into the original test. The
-    answer is read from stdin.
+    answer is read from stdin. If 'y', the test is patched using patch
+    based on the changed output.
     """
     cwd = os.getcwd()
     seen = set()
                 finally:
                     errfile.close()
                 if not quiet:
+                    if patchcmd:
+                        diff = list(diff)
                     for line in diff:
                         log(line)
-                    if interactive:
-                        if prompt('Accept this change?', 'yN', answer) == 'y':
-                            shutil.copy(errpath, abspath)
+                    if (patchcmd and
+                        prompt('Accept this change?', 'yN', answer) == 'y'):
+                        if patch(patchcmd, diff):
+                            log(None, '%s: merged output\n' % path, verbose)
                             os.remove(errpath)
-                            log(None, '%s: merged output\n' % path, verbose)
+                        else:
+                            log('%s: merge failed\n' % path)
     log('\n', None, verbose)
     log('# Ran %s tests, %s skipped, %s failed.\n'
         % (len(seen), skipped, failed))
 
+def which(cmd):
+    """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):
+            return path
+    return None
+
 def main(args):
     """Main entry point.
 
                              % (s1, s2))
             return 2
 
+    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(p.get_usage())
         return 2
         answer = None
 
     try:
-        run(paths, opts.quiet, opts.verbose, opts.interactive, basetmp,
-            opts.keep_tmpdir, answer)
+        run(paths, opts.quiet, opts.verbose, basetmp, opts.keep_tmpdir,
+            patchcmd, answer)
     finally:
         if not opts.keep_tmpdir:
             shutil.rmtree(basetmp)

File tests/cram.t

   $ [ -n "$PYTHON" ] || PYTHON=python
   $ if [ -n "$COVERAGE" ]; then
   >   coverage erase
-  >   alias cram="coverage run -a $TESTDIR/../cram.py"
+  >   alias cram="`which coverage` run -a $TESTDIR/../cram.py"
   > else
   >   alias cram="$PYTHON $TESTDIR/../cram.py"
   > fi
   -  #
   +  @
   Accept this change? [yN] y
+  patching file */fail.t (glob)
   
   # Ran 1 tests, 0 skipped, 1 failed.
   $ md5 examples/fail.t
-  .*\b6aed028cafd917d35ce7db5029e8f559\b.* (re)
+  .*\be977f4580168266f943f775a1923d157\b.* (re)
   $ mv examples/fail.t.orig examples/fail.t
 
 Verbose interactive mode (answer manually and don't merge):
      [A-Z] (re)
   -  #
   +  @
-  Accept this change? [yN] Accept this change? [yN] examples/fail.t: merged output
+  Accept this change? [yN] Accept this change? [yN] patching file */fail.t (glob)
+  examples/fail.t: merged output
   # Ran 1 tests, 0 skipped, 1 failed.
   $ md5 examples/fail.t
+  .*\be977f4580168266f943f775a1923d157\b.* (re)
+  $ mv examples/fail.t.orig examples/fail.t
+
+Test missing patch(1) and patch(1) error:
+
+  $ PATH=. cram -i examples/fail.t
+  patch(1) required for -i
+  [2]
+  $ cat > patch <<EOF
+  > #!/bin/sh
+  > echo "patch failed" 1>&2
+  > exit 1
+  > EOF
+  $ chmod +x patch
+  $ PATH=. cram -y -i examples/fail.t
+  !
+  --- */examples/fail.t (glob)
+  +++ */examples/fail.t.err (glob)
+  @@ -3,21 +3,22 @@
+     $ echo 1
+     1
+     $ echo 1
+  -  2
+  +  1
+     $ echo 1
+     1
+   
+   Invalid regex:
+   
+     $ echo 1
+  -  +++ (re)
+  +  1
+   
+   Offset regular expression:
+   
+     $ printf 'foo\nbar\nbaz\n\n1\nA\n@\n'
+     foo
+  +  bar
+     baz
+     
+     \d (re)
+     [A-Z] (re)
+  -  #
+  +  @
+  Accept this change? [yN] y
+  patch failed
+  examples/fail.t: merge failed
+  
+  # Ran 1 tests, 0 skipped, 1 failed.
+  $ md5 examples/fail.t examples/fail.t.err
+  .*\ba36d8e81925296ce794f1a3b35994a68\b.* (re)
   .*\b6aed028cafd917d35ce7db5029e8f559\b.* (re)
-  $ mv examples/fail.t.orig examples/fail.t
+  $ rm patch examples/fail.t.err
 
 Test that a fixed .err file is deleted: