diff options
-rw-r--r-- | doc/source/drivers.rst | 19 | ||||
-rw-r--r-- | doc/source/tutorial/index.rst | 1 | ||||
-rw-r--r-- | doc/source/tutorial/lock.rst | 14 | ||||
-rw-r--r-- | examples/coordinator.py | 2 | ||||
-rw-r--r-- | examples/coordinator_heartbeat.py | 2 | ||||
-rw-r--r-- | examples/group_membership.py | 2 | ||||
-rw-r--r-- | examples/group_membership_watch.py | 2 | ||||
-rw-r--r-- | examples/leader_election.py | 2 | ||||
-rw-r--r-- | examples/lock.py | 11 | ||||
-rwxr-xr-x | setup-mysql-env.sh | 37 | ||||
-rw-r--r-- | setup.cfg | 1 | ||||
-rw-r--r-- | test-requirements.txt | 2 | ||||
-rw-r--r-- | tooz/drivers/memcached.py | 3 | ||||
-rw-r--r-- | tooz/drivers/mysql.py | 126 | ||||
-rw-r--r-- | tooz/drivers/pgsql.py | 83 | ||||
-rw-r--r-- | tooz/tests/test_coordination.py | 22 | ||||
-rw-r--r-- | tooz/tests/test_postgresql.py | 114 | ||||
-rw-r--r-- | tox.ini | 35 |
18 files changed, 429 insertions, 49 deletions
diff --git a/doc/source/drivers.rst b/doc/source/drivers.rst index 4f73f61..4c6a560 100644 --- a/doc/source/drivers.rst +++ b/doc/source/drivers.rst @@ -10,17 +10,30 @@ API, some of them have different properties: features as it's possible to build a cluster of ZooKeeper that is resilient towards network partitions for example. -* `memcached`_ is a basic implementation and provides less resiliency, though - it's much simpler to setup. A lot of the features provided in tooz are - based on timeout (heartbeats, locks, etc) so are less resilient than other +* `memcached`_ is a basic implementation and provides little resiliency, though + it's much simpler to setup. A lot of the features provided in tooz are based + on timeout (heartbeats, locks, etc) so are less resilient than other backends. +* `redis`_ is a basic implementation and provides little resiliency. + A lot of the features provided in tooz are based on timeout (heartbeats, + locks, etc) so are less resilient than other backends. + * `ipc` is based on Posix IPC and only implements a lock mechanism for now. The lock can only be distributed locally to a computer processes. * `zake`_ is a driver using a fake implementation of ZooKeeper and can be used to use Tooz in your unit tests suite for example. +* `postgresql`_ is a driver providing only distributed lock (for now) + and based on the PostgreSQL database server. + +* `mysql`_ is a driver providing only distributed lock (for now) + and based on the MySQL database server. + .. _zookeeper: http://zookeeper.apache.org/ .. _memcached: http://memcached.org/ .. _zake: https://pypi.python.org/pypi/zake +.. _redis: http://redis.io +.. _postgresql: http://postgresql.org +.. _mysql: http://mysql.org diff --git a/doc/source/tutorial/index.rst b/doc/source/tutorial/index.rst index e83c0fc..8cb31a3 100644 --- a/doc/source/tutorial/index.rst +++ b/doc/source/tutorial/index.rst @@ -11,3 +11,4 @@ use tooz in your application. coordinator group_membership leader_election + lock diff --git a/doc/source/tutorial/lock.rst b/doc/source/tutorial/lock.rst new file mode 100644 index 0000000..91c3262 --- /dev/null +++ b/doc/source/tutorial/lock.rst @@ -0,0 +1,14 @@ +====== + Lock +====== + +Tooz provides distributed locks. A lock is identified by a name, and a lock can +only be acquired by one coordinator at a time. + +.. literalinclude:: ../../../examples/lock.py + :language: python + +The method :meth:`tooz.coordination.CoordinationDriver.get_lock` allows +to create a lock identified by a name. Once the you retrieve this lock, you can +use it as a context manager or use the :meth:`tooz.locking.Lock.acquire` and +:meth:`tooz.locking.Lock.release` methods to acquire and release the lock. diff --git a/examples/coordinator.py b/examples/coordinator.py index 6d5cf03..f0efef4 100644 --- a/examples/coordinator.py +++ b/examples/coordinator.py @@ -1,5 +1,5 @@ from tooz import coordination -coordinator = coordination.get_coordinator('kazoo://localhost', b'host-1') +coordinator = coordination.get_coordinator('zake://', b'host-1') coordinator.start() coordinator.stop() diff --git a/examples/coordinator_heartbeat.py b/examples/coordinator_heartbeat.py index 8b0659f..ddbea13 100644 --- a/examples/coordinator_heartbeat.py +++ b/examples/coordinator_heartbeat.py @@ -4,7 +4,7 @@ from tooz import coordination ALIVE_TIME = 5 -coordinator = coordination.get_coordinator('memcached://localhost', b'host-1') +coordinator = coordination.get_coordinator('zake://', b'host-1') coordinator.start() start = time.time() diff --git a/examples/group_membership.py b/examples/group_membership.py index 58adb2d..7b85e6c 100644 --- a/examples/group_membership.py +++ b/examples/group_membership.py @@ -4,7 +4,7 @@ import six from tooz import coordination -coordinator = coordination.get_coordinator('kazoo://localhost', b'host-1') +coordinator = coordination.get_coordinator('zake://', b'host-1') coordinator.start() # Create a group diff --git a/examples/group_membership_watch.py b/examples/group_membership_watch.py index 28b786f..51836c2 100644 --- a/examples/group_membership_watch.py +++ b/examples/group_membership_watch.py @@ -4,7 +4,7 @@ import six from tooz import coordination -coordinator = coordination.get_coordinator('kazoo://localhost', b'host-1') +coordinator = coordination.get_coordinator('zake://', b'host-1') coordinator.start() # Create a group diff --git a/examples/leader_election.py b/examples/leader_election.py index b27129e..661c314 100644 --- a/examples/leader_election.py +++ b/examples/leader_election.py @@ -6,7 +6,7 @@ import six from tooz import coordination ALIVE_TIME = 1 -coordinator = coordination.get_coordinator('kazoo://localhost', b'host-1') +coordinator = coordination.get_coordinator('zake://', b'host-1') coordinator.start() # Create a group diff --git a/examples/lock.py b/examples/lock.py new file mode 100644 index 0000000..42a1d03 --- /dev/null +++ b/examples/lock.py @@ -0,0 +1,11 @@ +from tooz import coordination + +coordinator = coordination.get_coordinator('zake://', b'host-1') +coordinator.start() + +# Create a lock +lock = coordinator.get_lock("foobar") +with lock: + print("Do something that is distributed") + +coordinator.stop() diff --git a/setup-mysql-env.sh b/setup-mysql-env.sh new file mode 100755 index 0000000..2a8b9bf --- /dev/null +++ b/setup-mysql-env.sh @@ -0,0 +1,37 @@ +#!/bin/bash +set -x -e + +. functions.sh + +clean_exit () { + local error_code="$?" + kill $(jobs -p) + rm -rf ${MYSQL_DATA} + return $error_code +} + +wait_for_line () { + while read line + do + echo "$line" | grep -q "$1" && break + done < "$2" + # Read the fifo for ever otherwise process would block + cat "$2" >/dev/null & +} + +trap "clean_exit" EXIT + +# On systems like Fedora here's where mysqld can be found +export PATH=$PATH:/usr/libexec + +# Start MySQL process for tests +MYSQL_DATA=`mktemp -d /tmp/tooz-mysql-XXXXX` +mkfifo ${MYSQL_DATA}/out +mysqld --datadir=${MYSQL_DATA} --pid-file=${MYSQL_DATA}/mysql.pid --socket=${MYSQL_DATA}/mysql.socket --skip-networking --skip-grant-tables &> ${MYSQL_DATA}/out & +# Wait for MySQL to start listening to connections +wait_for_line "mysqld: ready for connections." ${MYSQL_DATA}/out +mysql -S ${MYSQL_DATA}/mysql.socket -e 'CREATE DATABASE test;' +export TOOZ_TEST_MYSQL_URL="mysql://root@localhost/test?unix_socket=${MYSQL_DATA}/mysql.socket" + +# Yield execution to venv command +$* @@ -31,6 +31,7 @@ tooz.backends = ipc = tooz.drivers.ipc:IPCDriver redis = tooz.drivers.redis:RedisDriver postgresql = tooz.drivers.pgsql:PostgresDriver + mysql = tooz.drivers.mysql:MySQLDriver [build_sphinx] all_files = 1 diff --git a/test-requirements.txt b/test-requirements.txt index 97c53f6..83873b4 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -2,6 +2,7 @@ pep8>=1.4.5 pyflakes>=0.7.2,<0.7.4 flake8>=2.1.0 discover +mock>=1.0 # only needed on < python 3.3 sphinx>=1.1.2,<1.2 python-subunit testrepository>=0.0.17 @@ -10,3 +11,4 @@ testscenarios>=0.4 coverage>=3.6 sysv_ipc>=0.6.8 psycopg2 +pymysql diff --git a/tooz/drivers/memcached.py b/tooz/drivers/memcached.py index 5dde0c4..2c9e2f2 100644 --- a/tooz/drivers/memcached.py +++ b/tooz/drivers/memcached.py @@ -325,8 +325,7 @@ class MemcachedDriver(coordination.CoordinationDriver): def run_watchers(self): result = [] for group_id in self.client.get(self._GROUP_LIST_KEY): - encoded_group = self._encode_group_id(group_id) - group_members = set(self.client.get(encoded_group)) + group_members = set(self._get_members(group_id)) old_group_members = self._group_members[group_id] for member_id in (group_members - old_group_members): diff --git a/tooz/drivers/mysql.py b/tooz/drivers/mysql.py new file mode 100644 index 0000000..c517a0a --- /dev/null +++ b/tooz/drivers/mysql.py @@ -0,0 +1,126 @@ +# -*- coding: utf-8 -*- +# +# Copyright © 2014 eNovance +# +# Author: Julien Danjou <julien@danjou.info> +# +# Licensed 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 pymysql + +import tooz +from tooz import coordination +from tooz.drivers import _retry +from tooz import locking +from tooz import utils + + +class MySQLLock(locking.Lock): + """A MySQL based lock.""" + + def __init__(self, name, connection): + super(MySQLLock, self).__init__(name) + self._conn = connection + + def acquire(self, blocking=True): + if blocking is False: + try: + cur = self._conn.cursor() + cur.execute("SELECT GET_LOCK(%s, 0);", self.name) + # Can return NULL on error + if cur.fetchone()[0] is 1: + return True + return False + except pymysql.MySQLError as e: + raise coordination.ToozError(utils.exception_message(e)) + else: + def _acquire(): + try: + cur = self._conn.cursor() + cur.execute("SELECT GET_LOCK(%s, 0);", self.name) + if cur.fetchone()[0] is 1: + return True + except pymysql.MySQLError as e: + raise coordination.ToozError(utils.exception_message(e)) + raise _retry.Retry + kwargs = _retry.RETRYING_KWARGS.copy() + if blocking is not True: + kwargs['stop_max_delay'] = blocking + return _retry.Retrying(**kwargs).call(_acquire) + + def release(self): + try: + cur = self._conn.cursor() + cur.execute("SELECT RELEASE_LOCK(%s);", self.name) + return cur.fetchone()[0] + except pymysql.MySQLError as e: + raise coordination.ToozError(utils.exception_message(e)) + + +class MySQLDriver(coordination.CoordinationDriver): + + def __init__(self, member_id, parsed_url, options): + """Initialize the MySQL driver.""" + super(MySQLDriver, self).__init__() + self._host = parsed_url.netloc + self._port = parsed_url.port + self._dbname = parsed_url.path[1:] + self._username = parsed_url.username + self._password = parsed_url.password + self._unix_socket = options.get("unix_socket", [None])[-1] + + def _start(self): + try: + if self._unix_socket: + self._conn = pymysql.Connect(unix_socket=self._unix_socket, + port=self._port, + user=self._username, + passwd=self._password, + database=self._dbname) + else: + self._conn = pymysql.Connect(host=self._host, + port=self._port, + user=self._username, + passwd=self._password, + database=self._dbname) + except pymysql.err.OperationalError as e: + raise coordination.ToozConnectionError(utils.exception_message(e)) + + def _stop(self): + self._conn.close() + + def get_lock(self, name): + return MySQLLock(name, self._conn) + + @staticmethod + def watch_join_group(group_id, callback): + raise tooz.NotImplemented + + @staticmethod + def unwatch_join_group(group_id, callback): + raise tooz.NotImplemented + + @staticmethod + def watch_leave_group(group_id, callback): + raise tooz.NotImplemented + + @staticmethod + def unwatch_leave_group(group_id, callback): + raise tooz.NotImplemented + + @staticmethod + def watch_elected_as_leader(group_id, callback): + raise tooz.NotImplemented + + @staticmethod + def unwatch_elected_as_leader(group_id, callback): + raise tooz.NotImplemented diff --git a/tooz/drivers/pgsql.py b/tooz/drivers/pgsql.py index d142137..8494066 100644 --- a/tooz/drivers/pgsql.py +++ b/tooz/drivers/pgsql.py @@ -15,6 +15,8 @@ # 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 contextlib import hashlib import psycopg2 @@ -24,6 +26,64 @@ import tooz from tooz import coordination from tooz.drivers import _retry from tooz import locking +from tooz import utils + + +# See: psycopg/diagnostics_type.c for what kind of fields these +# objects may have (things like 'schema_name', 'internal_query' +# and so-on which are useful for figuring out what went wrong...) +_DIAGNOSTICS_ATTRS = tuple([ + 'column_name', + 'constraint_name', + 'context', + 'datatype_name', + 'internal_position', + 'internal_query', + 'message_detail', + 'message_hint', + 'message_primary', + 'schema_name', + 'severity', + 'source_file', + 'source_function', + 'source_line', + 'sqlstate', + 'statement_position', + 'table_name', +]) + + +def _format_exception(e): + lines = [ + "%s: %s" % (type(e).__name__, utils.exception_message(e).strip()), + ] + if hasattr(e, 'pgcode') and e.pgcode is not None: + lines.append("Error code: %s" % e.pgcode) + # The reason this hasattr check is done is that the 'diag' may not always + # be present, depending on how new of a psycopg is installed... so better + # to be safe than sorry... + if hasattr(e, 'diag') and e.diag is not None: + diagnostic_lines = [] + for attr_name in _DIAGNOSTICS_ATTRS: + if not hasattr(e.diag, attr_name): + continue + attr_value = getattr(e.diag, attr_name) + if attr_value is None: + continue + diagnostic_lines.append(" %s = %s" (attr_name, attr_value)) + if diagnostic_lines: + lines.append('Diagnostics:') + lines.extend(diagnostic_lines) + return "\n".join(lines) + + +@contextlib.contextmanager +def _translating_cursor(conn): + try: + with conn.cursor() as cur: + yield cur + except psycopg2.Error as e: + raise coordination.ToozError(_format_exception(e)) class PostgresLock(locking.Lock): @@ -41,16 +101,16 @@ class PostgresLock(locking.Lock): def acquire(self, blocking=True): if blocking is True: - with self._conn.cursor() as cur: + with _translating_cursor(self._conn) as cur: cur.execute("SELECT pg_advisory_lock(%s, %s);", self.key) - return True + return True elif blocking is False: - with self._conn.cursor() as cur: + with _translating_cursor(self._conn) as cur: cur.execute("SELECT pg_try_advisory_lock(%s, %s);", self.key) return cur.fetchone()[0] else: def _acquire(): - with self._conn.cursor() as cur: + with _translating_cursor(self._conn) as cur: cur.execute("SELECT pg_try_advisory_lock(%s, %s);", self.key) if cur.fetchone()[0] is True: @@ -61,7 +121,7 @@ class PostgresLock(locking.Lock): return _retry.Retrying(**kwargs).call(_acquire) def release(self): - with self._conn.cursor() as cur: + with _translating_cursor(self._conn) as cur: cur.execute("SELECT pg_advisory_unlock(%s, %s);", self.key) return cur.fetchone()[0] @@ -78,11 +138,14 @@ class PostgresDriver(coordination.CoordinationDriver): self._password = parsed_url.password def _start(self): - self._conn = psycopg2.connect(host=self._host, - port=self._port, - user=self._username, - password=self._password, - database=self._dbname) + try: + self._conn = psycopg2.connect(host=self._host, + port=self._port, + user=self._username, + password=self._password, + database=self._dbname) + except psycopg2.Error as e: + raise coordination.ToozConnectionError(_format_exception(e)) def _stop(self): self._conn.close() diff --git a/tooz/tests/test_coordination.py b/tooz/tests/test_coordination.py index e88047b..b76d578 100644 --- a/tooz/tests/test_coordination.py +++ b/tooz/tests/test_coordination.py @@ -29,12 +29,18 @@ class TestAPI(testscenarios.TestWithScenarios, tests.TestCaseSkipNotImplemented): scenarios = [ - ('zookeeper', {'url': 'kazoo://127.0.0.1:2181?timeout=5'}), + ('zookeeper', {'url': 'kazoo://127.0.0.1:2181?timeout=5', + 'bad_url': 'kazoo://localhost:1'}), ('zake', {'url': 'zake://?timeout=5'}), - ('memcached', {'url': 'memcached://?timeout=5'}), + ('memcached', {'url': 'memcached://?timeout=5', + 'bad_url': 'memcached://localhost:1'}), ('ipc', {'url': 'ipc://'}), - ('redis', {'url': 'redis://localhost:6379?timeout=5'}), - ('postgresql', {'url': os.getenv("TOOZ_TEST_PGSQL_URL")}), + ('redis', {'url': 'redis://localhost:6379?timeout=5', + 'bad_url': 'redis://localhost:1'}), + ('postgresql', {'url': os.getenv("TOOZ_TEST_PGSQL_URL"), + 'bad_url': 'postgresql://localhost:1'}), + ('mysql', {'url': os.getenv("TOOZ_TEST_MYSQL_URL"), + 'bad_url': 'mysql://localhost:1'}), ] # Only certain drivers have the tested support for timeouts that we test @@ -78,6 +84,14 @@ class TestAPI(testscenarios.TestWithScenarios, self._coord.stop() super(TestAPI, self).tearDown() + def test_connection_error(self): + if not hasattr(self, "bad_url"): + raise testcase.TestSkipped("No bad URL provided") + coord = tooz.coordination.get_coordinator(self.bad_url, + self.member_id) + self.assertRaises(tooz.coordination.ToozConnectionError, + coord.start) + def test_stop_first(self): c = tooz.coordination.get_coordinator(self.url, self.member_id) diff --git a/tooz/tests/test_postgresql.py b/tooz/tests/test_postgresql.py new file mode 100644 index 0000000..35c8d0e --- /dev/null +++ b/tooz/tests/test_postgresql.py @@ -0,0 +1,114 @@ +# -*- coding: utf-8 -*- + +# Copyright (C) 2014 Yahoo! Inc. All Rights Reserved. +# +# Licensed 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 uuid + +try: + # Added in python 3.3+ + from unittest import mock +except ImportError: + import mock + +import testtools +from testtools import testcase + +from tooz import coordination +from tooz import utils + +# Handle the case gracefully where the driver is not installed. +try: + import psycopg2 + PGSQL_AVAILABLE = True +except ImportError: + PGSQL_AVAILABLE = False + + +@testtools.skipUnless(PGSQL_AVAILABLE, 'psycopg2 is not available') +class TestPostgreSQLFailures(testcase.TestCase): + + # Not actually used (but required none the less), since we mock out + # the connect() method... + FAKE_URL = "postgresql://localhost:1" + + def _create_coordinator(self): + + def _safe_stop(coord): + try: + coord.stop() + except coordination.ToozError as e: + # TODO(harlowja): make this better, so that we don't have to + # do string checking... + message = utils.exception_message(e) + if (message != 'Can not stop a driver which has not' + ' been started'): + raise + + coord = coordination.get_coordinator(self.FAKE_URL, + str(uuid.uuid4()).encode('ascii')) + self.addCleanup(_safe_stop, coord) + return coord + + @mock.patch("tooz.drivers.pgsql.psycopg2.connect") + def test_connect_failure(self, psycopg2_connector): + psycopg2_connector.side_effect = psycopg2.Error("Broken") + c = self._create_coordinator() + self.assertRaises(coordination.ToozConnectionError, c.start) + + @mock.patch("tooz.drivers.pgsql.psycopg2.connect") + def test_connect_failure_operational(self, psycopg2_connector): + psycopg2_connector.side_effect = psycopg2.OperationalError("Broken") + c = self._create_coordinator() + self.assertRaises(coordination.ToozConnectionError, c.start) + + @mock.patch("tooz.drivers.pgsql.psycopg2.connect") + def test_failure_acquire_lock(self, psycopg2_connector): + execute_mock = mock.MagicMock() + execute_mock.execute.side_effect = psycopg2.OperationalError("Broken") + + cursor_mock = mock.MagicMock() + cursor_mock.__enter__ = mock.MagicMock(return_value=execute_mock) + cursor_mock.__exit__ = mock.MagicMock(return_value=False) + + conn_mock = mock.MagicMock() + conn_mock.cursor.return_value = cursor_mock + psycopg2_connector.return_value = conn_mock + + c = self._create_coordinator() + c.start() + test_lock = c.get_lock(b'test-lock') + self.assertRaises(coordination.ToozError, test_lock.acquire) + + @mock.patch("tooz.drivers.pgsql.psycopg2.connect") + def test_failure_release_lock(self, psycopg2_connector): + execute_mock = mock.MagicMock() + execute_mock.execute.side_effect = [ + True, + psycopg2.OperationalError("Broken"), + ] + + cursor_mock = mock.MagicMock() + cursor_mock.__enter__ = mock.MagicMock(return_value=execute_mock) + cursor_mock.__exit__ = mock.MagicMock(return_value=False) + + conn_mock = mock.MagicMock() + conn_mock.cursor.return_value = cursor_mock + psycopg2_connector.return_value = conn_mock + + c = self._create_coordinator() + c.start() + test_lock = c.get_lock(b'test-lock') + self.assertTrue(test_lock.acquire()) + self.assertRaises(coordination.ToozError, test_lock.release) @@ -1,7 +1,7 @@ [tox] minversion = 1.6 skipsdist = True -envlist = py26,py27,py33,py34,py27-zookeeper,py33-zookeeper,py34-zookeeper,py27-redis,py33-redis,py34-redis,py27-memcached,py33-memcached,py34-memcached,pep8 +envlist = py26,py27,py34,py27-zookeeper,py34-zookeeper,py27-redis,py34-redis,py27-memcached,py34-memcached,py27-postgresql,py34-postgresql,py27-mysql,py34-mysql,pep8 [testenv] deps = -r{toxinidir}/requirements.txt @@ -22,10 +22,6 @@ commands = python setup.py testr --slowest --testr-args="{posargs}" {toxinidir}/run-examples.sh doc8 doc/source -[testenv:py33] -deps = -r{toxinidir}/requirements-py3.txt - -r{toxinidir}/test-requirements.txt - [testenv:py34] deps = -r{toxinidir}/requirements-py3.txt -r{toxinidir}/test-requirements.txt @@ -33,11 +29,6 @@ deps = -r{toxinidir}/requirements-py3.txt [testenv:py27-zookeeper] commands = {toxinidir}/setup-zookeeper-env.sh python setup.py testr --slowest --testr-args="{posargs}" -[testenv:py33-zookeeper] -deps = {[testenv:py33]deps} -basepython = python3.3 -commands = {toxinidir}/setup-zookeeper-env.sh python setup.py testr --slowest --testr-args="{posargs}" - [testenv:py34-zookeeper] deps = {[testenv:py34]deps} basepython = python3.4 @@ -46,11 +37,6 @@ commands = {toxinidir}/setup-zookeeper-env.sh python setup.py testr --slowest -- [testenv:py27-redis] commands = {toxinidir}/setup-redis-env.sh python setup.py testr --slowest --testr-args="{posargs}" -[testenv:py33-redis] -deps = {[testenv:py33]deps} -basepython = python3.3 -commands = {toxinidir}/setup-redis-env.sh python setup.py testr --slowest --testr-args="{posargs}" - [testenv:py34-redis] deps = {[testenv:py34]deps} basepython = python3.4 @@ -59,11 +45,6 @@ commands = {toxinidir}/setup-redis-env.sh python setup.py testr --slowest --test [testenv:py27-memcached] commands = {toxinidir}/setup-memcached-env.sh python setup.py testr --slowest --testr-args="{posargs}" -[testenv:py33-memcached] -deps = {[testenv:py33]deps} -basepython = python3.3 -commands = {toxinidir}/setup-memcached-env.sh python setup.py testr --slowest --testr-args="{posargs}" - [testenv:py34-memcached] deps = {[testenv:py34]deps} basepython = python3.4 @@ -72,16 +53,19 @@ commands = {toxinidir}/setup-memcached-env.sh python setup.py testr --slowest -- [testenv:py27-postgresql] commands = {toxinidir}/setup-postgresql-env.sh python setup.py testr --slowest --testr-args="{posargs}" -[testenv:py33-postgresql] -deps = {[testenv:py33]deps} -basepython = python3.3 -commands = {toxinidir}/setup-postgresql-env.sh python setup.py testr --slowest --testr-args="{posargs}" - [testenv:py34-postgresql] deps = {[testenv:py34]deps} basepython = python3.4 commands = {toxinidir}/setup-postgresql-env.sh python setup.py testr --slowest --testr-args="{posargs}" +[testenv:py27-mysql] +commands = {toxinidir}/setup-mysql-env.sh python setup.py testr --slowest --testr-args="{posargs}" + +[testenv:py34-mysql] +deps = {[testenv:py34]deps} +basepython = python3.4 +commands = {toxinidir}/setup-mysql-env.sh python setup.py testr --slowest --testr-args="{posargs}" + [testenv:cover] commands = python setup.py testr --slowest --coverage --testr-args="{posargs}" @@ -103,3 +87,4 @@ show-source = True [hacking] import_exceptions = six.moves + unittest.mock |