Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Browse files

Merge branch 'cassandra-1.2' into trunk

Conflicts:
	pylib/cqlshlib/cql3handling.py
  • Loading branch information...
commit 125a8c9c79f605c1dcfe45e772441cdd0438dc74 2 parents 51947d9 + 6b093b4
@iamaleksey iamaleksey authored
View
1  CHANGES.txt
@@ -23,6 +23,7 @@
1.2.0
* Disallow counters in collections (CASSANDRA-5082)
+ * cqlsh: add unit tests (CASSANDRA-3920)
1.2.0-rc2
View
86 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
View
36 pylib/cqlshlib/cql3handling.py
@@ -38,7 +38,7 @@ def __init__(self, msg):
def __str__(self):
return 'Unexpected table structure; may not translate correctly to CQL. ' + self.msg
-SYSTEM_KEYSPACES = ('system', 'system_traces')
+SYSTEM_KEYSPACES = ('system', 'system_auth', 'system_traces')
class Cql3ParsingRuleSet(CqlParsingRuleSet):
keywords = set((
@@ -58,39 +58,37 @@ 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'),
- ('replicate_on_write', None),
- ('compaction_strategy_class', 'compaction_strategy'),
+ ('gc_grace_seconds', None),
('index_interval', None),
+ ('read_repair_chance', None),
+ ('replicate_on_write', None),
)
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),
- ('replicate_on_write', None),
- ('compaction_strategy_class', None),
('index_interval', None),
+ ('read_repair_chance', None),
+ ('replicate_on_write', 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),
- ('replicate_on_write', None),
- ('default_read_consistency', None),
- ('default_write_consistency', None),
('index_interval', None),
+ ('read_repair_chance', None),
+ ('replicate_on_write', None),
)
old_columnfamily_layout_map_options = (
@@ -106,18 +104,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
@@ -1517,7 +1515,7 @@ def get_columns_compact(self):
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
View
8 pylib/cqlshlib/displaying.py
@@ -53,20 +53,24 @@ def _pad(self, width, fill=' '):
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=' '):
View
27 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)
View
20 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
View
191 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
View
71 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))
View
159 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)
View
271 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
View
114 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));
View
87 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
View
42 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
View
243 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'")
View
78 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
View
965 pylib/cqlshlib/test/test_cqlsh_output.py
@@ -0,0 +1,965 @@
+# 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 itertools import izip
+from .basecase import (BaseTestCase, cqlshlog, dedent, at_a_time, cql,
+ TEST_HOST, TEST_PORT)
+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)
+
+CONTROL_C = '\x03'
+CONTROL_D = '\x04'
+
+class TestCqlshOutput(BaseTestCase):
+ def setUp(self):
+ pass
+
+ def tearDown(self):
+ pass
+
+ def assertNoHasColors(self, text, msg=None):
+ self.assertNotRegexpMatches(text, ansi_seq, msg='ANSI CSI sequence found in %r' % text)
+
+ def assertHasColors(self, text, msg=None):
+ self.assertRegexpMatches(text, ansi_seq, msg=msg)
+
+ def assertColored(self, coloredtext, colorname):
+ wanted_colorcode = lookup_colorcode(colorname)
+ for num, c in enumerate(coloredtext):
+ if not c.isspace():
+ ccolor = c.colorcode()
+ self.assertEqual(ccolor, wanted_colorcode,
+ msg='Output text %r (char #%d) is colored %s, not %s'
+ % (coloredtext, num, lookup_colorname(ccolor), colorname))
+
+ def assertColorFromTags(self, coloredtext, tags):
+ for (char, tag) in izip(coloredtext, tags):
+ if char.isspace():
+ continue
+ if tag.isspace():
+ tag = 'n' # neutral
+ self.assertEqual(char.colorcode(), lookup_colorletter(tag),
+ msg='Coloring mismatch.\nExpected coloring: %s\n'
+ 'Actually got: %s\ncolor code: %s'
+ % (tags, coloredtext.colored_version(), coloredtext.colortags()))
+
+ def assertCqlverQueriesGiveColoredOutput(self, queries_and_expected_outputs,
+ cqlver=(), **kwargs):
+ if not isinstance(cqlver, (tuple, list)):
+ cqlver = (cqlver,)
+ for ver in cqlver:
+ self.assertQueriesGiveColoredOutput(queries_and_expected_outputs, cqlver=ver, **kwargs)
+
+ def assertQueriesGiveColoredOutput(self, queries_and_expected_outputs,
+ sort_results=False, **kwargs):
+ """
+ Allow queries and expected output to be specified in structured tuples,
+ along with expected color information.
+ """
+ with testrun_cqlsh(tty=True, **kwargs) as c:
+ for query, expected in queries_and_expected_outputs:
+ cqlshlog.debug('Testing %r' % (query,))
+ output = c.cmd_and_response(query).lstrip("\r\n")
+ c_output = ColoredText(output)
+ pairs = at_a_time(dedent(expected).split('\n'), 2)
+ outlines = c_output.splitlines()
+ if sort_results:
+ if outlines[1].plain().startswith('---'):
+ firstpart = outlines[:2]
+ sortme = outlines[2:]
+ else:
+ firstpart = []
+ sortme = outlines
+ lastpart = []
+ while sortme[-1].plain() == '':
+ lastpart.append(sortme.pop(-1))
+ outlines = firstpart + sorted(sortme, key=lambda c:c.plain()) + lastpart
+ for (plain, colorcodes), outputline in zip(pairs, outlines):
+ self.assertEqual(outputline.plain().rstrip(), plain)
+ self.assertColorFromTags(outputline, colorcodes)
+
+ 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:
+ c.send('select * from has_all_types;\n')
+ self.assertNoHasColors(c.read_to_next_prompt())
+ c.send('select * from has_value_encoding_errors;\n')
+ self.assertNoHasColors(c.read_to_next_prompt())
+ c.send('select count(*) from has_all_types;\n')
+ self.assertNoHasColors(c.read_to_next_prompt())
+ c.send('totally invalid cql;\n')
+ self.assertNoHasColors(c.read_to_next_prompt())
+
+ def test_no_prompt_or_colors_output(self):
+ # CQL queries and number of lines expected in output:
+ queries = (('select * from has_all_types limit 1;', 5),
+ ('select * from has_value_encoding_errors limit 1;', 6))
+ for termname in ('', 'dumb', 'vt100', 'xterm'):
+ cqlshlog.debug('TERM=%r' % termname)
+ for cql, lines_expected in queries:
+ output, result = testcall_cqlsh(prompt=None, env={'TERM': termname},
+ tty=False, input=cql + '\n')
+ output = output.splitlines()
+ for line in output:
+ self.assertNoHasColors(line)
+ self.assertNotRegexpMatches(line, r'^cqlsh\S*>')
+ self.assertEqual(len(output), lines_expected,
+ msg='output: %r' % '\n'.join(output))
+ self.assertEqual(output[0], '')
+ self.assertNicelyFormattedTableHeader(output[1])
+ self.assertNicelyFormattedTableRule(output[2])
+ self.assertNicelyFormattedTableData(output[3])
+ self.assertEqual(output[4].strip(), '')
+
+ def test_color_output(self):
+ for termname in ('xterm', 'unknown-garbage'):
+ cqlshlog.debug('TERM=%r' % termname)
+ with testrun_cqlsh(tty=True, env={'TERM': termname}) as c:
+ c.send('select * from has_all_types;\n')
+ self.assertHasColors(c.read_to_next_prompt())
+ c.send('select * from has_value_encoding_errors;\n')
+ self.assertHasColors(c.read_to_next_prompt())
+ c.send('select count(*) from has_all_types;\n')
+ self.assertHasColors(c.read_to_next_prompt())
+ c.send('totally invalid cql;\n')
+ self.assertHasColors(c.read_to_next_prompt())
+
+ def test_count_output(self):
+ self.assertCqlverQueriesGiveColoredOutput((
+ ('select count(*) from has_all_types;', """
+ count
+ MMMMM
+ -------
+
+ 4
+ G
+
+ """),
+
+ ('select COUNT(*) FROM empty_table;', """
+ count
+ MMMMM
+ -------
+
+ 0
+ G
+
+ """),
+
+ ('select COUNT(*) FROM empty_composite_table;', """
+ count
+ MMMMM
+ -------
+
+ 0
+ G
+
+ """),
+
+ ('select COUNT(*) FROM twenty_rows_table limit 10;', """
+ count
+ MMMMM
+ -------
+
+ 10
+ GG
+
+ """),
+
+ ('select COUNT(*) FROM twenty_rows_table limit 1000000;', """
+ count
+ MMMMM
+ -------
+
+ 20
+ GG
+
+ """),
+ ), cqlver=(2, 3))
+
+ # different results cql2/cql3
+ q = 'select COUNT(*) FROM twenty_rows_composite_table limit 1000000;'
+
+ self.assertQueriesGiveColoredOutput((
+ (q, """
+ count
+ MMMMM
+ -------
+
+ 1
+ GG
+
+ """),
+ ), cqlver=2)
+
+ self.assertQueriesGiveColoredOutput((
+ (q, """
+ count
+ MMMMM
+ -------
+
+ 20
+ GG
+
+ """),
+ ), cqlver=3)
+
+ def test_dynamic_cf_output(self):
+ self.assertQueriesGiveColoredOutput((
+ ('select * from dynamic_columns;', """
+ somekey,1 | 1.2,one point two
+ nMMMMMMM G MMM YYYYYYYYYYYYY
+ somekey,2 | 2.3,two point three
+ MMMMMMM G MMM YYYYYYYYYYYYYYY
+ somekey,3 | -0.0001,negative ten thousandth | 3.46,three point four six | 99,ninety-nine point oh
+ MMMMMMM G MMMMMMM YYYYYYYYYYYYYYYYYYYYYYY MMMM YYYYYYYYYYYYYYYYYYYY n MM YYYYYYYYYYYYYYYYYYYY
+
+ """),
+
+ ('select somekey, 2.3 from dynamic_columns;', """
+ somekey | 2.3
+ MMMMMMM MMM
+ ---------+-----------------
+
+ 1 | null
+ G RRRR
+ 2 | two point three
+ G YYYYYYYYYYYYYYY
+ 3 | null
+ G RRRR
+
+ """),
+
+ ('select first 1 * from dynamic_columns where somekey = 2;', """
+ 2.3
+ MMM
+ -----------------
+
+ two point three
+ YYYYYYYYYYYYYYY
+
+ """),
+
+ ('select * from dynamic_columns where somekey = 2;', """
+ somekey | 2.3
+ MMMMMMM MMM
+ ---------+-----------------
+
+ 2 | two point three
+ G YYYYYYYYYYYYYYY
+
+ """),
+ ), cqlver=2, sort_results=True)
+
+ def test_static_cf_output(self):
+ self.assertCqlverQueriesGiveColoredOutput((
+ ('select a, b from twenty_rows_table where a in (1, 13, 2);', """
+ a | b
+ MM MM
+ ----+----
+
+ 1 | 1
+ YY YY
+ 13 | 13
+ YY YY
+ 2 | 2
+ YY YY
+
+ """),
+ ), cqlver=(2, 3))
+
+ self.assertQueriesGiveColoredOutput((
+ ('select * from dynamic_columns;', """
+ somekey | column1 | value
+ MMMMMMM MMMMMMM MMMMM
+ ---------+---------+-------------------------
+
+ 1 | 1.2 | one point two
+ GG GGGGGGG YYYYYYYYYYYYYYYYYYYYYYY
+ 2 | 2.3 | two point three
+ GG GGGGGGG YYYYYYYYYYYYYYYYYYYYYYY
+ 3 | 99 | ninety-nine point oh
+ GG GGGGGGG YYYYYYYYYYYYYYYYYYYYYYY
+ 3 | 3.46 | three point four six
+ GG GGGGGGG YYYYYYYYYYYYYYYYYYYYYYY
+ 3 | -0.0001 | negative ten thousandth
+ GG GGGGGGG YYYYYYYYYYYYYYYYYYYYYYY
+
+ """),
+ ), cqlver=3, sort_results=True)
+
+ def test_empty_cf_output(self):
+ self.assertCqlverQueriesGiveColoredOutput((
+ ('select * from empty_table;', """
+ """),
+ ), cqlver=(2, 3))
+
+ q = 'select * from has_all_types where num = 999;'
+ self.assertQueriesGiveColoredOutput((
+ (q, """
+ num
+ MMM
+ -----
+
+ 999
+ GGG
+
+ """),
+ ), cqlver=2)
+
+ # same query should show up as empty in cql 3
+ self.assertQueriesGiveColoredOutput((
+ (q, """
+ """),
+ ), cqlver=3)
+
+ def test_columnless_key_output(self):
+ q = 'select a from twenty_rows_table where a in (1, 2, -9192);'
+ self.assertQueriesGiveColoredOutput((
+ (q, """
+ a
+ MMMMM
+ -------
+
+ 1
+ YYYYY
+ 2
+ YYYYY
+ -9192
+ YYYYY
+
+ """),
+ ), cqlver=2)
+
+ self.assertQueriesGiveColoredOutput((
+ (q, """
+ a
+ M
+ ---
+
+ 1
+ Y
+ 2
+ Y
+
+ """),
+ ), cqlver=3)
+
+ def test_numeric_output(self):
+ self.assertCqlverQueriesGiveColoredOutput((
+ ('''select intcol, bigintcol, varintcol \
+ from has_all_types \
+ where num in (0, 1, 2, 3);''', """
+ intcol | bigintcol | varintcol
+ MMMMMM MMMMMMMMM MMMMMMMMM
+ -------------+----------------------+-----------------------------
+
+ -12 | 1234567890123456789 | 10000000000000000000000000
+ GGGGGGGGGGG GGGGGGGGGGGGGGGGGGGG GGGGGGGGGGGGGGGGGGGGGGGGGGG
+ 2147483647 | 9223372036854775807 | 9
+ GGGGGGGGGGG GGGGGGGGGGGGGGGGGGGG GGGGGGGGGGGGGGGGGGGGGGGGGGG
+ 0 | 0 | 0
+ GGGGGGGGGGG GGGGGGGGGGGGGGGGGGGG GGGGGGGGGGGGGGGGGGGGGGGGGGG
+ -2147483648 | -9223372036854775808 | -10000000000000000000000000
+ GGGGGGGGGGG GGGGGGGGGGGGGGGGGGGG GGGGGGGGGGGGGGGGGGGGGGGGGGG
+
+ """),
+
+ ('''select decimalcol, doublecol, floatcol \
+ from has_all_types \
+ where num in (0, 1, 2, 3);''', """
+ decimalcol | doublecol | floatcol
+ MMMMMMMMMM MMMMMMMMM MMMMMMMM
+ ------------------+-----------+----------
+
+ 19952.11882 | 1 | -2.1
+ GGGGGGGGGGGGGGGG GGGGGGG GGGGG
+ 1E-14 | 1e+07 | 1e+05
+ GGGGGGGGGGGGGGGG GGGGGGG GGGGG
+ 0.0 | 0 | 0
+ GGGGGGGGGGGGGGGG GGGGGGG GGGGG
+ 10.0000000000000 | -1004.1 | 1e+08
+ GGGGGGGGGGGGGGGG GGGGGGG GGGGG
+
+ """),
+ ), cqlver=(2, 3))
+
+ self.assertQueriesGiveColoredOutput((
+ ('''select * from dynamic_columns where somekey = 3;''', """
+ somekey | -0.0001 | 3.46 | 99
+ MMMMMMM MMMMMMM MMMM MM
+ ---------+-------------------------+----------------------+----------------------
+
+ 3 | negative ten thousandth | three point four six | ninety-nine point oh
+ G YYYYYYYYYYYYYYYYYYYYYYY YYYYYYYYYYYYYYYYYYYY YYYYYYYYYYYYYYYYYYYY
+
+ """),
+ ), cqlver=2)
+
+ def test_timestamp_output(self):
+ self.assertQueriesGiveColoredOutput((
+ ('''select timestampcol from has_all_types where num = 0;''', """
+ timestampcol
+ MMMMMMMMMMMM
+ --------------------------
+
+ 2012-05-14 12:53:20+0000
+ GGGGGGGGGGGGGGGGGGGGGGGG
+
+ """),
+ ), env={'TZ': 'Etc/UTC'})
+
+ self.assertQueriesGiveColoredOutput((
+ ('''select timestampcol from has_all_types where num = 0;''', """
+ timestampcol
+ MMMMMMMMMMMM
+ --------------------------
+
+ 2012-05-14 07:53:20-0500
+ GGGGGGGGGGGGGGGGGGGGGGGG
+
+ """),
+ ), env={'TZ': 'EST'})
+
+ def test_boolean_output(self):
+ self.assertCqlverQueriesGiveColoredOutput((
+ ('select num, booleancol from has_all_types where num in (0, 1, 2, 3);', """
+ num | booleancol
+ MMM MMMMMMMMMM
+ -----+------------
+
+ 0 | True
+ G GGGGG
+ 1 | True
+ G GGGGG
+ 2 | False
+ G GGGGG
+ 3 | False
+ G GGGGG
+
+ """),
+ ), cqlver=(2, 3))
+
+ def test_null_outp