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 2012/12/28 00:37:37 UTC

[2/3] git commit: cqlsh: add unit tests; patch by Paul Cannon with minor changes by Aleksey Yeschenko, reviewed by Aleksey Yeschenko for CASSANDRA-3920

cqlsh: add unit tests;
patch by Paul Cannon with minor changes by Aleksey Yeschenko, reviewed
by Aleksey Yeschenko for CASSANDRA-3920


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

Branch: refs/heads/cassandra-1.2
Commit: 14d62ab115001be9dfa872fb59e60ea532372d4f
Parents: 4b9d927
Author: Aleksey Yeschenko <al...@apache.org>
Authored: Fri Dec 28 02:30:35 2012 +0300
Committer: Aleksey Yeschenko <al...@apache.org>
Committed: Fri Dec 28 02:30:35 2012 +0300

----------------------------------------------------------------------
 CHANGES.txt                                  |    1 +
 bin/cqlsh                                    |   86 ++-
 pylib/cqlshlib/cql3handling.py               |   28 +-
 pylib/cqlshlib/displaying.py                 |    8 +-
 pylib/cqlshlib/formatting.py                 |   27 +-
 pylib/cqlshlib/test/__init__.py              |   20 +
 pylib/cqlshlib/test/ansi_colors.py           |  191 ++++
 pylib/cqlshlib/test/basecase.py              |   71 ++
 pylib/cqlshlib/test/cassconnect.py           |  159 ++++
 pylib/cqlshlib/test/run_cqlsh.py             |  271 ++++++
 pylib/cqlshlib/test/table_arrangements.cql   |  114 +++
 pylib/cqlshlib/test/test_cql_parsing.py      |   87 ++
 pylib/cqlshlib/test/test_cqlsh_commands.py   |   42 +
 pylib/cqlshlib/test/test_cqlsh_completion.py |  243 ++++++
 pylib/cqlshlib/test/test_cqlsh_invocation.py |   78 ++
 pylib/cqlshlib/test/test_cqlsh_output.py     |  965 +++++++++++++++++++++
 pylib/cqlshlib/test/test_keyspace_init2.cql  |  180 ++++
 pylib/cqlshlib/test/test_keyspace_init3.cql  |   36 +
 18 files changed, 2558 insertions(+), 49 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/cassandra/blob/14d62ab1/CHANGES.txt
----------------------------------------------------------------------
diff --git a/CHANGES.txt b/CHANGES.txt
index c4c2407..31563c6 100644
--- a/CHANGES.txt
+++ b/CHANGES.txt
@@ -1,5 +1,6 @@
 1.2.0
  * Disallow counters in collections (CASSANDRA-5082)
+ * cqlsh: add unit tests (CASSANDRA-3920)
 
 
 1.2.0-rc2

http://git-wip-us.apache.org/repos/asf/cassandra/blob/14d62ab1/bin/cqlsh
----------------------------------------------------------------------
diff --git a/bin/cqlsh b/bin/cqlsh
index e234e10..5d4ca00 100755
--- a/bin/cqlsh
+++ b/bin/cqlsh
@@ -53,10 +53,16 @@ import platform
 import warnings
 import csv
 
+
+readline = None
 try:
-    import readline
+    # check if tty first, cause readline doesn't check, and only cares
+    # about $TERM. we don't want the funky escape code stuff to be
+    # output if not a tty.
+    if sys.stdin.isatty():
+        import readline
 except ImportError:
-    readline = None
+    pass
 
 CQL_LIB_PREFIX = 'cql-internal-only-'
 THRIFT_LIB_PREFIX = 'thrift-python-internal-only-'
@@ -166,7 +172,7 @@ else:
 
 debug_completion = bool(os.environ.get('CQLSH_DEBUG_COMPLETION', '') == 'YES')
 
-SYSTEM_KEYSPACES = ('system', 'system_traces')
+SYSTEM_KEYSPACES = ('system', 'system_traces', 'system_auth')
 
 # we want the cql parser to understand our cqlsh-specific commands too
 my_commands_ending_with_newline = (
@@ -368,9 +374,13 @@ class VersionNotSupported(Exception):
     pass
 
 class DecodeError(Exception):
+    verb = 'decode'
+
     def __init__(self, thebytes, err, expectedtype, colname=None):
         self.thebytes = thebytes
         self.err = err
+        if isinstance(expectedtype, type) and issubclass(expectedtype, CassandraType):
+            expectedtype = expectedtype.cql_parameterized_type()
         self.expectedtype = expectedtype
         self.colname = colname
 
@@ -378,14 +388,18 @@ class DecodeError(Exception):
         return str(self.thebytes)
 
     def message(self):
-        what = 'column name %r' % (self.thebytes,)
+        what = 'value %r' % (self.thebytes,)
         if self.colname is not None:
             what = 'value %r (for column %r)' % (self.thebytes, self.colname)
-        return 'Failed to decode %s as %s: %s' % (what, self.expectedtype, self.err)
+        return 'Failed to %s %s as %s: %s' \
+               % (self.verb, what, self.expectedtype, self.err)
 
     def __repr__(self):
         return '<%s %s>' % (self.__class__.__name__, self.message())
 
+class FormatError(DecodeError):
+    verb = 'format'
+
 def full_cql_version(ver):
     while ver.count('.') < 2:
         ver += '.0'
@@ -397,9 +411,9 @@ def format_value(val, typeclass, output_encoding, addcolor=False, time_format=No
                  float_precision=None, colormap=None, nullval=None):
     if isinstance(val, DecodeError):
         if addcolor:
-            return colorme(val.thebytes, colormap, 'hex')
+            return colorme(repr(val.thebytes), colormap, 'error')
         else:
-            return FormattedValue(val.thebytes)
+            return FormattedValue(repr(val.thebytes))
     if not issubclass(typeclass, CassandraType):
         typeclass = lookup_casstype(typeclass)
     return format_by_type(typeclass, val, output_encoding, colormap=colormap,
@@ -449,7 +463,7 @@ class Shell(cmd.Cmd):
     def __init__(self, hostname, port, transport_factory, color=False,
                  username=None, password=None, encoding=None, stdin=None, tty=True,
                  completekey=DEFAULT_COMPLETEKEY, use_conn=None,
-                 cqlver=None, keyspace=None, tracing_enabled=False,
+                 cqlver=DEFAULT_CQLVER, keyspace=None, tracing_enabled=False,
                  display_time_format=DEFAULT_TIME_FORMAT,
                  display_float_precision=DEFAULT_FLOAT_PRECISION):
         cmd.Cmd.__init__(self, completekey=completekey)
@@ -530,9 +544,14 @@ class Shell(cmd.Cmd):
     def myformat_value(self, val, casstype, **kwargs):
         if isinstance(val, DecodeError):
             self.decoding_errors.append(val)
-        return format_value(val, casstype, self.output_codec.name,
-                            addcolor=self.color, time_format=self.display_time_format,
-                            float_precision=self.display_float_precision, **kwargs)
+        try:
+            return format_value(val, casstype, self.output_codec.name,
+                                addcolor=self.color, time_format=self.display_time_format,
+                                float_precision=self.display_float_precision, **kwargs)
+        except Exception, e:
+            err = FormatError(val, e, casstype)
+            self.decoding_errors.append(err)
+            return format_value(err, None, self.output_codec.name, addcolor=self.color)
 
     def myformat_colname(self, name, nametype):
         return self.myformat_value(name, nametype, colormap=COLUMN_NAME_COLORS)
@@ -644,7 +663,7 @@ class Shell(cmd.Cmd):
         raise ColumnFamilyNotFound("Unconfigured column family %r" % (cfname,))
 
     def get_columnfamily_names(self, ksname=None):
-        if self.cqlver_atleast(3):
+        if self.cqlver_atleast(3) and not self.is_cql3_beta():
             return self.get_columnfamily_names_cql3(ksname=ksname)
         return [c.name for c in self.get_columnfamilies(ksname)]
 
@@ -670,7 +689,7 @@ class Shell(cmd.Cmd):
     def get_column_names(self, ksname, cfname):
         if ksname is None:
             ksname = self.current_keyspace
-        if ksname not in SYSTEM_KEYSPACES and self.cqlver_atleast(3):
+        if self.cqlver_atleast(3) and not (self.is_cql3_beta() and ksname in SYSTEM_KEYSPACES):
             return self.get_column_names_from_layout(ksname, cfname)
         else:
             return self.get_column_names_from_cfdef(ksname, cfname)
@@ -898,7 +917,7 @@ class Shell(cmd.Cmd):
             print
         statement = self.statement.getvalue()
         if statement.strip():
-            if not self.onecmd(statement + ';'):
+            if not self.onecmd(statement):
                 self.printerr('Incomplete statement at end of file')
         self.do_exit()
 
@@ -960,7 +979,7 @@ class Shell(cmd.Cmd):
             print_trace_session(self, self.cursor, session_id)
             return result
         else:
-            return self.perform_statement_untraced(statement, decoder=None)
+            return self.perform_statement_untraced(statement, decoder=decoder)
 
     def perform_statement_untraced(self, statement, decoder=None):
         if not statement:
@@ -1048,6 +1067,7 @@ class Shell(cmd.Cmd):
         last_description = None
         for row in cursor:
             if last_description is not None and cursor.description != last_description:
+                cursor._reset()
                 return False
             last_description = cursor.description
         cursor._reset()
@@ -1085,13 +1105,13 @@ class Shell(cmd.Cmd):
                 widths[num] = max(widths[num], col.displaywidth)
 
         # print header
-        header = ' | '.join(hdr.color_ljust(w) for (hdr, w) in zip(formatted_names, widths))
+        header = ' | '.join(hdr.ljust(w, color=self.color) for (hdr, w) in zip(formatted_names, widths))
         self.writeresult(' ' + header.rstrip())
         self.writeresult('-%s-' % '-+-'.join('-' * w for w in widths))
 
         # print row data
         for row in formatted_values:
-            line = ' | '.join(col.color_rjust(w) for (col, w) in zip(row, widths))
+            line = ' | '.join(col.rjust(w, color=self.color) for (col, w) in zip(row, widths))
             self.writeresult(' ' + line)
 
     def print_dynamic_result(self, cursor):
@@ -1197,13 +1217,15 @@ class Shell(cmd.Cmd):
 
         # no metainfo available from system.schema_* for system CFs, so we have
         # to use cfdef-based description for those.
-
-        if self.cqlver_atleast(3) and not self.is_cql3_beta():
+        if self.cqlver_atleast(3) and not (self.is_cql3_beta() and ksname in SYSTEM_KEYSPACES):
             try:
                 layout = self.get_columnfamily_layout(ksname, cfname)
             except CQL_ERRORS:
                 # most likely a 1.1 beta where cql3 is supported, but not system.schema_*
-                pass
+                if self.debug:
+                    print 'warning: failed to use system.schema_* tables to describe cf'
+                    import traceback
+                    traceback.print_exc()
             else:
                 return self.print_recreate_columnfamily_from_layout(layout, out)
 
@@ -1293,14 +1315,22 @@ class Shell(cmd.Cmd):
         # work out how to determine that from a layout.
 
         cf_opts = []
+        compaction_strategy = trim_if_present(getattr(layout, 'compaction_strategy_class'),
+                                              'org.apache.cassandra.db.compaction.')
         for cql3option, layoutoption in cqlruleset.columnfamily_layout_options:
             if layoutoption is None:
                 layoutoption = cql3option
             optval = getattr(layout, layoutoption, None)
             if optval is None:
-                continue
+                if layoutoption == 'bloom_filter_fp_chance':
+                    if compaction_strategy == 'LeveledCompactionStrategy':
+                        optval = 0.1
+                    else:
+                        optval = 0.01
+                else:
+                    continue
             elif layoutoption == 'compaction_strategy_class':
-                optval = trim_if_present(optval, 'org.apache.cassandra.db.compaction.')
+                optval = compaction_strategy
             cf_opts.append((cql3option, self.cql_protect_value(optval)))
         for cql3option, layoutoption, _ in cqlruleset.columnfamily_layout_map_options:
             if layoutoption is None:
@@ -1311,6 +1341,9 @@ class Shell(cmd.Cmd):
                 if compclass is not None:
                     optmap['sstable_compression'] = \
                             trim_if_present(compclass, 'org.apache.cassandra.io.compress.')
+            if layoutoption == 'compaction_strategy_options':
+                optmap['class'] = compaction_strategy
+
             if self.cqlver_atleast(3) and not self.is_cql3_beta():
                 cf_opts.append((cql3option, optmap))
             else:
@@ -1356,8 +1389,9 @@ class Shell(cmd.Cmd):
         print
         if ksname is None:
             for k in self.get_keyspaces():
-                print 'Keyspace %s' % (k.name,)
-                print '---------%s' % ('-' * len(k.name))
+                name = self.cql_protect_name(k.name)
+                print 'Keyspace %s' % (name,)
+                print '---------%s' % ('-' * len(name))
                 cmd.Cmd.columnize(self, self.get_columnfamily_names(k.name))
                 print
         else:
@@ -2045,7 +2079,7 @@ def raw_option_with_default(configs, section, option, default=None):
 def should_use_color():
     if not sys.stdout.isatty():
         return False
-    if os.environ.get('TERM', 'dumb') == 'dumb':
+    if os.environ.get('TERM', '') in ('dumb', ''):
         return False
     try:
         import subprocess
@@ -2053,7 +2087,7 @@ def should_use_color():
         stdout, _ = p.communicate()
         if int(stdout.strip()) < 8:
             return False
-    except (OSError, ImportError):
+    except (OSError, ImportError, ValueError):
         # oh well, we tried. at least we know there's a $TERM and it's
         # not "dumb".
         pass

http://git-wip-us.apache.org/repos/asf/cassandra/blob/14d62ab1/pylib/cqlshlib/cql3handling.py
----------------------------------------------------------------------
diff --git a/pylib/cqlshlib/cql3handling.py b/pylib/cqlshlib/cql3handling.py
index 0089f12..5293857 100644
--- a/pylib/cqlshlib/cql3handling.py
+++ b/pylib/cqlshlib/cql3handling.py
@@ -58,36 +58,34 @@ class Cql3ParsingRuleSet(CqlParsingRuleSet):
     columnfamily_options = (
         # (CQL option name, Thrift option name (or None if same))
         ('comment', None),
+        ('compaction_strategy_class', 'compaction_strategy'),
         ('comparator', 'comparator_type'),
-        ('read_repair_chance', None),
-        ('gc_grace_seconds', None),
         ('default_validation', 'default_validation_class'),
+        ('gc_grace_seconds', None),
+        ('read_repair_chance', None),
         ('replicate_on_write', None),
-        ('compaction_strategy_class', 'compaction_strategy'),
     )
 
     old_columnfamily_layout_options = (
         # (CQL3 option name, schema_columnfamilies column name (or None if same))
-        ('comment', None),
         ('bloom_filter_fp_chance', None),
         ('caching', None),
-        ('read_repair_chance', None),
+        ('comment', None),
+        ('compaction_strategy_class', None),
         ('dclocal_read_repair_chance', 'local_read_repair_chance'),
         ('gc_grace_seconds', None),
+        ('read_repair_chance', None),
         ('replicate_on_write', None),
-        ('compaction_strategy_class', None),
     )
 
     new_columnfamily_layout_options = (
-        ('comment', None),
         ('bloom_filter_fp_chance', None),
         ('caching', None),
-        ('read_repair_chance', None),
+        ('comment', None),
         ('dclocal_read_repair_chance', 'local_read_repair_chance'),
         ('gc_grace_seconds', None),
+        ('read_repair_chance', None),
         ('replicate_on_write', None),
-        ('default_read_consistency', None),
-        ('default_write_consistency', None),
     )
 
     old_columnfamily_layout_map_options = (
@@ -103,18 +101,18 @@ class Cql3ParsingRuleSet(CqlParsingRuleSet):
         # (CQL3 option name, schema_columnfamilies column name (or None if same),
         #  list of known map keys)
         ('compaction', 'compaction_strategy_options',
-            ('min_threshold', 'max_threshold')),
+            ('class', 'min_threshold', 'max_threshold')),
         ('compression', 'compression_parameters',
             ('sstable_compression', 'chunk_length_kb', 'crc_check_chance')),
     )
 
     new_obsolete_cf_options = (
+        'compaction_parameters',
         'compaction_strategy_class',
         'compaction_strategy_options',
-        'min_compaction_threshold',
-        'max_compaction_threshold',
-        'compaction_parameters',
         'compression_parameters',
+        'max_compaction_threshold',
+        'min_compaction_threshold',
     )
 
     @staticmethod
@@ -1514,7 +1512,7 @@ class CqlTableDef:
         if len(self.column_aliases) == 0:
             if self.comparator is not UTF8Type:
                 warn(UnexpectedTableStructure("Compact storage CF %s has no column aliases,"
-                                              " but comparator is not UTF8Type." % (self,)))
+                                              " but comparator is not UTF8Type." % (self.name,)))
             colalias_types = []
         elif issubclass(self.comparator, CompositeType):
             colalias_types = self.comparator.subtypes

http://git-wip-us.apache.org/repos/asf/cassandra/blob/14d62ab1/pylib/cqlshlib/displaying.py
----------------------------------------------------------------------
diff --git a/pylib/cqlshlib/displaying.py b/pylib/cqlshlib/displaying.py
index 634d37b..22ff763 100644
--- a/pylib/cqlshlib/displaying.py
+++ b/pylib/cqlshlib/displaying.py
@@ -53,20 +53,24 @@ class FormattedValue:
         else:
             return ''
 
-    def ljust(self, width, fill=' '):
+    def ljust(self, width, fill=' ', color=False):
         """
         Similar to self.strval.ljust(width), but takes expected terminal
         display width into account for special characters, and does not
         take color escape codes into account.
         """
+        if color:
+            return self.color_ljust(width, fill=fill)
         return self.strval + self._pad(width, fill)
 
-    def rjust(self, width, fill=' '):
+    def rjust(self, width, fill=' ', color=False):
         """
         Similar to self.strval.rjust(width), but takes expected terminal
         display width into account for special characters, and does not
         take color escape codes into account.
         """
+        if color:
+            return self.color_rjust(width, fill=fill)
         return self._pad(width, fill) + self.strval
 
     def color_rjust(self, width, fill=' '):

http://git-wip-us.apache.org/repos/asf/cassandra/blob/14d62ab1/pylib/cqlshlib/formatting.py
----------------------------------------------------------------------
diff --git a/pylib/cqlshlib/formatting.py b/pylib/cqlshlib/formatting.py
index f2fdb95..793a1d0 100644
--- a/pylib/cqlshlib/formatting.py
+++ b/pylib/cqlshlib/formatting.py
@@ -16,6 +16,7 @@
 
 import re
 import time
+import binascii
 from collections import defaultdict
 from . import wcwidth
 from .displaying import colorme, FormattedValue, DEFAULT_VALUE_COLORS
@@ -66,13 +67,29 @@ def format_by_type(cqltype, val, encoding, colormap=None, addcolor=False,
                         time_format=time_format, float_precision=float_precision,
                         nullval=nullval)
 
+def color_text(bval, colormap, displaywidth=None):
+    # note that here, we render natural backslashes as just backslashes,
+    # in the same color as surrounding text, when using color. When not
+    # using color, we need to double up the backslashes so it's not
+    # ambiguous. This introduces the unique difficulty of having different
+    # display widths for the colored and non-colored versions. To avoid
+    # adding the smarts to handle that in to FormattedValue, we just
+    # make an explicit check to see if a null colormap is being used or
+    # not.
+
+    if displaywidth is None:
+        displaywidth = len(bval)
+    tbr = _make_turn_bits_red_f(colormap['hex'], colormap['text'])
+    coloredval = colormap['text'] + bits_to_turn_red_re.sub(tbr, bval) + colormap['reset']
+    if colormap['text']:
+        displaywidth -= bval.count(r'\\')
+    return FormattedValue(bval, coloredval, displaywidth)
+
 def format_value_default(val, colormap, **_):
     val = str(val)
     escapedval = val.replace('\\', '\\\\')
     bval = controlchars_re.sub(_show_control_chars, escapedval)
-    tbr = _make_turn_bits_red_f(colormap['hex'], colormap['text'])
-    coloredval = colormap['text'] + bits_to_turn_red_re.sub(tbr, bval) + colormap['reset']
-    return FormattedValue(bval, coloredval)
+    return color_text(bval, colormap)
 
 # Mapping cql type base names ("int", "map", etc) to formatter functions,
 # making format_value a generic function
@@ -164,9 +181,7 @@ def format_value_text(val, encoding, colormap, **_):
     escapedval = unicode_controlchars_re.sub(_show_control_chars, escapedval)
     bval = escapedval.encode(encoding, 'backslashreplace')
     displaywidth = wcwidth.wcswidth(bval.decode(encoding))
-    tbr = _make_turn_bits_red_f(colormap['hex'], colormap['text'])
-    coloredval = colormap['text'] + bits_to_turn_red_re.sub(tbr, bval) + colormap['reset']
-    return FormattedValue(bval, coloredval)
+    return color_text(bval, colormap, displaywidth)
 
 # name alias
 formatter_for('varchar')(format_value_text)

http://git-wip-us.apache.org/repos/asf/cassandra/blob/14d62ab1/pylib/cqlshlib/test/__init__.py
----------------------------------------------------------------------
diff --git a/pylib/cqlshlib/test/__init__.py b/pylib/cqlshlib/test/__init__.py
new file mode 100644
index 0000000..31f66f3
--- /dev/null
+++ b/pylib/cqlshlib/test/__init__.py
@@ -0,0 +1,20 @@
+# 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 .cassconnect import create_test_db, remove_test_db
+
+setUp = create_test_db
+tearDown = remove_test_db

http://git-wip-us.apache.org/repos/asf/cassandra/blob/14d62ab1/pylib/cqlshlib/test/ansi_colors.py
----------------------------------------------------------------------
diff --git a/pylib/cqlshlib/test/ansi_colors.py b/pylib/cqlshlib/test/ansi_colors.py
new file mode 100644
index 0000000..b0bc738
--- /dev/null
+++ b/pylib/cqlshlib/test/ansi_colors.py
@@ -0,0 +1,191 @@
+# 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.
+
+import re
+
+LIGHT = 010
+
+ansi_CSI = '\033['
+ansi_seq = re.compile(re.escape(ansi_CSI) + r'(?P<params>[\x20-\x3f]*)(?P<final>[\x40-\x7e])')
+ansi_cmd_SGR = 'm'  # set graphics rendition
+
+color_defs = (
+    (000, 'k', 'black'),
+    (001, 'r', 'dark red'),
+    (002, 'g', 'dark green'),
+    (003, 'w', 'brown', 'dark yellow'),
+    (004, 'b', 'dark blue'),
+    (005, 'm', 'dark magenta', 'dark purple'),
+    (006, 'c', 'dark cyan'),
+    (007, 'n', 'light grey', 'light gray', 'neutral', 'dark white'),
+    (010, 'B', 'dark grey', 'dark gray', 'light black'),
+    (011, 'R', 'red', 'light red'),
+    (012, 'G', 'green', 'light green'),
+    (013, 'Y', 'yellow', 'light yellow'),
+    (014, 'B', 'blue', 'light blue'),
+    (015, 'M', 'magenta', 'purple', 'light magenta', 'light purple'),
+    (016, 'C', 'cyan', 'light cyan'),
+    (017, 'W', 'white', 'light white'),
+)
+
+colors_by_num = {}
+colors_by_letter = {}
+colors_by_name = {}
+letters_by_num = {}
+
+for colordef in color_defs:
+    colorcode = colordef[0]
+    colorletter = colordef[1]
+    colors_by_num[colorcode] = nameset = set(colordef[2:])
+    colors_by_letter[colorletter] = colorcode
+    letters_by_num[colorcode] = colorletter
+    for c in list(nameset):
+        # equivalent names without spaces
+        nameset.add(c.replace(' ', ''))
+    for c in list(nameset):
+        # with "bright" being an alias for "light"
+        nameset.add(c.replace('light', 'bright'))
+    for c in nameset:
+        colors_by_name[c] = colorcode
+
+class ColoredChar:
+    def __init__(self, c, colorcode):
+        self.c = c
+        self._colorcode = colorcode
+
+    def colorcode(self):
+        return self._colorcode
+
+    def plain(self):
+        return self.c
+
+    def __getattr__(self, name):
+        return getattr(self.c, name)
+
+    def ansi_color(self):
+        clr = str(30 + (07 & self._colorcode))
+        if self._colorcode & 010:
+            clr = '1;' + clr
+        return clr
+
+    def __str__(self):
+        return "<%s '%r'>" % (self.__class__.__name__, self.colored_repr())
+    __repr__ = __str__
+
+    def colored_version(self):
+        return '%s0;%sm%s%s0m' % (ansi_CSI, self.ansi_color(), self.c, ansi_CSI)
+
+    def colored_repr(self):
+        if self.c == "'":
+            crepr = r"\'"
+        elif self.c == '"':
+            crepr = self.c
+        else:
+            crepr = repr(self.c)[1:-1]
+        return '%s0;%sm%s%s0m' % (ansi_CSI, self.ansi_color(), crepr, ansi_CSI)
+
+    def colortag(self):
+        return lookup_letter_from_code(self._colorcode)
+
+class ColoredText:
+    def __init__(self, source=''):
+        if isinstance(source, basestring):
+            plain, colors = self.parse_ansi_colors(source)
+            self.chars = map(ColoredChar, plain, colors)
+        else:
+            # expected that source is an iterable of ColoredChars (or duck-typed as such)
+            self.chars = tuple(source)
+
+    def splitlines(self):
+        lines = [[]]
+        for c in self.chars:
+            if c.plain() == '\n':
+                lines.append([])
+            else:
+                lines[-1].append(c)
+        return [self.__class__(line) for line in lines]
+
+    def plain(self):
+        return ''.join([c.plain() for c in self.chars])
+
+    def __getitem__(self, index):
+        return self.chars[index]
+
+    @classmethod
+    def parse_ansi_colors(cls, source):
+        # note: strips all control sequences, even if not SGRs.
+        colors = []
+        plain = ''
+        last = 0
+        curclr = 0
+        for match in ansi_seq.finditer(source):
+            prevsegment = source[last:match.start()]
+            plain += prevsegment
+            colors.extend([curclr] * len(prevsegment))
+            if match.group('final') == ansi_cmd_SGR:
+                try:
+                    curclr = cls.parse_sgr_param(curclr, match.group('params'))
+                except ValueError:
+                    pass
+            last = match.end()
+        prevsegment = source[last:]
+        plain += prevsegment
+        colors.extend([curclr] * len(prevsegment))
+        return ''.join(plain), colors
+
+    @staticmethod
+    def parse_sgr_param(curclr, paramstr):
+        oldclr = curclr
+        args = map(int, paramstr.split(';'))
+        for a in args:
+            if a == 0:
+                curclr = lookup_colorcode('neutral')
+            elif a == 1:
+                curclr |= LIGHT
+            elif 30 <= a <= 37:
+                curclr = (curclr & LIGHT) | (a - 30)
+            else:
+                # not supported renditions here; ignore for now
+                pass
+        return curclr
+
+    def __repr__(self):
+        return "<%s '%s'>" % (self.__class__.__name__, ''.join([c.colored_repr() for c in self.chars]))
+    __str__ = __repr__
+
+    def __iter__(self):
+        return iter(self.chars)
+
+    def colored_version(self):
+        return ''.join([c.colored_version() for c in self.chars])
+
+    def colortags(self):
+        return ''.join([c.colortag() for c in self.chars])
+
+def lookup_colorcode(name):
+    return colors_by_name[name]
+
+def lookup_colorname(code):
+    return colors_by_num.get(code, 'Unknown-color-0%o' % code)
+
+def lookup_colorletter(letter):
+    return colors_by_letter[letter]
+
+def lookup_letter_from_code(code):
+    letr = letters_by_num.get(code, ' ')
+    if letr == 'n':
+        letr = ' '
+    return letr

http://git-wip-us.apache.org/repos/asf/cassandra/blob/14d62ab1/pylib/cqlshlib/test/basecase.py
----------------------------------------------------------------------
diff --git a/pylib/cqlshlib/test/basecase.py b/pylib/cqlshlib/test/basecase.py
new file mode 100644
index 0000000..efc2555
--- /dev/null
+++ b/pylib/cqlshlib/test/basecase.py
@@ -0,0 +1,71 @@
+# 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.
+
+import os
+import sys
+import logging
+from itertools import izip
+from os.path import dirname, join, normpath, islink
+
+cqlshlog = logging.getLogger('test_cqlsh')
+
+try:
+    # a backport of python2.7 unittest features, so we can test against older
+    # pythons as necessary. python2.7 users who don't care about testing older
+    # versions need not install.
+    import unittest2 as unittest
+except ImportError:
+    import unittest
+
+rundir = dirname(__file__)
+path_to_cqlsh = normpath(join(rundir, '..', '..', '..', 'bin', 'cqlsh'))
+
+# symlink a ".py" file to cqlsh main script, so we can load it as a module
+modulepath = join(rundir, 'cqlsh.py')
+try:
+    if islink(modulepath):
+        os.unlink(modulepath)
+except OSError:
+    pass
+os.symlink(path_to_cqlsh, modulepath)
+
+sys.path.append(rundir)
+import cqlsh
+cql = cqlsh.cql
+
+TEST_HOST = os.environ.get('CQL_TEST_HOST', 'localhost')
+TEST_PORT = int(os.environ.get('CQL_TEST_PORT', 9160))
+
+class BaseTestCase(unittest.TestCase):
+    def assertNicelyFormattedTableHeader(self, line, msg=None):
+        return self.assertRegexpMatches(line, r'^ +\w+( +\| \w+)*\s*$', msg=msg)
+
+    def assertNicelyFormattedTableRule(self, line, msg=None):
+        return self.assertRegexpMatches(line, r'^-+(\+-+)*\s*$', msg=msg)
+
+    def assertNicelyFormattedTableData(self, line, msg=None):
+        return self.assertRegexpMatches(line, r'^ .* \| ', msg=msg)
+
+def dedent(s):
+    lines = [ln.rstrip() for ln in s.splitlines()]
+    if lines[0] == '':
+        lines = lines[1:]
+    spaces = [len(line) - len(line.lstrip()) for line in lines if line]
+    minspace = min(spaces if len(spaces) > 0 else (0,))
+    return '\n'.join(line[minspace:] for line in lines)
+
+def at_a_time(i, num):
+    return izip(*([iter(i)] * num))

http://git-wip-us.apache.org/repos/asf/cassandra/blob/14d62ab1/pylib/cqlshlib/test/cassconnect.py
----------------------------------------------------------------------
diff --git a/pylib/cqlshlib/test/cassconnect.py b/pylib/cqlshlib/test/cassconnect.py
new file mode 100644
index 0000000..2883dbc
--- /dev/null
+++ b/pylib/cqlshlib/test/cassconnect.py
@@ -0,0 +1,159 @@
+# 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 __future__ import with_statement
+
+import contextlib
+import tempfile
+import os.path
+from .basecase import cql, cqlsh, cqlshlog, TEST_HOST, TEST_PORT, rundir
+from .run_cqlsh import run_cqlsh, call_cqlsh
+
+test_keyspace_init2 = os.path.join(rundir, 'test_keyspace_init2.cql')
+test_keyspace_init3 = os.path.join(rundir, 'test_keyspace_init3.cql')
+
+def get_cassandra_connection(cql_version=None):
+    if cql_version is None:
+        cql_version = '2.0.0'
+    conn = cql.connect(TEST_HOST, TEST_PORT, cql_version=cql_version)
+    # until the cql lib does this for us
+    conn.cql_version = cql_version
+    return conn
+
+def get_cassandra_cursor(cql_version=None):
+    return get_cassandra_connection(cql_version=cql_version).cursor()
+
+TEST_KEYSPACES_CREATED = []
+
+def get_test_keyspace():
+    return TEST_KEYSPACES_CREATED[-1]
+
+def make_test_ks_name():
+    # abuse mktemp to get a quick random-ish name
+    return os.path.basename(tempfile.mktemp(prefix='CqlshTests_'))
+
+def create_test_keyspace(cursor):
+    ksname = make_test_ks_name()
+    qksname = quote_name(cursor, ksname)
+    cursor.execute('''
+        CREATE KEYSPACE %s WITH strategy_class = 'SimpleStrategy'
+                           AND strategy_options:replication_factor = 1;
+    ''' % quote_name(cursor, ksname))
+    cursor.execute('USE %s;' % qksname)
+    TEST_KEYSPACES_CREATED.append(ksname)
+    return ksname
+
+def split_cql_commands(source, cqlver='2.0.0'):
+    ruleset = cql_rule_set(cqlver)
+    statements, in_batch = ruleset.cql_split_statements(source)
+    if in_batch:
+        raise ValueError("CQL source ends unexpectedly")
+
+    return [ruleset.cql_extract_orig(toks, source) for toks in statements if toks]
+
+def execute_cql_commands(cursor, source, logprefix='INIT: '):
+    for cql in split_cql_commands(source, cqlver=cursor._connection.cql_version):
+        cqlshlog.debug(logprefix + cql)
+        cursor.execute(cql)
+
+def execute_cql_file(cursor, fname):
+    with open(fname) as f:
+        return execute_cql_commands(cursor, f.read())
+
+def populate_test_db_cql3(cursor):
+    execute_cql_file(cursor, test_keyspace_init3)
+
+def populate_test_db_cql2(cursor):
+    execute_cql_file(cursor, test_keyspace_init2)
+
+def create_test_db():
+    with cassandra_cursor(ks=None) as c:
+        k = create_test_keyspace(c)
+        populate_test_db_cql2(c)
+    with cassandra_cursor(ks=k, cql_version='3.0.0') as c:
+        populate_test_db_cql3(c)
+    return k
+
+def remove_test_db():
+    with cassandra_cursor(ks=None) as c:
+        c.execute('DROP KEYSPACE %s' % quote_name(c, TEST_KEYSPACES_CREATED.pop(-1)))
+
+@contextlib.contextmanager
+def cassandra_connection(cql_version=None):
+    """
+    Make a Cassandra CQL connection with the given CQL version and get a cursor
+    for it, and optionally connect to a given keyspace.
+
+    The connection is returned as the context manager's value, and it will be
+    closed when the context exits.
+    """
+
+    conn = get_cassandra_connection(cql_version=cql_version)
+    try:
+        yield conn
+    finally:
+        conn.close()
+
+@contextlib.contextmanager
+def cassandra_cursor(cql_version=None, ks=''):
+    """
+    Make a Cassandra CQL connection with the given CQL version and get a cursor
+    for it, and optionally connect to a given keyspace. If ks is the empty
+    string (default), connect to the last test keyspace created. If ks is None,
+    do not connect to any keyspace. Otherwise, attempt to connect to the
+    keyspace named.
+
+    The cursor is returned as the context manager's value, and the connection
+    will be closed when the context exits.
+    """
+
+    if ks == '':
+        ks = get_test_keyspace()
+    conn = get_cassandra_connection(cql_version=cql_version)
+    try:
+        c = conn.cursor()
+        if ks is not None:
+            c.execute('USE %s;' % quote_name(c, ks))
+        yield c
+    finally:
+        conn.close()
+
+def cql_rule_set(cqlver):
+    if str(cqlver).startswith('2'):
+        return cqlsh.cqlhandling.CqlRuleSet
+    else:
+        return cqlsh.cql3handling.CqlRuleSet
+
+def quote_name(cqlver, name):
+    if isinstance(cqlver, cql.cursor.Cursor):
+        cqlver = cqlver._connection
+    if isinstance(cqlver, cql.connection.Connection):
+        cqlver = cqlver.cql_version
+    return cql_rule_set(cqlver).maybe_escape_name(name)
+
+class DEFAULTVAL: pass
+
+def testrun_cqlsh(keyspace=DEFAULTVAL, **kwargs):
+    # use a positive default sentinel so that keyspace=None can be used
+    # to override the default behavior
+    if keyspace is DEFAULTVAL:
+        keyspace = get_test_keyspace()
+    return run_cqlsh(keyspace=keyspace, **kwargs)
+
+def testcall_cqlsh(keyspace=None, **kwargs):
+    if keyspace is None:
+        keyspace = get_test_keyspace()
+    return call_cqlsh(keyspace=keyspace, **kwargs)

http://git-wip-us.apache.org/repos/asf/cassandra/blob/14d62ab1/pylib/cqlshlib/test/run_cqlsh.py
----------------------------------------------------------------------
diff --git a/pylib/cqlshlib/test/run_cqlsh.py b/pylib/cqlshlib/test/run_cqlsh.py
new file mode 100644
index 0000000..929849c
--- /dev/null
+++ b/pylib/cqlshlib/test/run_cqlsh.py
@@ -0,0 +1,271 @@
+# 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.
+
+# NOTE: this testing tool is *nix specific
+
+import os
+import re
+import pty
+import fcntl
+import contextlib
+import subprocess
+import signal
+import math
+from time import time
+from . import basecase
+
+DEFAULT_CQLSH_PROMPT = '\ncqlsh(:\S+)?> '
+DEFAULT_CQLSH_TERM = 'xterm'
+
+cqlshlog = basecase.cqlshlog
+
+def set_controlling_pty(master, slave):
+    os.setsid()
+    os.close(master)
+    for i in range(3):
+        os.dup2(slave, i)
+    if slave > 2:
+        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):
+    """
+    Within the wrapped context, the given signal will interrupt signal
+    calls and will raise the given exception class. The preexisting signal
+    handling will be reinstated on context exit.
+    """
+    def raiser(signum, frames):
+        raise exc()
+    oldhandlr = signal.signal(signum, raiser)
+    try:
+        yield
+    finally:
+        signal.signal(signum, oldhandlr)
+
+class TimeoutError(Exception):
+    pass
+
+@contextlib.contextmanager
+def timing_out_itimer(seconds):
+    if seconds is None:
+        yield
+        return
+    with raising_signal(signal.SIGALRM, TimeoutError):
+        oldval, oldint = signal.getitimer(signal.ITIMER_REAL)
+        if oldval != 0.0:
+            raise RuntimeError("ITIMER_REAL already in use")
+        signal.setitimer(signal.ITIMER_REAL, seconds)
+        try:
+            yield
+        finally:
+            signal.setitimer(signal.ITIMER_REAL, 0)
+
+@contextlib.contextmanager
+def timing_out_alarm(seconds):
+    if seconds is None:
+        yield
+        return
+    with raising_signal(signal.SIGALRM, TimeoutError):
+        oldval = signal.alarm(int(math.ceil(seconds)))
+        if oldval != 0:
+            signal.alarm(oldval)
+            raise RuntimeError("SIGALRM already in use")
+        try:
+            yield
+        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
+else:
+    timing_out = timing_out_alarm
+
+def noop(*a):
+    pass
+
+class ProcRunner:
+    def __init__(self, path, tty=True, env=None, args=()):
+        self.exe_path = path
+        self.args = args
+        self.tty = bool(tty)
+        if env is None:
+            env = {}
+        self.env = env
+        self.readbuf = ''
+
+        self.start_proc()
+
+    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((self.exe_path,) + tuple(self.args),
+                                     env=self.env, preexec_fn=preexec,
+                                     stdin=stdin, stdout=stdout, stderr=stderr,
+                                     close_fds=False)
+        if self.tty:
+            os.close(slavefd)
+            self.childpty = masterfd
+            self.send = self.send_tty
+            self.read = self.read_tty
+        else:
+            self.send = self.send_pipe
+            self.read = self.read_pipe
+
+    def close(self):
+        cqlshlog.info("Closing %r subprocess." % (self.exe_path,))
+        if self.tty:
+            os.close(self.childpty)
+        else:
+            self.proc.stdin.close()
+        cqlshlog.debug("Waiting for exit")
+        return self.proc.wait()
+
+    def send_tty(self, data):
+        os.write(self.childpty, data)
+
+    def send_pipe(self, data):
+        self.proc.stdin.write(data)
+
+    def read_tty(self, blksize):
+        return os.read(self.childpty, blksize)
+
+    def read_pipe(self, blksize):
+        return self.proc.stdout.read(blksize)
+
+    def read_until(self, until, blksize=4096, timeout=None, flags=0):
+        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)
+                cqlshlog.debug("read %r from subproc" % (val,))
+                if val == '':
+                    raise EOFError("'until' pattern %r not found" % (until.pattern,))
+                got += val
+                m = until.search(got)
+                if m is not None:
+                    self.readbuf = got[m.end():]
+                    got = got[:m.end()]
+                    return got
+
+    def read_lines(self, numlines, blksize=4096, timeout=None):
+        lines = []
+        with timing_out(timeout):
+            for n in range(numlines):
+                lines.append(self.read_until('\n', blksize=blksize))
+        return lines
+
+    def read_up_to_timeout(self, timeout, blksize=4096):
+        got = self.readbuf
+        self.readbuf = ''
+        curtime = time()
+        stoptime = curtime + timeout
+        while curtime < stoptime:
+            try:
+                with timing_out(stoptime - curtime):
+                    stuff = self.read(blksize)
+            except TimeoutError:
+                break
+            cqlshlog.debug("read %r from subproc" % (stuff,))
+            if stuff == '':
+                break
+            got += stuff
+            curtime = time()
+        return got
+
+class CqlshRunner(ProcRunner):
+    def __init__(self, path=None, host=None, port=None, keyspace=None, cqlver=None,
+                 args=(), prompt=DEFAULT_CQLSH_PROMPT, env=None, **kwargs):
+        if path is None:
+            path = basecase.path_to_cqlsh
+        if host is None:
+            host = basecase.TEST_HOST
+        if port is None:
+            port = basecase.TEST_PORT
+        if env is None:
+            env = {}
+        env.setdefault('TERM', 'xterm')
+        env.setdefault('CQLSH_NO_BUNDLED', os.environ.get('CQLSH_NO_BUNDLED', ''))
+        env.setdefault('PYTHONPATH', os.environ.get('PYTHONPATH', ''))
+        args = tuple(args) + (host, str(port))
+        if cqlver is not None:
+            args += ('--cqlversion', str(cqlver))
+        if keyspace is not None:
+            args += ('--keyspace', keyspace)
+        self.keyspace = keyspace
+        ProcRunner.__init__(self, path, args=args, env=env, **kwargs)
+        self.prompt = prompt
+        if self.prompt is None:
+            self.output_header = ''
+        else:
+            self.output_header = self.read_to_next_prompt()
+
+    def read_to_next_prompt(self):
+        return self.read_until(self.prompt, timeout=4.0)
+
+    def read_up_to_timeout(self, timeout, blksize=4096):
+        output = ProcRunner.read_up_to_timeout(self, timeout, blksize=blksize)
+        # readline trying to be friendly- remove these artifacts
+        output = output.replace(' \r', '')
+        output = output.replace('\r', '')
+        return output
+
+    def cmd_and_response(self, cmd):
+        self.send(cmd + '\n')
+        output = self.read_to_next_prompt()
+        # readline trying to be friendly- remove these artifacts
+        output = output.replace(' \r', '')
+        output = output.replace('\r', '')
+        if self.tty:
+            echo, output = output.split('\n', 1)
+            assert echo == cmd, "unexpected echo %r instead of %r" % (echo, cmd)
+        try:
+            output, promptline = output.rsplit('\n', 1)
+        except ValueError:
+            promptline = output
+            output = ''
+        assert re.match(self.prompt, '\n' + promptline), \
+                'last line of output %r does not match %r?' % (promptline, self.prompt)
+        return output + '\n'
+
+def run_cqlsh(**kwargs):
+    return contextlib.closing(CqlshRunner(**kwargs))
+
+def call_cqlsh(**kwargs):
+    kwargs.setdefault('prompt', None)
+    proginput = kwargs.pop('input', '')
+    kwargs['tty'] = False
+    c = CqlshRunner(**kwargs)
+    output, _ = c.proc.communicate(proginput)
+    result = c.close()
+    return output, result

http://git-wip-us.apache.org/repos/asf/cassandra/blob/14d62ab1/pylib/cqlshlib/test/table_arrangements.cql
----------------------------------------------------------------------
diff --git a/pylib/cqlshlib/test/table_arrangements.cql b/pylib/cqlshlib/test/table_arrangements.cql
new file mode 100644
index 0000000..c3ccc41
--- /dev/null
+++ b/pylib/cqlshlib/test/table_arrangements.cql
@@ -0,0 +1,114 @@
+-- type A: single-column PK, compact storage
+-- FAILS: CREATE TABLE type_a_1 (a int PRIMARY KEY) WITH COMPACT STORAGE;
+-- Bad Request: No definition found that is not part of the PRIMARY KEY
+CREATE TABLE type_a_2 (a int PRIMARY KEY, b int) WITH COMPACT STORAGE;
+CREATE TABLE type_a_3 (a int PRIMARY KEY, b int, c int) WITH COMPACT STORAGE;
+
+-- type B: single-column PK, dynamic storage
+CREATE TABLE type_b_1 (a int PRIMARY KEY);
+CREATE TABLE type_b_2 (a int PRIMARY KEY, b int);
+CREATE TABLE type_b_3 (a int PRIMARY KEY, b int, c int);
+
+-- type C: compound PK, plain partition key, compact storage
+CREATE TABLE type_c_2_2 (a int, b int, PRIMARY KEY (a, b)) WITH COMPACT STORAGE;
+CREATE TABLE type_c_3_3 (a int, b int, c int, PRIMARY KEY (a, b, c)) WITH COMPACT STORAGE;
+CREATE TABLE type_c_3_2 (a int, b int, c int, PRIMARY KEY (a, b)) WITH COMPACT STORAGE;
+-- FAILS: CREATE TABLE type_c_4_2 (a int, b int, c int, d int, PRIMARY KEY (a, b)) WITH COMPACT STORAGE;
+-- Bad Request: COMPACT STORAGE WITH composite PRIMARY KEY allows no more than one column not part of the PRIMARY KEY (got: d, c)
+CREATE TABLE type_c_4_3 (a int, b int, c int, d int, PRIMARY KEY (a, b, c)) WITH COMPACT STORAGE;
+
+-- type D: compound PK, plain partition key, dynamic storage
+CREATE TABLE type_d_2_2 (a int, b int, PRIMARY KEY (a, b));
+CREATE TABLE type_d_3_2 (a int, b int, c int, PRIMARY KEY (a, b));
+CREATE TABLE type_d_3_3 (a int, b int, c int, PRIMARY KEY (a, b, c));
+CREATE TABLE type_d_4_2 (a int, b int, c int, d int, PRIMARY KEY (a, b));
+
+-- type E: compound PK, multipart partition key, all key components used in partitioning,
+-- compact storage
+-- FAILS: CREATE TABLE type_e_2_2 (a int, b int, PRIMARY KEY ((a, b))) WITH COMPACT STORAGE;
+-- Bad Request: No definition found that is not part of the PRIMARY KEY
+CREATE TABLE type_e_3_2 (a int, b int, c int, PRIMARY KEY ((a, b))) WITH COMPACT STORAGE;
+CREATE TABLE type_e_4_2 (a int, b int, c int, d int, PRIMARY KEY ((a, b))) WITH COMPACT STORAGE;
+CREATE TABLE type_e_4_3 (a int, b int, c int, d int, PRIMARY KEY ((a, b, c))) WITH COMPACT STORAGE;
+
+-- type F: compound PK, multipart partition key, all key components used in partitioning,
+-- dynamic storage
+CREATE TABLE type_f_2_2 (a int, b int, PRIMARY KEY ((a, b)));
+CREATE TABLE type_f_3_2 (a int, b int, c int, PRIMARY KEY ((a, b)));
+CREATE TABLE type_f_4_2 (a int, b int, c int, d int, PRIMARY KEY ((a, b)));
+CREATE TABLE type_f_4_3 (a int, b int, c int, d int, PRIMARY KEY ((a, b, c)));
+
+-- type G: compound PK, multipart partition key, not all key components used in partitioning,
+-- compact storage
+CREATE TABLE type_g_3_3_2 (a int, b int, c int, PRIMARY KEY ((a, b), c)) WITH COMPACT STORAGE;
+CREATE TABLE type_g_4_3_2 (a int, b int, c int, d int, PRIMARY KEY ((a, b), c)) WITH COMPACT STORAGE;
+CREATE TABLE type_g_4_4_2 (a int, b int, c int, d int, PRIMARY KEY ((a, b), c, d)) WITH COMPACT STORAGE;
+CREATE TABLE type_g_4_4_3 (a int, b int, c int, d int, PRIMARY KEY ((a, b, c), d)) WITH COMPACT STORAGE;
+-- FAILS: CREATE TABLE type_g_5_3_2 (a int, b int, c int, d int, e int, PRIMARY KEY ((a, b), c)) WITH COMPACT STORAGE;
+-- Bad Request: COMPACT STORAGE with composite PRIMARY KEY allows no more than one column not part of the PRIMARY KEY (got: d, e)
+CREATE TABLE type_g_5_4_2 (a int, b int, c int, d int, e int, PRIMARY KEY ((a, b), c, d)) WITH COMPACT STORAGE;
+
+-- type H: compound PK, multipart partition key, not all key components used in partitioning,
+-- dynamic storage
+CREATE TABLE type_h_3_3_2 (a int, b int, c int, PRIMARY KEY ((a, b), c)); 
+CREATE TABLE type_h_4_3_2 (a int, b int, c int, d int, PRIMARY KEY ((a, b), c));
+CREATE TABLE type_h_4_4_2 (a int, b int, c int, d int, PRIMARY KEY ((a, b), c, d));
+CREATE TABLE type_h_4_4_3 (a int, b int, c int, d int, PRIMARY KEY ((a, b, c), d));
+CREATE TABLE type_h_5_3_2 (a int, b int, c int, d int, e int, PRIMARY KEY ((a, b), c));
+CREATE TABLE type_h_5_4_2 (a int, b int, c int, d int, e int, PRIMARY KEY ((a, b), c, d));
+
+-- type A with collections (these should fail, but don't)
+CREATE TABLE type_aa_2_2 (a int PRIMARY KEY, b map<float, text>) WITH COMPACT STORAGE;
+CREATE TABLE type_aa_3_2 (a int PRIMARY KEY, b map<float, text>, c int) WITH COMPACT STORAGE;
+
+-- type B with collections
+CREATE TABLE type_bb_2_2 (a int PRIMARY KEY, b map<float, text>);
+CREATE TABLE type_bb_3_2 (a int PRIMARY KEY, b map<float, text>, c int);
+CREATE TABLE type_bb_3_23 (a int PRIMARY KEY, b map<float, text>, c set<int>);
+
+-- type C with collections
+-- FAILS: CREATE TABLE type_cc_4_3_2 (a int, b map<float, text>, c int, d int, PRIMARY KEY (a, b, c)) WITH COMPACT STORAGE;
+-- Bad Request: Invalid collection type for PRIMARY KEY component b
+-- FAILS: CREATE TABLE type_cc_4_3_4 (a int, b int, c int, d map<float, text>, PRIMARY KEY (a, b, c)) WITH COMPACT STORAGE;
+-- Bad Request: Collection types are not supported with COMPACT STORAGE
+
+-- type D with collections
+-- FAILS: CREATE TABLE type_dd_3_2_1 (a map<float, text>, b int, c int, PRIMARY KEY (a, b));
+-- Bad Request: Invalid collection type for PRIMARY KEY component a
+-- FAILS: CREATE TABLE type_dd_3_2_2 (a int, b map<float, text>, c int, PRIMARY KEY (a, b));
+-- Bad Request: Invalid collection type for PRIMARY KEY component b
+CREATE TABLE type_dd_3_2_3 (a int, b int, c map<float, text>, PRIMARY KEY (a, b));
+CREATE TABLE type_dd_4_3_4 (a int, b int, c int, d map<float, text>, PRIMARY KEY (a, b, c));
+CREATE TABLE type_dd_5_3_4 (a int, b int, c int, d map<float, text>, e int, PRIMARY KEY (a, b, c));
+CREATE TABLE type_dd_5_3_45 (a int, b int, c int, d map<float, text>, e list<int>, PRIMARY KEY (a, b, c));
+
+-- type E with collections (these should all fail, but some don't)
+-- FAILS: CREATE TABLE type_ee_3_2_2 (a int, b map<float, text>, c int, PRIMARY KEY ((a, b))) WITH COMPACT STORAGE;
+-- Bad Request: Invalid collection type for PRIMARY KEY component b
+CREATE TABLE type_ee_3_2_3 (a int, b int, c map<float, text>, PRIMARY KEY ((a, b))) WITH COMPACT STORAGE;
+CREATE TABLE type_ee_4_3_4 (a int, b int, c int, d map<float, text>, PRIMARY KEY ((a, b, c))) WITH COMPACT STORAGE;
+CREATE TABLE type_ee_5_3_45 (a int, b int, c int, d map<float, text>, e list<int>, PRIMARY KEY ((a, b, c))) WITH COMPACT STORAGE;
+
+-- type F with collections
+-- FAILS: CREATE TABLE type_ff_3_2_1 (a list<int>, b int, c int, PRIMARY KEY ((a, b)));
+-- Bad Request: Invalid collection type for PRIMARY KEY component a
+CREATE TABLE type_ff_3_2_3 (a int, b int, c map<float, text>, PRIMARY KEY ((a, b)));
+CREATE TABLE type_ff_4_3_4 (a int, b int, c int, d map<float, text>, PRIMARY KEY ((a, b, c)));
+CREATE TABLE type_ff_5_2_45 (a int, b int, c int, d map<float, text>, e list<int>, PRIMARY KEY ((a, b)));
+CREATE TABLE type_ff_5_3_45 (a int, b int, c int, d map<float, text>, e list<int>, PRIMARY KEY ((a, b, c)));
+
+-- type G with collections
+-- FAILS: CREATE TABLE type_gg_4_3_2_1 (a set<int>, b int, c int, d int, PRIMARY KEY ((a, b), c)) WITH COMPACT STORAGE;
+-- Bad Request: Invalid collection type for PRIMARY KEY component a
+-- FAILS: CREATE TABLE type_gg_4_3_2_4 (a int, b int, c int, d list<int>, PRIMARY KEY ((a, b), c)) WITH COMPACT STORAGE;
+-- Bad Request: Collection types are not supported with COMPACT STORAGE
+-- FAILS: CREATE TABLE type_gg_5_3_2_4 (a int, b int, c int, d map<float, text>, e list<int>, PRIMARY KEY ((a, b), c)) WITH COMPACT STORAGE;
+-- Bad Request: Collection types are not supported with COMPACT STORAGE
+
+-- type H with collections
+-- FAILS: CREATE TABLE type_hh_4_3_2_1 (a set<int>, b int, c int, d int, PRIMARY KEY ((a, b), c));
+-- Bad Request: Invalid collection type for PRIMARY KEY component a
+-- FAILS: CREATE TABLE type_hh_4_3_2_3 (a int, b int, c list<int>, d int, PRIMARY KEY ((a, b), c));
+-- Bad Request: Invalid collection type for PRIMARY KEY component c
+CREATE TABLE type_hh_4_3_2_4 (a int, b int, c int, d list<int>, PRIMARY KEY ((a, b), c));
+CREATE TABLE type_hh_5_3_2_45 (a int, b int, c int, d map<float, text>, e list<int>, PRIMARY KEY ((a, b), c));

http://git-wip-us.apache.org/repos/asf/cassandra/blob/14d62ab1/pylib/cqlshlib/test/test_cql_parsing.py
----------------------------------------------------------------------
diff --git a/pylib/cqlshlib/test/test_cql_parsing.py b/pylib/cqlshlib/test/test_cql_parsing.py
new file mode 100644
index 0000000..7e4e6f3
--- /dev/null
+++ b/pylib/cqlshlib/test/test_cql_parsing.py
@@ -0,0 +1,87 @@
+# 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.
+
+# to configure behavior, define $CQL_TEST_HOST to the destination address
+# for Thrift connections, and $CQL_TEST_PORT to the associated port.
+
+from .basecase import BaseTestCase, cqlsh
+
+class TestCqlParsing(BaseTestCase):
+    def setUp(self):
+        pass
+
+    def tearDown(self):
+        pass
+
+    def test_parse_string_literals(self):
+        pass
+
+    def test_parse_numbers(self):
+        pass
+
+    def test_parse_uuid(self):
+        pass
+
+    def test_comments_in_string_literals(self):
+        pass
+
+    def test_colons_in_string_literals(self):
+        pass
+
+    def test_partial_parsing(self):
+        pass
+
+    def test_parse_select(self):
+        pass
+
+    def test_parse_insert(self):
+        pass
+
+    def test_parse_update(self):
+        pass
+
+    def test_parse_delete(self):
+        pass
+
+    def test_parse_batch(self):
+        pass
+
+    def test_parse_create_keyspace(self):
+        pass
+
+    def test_parse_drop_keyspace(self):
+        pass
+
+    def test_parse_create_columnfamily(self):
+        pass
+
+    def test_parse_drop_columnfamily(self):
+        pass
+
+    def test_parse_truncate(self):
+        pass
+
+    def test_parse_alter_columnfamily(self):
+        pass
+
+    def test_parse_use(self):
+        pass
+
+    def test_parse_create_index(self):
+        pass
+
+    def test_parse_drop_index(self):
+        pass

http://git-wip-us.apache.org/repos/asf/cassandra/blob/14d62ab1/pylib/cqlshlib/test/test_cqlsh_commands.py
----------------------------------------------------------------------
diff --git a/pylib/cqlshlib/test/test_cqlsh_commands.py b/pylib/cqlshlib/test/test_cqlsh_commands.py
new file mode 100644
index 0000000..b8dd6f7
--- /dev/null
+++ b/pylib/cqlshlib/test/test_cqlsh_commands.py
@@ -0,0 +1,42 @@
+# 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.
+
+# to configure behavior, define $CQL_TEST_HOST to the destination address
+# for Thrift connections, and $CQL_TEST_PORT to the associated port.
+
+from .basecase import BaseTestCase, cqlsh
+
+class TestCqlshCommands(BaseTestCase):
+    def setUp(self):
+        pass
+
+    def tearDown(self):
+        pass
+
+    def test_assume(self):
+        pass
+
+    def test_show(self):
+        pass
+
+    def test_describe(self):
+        pass
+
+    def test_exit(self):
+        pass
+
+    def test_help(self):
+        pass

http://git-wip-us.apache.org/repos/asf/cassandra/blob/14d62ab1/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
new file mode 100644
index 0000000..edb2b51
--- /dev/null
+++ b/pylib/cqlshlib/test/test_cqlsh_completion.py
@@ -0,0 +1,243 @@
+# 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.
+
+# to configure behavior, define $CQL_TEST_HOST to the destination address
+# for Thrift connections, and $CQL_TEST_PORT to the associated port.
+
+from __future__ import with_statement
+
+import re
+from .basecase import BaseTestCase, cqlsh
+from .cassconnect import testrun_cqlsh
+
+BEL = '\x07' # the terminal-bell character
+CTRL_C = '\x03'
+TAB = '\t'
+
+# completions not printed out in this many seconds may not be acceptable.
+# tune if needed for a slow system, etc, but be aware that the test will
+# need to wait this long for each completion test, to make sure more info
+# isn't coming
+COMPLETION_RESPONSE_TIME = 0.5
+
+completion_separation_re = re.compile(r'\s\s+')
+
+class CqlshCompletionCase(BaseTestCase):
+    def setUp(self):
+        self.cqlsh_runner = testrun_cqlsh(cqlver=self.cqlver, env={'COLUMNS': '100000'})
+        self.cqlsh = self.cqlsh_runner.__enter__()
+
+    def tearDown(self):
+        self.cqlsh_runner.__exit__(None, None, None)
+
+    def _trycompletions_inner(self, inputstring, immediate='', choices=(), other_choices_ok=False):
+        """
+        Test tab completion in cqlsh. Enters in the text in inputstring, then
+        simulates a tab keypress to see what is immediately completed (this
+        should only happen when there is only one completion possible). If
+        there is an immediate completion, the new text is expected to match
+        'immediate'. If there is no immediate completion, another tab keypress
+        is simulated in order to get a list of choices, which are expected to
+        match the items in 'choices' (order is not important, but case is).
+        """
+        self.cqlsh.send(inputstring)
+        self.cqlsh.send(TAB)
+        completed = self.cqlsh.read_up_to_timeout(COMPLETION_RESPONSE_TIME)
+        self.assertEqual(completed[:len(inputstring)], inputstring)
+        completed = completed[len(inputstring):]
+        completed = completed.replace(BEL, '')
+        self.assertEqual(completed, immediate, 'cqlsh completed %r, but we expected %r'
+                                               % (completed, immediate))
+        if immediate:
+            return
+
+        self.cqlsh.send(TAB)
+        choice_output = self.cqlsh.read_up_to_timeout(COMPLETION_RESPONSE_TIME)
+        if choice_output == BEL:
+            lines = ()
+        else:
+            lines = choice_output.splitlines()
+            self.assertRegexpMatches(lines[-1], self.cqlsh.prompt.lstrip() + re.escape(inputstring))
+        choicesseen = set()
+        for line in lines[:-1]:
+            choicesseen.update(completion_separation_re.split(line.strip()))
+        choicesseen.discard('')
+        if other_choices_ok:
+            self.assertEqual(set(choices), choicesseen.intersection(choices))
+        else:
+            self.assertEqual(set(choices), choicesseen)
+
+    def trycompletions(self, inputstring, immediate='', choices=(), other_choices_ok=False):
+        try:
+            self._trycompletions_inner(inputstring, immediate, choices, other_choices_ok)
+        finally:
+            self.cqlsh.send(CTRL_C) # cancel any current line
+            self.cqlsh.read_to_next_prompt()
+
+    def strategies(self):
+        return self.module.CqlRuleSet.replication_strategies
+
+class TestCqlshCompletion_CQL2(CqlshCompletionCase):
+    cqlver = 2
+    module = cqlsh.cqlhandling
+
+    def test_complete_on_empty_string(self):
+        self.trycompletions('', choices=('?', 'ALTER', 'ASSUME', 'BEGIN', 'CAPTURE', 'CONSISTENCY',
+                                         'COPY', 'CREATE', 'DEBUG', 'DELETE', 'DESC', 'DESCRIBE',
+                                         'DROP', 'HELP', 'INSERT', 'SELECT', 'SHOW', 'SOURCE',
+                                         'TRACING', 'TRUNCATE', 'UPDATE', 'USE', 'exit', 'quit'))
+
+    def test_complete_command_words(self):
+        self.trycompletions('alt', '\b\b\bALTER ')
+        self.trycompletions('I', 'NSERT INTO ')
+        self.trycompletions('exit', ' ')
+
+    def test_complete_in_string_literals(self):
+        # would be great if we could get a space after this sort of completion,
+        # but readline really wants to make things difficult for us
+        self.trycompletions("insert into system.'NodeId", "Info'")
+        self.trycompletions("USE '", choices=('system', self.cqlsh.keyspace), other_choices_ok=True)
+        self.trycompletions("create keyspace blah with strategy_class = 'Sim",
+                            "pleStrategy'")
+
+    def test_complete_in_uuid(self):
+        pass
+
+    def test_complete_in_select(self):
+        pass
+
+    def test_complete_in_insert(self):
+        pass
+
+    def test_complete_in_update(self):
+        pass
+
+    def test_complete_in_delete(self):
+        pass
+
+    def test_complete_in_batch(self):
+        pass
+
+    def test_complete_in_create_keyspace(self):
+        self.trycompletions('create keyspace ', '', choices=('<new_keyspace_name>',))
+        self.trycompletions('create keyspace moo ', "WITH strategy_class = '")
+        self.trycompletions("create keyspace '12SomeName' with ", "strategy_class = '")
+        self.trycompletions("create keyspace moo with strategy_class", " = '")
+        self.trycompletions("create keyspace moo with strategy_class='",
+                            choices=self.strategies())
+        self.trycompletions("create keySPACE 123 with strategy_class='SimpleStrategy' A",
+                            "ND strategy_options:replication_factor = ")
+        self.trycompletions("create keyspace fish with strategy_class='SimpleStrategy'"
+                                  "and strategy_options:replication_factor = ", '',
+                            choices=('<option_value>',))
+        self.trycompletions("create keyspace 'PB and J' with strategy_class="
+                               "'NetworkTopologyStrategy' AND", ' ')
+        self.trycompletions("create keyspace 'PB and J' with strategy_class="
+                               "'NetworkTopologyStrategy' AND ", '',
+                            choices=('<strategy_option_name>',))
+
+    def test_complete_in_drop_keyspace(self):
+        pass
+
+    def test_complete_in_create_columnfamily(self):
+        pass
+
+    def test_complete_in_drop_columnfamily(self):
+        pass
+
+    def test_complete_in_truncate(self):
+        pass
+
+    def test_complete_in_alter_columnfamily(self):
+        pass
+
+    def test_complete_in_use(self):
+        pass
+
+    def test_complete_in_create_index(self):
+        pass
+
+    def test_complete_in_drop_index(self):
+        pass
+
+class TestCqlshCompletion_CQL3final(TestCqlshCompletion_CQL2):
+    cqlver = '3.0.0'
+    module = cqlsh.cql3handling
+
+    def test_complete_on_empty_string(self):
+        self.trycompletions('', choices=('?', 'ALTER', 'ASSUME', 'BEGIN', 'CAPTURE', 'CONSISTENCY',
+                                         'COPY', 'CREATE', 'DEBUG', 'DELETE', 'DESC', 'DESCRIBE',
+                                         'DROP', 'GRANT', 'HELP', 'INSERT', 'LIST', 'REVOKE',
+                                         'SELECT', 'SHOW', 'SOURCE', 'TRACING', 'TRUNCATE', 'UPDATE',
+                                         'USE', 'exit', 'quit'))
+
+    def test_complete_in_create_keyspace(self):
+        self.trycompletions('create keyspace ', '', choices=('<identifier>', '<quotedName>'))
+        self.trycompletions('create keyspace moo ',
+                            "WITH replication = {'class': '")
+        self.trycompletions('create keyspace "12SomeName" with ',
+                            "replication = {'class': '")
+        self.trycompletions("create keyspace fjdkljf with foo=bar ", "",
+                            choices=('AND', ';'))
+        self.trycompletions("create keyspace fjdkljf with foo=bar AND ",
+                            "replication = {'class': '")
+        self.trycompletions("create keyspace moo with replication", " = {'class': '")
+        self.trycompletions("create keyspace moo with replication=", " {'class': '")
+        self.trycompletions("create keyspace moo with replication={", "'class':'")
+        self.trycompletions("create keyspace moo with replication={'class'", ":'")
+        self.trycompletions("create keyspace moo with replication={'class': ", "'")
+        self.trycompletions("create keyspace moo with replication={'class': '", "",
+                            choices=self.strategies())
+        # ttl is an "unreserved keyword". should work
+        self.trycompletions("create keySPACE ttl with replication ="
+                               "{ 'class' : 'SimpleStrategy'", ", 'replication_factor': ")
+        self.trycompletions("create   keyspace ttl with replication ="
+                               "{'class':'SimpleStrategy',", " 'replication_factor': ")
+        self.trycompletions("create keyspace \"ttl\" with replication ="
+                               "{'class': 'SimpleStrategy', ", "'replication_factor': ")
+        self.trycompletions("create keyspace \"ttl\" with replication ="
+                               "{'class': 'SimpleStrategy', 'repl", "ication_factor'")
+        self.trycompletions("create keyspace foo with replication ="
+                               "{'class': 'SimpleStrategy', 'replication_factor': ", '',
+                            choices=('<value>',))
+        self.trycompletions("create keyspace foo with replication ="
+                               "{'class': 'SimpleStrategy', 'replication_factor': 1", '',
+                            choices=('<value>',))
+        self.trycompletions("create keyspace foo with replication ="
+                               "{'class': 'SimpleStrategy', 'replication_factor': 1 ", '}')
+        self.trycompletions("create keyspace foo with replication ="
+                               "{'class': 'SimpleStrategy', 'replication_factor': 1, ",
+                            '', choices=())
+        self.trycompletions("create keyspace foo with replication ="
+                               "{'class': 'SimpleStrategy', 'replication_factor': 1} ",
+                            '', choices=('AND', ';'))
+        self.trycompletions("create keyspace foo with replication ="
+                               "{'class': 'NetworkTopologyStrategy', ", '',
+                            choices=('<dc_name>',))
+        self.trycompletions("create keyspace \"PB and J\" with replication={"
+                               "'class': 'NetworkTopologyStrategy'", ', ')
+        self.trycompletions("create keyspace PBJ with replication={"
+                               "'class': 'NetworkTopologyStrategy'} and ",
+                            "durable_writes = '")
+
+    def test_complete_in_string_literals(self):
+        # would be great if we could get a space after this sort of completion,
+        # but readline really wants to make things difficult for us
+        self.trycompletions('insert into system."NodeId', 'Info"')
+        self.trycompletions('USE "', choices=('system', self.cqlsh.keyspace),
+                            other_choices_ok=True)
+        self.trycompletions("create keyspace blah with replication = {'class': 'Sim",
+                            "pleStrategy'")

http://git-wip-us.apache.org/repos/asf/cassandra/blob/14d62ab1/pylib/cqlshlib/test/test_cqlsh_invocation.py
----------------------------------------------------------------------
diff --git a/pylib/cqlshlib/test/test_cqlsh_invocation.py b/pylib/cqlshlib/test/test_cqlsh_invocation.py
new file mode 100644
index 0000000..67fa76f
--- /dev/null
+++ b/pylib/cqlshlib/test/test_cqlsh_invocation.py
@@ -0,0 +1,78 @@
+# 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.
+
+# to configure behavior, define $CQL_TEST_HOST to the destination address
+# for Thrift connections, and $CQL_TEST_PORT to the associated port.
+
+from .basecase import BaseTestCase
+
+class TestCqlshInvocation(BaseTestCase):
+    def setUp(self):
+        pass
+
+    def tearDown(self):
+        pass
+
+    def test_normal_run(self):
+        pass
+
+    def test_python_interpreter_location(self):
+        pass
+
+    def test_color_capability_detection(self):
+        pass
+
+    def test_colored_output(self):
+        pass
+
+    def test_color_cmdline_option(self):
+        pass
+
+    def test_debug_option(self):
+        pass
+
+    def test_connection_args(self):
+        pass
+
+    def test_connection_config(self):
+        pass
+
+    def test_connection_envvars(self):
+        pass
+
+    def test_command_history(self):
+        pass
+
+    def test_missing_dependencies(self):
+        pass
+
+    def test_completekey_config(self):
+        pass
+
+    def test_ctrl_c(self):
+        pass
+
+    def test_eof(self):
+        pass
+
+    def test_output_encoding_detection(self):
+        pass
+
+    def test_output_encoding(self):
+        pass
+
+    def test_retries(self):
+        pass