Patchwork D10947: test: use a python script in `test-transaction-rollback-on-sigpipe.t`

login
register
mail settings
Submitter phabricator
Date July 3, 2021, 1:42 a.m.
Message ID <differential-rev-PHID-DREV-qt7lis7j4hzfxnwe2ovw-req@mercurial-scm.org>
Download mbox | patch
Permalink /patch/49261/
State Superseded
Headers show

Comments

phabricator - July 3, 2021, 1:42 a.m.
marmoute created this revision.
Herald added a reviewer: hg-reviewers.
Herald added a subscriber: mercurial-patches.

REVISION SUMMARY
  This still does not work on Windows, but at least this is a python script now.

REPOSITORY
  rHG Mercurial

BRANCH
  default

REVISION DETAIL
  https://phab.mercurial-scm.org/D10947

AFFECTED FILES
  tests/test-transaction-rollback-on-sigpipe.t
  tests/testlib/sigpipe-remote.py

CHANGE DETAILS




To: marmoute, #hg-reviewers
Cc: mercurial-patches, mercurial-devel

Patch

diff --git a/tests/testlib/sigpipe-remote.py b/tests/testlib/sigpipe-remote.py
new file mode 100755
--- /dev/null
+++ b/tests/testlib/sigpipe-remote.py
@@ -0,0 +1,176 @@ 
+#!/usr/bin/env -S python3 -u
+from __future__ import print_function
+
+import os
+import subprocess
+import sys
+import threading
+import time
+
+# we cannot use mercurial.testing as long as python2 is not dropped as the test will only install the mercurial module for python2 in python2 run
+
+
+def _timeout_factor():
+    """return the current modification to timeout"""
+    default = int(os.environ.get('HGTEST_TIMEOUT_DEFAULT', 360))
+    current = int(os.environ.get('HGTEST_TIMEOUT', default))
+    if current == 0:
+        return 1
+    return current / float(default)
+
+
+def wait_file(path, timeout=10):
+    timeout *= _timeout_factor()
+    start = time.time()
+    while not os.path.exists(path):
+        if (time.time() - start) > timeout:
+            raise RuntimeError(b"timed out waiting for file: %s" % path)
+        time.sleep(0.01)
+
+
+def write_file(path, content=b''):
+    with open(path, 'wb') as f:
+        f.write(content)
+
+
+# end of mercurial.testing content
+
+if sys.version_info[0] < 3:
+    print('SIGPIPE-HELPER: script should run with Python 3', file=sys.stderr)
+    sys.exit(255)
+
+
+def sysbytes(s):
+    return s.encode('utf-8')
+
+
+def sysstr(s):
+    return s.decode('latin-1')
+
+
+piped_stdout = os.pipe2(os.O_NONBLOCK | os.O_CLOEXEC)
+piped_stderr = os.pipe2(os.O_NONBLOCK | os.O_CLOEXEC)
+
+stdout_writer = os.fdopen(piped_stdout[1], "rb")
+stdout_reader = os.fdopen(piped_stdout[0], "rb")
+stderr_writer = os.fdopen(piped_stderr[1], "rb")
+stderr_reader = os.fdopen(piped_stderr[0], "rb")
+
+DEBUG_FILE = os.environ.get('SIGPIPE_REMOTE_DEBUG_FILE')
+if DEBUG_FILE is None:
+    debug_stream = sys.stderr.buffer
+else:
+    debug_stream = open(DEBUG_FILE, 'bw', buffering=0)
+
+SYNCFILE1 = os.environ.get('SYNCFILE1')
+SYNCFILE2 = os.environ.get('SYNCFILE2')
+if SYNCFILE1 is None:
+    print('SIGPIPE-HELPER: missing variable $SYNCFILE1', file=sys.stderr)
+    sys.exit(255)
+if SYNCFILE2 is None:
+    print('SIGPIPE-HELPER: missing variable $SYNCFILE2', file=sys.stderr)
+    sys.exit(255)
+
+debug_stream.write(b'SIGPIPE-HELPER: Starting\n')
+
+TESTLIB_DIR = os.path.dirname(sys.argv[0])
+WAIT_SCRIPT = os.path.join(TESTLIB_DIR, 'wait-on-file')
+
+hooks_cmd = '%s 10 %s %s'
+hooks_cmd %= (
+    WAIT_SCRIPT,
+    SYNCFILE2,
+    SYNCFILE1,
+)
+
+cmd = ['hg']
+cmd += sys.argv[1:]
+sub = subprocess.Popen(
+    cmd,
+    bufsize=0,
+    close_fds=True,
+    stdin=sys.stdin,
+    stdout=stdout_writer,
+    stderr=stderr_writer,
+)
+
+debug_stream.write(b'SIGPIPE-HELPER: Mercurial started\n')
+
+
+shut_down = threading.Event()
+
+close_lock = threading.Lock()
+
+
+def _read(stream):
+    try:
+        return stream.read()
+    except ValueError:
+        # read on closed file
+        return None
+
+
+def forward_stdout():
+    while not shut_down.is_set():
+        c = _read(stdout_reader)
+        while c is not None:
+            sys.stdout.buffer.write(c)
+            c = _read(stdout_reader)
+        time.sleep(0.001)
+    with close_lock:
+        if not stdout_reader.closed:
+            stdout_reader.close()
+            debug_stream.write(b'SIGPIPE-HELPER: stdout closed\n')
+
+
+def forward_stderr():
+    while not shut_down.is_set():
+        c = _read(stderr_reader)
+        if c is not None:
+            sys.stderr.buffer.write(c)
+            c = _read(stderr_reader)
+        time.sleep(0.001)
+    with close_lock:
+        if not stderr_reader.closed:
+            stderr_reader.close()
+            debug_stream.write(b'SIGPIPE-HELPER: stderr closed\n')
+
+
+stdout_thread = threading.Thread(target=forward_stdout, daemon=True)
+stderr_thread = threading.Thread(target=forward_stderr, daemon=True)
+
+try:
+    stdout_thread.start()
+    stderr_thread.start()
+
+    debug_stream.write(b'SIGPIPE-HELPER: Redirection in place\n')
+
+    try:
+        wait_file(sysbytes(SYNCFILE1))
+    except RuntimeError as exc:
+        msg = sysbytes(str(exc))
+        debug_stream.write(b'SIGPIPE-HELPER: wait failed: %s\n' % msg)
+    else:
+        debug_stream.write(b'SIGPIPE-HELPER: SYNCFILE1 detected\n')
+    with close_lock:
+        if not stdout_reader.closed:
+            stdout_reader.close()
+        if not stderr_reader.closed:
+            stderr_reader.close()
+        sys.stdin.close()
+        debug_stream.write(b'SIGPIPE-HELPER: pipes closed\n')
+    debug_stream.write(b'SIGPIPE-HELPER: creating SYNCFILE2\n')
+    write_file(sysbytes(SYNCFILE2))
+finally:
+    debug_stream.write(b'SIGPIPE-HELPER: Shutting down\n')
+    shut_down.set()
+    if not sys.stdin.closed:
+        sys.stdin.close()
+    try:
+        sub.wait(timeout=30)
+    except subprocess.TimeoutExpired:
+        msg = b'SIGPIPE-HELPER: Server process failed to terminate\n'
+        debug_stream.write(msg)
+    else:
+        debug_stream.write(b'SIGPIPE-HELPER: Server process terminated\n')
+    debug_stream.write(b'SIGPIPE-HELPER: Shut down\n')
diff --git a/tests/test-transaction-rollback-on-sigpipe.t b/tests/test-transaction-rollback-on-sigpipe.t
--- a/tests/test-transaction-rollback-on-sigpipe.t
+++ b/tests/test-transaction-rollback-on-sigpipe.t
@@ -3,51 +3,27 @@ 
 the remote hg is able to successfully roll back the transaction.
 
   $ hg init -q remote
-  $ hg clone -e "\"$PYTHON\" \"$TESTDIR/dummyssh\"" -q ssh://user@dummy/`pwd`/remote local
-
-  $ pidfile=`pwd`/pidfile
-  $ >$pidfile
-
-  $ script() {
-  >     cat >"$1"
-  >     chmod +x "$1"
-  > }
+  $ hg clone -e "\"$PYTHON\" \"$RUNTESTDIR/dummyssh\"" -q ssh://user@dummy/`pwd`/remote local
+  $ SIGPIPE_REMOTE_DEBUG_FILE="$TESTTMP/DEBUGFILE"
+  $ SYNCFILE1="$TESTTMP/SYNCFILE1"
+  $ SYNCFILE2="$TESTTMP/SYNCFILE2"
+  $ export SIGPIPE_REMOTE_DEBUG_FILE
+  $ export SYNCFILE1
+  $ export SYNCFILE2
 
 On the remote end, run hg, piping stdout and stderr through processes that we
 know the PIDs of. We will later kill these to simulate an ssh client
 disconnecting.
 
-  $ killable_pipe=`pwd`/killable_pipe.sh
-  $ script $killable_pipe <<EOF
-  > #!/usr/bin/env bash
-  > echo \$\$ >> $pidfile
-  > exec cat
-  > EOF
-
-  $ remotecmd=`pwd`/remotecmd.sh
-  $ script $remotecmd <<EOF
-  > #!/usr/bin/env bash
-  > hg "\$@" 1> >($killable_pipe) 2> >($killable_pipe >&2)
-  > EOF
+  $ remotecmd="$RUNTESTDIR/testlib/sigpipe-remote.py"
 
 In the pretxnchangegroup hook, kill the PIDs recorded above to simulate ssh
 disconnecting. Then exit nonzero, to force a transaction rollback.
 
-  $ hook_script=`pwd`/pretxnchangegroup.sh
-  $ script $hook_script <<EOF
-  > #!/usr/bin/env bash
-  > for pid in \$(cat $pidfile) ; do
-  >   kill \$pid
-  >   while kill -0 \$pid 2>/dev/null ; do
-  >     sleep 0.1
-  >   done
-  > done
-  > exit 1
-  > EOF
 
   $ cat >remote/.hg/hgrc <<EOF
   > [hooks]
-  > pretxnchangegroup.00-break-things=$hook_script
+  > pretxnchangegroup.00-break-things="$RUNTESTDIR/testlib/wait-on-file" 10 "$SYNCFILE2" "$SYNCFILE1"
   > pretxnchangegroup.01-output-things=echo "some remote output to be forward to the closed pipe"
   > EOF
 
@@ -55,8 +31,24 @@ 
   000000000000
   $ cd local
   $ echo foo > foo ; hg commit -qAm "commit"
-  $ hg push -q -e "\"$PYTHON\" \"$TESTDIR/dummyssh\"" --remotecmd $remotecmd 2>&1 | grep -v $killable_pipe
+  $ hg push -e "\"$PYTHON\" \"$TESTDIR/dummyssh\"" --remotecmd "$remotecmd"
+  pushing to ssh://user@dummy/$TESTTMP/remote
+  searching for changes
+  remote: adding changesets
+  remote: adding manifests
+  remote: adding file changes
   abort: stream ended unexpectedly (got 0 bytes, expected 4)
+  [255]
+  $ cat $SIGPIPE_REMOTE_DEBUG_FILE
+  SIGPIPE-HELPER: Starting
+  SIGPIPE-HELPER: Mercurial started
+  SIGPIPE-HELPER: Redirection in place
+  SIGPIPE-HELPER: SYNCFILE1 detected
+  SIGPIPE-HELPER: pipes closed
+  SIGPIPE-HELPER: creating SYNCFILE2
+  SIGPIPE-HELPER: Shutting down
+  SIGPIPE-HELPER: Server process terminated
+  SIGPIPE-HELPER: Shut down
 
 The remote should be left in a good state
   $ hg --cwd ../remote tip -T '{node|short}\n'