Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion python/adelphi/adelphi/cql.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,5 +49,5 @@ def export_schema(self, keyspace=None):


def each_keyspace(self, ks_fn):
for (ks_name, ks_tuple) in self.keyspaces.items():
for (_, ks_tuple) in self.keyspaces.items():
ks_fn(ks_tuple.ks_obj, ks_tuple.ks_id)
5 changes: 3 additions & 2 deletions python/adelphi/adelphi/export.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
import hashlib
import logging
from base64 import urlsafe_b64encode
from collections import namedtuple
from collections import namedtuple, OrderedDict
from datetime import datetime, tzinfo, timedelta

try:
Expand Down Expand Up @@ -95,7 +95,8 @@ def make_tuple(ks):
if props['anonymize']:
anonymize_keyspace(ks)
return KsTuple(ids[orig_name], ks)
return {t.ks_obj.name : t for t in [make_tuple(ks) for ks in keyspaces]}
tuples = sorted([make_tuple(ks) for ks in keyspaces], key=lambda ks: ks.ks_obj.name)
return OrderedDict([(t.ks_obj.name,t) for t in tuples])
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Keyspaces returned via export should be handled in an orderly way. This is essential for legit compares of prior output to what's done by the current code, which in turn is an essential feature for our integration tests.



def get_cluster_metadata(self, cluster):
Expand Down
5 changes: 5 additions & 0 deletions python/adelphi/adelphi/store.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
# Functions to facilitate interactions with the underlying data store

import logging
from collections import OrderedDict
from itertools import tee

# Account for name change in itertools as of py3k
Expand Down Expand Up @@ -59,6 +60,10 @@ def build_keyspace_objects(keyspaces, metadata):
"""Build a list of cassandra.metadata.KeyspaceMetadata objects from a list of strings and a c.m.Metadata instance. System keyspaces will be excluded."""
all_keyspace_objs = [metadata.keyspaces[ks] for ks in keyspaces] if keyspaces is not None else metadata.keyspaces.values()

# Make sure tables are ordered (by table name)
for ks_obj in all_keyspace_objs:
ks_obj.tables = OrderedDict(sorted(ks_obj.tables.items(), key=lambda item: item[0]))

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A similar problem to what's discussed above: tables should also follow a coherent order. Tests on Python 2.7 were seeing tables returned in an apparently random order, which in turn caused comparisons to a reference output to fail only because of the ordering of elements within the output.

# Borrowed from itertools
def partition(pred, iterable):
t1, t2 = tee(iterable)
Expand Down
2 changes: 0 additions & 2 deletions python/adelphi/bin/adelphi
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,9 @@
# See the License for the specific language governing permissions and
# limitations under the License.

import argparse
import json
import logging
import os
import os.path
import sys
from functools import partial

Expand Down
15 changes: 10 additions & 5 deletions python/adelphi/bin/test-adelphi
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import tox


# Default C* versions to include in all integration tests
DEFAULT_CASSANDRA_VERSIONS = ["2.1.22", "2.2.19", "3.0.23", "3.11.10", "4.0-rc1"]
DEFAULT_CASSANDRA_VERSIONS = ["2.1.22", "2.2.19", "3.0.23", "3.11.10", "4.0.0"]
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

An update to 4.0.0... since I was already here :)


TOX_DEPENDENCIES = """pytest
subprocess32 ~= 3.5
Expand All @@ -33,21 +33,26 @@ def runCassandraContainer(client, version):
return client.containers.run(name="adelphi", remove=True, detach=True, ports={9042: 9042}, image="cassandra:{}".format(version))


def writeToxIni(version):
def writeToxIni(version, keep_tmpdirs = False):
config = configparser.ConfigParser()
config["tox"] = { "envlist": "py2, py3" }
envs = {"CASSANDRA_VERSION": version}
if keep_tmpdirs:
print("Preserving temporary directories")
envs["KEEP_LOGS"] = True
envStr = "\n ".join(["{} = {}".format(k,v) for (k,v) in envs.items()])
config["testenv"] = {"deps": TOX_DEPENDENCIES, \
"commands": "pytest {posargs}", \
"setenv": "CASSANDRA_VERSION = {}".format(version)}
"setenv": envStr}
with open(TOX_CONFIG, 'w') as configfile:
config.write(configfile)

@click.command()
@click.option('--cassandra', '-c', multiple=True, type=str)
@click.option('--python', '-p', multiple=True, type=click.Choice(["py2","py3"], case_sensitive = False))
@click.option("--pytest", "-t", type=str, help="Arguments to be passed to pytest")
def runtests(cassandra, python, pytest):
@click.option('--keep-tmpdirs', help="Preserve temporary directories", is_flag=True)
def runtests(cassandra, python, pytest, keep_tmpdirs):
client = docker.from_env()
tox_args = ["-e {}".format(py) for py in python] if python else []
if pytest:
Expand All @@ -68,7 +73,7 @@ def runtests(cassandra, python, pytest):
try:
if os.path.exists(TOX_CONFIG):
os.remove(TOX_CONFIG)
writeToxIni(version)
writeToxIni(version, keep_tmpdirs)

# cmdline() will raise SystemExit when it's done so trap that here to avoid
# exiting all the things
Expand Down
23 changes: 11 additions & 12 deletions python/adelphi/tests/integration/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,17 @@ def makeTempDirs(self):
outputDir = os.path.join(base, "outputDir")
os.mkdir(outputDir)
self.dirs = TempDirs(base, outputDir)
self.addCleanup(self.cleanUpTempDirs)


def cleanUpTempDirs(self):
# TODO: Note that there's no easy way to access this from test-adelphi unless we modify the
# ini generation code... and I'm not completely sure that's worth it. Might want to think
# about just deleting this outright... or making it a CLI option that can be easily accessed.
if "KEEP_LOGS" in os.environ:
log.info("KEEP_LOGS env var set, preserving logs/output at {}".format(self.dirs.basePath))
else:
shutil.rmtree(self.dirs.basePath)
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Switched to cleanup functions rather than an explicit tearDown() when trying to diagnose a different problem but in the end I decided to keep the change. tearDown() does have some odd semantics; it's not called if setUp() doesn't succeed, for instance. Rather than worry about it in the future it seemed saner to just convert to cleanup methods directly.



def setUp(self):
Expand All @@ -73,15 +84,3 @@ def setUp(self):
log.info("Testing Cassandra version {}".format(self.version))

self.makeTempDirs()


def tearDown(self):
super(SchemaTestCase, self).tearDown()

# TODO: Note that there's no easy way to access this from test-adelphi unless we modify the
# ini generation code... and I'm not completely sure that's worth it. Might want to think
# about just deleting this outright... or making it a CLI option that can be easily accessed.
if "KEEP_LOGS" in os.environ:
log.info("KEEP_LOGS env var set, preserving logs/output at {}".format(self.dirs.basePath))
else:
shutil.rmtree(self.dirs.basePath)
106 changes: 62 additions & 44 deletions python/adelphi/tests/integration/test_cql.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import difflib
import glob
import logging
import os
import re
import shutil
import sys

Expand All @@ -15,25 +17,36 @@
import subprocess

from tests.integration import SchemaTestCase, setupSchema, getAllKeyspaces, dropNewKeyspaces
from tests.util.schemadiff import cqlDigestGenerator
from tests.util.schema_util import get_schema

log = logging.getLogger('adelphi')

CQL_REFERENCE_SCHEMA_PATH = "tests/integration/resources/cql-schemas/{}.cql"
CQL_REFERENCE_KS0_SCHEMA_PATH = "tests/integration/resources/cql-schemas/{}-ks0.cql"

def digestSet(schemaFile):
rv = set()
for (_, digest) in cqlDigestGenerator(schemaFile):
rv.add(digest)
return rv
KEYSPACE_LINE_REGEX = re.compile(r'\s*CREATE KEYSPACE IF NOT EXISTS (\w+) ')

def linesWithNewline(fpath):
if not os.path.exists(fpath):
print("File {} does not exist".format(fpath))
if os.path.getsize(fpath) <= 0:
print("File {} is empty".format(fpath))
print("Reading lines for file {}".format(fpath))
with open(fpath) as f:
rv = f.readlines()
lastLine = rv[-1]
if not lastLine.endswith("\n"):
rv[-1] = lastLine + "\n"
return rv

def logCqlDigest(schemaFile, digestSet):
for (cql, digest) in cqlDigestGenerator(schemaFile):
if digest in digestSet:
log.info("Digest: {}, CQL: {}".format(digest,cql))

def extractKeyspaceName(schemaPath):
with open(schemaPath) as schemaFile:
for line in schemaFile:
matcher = KEYSPACE_LINE_REGEX.match(line)
if matcher:
return matcher.group(1)
return None


class TestCql(SchemaTestCase):
Expand All @@ -44,11 +57,7 @@ def setUp(self):
self.origKeyspaces = getAllKeyspaces()
log.info("Creating schema")
setupSchema(self.buildSchema())


def tearDown(self):
super(TestCql, self).tearDown()
dropNewKeyspaces(self.origKeyspaces)
self.addCleanup(dropNewKeyspaces, self.origKeyspaces)


# ========================== Helper functions ==========================
Expand All @@ -64,19 +73,43 @@ def buildSchema(self):


def compareToReferenceCql(self, referencePath, comparePath):
referenceSet = digestSet(referencePath)
compareSet = digestSet(comparePath)
compareLines = linesWithNewline(comparePath)
referenceLines = linesWithNewline(referencePath)

diffGen = difflib.unified_diff(
compareLines,
referenceLines,
fromfile=os.path.abspath(comparePath),
tofile=os.path.abspath(referencePath))

diffEmpty = True
for line in diffGen:
if diffEmpty:
print("Diff of generated file ({}) against reference file ({})".format(
os.path.basename(comparePath),
os.path.basename(referencePath)))
diffEmpty = False
print(line.strip())

refOnlySet = referenceSet - compareSet
if len(refOnlySet) > 0:
log.info("Statements in reference file {} but not in compare file {}:".format(referencePath, comparePath))
logCqlDigest(referencePath, refOnlySet)
compareOnlySet = compareSet - referenceSet
if len(compareOnlySet) > 0:
log.info("Statements in compare file {} but not in reference file {}:".format(comparePath, referencePath))
logCqlDigest(comparePath, compareOnlySet)
if not diffEmpty:
self.fail()

self.assertEqual(referenceSet, compareSet)

def combineSchemas(self):
outputDirPath = self.outputDirPath(self.version)
allOutputFileName = "{}-all".format(self.version)
allOutputPath = self.outputDirPath(allOutputFileName)

schemaPaths = glob.glob("{}/*/schema".format(outputDirPath))
self.assertGreater(len(schemaPaths), 0)
schemas = { extractKeyspaceName(p) : p for p in schemaPaths}
sortedKeyspaces = sorted(schemas.keys())

with open(allOutputPath, "w+") as allOutputFile:
cqlStr = "\n\n".join(open(schemas[ks]).read() for ks in sortedKeyspaces)
allOutputFile.write(cqlStr)

return allOutputPath
Copy link
Collaborator Author

@absurdfarce absurdfarce Aug 18, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Another problem observed in testing: the ordering of individual keyspace schemas returned by the glob above isn't deterministic so there's no guarantee we'll get the right ordering of keyspaces when we reconstruct the "all" schema containing everything. To fix this we extract the keyspace name from each individual keyspace schema and traverse them in order when generating the "all" schema. Note that this assumes deterministic output of keyspace names as described above.



# ========================== Test functions ==========================
Expand All @@ -87,7 +120,7 @@ def test_stdout(self):

self.compareToReferenceCql(
CQL_REFERENCE_SCHEMA_PATH.format(self.version),
self.stdoutPath(self.version))
stdoutPath)


def test_outputdir(self):
Expand All @@ -96,24 +129,9 @@ def test_outputdir(self):
os.mkdir(outputDirPath)
subprocess.run("adelphi --output-dir={} export-cql --no-metadata 2>> {}".format(outputDirPath, stderrPath), shell=True)

# Basic idea here is to find all schemas written to the output dir and aggregate them into a single schema
# file. We then compare this aggregated file to the reference schema. Ordering is important here but
# the current keyspace names hash to something that causes individual keyspaces to be discovered in the
# correct order.
outputDirPath = self.outputDirPath(self.version)
allOutputFileName = "{}-all".format(self.version)
allOutputPath = self.outputDirPath(allOutputFileName)

outputSchemas = glob.glob("{}/*/schema".format(outputDirPath))
self.assertGreater(len(outputSchemas), 0)
with open(allOutputPath, "w+") as allOutputFile:
for outputSchema in outputSchemas:
with open(outputSchema) as outputSchemaFile:
shutil.copyfileobj(outputSchemaFile, allOutputFile)
allOutputFile.write("\n")
self.compareToReferenceCql(
CQL_REFERENCE_SCHEMA_PATH.format(self.version),
allOutputPath)
self.combineSchemas())


def test_some_keyspaces_stdout(self):
Expand All @@ -123,7 +141,7 @@ def test_some_keyspaces_stdout(self):

self.compareToReferenceCql(
CQL_REFERENCE_KS0_SCHEMA_PATH.format(self.version),
self.stdoutPath(self.version))
stdoutPath)


def test_some_keyspaces_outputdir(self):
Expand Down
27 changes: 12 additions & 15 deletions python/adelphi/tests/util/schema_util.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
from functools import partial

from cassandra.metadata import Metadata,\
KeyspaceMetadata,\
TableMetadata,\
ColumnMetadata,\
IndexMetadata,\
SimpleStrategy, \
UserType

# types compatible with C* 2.1+
Expand Down Expand Up @@ -92,21 +93,17 @@ def get_keyspace(name, durable_writes, strategy_class, strategy_options, sasi=Tr
return keyspace

def get_schema(sasi=True):
# build a couple of keyspaces
keyspaces = []
for k in range(2):
keyspace = get_keyspace("my_ks_%s" % k, True, "SimpleStrategy", {"replication_factor": 1}, sasi=sasi)
keyspaces.append(keyspace)

schema = Metadata()
schema.keyspaces = keyspaces
buildKs = partial(
get_keyspace,
durable_writes=True,
strategy_class="SimpleStrategy",
strategy_options={"replication_factor": 1},
sasi=sasi)
schema.keyspaces = [buildKs("my_ks_%s" % k) for k in range(2)]
return schema

if __name__ == "__main__":
"""
Use this to print the test schema.
The output can be used in the integration tests too.
"""
# As discussed elsewhere SASI support is disabled until https://github.com/datastax/adelphi/issues/105
# is completed
print("\n\n".join(ks.export_as_string() for ks in get_schema(sasi=False).keyspaces))
# As discussed elsewhere SASI support is disabled until https://github.com/datastax/adelphi/issues/105
# is completed
print("\n\n".join(ks.export_as_string() for ks in get_schema(sasi=False).keyspaces))
28 changes: 0 additions & 28 deletions python/adelphi/tests/util/schemadiff.py

This file was deleted.