Skip to content

Commit

Permalink
issue #62 - docker test improvements including phantomjs testing and …
Browse files Browse the repository at this point in the history
…run tests against an existing image
  • Loading branch information
jantman committed May 13, 2017
1 parent 10444c1 commit 3d9aa20
Show file tree
Hide file tree
Showing 3 changed files with 160 additions and 60 deletions.
5 changes: 5 additions & 0 deletions CHANGES.rst
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,11 @@ Unreleased Changes
------------------

* Improve ofxgetter/ofxupdater error handling; catch OFX files with error messages in them.
* `Issue #62 <https://github.com/jantman/biweeklybudget/issues/62>`_ - Fix phantomjs in Docker image.
* Allow docker image tests to run against an existing image, defined by ``DOCKER_TEST_TAG``.
* Retry MySQL DB creation during Docker tests until it succeeds, or fails 10 times.
* Add testing of PhantomJS in Docker image testing; check version and that it actually works (GET a page).
* More reliable stopping and removing of Docker containers during Docker image tests.

0.1.0 (2017-05-07)
------------------
Expand Down
213 changes: 154 additions & 59 deletions biweeklybudget/tests/docker_build.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@
Actions are largely driven by environment variables, and some information
about the version of biweeklybudget installed...
If ``DOCKER_TEST_TAG`` is set, just run tests against an existing (local) image
with that tag, and exit.
If ``TRAVIS=="true"``:
- build the image using the sdist created by ``tox``
Expand Down Expand Up @@ -66,6 +69,7 @@
from io import BytesIO
import tarfile
from biweeklybudget.version import VERSION
from textwrap import dedent

FORMAT = "[%(asctime)s %(levelname)s] %(message)s"
logging.basicConfig(level=logging.INFO, format=FORMAT)
Expand Down Expand Up @@ -122,6 +126,7 @@
class DockerImageBuilder(object):

image_name = 'jantman/biweeklybudget'
_phantomjs_version = '2.1.1'

def __init__(self, toxinidir, distdir):
"""
Expand Down Expand Up @@ -252,58 +257,64 @@ def test(self, tag):
:param tag: tag of built image
:type tag: str
"""
db_container = self._run_mysql()
dbname = db_container.name
kwargs = {
'detach': True,
'name': 'biweeklybudget-test-%s' % int(time.time()),
'environment': {
'DB_CONNSTRING': 'mysql+pymysql://'
'root:root@mysql:3306/'
'budgetfoo?charset=utf8mb4'
},
'links': {dbname: 'mysql'},
'ports': {
'80/tcp': None
}
}
img = '%s:%s' % (self.image_name, tag)
logger.info('Docker run %s with kwargs: %s', img, kwargs)
container = self._docker.containers.run(img, **kwargs)
logger.info('Running biweeklybudget container; name=%s id=%s',
container.name, container.id)
logger.info('Container status: %s', container.status)
logger.info('Sleeping 20s for stabilization...')
time.sleep(20)
container.reload()
logger.info('Container status: %s', container.status)
if container.status != 'running':
logger.critical('Container did not stay running! Logs:')
logger.critical(
container.logs(stderr=True, stdout=True, stream=False,
timestamps=True).decode()
)
raise RuntimeError('Container did not stay running')
else:
logger.info(
'Container logs:\n%s',
container.logs(stderr=True, stdout=True, stream=False,
timestamps=True).decode()
)
# do the tests
db_container = None
container = None
try:
self._run_tests(container)
db_container.stop()
db_container.remove()
container.stop()
container.remove()
except Exception as exc:
logger.critical("Tests failed: %s", exc, exc_info=True)
db_container.stop()
db_container.remove()
container.stop()
container.remove()
raise
db_container = self._run_mysql()
dbname = db_container.name
kwargs = {
'detach': True,
'name': 'biweeklybudget-test-%s' % int(time.time()),
'environment': {
'DB_CONNSTRING': 'mysql+pymysql://'
'root:root@mysql:3306/'
'budgetfoo?charset=utf8mb4'
},
'links': {dbname: 'mysql'},
'ports': {
'80/tcp': None
}
}
img = '%s:%s' % (self.image_name, tag)
logger.info('Docker run %s with kwargs: %s', img, kwargs)
container = self._docker.containers.run(img, **kwargs)
logger.info('Running biweeklybudget container; name=%s id=%s',
container.name, container.id)
logger.info('Container status: %s', container.status)
logger.info('Sleeping 20s for stabilization...')
time.sleep(20)
container.reload()
logger.info('Container status: %s', container.status)
if container.status != 'running':
logger.critical('Container did not stay running! Logs:')
logger.critical(
container.logs(stderr=True, stdout=True, stream=False,
timestamps=True).decode()
)
raise RuntimeError('Container did not stay running')
else:
logger.info(
'Container logs:\n%s',
container.logs(stderr=True, stdout=True, stream=False,
timestamps=True).decode()
)
# do the tests
try:
self._run_tests(container)
except Exception as exc:
logger.critical("Tests failed: %s", exc, exc_info=True)
raise
finally:
try:
db_container.stop()
db_container.remove()
except Exception:
pass
try:
container.stop()
container.remove()
except Exception:
pass

def _run_tests(self, container):
"""
Expand Down Expand Up @@ -336,9 +347,63 @@ def _run_tests(self, container):
else:
logger.error('GET /payperiods: %s', r.status_code)
failures = True
try:
self._test_phantomjs(container)
logger.info('phantomjs test PASSED')
except Exception as exc:
logger.error('PhantomJS test failed: %s', exc, exc_info=True)
failures = True
if failures:
raise RuntimeError('Tests FAILED.')

def _test_phantomjs(self, container):
"""
Test phantomjs on the container.
:param container: biweeklybudget Docker container
:type container: ``docker.models.containers.Container``
"""
cmd = [
'/bin/bash',
'-c',
'QT_QPA_PLATFORM=offscreen /usr/bin/phantomjs --version'
]
logger.debug('Running: %s', cmd)
res = container.exec_run(cmd).decode().strip()
logger.debug('Command output:\n%s', res)
if res != self._phantomjs_version:
raise RuntimeError('ERROR: expected phantomjs version to be %s'
' but got %s', self._phantomjs_version, res)
logger.debug('phamtomjs version correct: %s', res)
phantomtest = dedent("""
"use strict";
var page = require("webpage").create();
page.open("http://127.0.0.1:80/", function(status) {
console.log("Status: " + status);
if(status === "success") {
console.log(page.content);
}
phantom.exit();
});
""")
phantom_script = self._string_to_tarfile('phantomtest.js', phantomtest)
container.put_archive('/', phantom_script)
cmd = [
'/bin/bash',
'-c',
'QT_QPA_PLATFORM=offscreen /usr/bin/phantomjs /phantomtest.js; '
'echo "exitcode=$?"'
]
logger.debug('Running: %s', cmd)
res = container.exec_run(cmd).decode().strip()
logger.debug('Command output:\n%s', res)
if 'FATAL' in res or 'PhantomJS has crashed' in res:
raise RuntimeError('PhantomJS crashed during test')
exitcode = res.split("\n")[-1]
if exitcode != 'exitcode=0':
raise RuntimeError('Expected PhantomJS to exit with code 0, but'
' got %s' % exitcode)

def _run_mysql(self):
"""
Run a MySQL (well, MariaDB) container to test the Docker image
Expand All @@ -359,13 +424,19 @@ def _run_mysql(self):
cont = self._docker.containers.run(img, **kwargs)
logger.debug('MySQL container running; name=%s id=%s',
cont.name, cont.id)
logger.info('Sleeping 10s for stabilization...')
time.sleep(10)
logger.info('Creating database...')
cmd = '/usr/bin/mysql -uroot -proot -e "CREATE DATABASE budgetfoo;"'
logger.debug('Running: %s', cmd)
res = cont.exec_run(cmd)
logger.debug('Command output:\n%s', res)
count = 0
while count < 10:
count += 1
logger.info('Creating database...')
cmd = '/usr/bin/mysql -uroot -proot -e "CREATE DATABASE budgetfoo;"'
logger.debug('Running: %s', cmd)
res = cont.exec_run(cmd)
logger.debug('Command output:\n%s', res)
if 'ERROR' not in res.decode():
logger.info('Database creation appears successful.')
break
logger.warning('Database creation errored; sleep 5s and retry')
time.sleep(5)
return cont

def _build_image(self, tag):
Expand Down Expand Up @@ -483,6 +554,25 @@ def _docker_context(self):
logger.debug('Docker context created')
return b

def _string_to_tarfile(self, path, s):
"""
Return a BytesIO object containing a tarfile containing one file at
``path``, with contents ``s``.
:param path: path in the tar archive to write the file at
:type path: str
:param s: file contents
:type s: str
:return: tarfile object containing one file
:rtype: io.BytesIO
"""
b = BytesIO()
tar = tarfile.open(fileobj=b, mode='w')
self._tar_add_string_file(tar, path, s)
tar.close()
b.seek(0)
return b

def _tar_add_string_file(self, tarobj, fpath, content):
"""
Given a tarfile object, add a file to it at ``fpath``, with content
Expand Down Expand Up @@ -574,4 +664,9 @@ def set_log_level_format(level, format):
toxinidir = sys.argv[0]
distdir = sys.argv[1]
b = DockerImageBuilder(toxinidir, distdir)
b.build()
test_tag = os.environ.get('DOCKER_TEST_TAG', None)
if test_tag is not None:
logger.info('TEST-ONLY RUN for tag: %s', test_tag)
b.test(test_tag)
else:
b.build()
2 changes: 1 addition & 1 deletion tox.ini
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ commands =
deps =
-r{toxinidir}/requirements.txt
docker==2.2.1
passenv = {[testenv]passenv}
passenv = {[testenv]passenv} DOCKER_TEST_TAG
setenv = {[testenv]setenv}
basepython = python3.6
sitepackages = False
Expand Down

0 comments on commit 3d9aa20

Please sign in to comment.