Return-Path: X-Original-To: apmail-cassandra-commits-archive@www.apache.org Delivered-To: apmail-cassandra-commits-archive@www.apache.org Received: from mail.apache.org (hermes.apache.org [140.211.11.3]) by minotaur.apache.org (Postfix) with SMTP id 864F6E5FB for ; Thu, 27 Dec 2012 23:46:01 +0000 (UTC) Received: (qmail 24422 invoked by uid 500); 27 Dec 2012 23:46:01 -0000 Delivered-To: apmail-cassandra-commits-archive@cassandra.apache.org Received: (qmail 24362 invoked by uid 500); 27 Dec 2012 23:46:01 -0000 Mailing-List: contact commits-help@cassandra.apache.org; run by ezmlm Precedence: bulk List-Help: List-Unsubscribe: List-Post: List-Id: Reply-To: dev@cassandra.apache.org Delivered-To: mailing list commits@cassandra.apache.org Received: (qmail 24209 invoked by uid 99); 27 Dec 2012 23:46:01 -0000 Received: from tyr.zones.apache.org (HELO tyr.zones.apache.org) (140.211.11.114) by apache.org (qpsmtpd/0.29) with ESMTP; Thu, 27 Dec 2012 23:46:01 +0000 Received: by tyr.zones.apache.org (Postfix, from userid 65534) id BE43D820BDD; Thu, 27 Dec 2012 23:46:00 +0000 (UTC) Content-Type: text/plain; charset="us-ascii" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit From: aleksey@apache.org To: commits@cassandra.apache.org X-Mailer: ASF-Git Admin Mailer Subject: [2/4] git commit: cqlsh: add unit tests; patch by Paul Cannon with minor changes by Aleksey Yeschenko, reviewed by Aleksey Yeschenko for CASSANDRA-3920 Message-Id: <20121227234600.BE43D820BDD@tyr.zones.apache.org> Date: Thu, 27 Dec 2012 23:46:00 +0000 (UTC) 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/trunk Commit: 14d62ab115001be9dfa872fb59e60ea532372d4f Parents: 4b9d927 Author: Aleksey Yeschenko Authored: Fri Dec 28 02:30:35 2012 +0300 Committer: Aleksey Yeschenko 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[\x20-\x3f]*)(?P[\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) WITH COMPACT STORAGE; +CREATE TABLE type_aa_3_2 (a int PRIMARY KEY, b map, c int) WITH COMPACT STORAGE; + +-- type B with collections +CREATE TABLE type_bb_2_2 (a int PRIMARY KEY, b map); +CREATE TABLE type_bb_3_2 (a int PRIMARY KEY, b map, c int); +CREATE TABLE type_bb_3_23 (a int PRIMARY KEY, b map, c set); + +-- type C with collections +-- FAILS: CREATE TABLE type_cc_4_3_2 (a int, b map, 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, 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, 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, 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, PRIMARY KEY (a, b)); +CREATE TABLE type_dd_4_3_4 (a int, b int, c int, d map, PRIMARY KEY (a, b, c)); +CREATE TABLE type_dd_5_3_4 (a int, b int, c int, d map, e int, PRIMARY KEY (a, b, c)); +CREATE TABLE type_dd_5_3_45 (a int, b int, c int, d map, e list, 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, 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, PRIMARY KEY ((a, b))) WITH COMPACT STORAGE; +CREATE TABLE type_ee_4_3_4 (a int, b int, c int, d map, PRIMARY KEY ((a, b, c))) WITH COMPACT STORAGE; +CREATE TABLE type_ee_5_3_45 (a int, b int, c int, d map, e list, PRIMARY KEY ((a, b, c))) WITH COMPACT STORAGE; + +-- type F with collections +-- FAILS: CREATE TABLE type_ff_3_2_1 (a list, 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, PRIMARY KEY ((a, b))); +CREATE TABLE type_ff_4_3_4 (a int, b int, c int, d map, PRIMARY KEY ((a, b, c))); +CREATE TABLE type_ff_5_2_45 (a int, b int, c int, d map, e list, PRIMARY KEY ((a, b))); +CREATE TABLE type_ff_5_3_45 (a int, b int, c int, d map, e list, PRIMARY KEY ((a, b, c))); + +-- type G with collections +-- FAILS: CREATE TABLE type_gg_4_3_2_1 (a set, 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, 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, e list, 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, 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, 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, PRIMARY KEY ((a, b), c)); +CREATE TABLE type_hh_5_3_2_45 (a int, b int, c int, d map, e list, 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=('',)) + 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=('',)) + 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=('',)) + + 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=('', '')) + 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=('',)) + 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 ", '}') + 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=('',)) + 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