You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@cassandra.apache.org by al...@apache.org on 2015/12/18 23:49:41 UTC

[2/6] cassandra git commit: Fix cqlshlib tests on Windows

Fix cqlshlib tests on Windows

patch by Paulo Motta; reviewed by Jim Witschey for CASSANDRA-10541


Project: http://git-wip-us.apache.org/repos/asf/cassandra/repo
Commit: http://git-wip-us.apache.org/repos/asf/cassandra/commit/9dd2b5ea
Tree: http://git-wip-us.apache.org/repos/asf/cassandra/tree/9dd2b5ea
Diff: http://git-wip-us.apache.org/repos/asf/cassandra/diff/9dd2b5ea

Branch: refs/heads/cassandra-3.0
Commit: 9dd2b5ea0f842eb465ad38ac98f9dd987ba6b7d2
Parents: b03ce9f
Author: Paulo Motta <pa...@gmail.com>
Authored: Mon Nov 16 15:48:20 2015 -0200
Committer: Aleksey Yeschenko <al...@apache.org>
Committed: Fri Dec 18 22:46:31 2015 +0000

----------------------------------------------------------------------
 bin/cqlsh.py                                 |   4 +-
 pylib/cqlshlib/test/run_cqlsh.py             | 108 +++++++++++++++-------
 pylib/cqlshlib/test/test_cqlsh_completion.py |   3 +
 pylib/cqlshlib/test/test_cqlsh_output.py     |  22 +++--
 pylib/cqlshlib/test/winpty.py                |  50 ++++++++++
 5 files changed, 144 insertions(+), 43 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/cassandra/blob/9dd2b5ea/bin/cqlsh.py
----------------------------------------------------------------------
diff --git a/bin/cqlsh.py b/bin/cqlsh.py
index 42c2923..1119289 100644
--- a/bin/cqlsh.py
+++ b/bin/cqlsh.py
@@ -210,6 +210,8 @@ parser.add_option('--cqlversion', default=DEFAULT_CQLVER,
 parser.add_option("-e", "--execute", help='Execute the statement and quit.')
 parser.add_option("--connect-timeout", default=DEFAULT_CONNECT_TIMEOUT_SECONDS, dest='connect_timeout',
                   help='Specify the connection timeout in seconds (default: %default seconds).')
+parser.add_option("-t", "--tty", action='store_true', dest='tty',
+                  help='Force tty mode (command prompt).')
 
 optvalues = optparse.Values()
 (options, arguments) = parser.parse_args(sys.argv[1:], values=optvalues)
@@ -2328,7 +2330,7 @@ def read_options(cmdlineargs, environment):
     optvalues.ssl = False
     optvalues.encoding = None
 
-    optvalues.tty = sys.stdin.isatty()
+    optvalues.tty = option_with_default(configs.getboolean, 'ui', 'tty', sys.stdin.isatty())
     optvalues.cqlversion = option_with_default(configs.get, 'cql', 'version', DEFAULT_CQLVER)
     optvalues.connect_timeout = option_with_default(configs.getint, 'connection', 'timeout', DEFAULT_CONNECT_TIMEOUT_SECONDS)
     optvalues.execute = None

http://git-wip-us.apache.org/repos/asf/cassandra/blob/9dd2b5ea/pylib/cqlshlib/test/run_cqlsh.py
----------------------------------------------------------------------
diff --git a/pylib/cqlshlib/test/run_cqlsh.py b/pylib/cqlshlib/test/run_cqlsh.py
index 08e9e41..b011df4 100644
--- a/pylib/cqlshlib/test/run_cqlsh.py
+++ b/pylib/cqlshlib/test/run_cqlsh.py
@@ -17,17 +17,28 @@
 # NOTE: this testing tool is *nix specific
 
 import os
+import sys
 import re
-import pty
-import fcntl
 import contextlib
 import subprocess
 import signal
 import math
 from time import time
 from . import basecase
+from os.path import join, normpath
 
-DEFAULT_CQLSH_PROMPT = os.linesep + '(\S+@)?cqlsh(:\S+)?> '
+
+def is_win():
+    return sys.platform in ("cygwin", "win32")
+
+if is_win():
+    from winpty import WinPty
+    DEFAULT_PREFIX = ''
+else:
+    import pty
+    DEFAULT_PREFIX = os.linesep
+
+DEFAULT_CQLSH_PROMPT = DEFAULT_PREFIX + '(\S+@)?cqlsh(:\S+)?> '
 DEFAULT_CQLSH_TERM = 'xterm'
 
 cqlshlog = basecase.cqlshlog
@@ -41,10 +52,6 @@ def set_controlling_pty(master, slave):
         os.close(slave)
     os.close(os.open(os.ttyname(1), os.O_RDWR))
 
-def set_nonblocking(fd):
-    flags = fcntl.fcntl(fd, fcntl.F_GETFL)
-    fcntl.fcntl(fd, fcntl.F_SETFL, flags | os.O_NONBLOCK)
-
 @contextlib.contextmanager
 def raising_signal(signum, exc):
     """
@@ -93,12 +100,21 @@ def timing_out_alarm(seconds):
         finally:
             signal.alarm(0)
 
-# setitimer is new in 2.6, but it's still worth supporting, for potentially
-# faster tests because of sub-second resolution on timeouts.
-if hasattr(signal, 'setitimer'):
-    timing_out = timing_out_itimer
+if is_win():
+    try:
+        import eventlet
+    except ImportError, e:
+        sys.exit("evenlet library required to run cqlshlib tests on Windows")
+
+    def timing_out(seconds):
+        return eventlet.Timeout(seconds, TimeoutError)
 else:
-    timing_out = timing_out_alarm
+    # setitimer is new in 2.6, but it's still worth supporting, for potentially
+    # faster tests because of sub-second resolution on timeouts.
+    if hasattr(signal, 'setitimer'):
+        timing_out = timing_out_itimer
+    else:
+        timing_out = timing_out_alarm
 
 def noop(*a):
     pass
@@ -108,6 +124,7 @@ class ProcRunner:
         self.exe_path = path
         self.args = args
         self.tty = bool(tty)
+        self.realtty = self.tty and not is_win()
         if env is None:
             env = {}
         self.env = env
@@ -118,30 +135,35 @@ class ProcRunner:
     def start_proc(self):
         preexec = noop
         stdin = stdout = stderr = None
-        if self.tty:
-            masterfd, slavefd = pty.openpty()
-            preexec = lambda: set_controlling_pty(masterfd, slavefd)
-        else:
-            stdin = stdout = subprocess.PIPE
-            stderr = subprocess.STDOUT
         cqlshlog.info("Spawning %r subprocess with args: %r and env: %r"
                       % (self.exe_path, self.args, self.env))
-        self.proc = subprocess.Popen(('python', self.exe_path,) + tuple(self.args),
-                                     env=self.env, preexec_fn=preexec,
-                                     stdin=stdin, stdout=stdout, stderr=stderr,
-                                     close_fds=False)
-        if self.tty:
+        if self.realtty:
+            masterfd, slavefd = pty.openpty()
+            preexec = (lambda: set_controlling_pty(masterfd, slavefd))
+            self.proc = subprocess.Popen((self.exe_path,) + tuple(self.args),
+                                         env=self.env, preexec_fn=preexec,
+                                         stdin=stdin, stdout=stdout, stderr=stderr,
+                                         close_fds=False)
             os.close(slavefd)
             self.childpty = masterfd
             self.send = self.send_tty
             self.read = self.read_tty
         else:
+            stdin = stdout = subprocess.PIPE
+            stderr = subprocess.STDOUT
+            self.proc = subprocess.Popen((self.exe_path,) + tuple(self.args),
+                                         env=self.env, stdin=stdin, stdout=stdout,
+                                         stderr=stderr, bufsize=0, close_fds=False)
             self.send = self.send_pipe
-            self.read = self.read_pipe
+            if self.tty:
+                self.winpty = WinPty(self.proc.stdout)
+                self.read = self.read_winpty
+            else:
+                self.read = self.read_pipe
 
     def close(self):
         cqlshlog.info("Closing %r subprocess." % (self.exe_path,))
-        if self.tty:
+        if self.realtty:
             os.close(self.childpty)
         else:
             self.proc.stdin.close()
@@ -154,20 +176,24 @@ class ProcRunner:
     def send_pipe(self, data):
         self.proc.stdin.write(data)
 
-    def read_tty(self, blksize):
+    def read_tty(self, blksize, timeout=None):
         return os.read(self.childpty, blksize)
 
-    def read_pipe(self, blksize):
+    def read_pipe(self, blksize, timeout=None):
         return self.proc.stdout.read(blksize)
 
-    def read_until(self, until, blksize=4096, timeout=None, flags=0):
+    def read_winpty(self, blksize, timeout=None):
+        return self.winpty.read(blksize, timeout)
+
+    def read_until(self, until, blksize=4096, timeout=None,
+                   flags=0, ptty_timeout=None):
         if not isinstance(until, re._pattern_type):
             until = re.compile(until, flags)
         got = self.readbuf
         self.readbuf = ''
         with timing_out(timeout):
             while True:
-                val = self.read(blksize)
+                val = self.read(blksize, ptty_timeout)
                 cqlshlog.debug("read %r from subproc" % (val,))
                 if val == '':
                     raise EOFError("'until' pattern %r not found" % (until.pattern,))
@@ -205,15 +231,22 @@ class ProcRunner:
 
 class CqlshRunner(ProcRunner):
     def __init__(self, path=None, host=None, port=None, keyspace=None, cqlver=None,
-                 args=(), prompt=DEFAULT_CQLSH_PROMPT, env=None, **kwargs):
+                 args=(), prompt=DEFAULT_CQLSH_PROMPT, env=None,
+                 win_force_colors=True, tty=True, **kwargs):
         if path is None:
-            path = basecase.path_to_cqlsh
+            cqlsh_bin = 'cqlsh'
+            if is_win():
+                cqlsh_bin = 'cqlsh.bat'
+            path = normpath(join(basecase.cqlshdir, cqlsh_bin))
         if host is None:
             host = basecase.TEST_HOST
         if port is None:
             port = basecase.TEST_PORT
         if env is None:
             env = {}
+        if is_win():
+            env['PYTHONUNBUFFERED'] = '1'
+            env.update(os.environ.copy())
         env.setdefault('TERM', 'xterm')
         env.setdefault('CQLSH_NO_BUNDLED', os.environ.get('CQLSH_NO_BUNDLED', ''))
         env.setdefault('PYTHONPATH', os.environ.get('PYTHONPATH', ''))
@@ -222,8 +255,13 @@ class CqlshRunner(ProcRunner):
             args += ('--cqlversion', str(cqlver))
         if keyspace is not None:
             args += ('--keyspace', keyspace)
+        if tty and is_win():
+            args += ('--tty',)
+            args += ('--encoding', 'utf-8')
+            if win_force_colors:
+                args += ('--color',)
         self.keyspace = keyspace
-        ProcRunner.__init__(self, path, args=args, env=env, **kwargs)
+        ProcRunner.__init__(self, path, tty=tty, args=args, env=env, **kwargs)
         self.prompt = prompt
         if self.prompt is None:
             self.output_header = ''
@@ -231,7 +269,7 @@ class CqlshRunner(ProcRunner):
             self.output_header = self.read_to_next_prompt()
 
     def read_to_next_prompt(self):
-        return self.read_until(self.prompt, timeout=10.0)
+        return self.read_until(self.prompt, timeout=10.0, ptty_timeout=3)
 
     def read_up_to_timeout(self, timeout, blksize=4096):
         output = ProcRunner.read_up_to_timeout(self, timeout, blksize=blksize)
@@ -247,7 +285,7 @@ class CqlshRunner(ProcRunner):
         output = output.replace(' \r', '')
         output = output.replace('\r', '')
         output = output.replace(' \b', '')
-        if self.tty:
+        if self.realtty:
             echo, output = output.split('\n', 1)
             assert echo == cmd, "unexpected echo %r instead of %r" % (echo, cmd)
         try:
@@ -255,7 +293,7 @@ class CqlshRunner(ProcRunner):
         except ValueError:
             promptline = output
             output = ''
-        assert re.match(self.prompt, '\n' + promptline), \
+        assert re.match(self.prompt, DEFAULT_PREFIX + promptline), \
                 'last line of output %r does not match %r?' % (promptline, self.prompt)
         return output + '\n'
 

http://git-wip-us.apache.org/repos/asf/cassandra/blob/9dd2b5ea/pylib/cqlshlib/test/test_cqlsh_completion.py
----------------------------------------------------------------------
diff --git a/pylib/cqlshlib/test/test_cqlsh_completion.py b/pylib/cqlshlib/test/test_cqlsh_completion.py
index e5eb9e1..e67fefe 100644
--- a/pylib/cqlshlib/test/test_cqlsh_completion.py
+++ b/pylib/cqlshlib/test/test_cqlsh_completion.py
@@ -22,6 +22,8 @@ from __future__ import with_statement
 import re
 from .basecase import BaseTestCase, cqlsh
 from .cassconnect import testrun_cqlsh
+import unittest
+import sys
 
 BEL = '\x07'  # the terminal-bell character
 CTRL_C = '\x03'
@@ -36,6 +38,7 @@ COMPLETION_RESPONSE_TIME = 0.5
 completion_separation_re = re.compile(r'\s+')
 
 
+@unittest.skipIf(sys.platform == "win32", 'Tab completion tests not supported on Windows')
 class CqlshCompletionCase(BaseTestCase):
 
     def setUp(self):

http://git-wip-us.apache.org/repos/asf/cassandra/blob/9dd2b5ea/pylib/cqlshlib/test/test_cqlsh_output.py
----------------------------------------------------------------------
diff --git a/pylib/cqlshlib/test/test_cqlsh_output.py b/pylib/cqlshlib/test/test_cqlsh_output.py
index 7a2fc86..c62ed69 100644
--- a/pylib/cqlshlib/test/test_cqlsh_output.py
+++ b/pylib/cqlshlib/test/test_cqlsh_output.py
@@ -27,11 +27,12 @@ from .cassconnect import (get_test_keyspace, testrun_cqlsh, testcall_cqlsh,
                           cassandra_cursor, split_cql_commands, quote_name)
 from .ansi_colors import (ColoredText, lookup_colorcode, lookup_colorname,
                           lookup_colorletter, ansi_seq)
+import unittest
+import sys
 
 CONTROL_C = '\x03'
 CONTROL_D = '\x04'
 
-
 class TestCqlshOutput(BaseTestCase):
 
     def setUp(self):
@@ -92,7 +93,8 @@ class TestCqlshOutput(BaseTestCase):
     def test_no_color_output(self):
         for termname in ('', 'dumb', 'vt100'):
             cqlshlog.debug('TERM=%r' % termname)
-            with testrun_cqlsh(tty=True, env={'TERM': termname}) as c:
+            with testrun_cqlsh(tty=True, env={'TERM': termname},
+                               win_force_colors=False) as c:
                 c.send('select * from has_all_types;\n')
                 self.assertNoHasColors(c.read_to_next_prompt())
                 c.send('select count(*) from has_all_types;\n')
@@ -526,9 +528,13 @@ class TestCqlshOutput(BaseTestCase):
             c.send('use NONEXISTENTKEYSPACE;\n')
             outputlines = c.read_to_next_prompt().splitlines()
 
-            self.assertEqual(outputlines[0], 'use NONEXISTENTKEYSPACE;')
-            self.assertTrue(outputlines[2].endswith('cqlsh:system> '))
-            midline = ColoredText(outputlines[1])
+            start_index = 0
+            if c.realtty:
+                self.assertEqual(outputlines[start_index], 'use NONEXISTENTKEYSPACE;')
+                start_index = 1
+
+            self.assertTrue(outputlines[start_index+1].endswith('cqlsh:system> '))
+            midline = ColoredText(outputlines[start_index])
             self.assertEqual(midline.plain(),
                              'InvalidRequest: code=2200 [Invalid query] message="Keyspace \'nonexistentkeyspace\' does not exist"')
             self.assertColorFromTags(midline,
@@ -716,6 +722,7 @@ class TestCqlshOutput(BaseTestCase):
             self.assertRegexpMatches(output, '^Connected to .* at %s:%d\.$'
                                              % (re.escape(TEST_HOST), TEST_PORT))
 
+    @unittest.skipIf(sys.platform == "win32", 'EOF signaling not supported on Windows')
     def test_eof_prints_newline(self):
         with testrun_cqlsh(tty=True) as c:
             c.send(CONTROL_D)
@@ -730,8 +737,9 @@ class TestCqlshOutput(BaseTestCase):
             with testrun_cqlsh(tty=True) as c:
                 cmd = 'exit%s\n' % semicolon
                 c.send(cmd)
-                out = c.read_lines(1)[0].replace('\r', '')
-                self.assertEqual(out, cmd)
+                if c.realtty:
+                    out = c.read_lines(1)[0].replace('\r', '')
+                    self.assertEqual(out, cmd)
                 with self.assertRaises(BaseException) as cm:
                     c.read_lines(1)
                 self.assertIn(type(cm.exception), (EOFError, OSError))

http://git-wip-us.apache.org/repos/asf/cassandra/blob/9dd2b5ea/pylib/cqlshlib/test/winpty.py
----------------------------------------------------------------------
diff --git a/pylib/cqlshlib/test/winpty.py b/pylib/cqlshlib/test/winpty.py
new file mode 100644
index 0000000..0db9ec3
--- /dev/null
+++ b/pylib/cqlshlib/test/winpty.py
@@ -0,0 +1,50 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from threading import Thread
+from cStringIO import StringIO
+from Queue import Queue, Empty
+
+
+class WinPty:
+
+    def __init__(self, stdin):
+        self._s = stdin
+        self._q = Queue()
+
+        def _read_next_char(stdin, queue):
+            while True:
+                char = stdin.read(1)  # potentially blocking read
+                if char:
+                    queue.put(char)
+                else:
+                    break
+
+        self._t = Thread(target=_read_next_char, args=(self._s, self._q))
+        self._t.daemon = True
+        self._t.start()  # read characters asynchronously from stdin
+
+    def read(self, blksize=-1, timeout=1):
+        buf = StringIO()
+        count = 0
+        try:
+            while count < blksize or blksize == -1:
+                next = self._q.get(block=timeout is not None, timeout=timeout)
+                buf.write(next)
+                count = count + 1
+        except Empty:
+            pass
+        return buf.getvalue()