Merge pull request #2 from aismail/285-base-test-class

285 base test class
pull/275/head
Andrei-Adnan Ismail 11 years ago
commit b9ea0d92c7

@ -0,0 +1,4 @@
nose==1.3.6
docker-compose==1.1.0
requests==2.4.3
MySQL-python==1.2.5

@ -0,0 +1,16 @@
proxysql:
build: ../base/proxysql
links:
- backend1hostgroup0
ports:
- "6032:6032"
- "6033:6033"
backend1hostgroup0:
build: ./mysql
environment:
MYSQL_ROOT_PASSWORD: root
expose:
- "3306"
ports:
- "13306:3306"

@ -0,0 +1,7 @@
# We are creating a custom Dockerfile for MySQL as there is no easy way to
# move a file from host into the container. In our case, it's schema.sql
# There is a proposed improvement to "docker cp" but it's still being
# discussed (https://github.com/docker/docker/issues/5846).
FROM mysql:latest
ADD ./schema.sql /tmp/
ADD ./import_schema.sh /tmp/

@ -0,0 +1 @@
cat /tmp/schema.sql | mysql -h 127.0.0.1 -u root -proot

@ -0,0 +1,12 @@
DROP DATABASE IF EXISTS test;
CREATE DATABASE test;
USE test;
CREATE TABLE strings(value LONGTEXT);
INSERT INTO strings(value) VALUES('a');
INSERT INTO strings(value) VALUES('ab');
INSERT INTO strings(value) VALUES('abc');
INSERT INTO strings(value) VALUES('abcd');

@ -11,7 +11,10 @@ RUN apt-get update && apt-get install -y\
libssl-dev\
libmysqlclient-dev
RUN cd /opt; git clone https://github.com/sysown/proxysql-0.2.git
RUN cd /opt/proxysql-0.2; make clean && make
RUN cd /opt; git clone https://github.com/sysown/proxysql-0.2.git proxysql
RUN cd /opt/proxysql; make clean && make
RUN mkdir -p /var/run/proxysql
RUN cp /opt/proxysql-0.2/proxysql.cfg /etc
ADD ./proxysql.cnf /etc/
WORKDIR /opt/proxysql/src
CMD ["/opt/proxysql/src/proxysql", "--initial"]

@ -0,0 +1,18 @@
datadir="/tmp"
admin_variables =
{
admin_credentials="admin:admin"
mysql_ifaces="0.0.0.0:6032"
refresh_interval=2000
debug=true
}
mysql_users =
(
{
username = "root"
password = "root"
default_hostgroup = 0
}
)

@ -0,0 +1,46 @@
# How are the tests built?
First off, a few words about how the infrastructure of the tests looks like.
Tests are written in Python, and the services needed for running a test
(a ProxySQL instance and one or more MySQL instances) are specified in a
docker-compose.yml file and are started by using Docker's docker-compose.
Tests are ran using nosetests (https://nose.readthedocs.org/en/latest/),
Python's de facto leader in terms of how tests are written and ran. The command
to run the tests is, from the root of the repository:
```python
nosetests --nocapture
```
The "--nocapture" flag is present in order to have detailed output on what is
going on. Otherwise, the output will be suppressed by nosetests to give you only
a high-level report of how many tests passed and failed.
# Where can I find the tests?
Tests are grouped in scenarios. A __scenario__ specifies a configuration of
ProxySQL and MySQL backends, together with initial data to populate the MySQL
instances (a text file containing SQL queries).
The folder "scenarios" found in the root folder of the repository contains
these scenarios. There is a "base" folder with common utilities, and then there
is one folder for each scenario. For example, "1backend" is the name for the
scenario of 1 ProxySQL proxy, and 1 MySQL backend.
To create such a scenario, the simplest way to go about, is to copy-paste the
"1backend" scenario and modify it. Some of the important things to modify:
- docker-compose.yml. This is where the list of services is described, and
where you actuall specify how many MySQL backends there are, and which ports
they expose and how. Be careful, there is a naming convention
- mysql/schema.sql. This is where the MySQL backends get populated
# How do I write a test?
It's pretty simple. Once you have a working scenario, you write a class in
the top-level "test" folder, which inherits from ProxySQLBaseTest. One such
example is one_backend_test.py. The only thing which you should specify is
the docker-compose filename, and then start querying both the proxy and the
MySQL backends and testing assertions by using the `run_query_proxysql` and
`run_query_mysql' class methods.

@ -0,0 +1,13 @@
import MySQLdb
from proxysql_base_test import ProxySQLBaseTest
class OneBackendTest(ProxySQLBaseTest):
DOCKER_COMPOSE_FILE = "./scenarios/1backend"
def test_select_strings_returns_correct_result(self):
rows = self.run_query_proxysql("SELECT * FROM strings", "test")
self.assertEqual(set([row[0] for row in rows]),
set(['a', 'ab', 'abc', 'abcd']))

@ -0,0 +1,270 @@
import random
import re
import subprocess
import time
from unittest import TestCase
from docker import Client
from docker.utils import kwargs_from_env
import MySQLdb
class ProxySQLBaseTest(TestCase):
DOCKER_COMPOSE_FILE = None
PROXYSQL_ADMIN_PORT = 6032
PROXYSQL_ADMIN_USERNAME = "admin"
PROXYSQL_ADMIN_PASSWORD = "admin"
PROXYSQL_RW_PORT = 6033
PROXYSQL_RW_USERNAME = "root"
PROXYSQL_RW_PASSWORD = "root"
@classmethod
def _startup_docker_services(cls):
"""Start up all the docker services necessary to start this test.
They are specified in the docker compose file specified in the variable
cls.DOCKER_COMPOSE_FILE.
"""
# We have to perform docker-compose build + docker-compose up,
# instead of just doing the latter because of a bug which will give a
# 500 internal error for the Docker bug. When this is fixed, we should
# remove this first extra step.
subprocess.call(["docker-compose", "build"], cwd=cls.DOCKER_COMPOSE_FILE)
subprocess.call(["docker-compose", "up", "-d"], cwd=cls.DOCKER_COMPOSE_FILE)
@classmethod
def _shutdown_docker_services(cls):
"""Shut down all the docker services necessary to start this test.
They are specified in the docker compose file specified in the variable
cls.DOCKER_COMPOSE_FILE.
"""
subprocess.call(["docker-compose", "stop"], cwd=cls.DOCKER_COMPOSE_FILE)
subprocess.call(["docker-compose", "rm", "--force"], cwd=cls.DOCKER_COMPOSE_FILE)
@classmethod
def _get_proxysql_container(cls):
"""Out of all the started docker containers, select the one which
represents the proxy instance.
Note that this only supports one proxy instance for now. This method
relies on interogating the Docker daemon via its REST API.
"""
containers = Client(**kwargs_from_env()).containers()
for container in containers:
if 'proxysql' in container['Image']:
return container
@classmethod
def _get_mysql_containers(cls):
"""Out of all the started docker containers, select the ones which
represent the MySQL backend instances.
This method relies on interogating the Docker daemon via its REST API.
"""
result = []
containers = Client(**kwargs_from_env()).containers()
for container in containers:
if 'proxysql' not in container['Image']:
result.append(container)
return result
@classmethod
def _populate_mysql_containers_with_dump(cls):
"""Populates the started MySQL backend containers with the specified
SQL dump file.
The reason for doing this __after__ the containers are started is
because we want to keep them as generic as possible.
"""
mysql_containers = cls._get_mysql_containers()
# We have already added the SQL dump to the container by using
# the ADD mysql command in the Dockerfile for mysql -- check it
# out. The standard agreed location is at /tmp/schema.sql.
#
# Unfortunately we can't do this step at runtime due to limitations
# on how transfer between host and container is supposed to work by
# design. See the Dockerfile for MySQL for more details.
for mysql_container in mysql_containers:
container_id = mysql_container['Names'][0][1:]
subprocess.call(["docker", "exec", container_id, "bash", "/tmp/import_schema.sh"])
@classmethod
def _extract_hostgroup_from_container_name(cls, container_name):
"""MySQL backend containers are named using a naming convention:
backendXhostgroupY, where X and Y can be multi-digit numbers.
This extracts the value of the hostgroup from the container name.
I made this choice because I wasn't able to find another easy way to
associate arbitrary metadata with a Docker container through the
docker compose file.
"""
service_name = container_name.split('_')[1]
return int(re.search(r'BACKEND(\d+)HOSTGROUP(\d+)', service_name).group(2))
@classmethod
def _extract_port_number_from_uri(cls, uri):
"""Given a Docker container URI (exposed as an environment variable by
the host linking mechanism), extract the TCP port number from it."""
return int(uri.split(':')[2])
@classmethod
def _get_environment_variables_from_container(cls, container_name):
"""Retrieve the environment variables from the given container.
This is useful because the host linking mechanism will expose
connectivity information to the linked hosts by the use of environment
variables.
"""
output = Client(**kwargs_from_env()).execute(container_name, 'env')
result = {}
lines = output.split('\n')
for line in lines:
line = line.strip()
if len(line) == 0:
continue
(k, v) = line.split('=')
result[k] = v
return result
@classmethod
def _populate_proxy_configuration_with_backends(cls):
"""Populate ProxySQL's admin information with the MySQL backends
and their associated hostgroups.
This is needed because I do not want to hardcode this into the ProxySQL
config file of the test scenario, as it leaves more room for quick
iteration.
In order to configure ProxySQL with the correct backends, we are using
the MySQL admin interface of ProxySQL, and inserting rows into the
`mysql_servers` table, which contains a list of which servers go into
which hostgroup.
"""
proxysql_container = cls._get_proxysql_container()
mysql_containers = cls._get_mysql_containers()
environment_variables = cls._get_environment_variables_from_container(
proxysql_container['Names'][0][1:])
proxy_admin_connection = MySQLdb.connect("127.0.0.1",
cls.PROXYSQL_ADMIN_USERNAME,
cls.PROXYSQL_ADMIN_PASSWORD,
port=cls.PROXYSQL_ADMIN_PORT)
cursor = proxy_admin_connection.cursor()
for mysql_container in mysql_containers:
container_name = mysql_container['Names'][0][1:].upper()
port_uri = environment_variables['%s_PORT' % container_name]
port_no = cls._extract_port_number_from_uri(port_uri)
ip = environment_variables['%s_PORT_%d_TCP_ADDR' % (container_name, port_no)]
hostgroup = cls._extract_hostgroup_from_container_name(container_name)
cursor.execute("INSERT INTO mysql_servers(hostgroup_id, hostname, port, status) "
"VALUES(%d, '%s', %d, 'ONLINE')" %
(hostgroup, ip, port_no))
cursor.execute("LOAD MYSQL SERVERS TO RUNTIME")
cursor.close()
proxy_admin_connection.close()
@classmethod
def setUpClass(cls):
# Always shutdown docker services because the previous test might have
# left them in limbo.
cls._shutdown_docker_services()
cls._startup_docker_services()
# Sleep for 30 seconds because we want to populate the MySQL containers
# with SQL dumps, but there is a race condition because we do not know
# when the MySQL daemons inside them have actually started or not.
# TODO(andrei): find a better solution
time.sleep(30)
cls._populate_mysql_containers_with_dump()
cls._populate_proxy_configuration_with_backends()
@classmethod
def tearDownClass(cls):
cls._shutdown_docker_services()
def run_query_proxysql(self, query, db, return_result=True):
"""Run a query against the ProxySQL proxy and optionally return its
results as a set of rows."""
proxy_connection = MySQLdb.connect("127.0.0.1",
ProxySQLBaseTest.PROXYSQL_RW_USERNAME,
ProxySQLBaseTest.PROXYSQL_RW_PASSWORD,
port=ProxySQLBaseTest.PROXYSQL_RW_PORT,
db=db)
cursor = proxy_connection.cursor()
cursor.execute(query)
if return_result:
rows = cursor.fetchall()
cursor.close()
proxy_connection.close()
if return_result:
return rows
def run_query_mysql(self, query, db, return_result=True, hostgroup=0):
"""Run a query against the MySQL backend and optionally return its
results as a set of rows.
IMPORTANT: since the queries are actually ran against the MySQL backend,
that backend needs to expose its MySQL port to the outside through
docker compose's port mapping mechanism.
This will actually parse the docker-compose configuration file to
retrieve the available backends and hostgroups and will pick a backend
from the specified hostgroup."""
# Figure out which are the containers for the specified hostgroup
mysql_backends = ProxySQLBaseTest._get_mysql_containers()
mysql_backends_in_hostgroup = []
for backend in mysql_backends:
container_name = backend['Names'][0][1:].upper()
backend_hostgroup = ProxySQLBaseTest._extract_hostgroup_from_container_name(container_name)
mysql_port_exposed=False
if not backend.get('Ports'):
continue
for exposed_port in backend.get('Ports', []):
if exposed_port['PrivatePort'] == 3306:
mysql_port_exposed = True
if backend_hostgroup == hostgroup and mysql_port_exposed:
mysql_backends_in_hostgroup.append(backend)
if len(mysql_backends_in_hostgroup) == 0:
raise Exception('No backends with a publicly exposed port were '
'found in hostgroup %d' % hostgroup)
# Pick a random container, extract its connection details
container = random.choice(mysql_backends_in_hostgroup)
for exposed_port in container.get('Ports', []):
if exposed_port['PrivatePort'] == 3306:
mysql_port = exposed_port['PublicPort']
mysql_connection = MySQLdb.connect("127.0.0.1",
# Warning: this assumes that ProxySQL
# and all the backends have the same
# credentials.
# TODO(andrei): revisit this assumption
# in authentication tests.
ProxySQLBaseTest.PROXYSQL_RW_USERNAME,
ProxySQLBaseTest.PROXYSQL_RW_PASSWORD,
port=mysql_port,
db=db)
cursor = mysql_connection.cursor()
cursor.execute(query)
if return_result:
rows = cursor.fetchall()
cursor.close()
mysql_connection.close()
if return_result:
return rows
Loading…
Cancel
Save