{ "info": { "author": "Lovely Systems", "author_email": "office@lovelysystems.com", "bugtrack_url": null, "classifiers": [], "description": "**************************************************\nLovely Testing Layers for use with zope.testrunner\n**************************************************\n\nIntroduction\n============\nThis package includes various server test layers and\na generic server layer for use with any network based\nserver implementation.\n\nIt currently provides server layers for these fine\ndatabase and web servers (in alphabetical order):\n\n- ApacheDS\n- Cassandra\n- Memcached\n- MongoDB\n- MySQL\n- Nginx\n- OpenLDAP\n- PostgreSQL\n\n\nSetup\n=====\nWhile there are buildout targets based on ``hexagonit.recipe.cmmi`` and\n``zc.recipe.cmmi`` included for building PostgreSQL and Memcached inline,\nit is perfectly fine to use the native system installments of the\nrespective services.\n\n\nSelf-tests\n==========\n``lovely.testlayers`` ships with a bunch of built-in self-tests\nfor verifying the functionality of the respective test layers.\n\nTo get started on that, please follow up reading ``__.\n\n\n====================================\nTest layers with working directories\n====================================\n\nThere is a mixin class that provides usefull methods to generate a\nworking directory and make snapshots thereof.\n\n >>> from lovely.testlayers.layer import WorkDirectoryLayer\n\nLet us create a sample layer.\n\n >>> class MyLayer(WorkDirectoryLayer):\n ... def __init__(self, name):\n ... self.__name__ = name\n\n >>> myLayer = MyLayer('mylayer')\n\nTo initialize the directories we need to create the directory structure.\n\n >>> myLayer.setUpWD()\n\nWe can get relative paths by using the os.path join syntax.\n\n >>> myLayer.wdPath('a', 'b')\n '.../__builtin__.MyLayer.mylayer/work/a/b'\n\nLet us create a directory.\n\n >>> import os\n >>> os.mkdir(myLayer.wdPath('firstDirectory'))\n\nAnd make a snapshot.\n\n >>> myLayer.makeSnapshot('first')\n\nWe can check if we have a snapshot.\n\n >>> myLayer.hasSnapshot('first')\n True\n\nAnd get the info for the snapshot.\n\n >>> exists, path = myLayer.snapshotInfo('first')\n >>> exists\n True\n >>> path\n '...ss_first.tar.gz'\n\nAnd now we make a second directory and another snapshot.\n\n >>> os.mkdir(myLayer.wdPath('secondDirectory'))\n >>> myLayer.makeSnapshot('second')\n\nWe now have 2 directories.\n\n >>> sorted(os.listdir(myLayer.wdPath()))\n ['firstDirectory', 'secondDirectory']\n\nWe now restore the \"first\" snapshot\n\n >>> myLayer.restoreSnapshot('first')\n >>> sorted(os.listdir(myLayer.wdPath()))\n ['firstDirectory']\n\nWe can also restore the \"second\" snapshot.\n\n >>> myLayer.restoreSnapshot('second')\n >>> sorted(os.listdir(myLayer.wdPath()))\n ['firstDirectory', 'secondDirectory']\n\nWe can also override snapshots.\n\n >>> os.mkdir(myLayer.wdPath('thirdDirectory'))\n >>> myLayer.makeSnapshot('first')\n >>> myLayer.restoreSnapshot('first')\n >>> sorted(os.listdir(myLayer.wdPath()))\n ['firstDirectory', 'secondDirectory', 'thirdDirectory']\n\nThe snapshot directory can be specified, this is usefull if snapshots\nneed to be persistet to the project directory for example.\n\n >>> myLayer2 = MyLayer('mylayer2')\n >>> import tempfile\n >>> myLayer2.setUpWD()\n\n >>> myLayer2.snapDir = tempfile.mkdtemp()\n >>> os.mkdir(myLayer2.wdPath('adir'))\n\n >>> myLayer2.makeSnapshot('first')\n >>> os.listdir(myLayer2.snapDir)\n ['ss_first.tar.gz']\n\n >>> os.mkdir(myLayer2.wdPath('bdir'))\n >>> sorted(os.listdir(myLayer2.wdPath()))\n ['adir', 'bdir']\n\n >>> myLayer2.restoreSnapshot('first')\n\n >>> sorted(os.listdir(myLayer2.wdPath()))\n ['adir']\n\n\n===================\nBasic Servier Layer\n===================\n\nThe server layer allows to start servers which are listening to a\nspecific port, by providing the startup command.\n\n >>> from lovely.testlayers import server\n >>> sl = server.ServerLayer('sl1', servers=['localhost:33333'],\n ... start_cmd='nc -k -l 33333')\n\nSetting up the layer starts the server.\n\n >>> sl.setUp()\n\nNow we can acces the server port.\n\n >>> from lovely.testlayers import util\n >>> util.isUp('localhost', 33333)\n True\n\nNo more after teardown.\n\n >>> sl.tearDown()\n >>> util.isUp('localhost', 33333)\n False\n\nIf the command startup fails an error gets raised.\n\n >>> sl = server.ServerLayer('sl1', servers=['localhost:33333'],\n ... start_cmd='false')\n >>> sl.setUp()\n Traceback (most recent call last):\n ...\n SystemError: Failed to start server rc=1 cmd=false\n\nLogging\n-------\n\nIt's possible to specify a logfile for stdout and stderr::\n\n >>> import os\n >>> logPath = project_path('var', 'log', 'stdout.log')\n >>> sl = server.ServerLayer('sl2', servers=['localhost:33333'],\n ... start_cmd='nc -k -l 33333',\n ... stdout=logPath)\n\nSetup the layer starts the server::\n\n >>> sl.setUp()\n\nGet the current position of stdout::\n\n >>> pos = sl.stdout.tell()\n\nSend a message to the server::\n\n >>> _ = run('echo \"GET / HTTP/1.0\" | nc localhost 33333')\n\nThe message gets logged to stdout::\n\n >>> _ = sl.stdout.seek(pos)\n >>> print(sl.stdout.read())\n GET / HTTP/1.0\n\nAfter teardown the file gets closed::\n\n >>> sl.tearDown()\n >>> sl.stdout.closed\n True\n\nAfter calling setUp again, the file gets repoened::\n\n >>> sl.setUp()\n >>> pos = sl.stdout.tell()\n >>> _ = run('echo \"Hi\" | nc localhost 33333')\n >>> _ = sl.stdout.seek(pos)\n >>> print(sl.stdout.read())\n Hi\n >>> sl.tearDown()\n\nIt's also possible to initialize a ServerLayer with a file object::\n\n >>> path = project_path('var', 'log', 'stdout_2.log')\n >>> f = open(path, 'w+')\n >>> sl = server.ServerLayer('sl2', servers=['localhost:33333'],\n ... start_cmd='nc -k -l 33333',\n ... stdout=f)\n >>> sl.setUp()\n\n >>> pos = sl.stdout.tell()\n >>> _ = run('echo \"Test\" | nc localhost 33333')\n >>> _ = sl.stdout.seek(pos)\n >>> print(sl.stdout.read())\n Test\n\n >>> sl.tearDown()\n\nAfter teardown the file gets closed::\n\n >>> sl.stdout.closed\n True\n\nThe file gets reopened after setUp::\n\n >>> sl.setUp()\n >>> pos = sl.stdout.tell()\n >>> _ = run('echo \"File gets reopened\" | nc localhost 33333')\n >>> _ = sl.stdout.seek(pos)\n >>> print(sl.stdout.read())\n File gets reopened\n >>> sl.tearDown()\n\nIf a directory gets specified, a logfile within the directory gets created::\n\n >>> path = project_path('var', 'log')\n >>> sl = server.ServerLayer('myLayer', servers=['localhost:33333'],\n ... start_cmd='nc -k -l 33333',\n ... stdout=path,\n ... stderr=path)\n >>> sl.setUp()\n >>> sl.stdout.name\n '...var/log/myLayer_stdout.log'\n\n >>> sl.stderr.name\n '...var/log/myLayer_stderr.log'\n\n >>> sl.tearDown()\n\n====================\nmemcached test layer\n====================\n\nThis layer starts and stops a memcached daemon on given port (default\nis 11222)\n\n\n >>> import os\n >>> here = os.path.dirname(__file__)\n >>> project_root = os.path.dirname(os.path.dirname(os.path.dirname(here)))\n >>> path = os.path.join(project_root, 'parts', 'memcached', 'bin', 'memcached')\n >>> from lovely.testlayers import memcached\n >>> ml = memcached.MemcachedLayer('ml', path=path)\n\nSo let us setup the server.\n\n >>> ml.setUp()\n\nNow we can acces memcached on port 11222.\n\n >>> import telnetlib\n >>> tn = telnetlib.Telnet('localhost', 11222)\n >>> tn.close()\n\nNo more after teardown.\n\n >>> ml.tearDown()\n >>> tn = telnetlib.Telnet('localhost', 11222)\n Traceback (most recent call last):\n ...\n error:...Connection refused...\n\n\n================\nNginx test layer\n================\n\nThis test layer starts and stops an nginx server.\n\nThe layer is constructed with the optional path to the nginx command\nand a prefix directory for nginx to run. To demonstrate this, we\ncreate a temporary nginx home, where nginx should run.\n\n >>> import tempfile, shutil, os\n >>> tmp = tempfile.mkdtemp()\n >>> nginx_prefix = os.path.join(tmp, 'nginx_home')\n >>> os.mkdir(nginx_prefix)\n\nWe have to add a config file at the default location. Let us define a\nminimal configuration file.\n\n >>> os.mkdir(os.path.join(nginx_prefix, 'conf'))\n >>> cfg = file(os.path.join(nginx_prefix, 'conf', 'nginx.conf'), 'w')\n >>> cfg.write(\"\"\"\n ... events {\n ... worker_connections 10;\n ... }\n ... http {\n ... server {\n ... listen 127.0.0.1:12345;\n ... }\n ... }\"\"\")\n >>> cfg.close()\n\nAnd the log directory.\n\n >>> os.mkdir(os.path.join(nginx_prefix, 'logs'))\n\nLet us also define the nginx executable. There is already one\ninstalled via buildout in the root directory of this package, so we\nget the path to this executable. Using a special nginx that is built\nvia buildout is the common way to use this layer. This way the same\nnginx might be used for local development with the configuration\ndefined by the buildout.\n\n >>> nginx_cmd = os.path.join(os.path.dirname(os.path.dirname(\n ... os.path.dirname(os.path.dirname(os.path.abspath(__file__))))),\n ... 'parts', 'openresty', 'nginx', 'sbin', 'nginx')\n\n\nNow we can instantiate the layer.\n\n >>> from lovely.testlayers import nginx\n >>> nl = nginx.NginxLayer('nl', nginx_prefix, nginx_cmd=nginx_cmd)\n\nUpon layer setup the server gets started.\n\n >>> nl.setUp()\n\nWe can now issue requests, we will get a 404 because we didn't setup\nany urls, but for testing this is ok.\n\n >>> import urllib2\n >>> urllib2.urlopen('http://localhost:12345/', None, 1)\n Traceback (most recent call last):\n ...\n HTTPError: HTTP Error 404: Not Found\n\nUpon layer tearDown the server gets stopped.\n\n >>> nl.tearDown()\n\nWe cannot connect to the server anymore now.\n\n >>> urllib2.urlopen('http://localhost:12345/', None, 1)\n Traceback (most recent call last):\n ...\n URLError: \n\nThe configuration can be located at a different location than nginx' default\nlocation (/conf/nginx.conf):\n\n >>> shutil.copytree(nginx_prefix, nginx_prefix + \"2\")\n\n >>> cfg_file = tempfile.mktemp()\n >>> cfg = file(cfg_file, 'w')\n >>> cfg.write(\"\"\"\n ... events {\n ... worker_connections 10;\n ... }\n ... http {\n ... server {\n ... listen 127.0.0.1:23456;\n ... }\n ... }\"\"\")\n >>> cfg.close()\n\n >>> nginx.NginxLayer('nl', nginx_prefix+\"2\", nginx_cmd, cfg_file)\n \n\nFailures\n========\n\nStartup and shutdown failures are also catched. For example if we try\nto tear down the layer twice.\n\n >>> nl.tearDown()\n Traceback (most recent call last):\n ...\n RuntimeError: Nginx stop failed ...nginx.pid\" failed\n (2: No such file or directory)\n\nOr if we try to start the server twice.\n\n >>> nl.setUp()\n >>> nl.setUp()\n Traceback (most recent call last):\n ...\n RuntimeError: Nginx start failed nginx: [emerg] bind() ...\n nginx: [emerg] bind() to 127.0.0.1:12345 failed (48: Address already in use)\n ...\n nginx: [emerg] still could not bind()\n\n >>> nl.tearDown()\n\nCleanup the temporary directory, we don't need it for testing from\nthis point.\n\n >>> shutil.rmtree(tmp)\n\nNearly all failures should be catched upon initialization, because the\nlayer does a config check then.\n\nLet us provide a non existing prefix path.\n\n >>> nginx.NginxLayer('nl', 'something')\n Traceback (most recent call last):\n ...\n AssertionError: prefix not a directory '.../something/'\n\nOr a not existing nginx_cmd.\n\n >>> nginx.NginxLayer('nl', '.', 'not-an-nginx')\n Traceback (most recent call last):\n ...\n RuntimeError: Nginx check failed /bin/sh: not-an-nginx: command not found\n\nOr some missing aka broken configuration. We just provide our working\ndirectory as the prefix, which actually does not contain any configs.\n\n >>> nginx.NginxLayer('nl', '.', nginx_cmd)\n Traceback (most recent call last):\n RuntimeError: Nginx check failed nginx version: ngx_openresty/...\n nginx: [alert] could not open error log file...\n ... [emerg] ...\n nginx: configuration file .../conf/nginx.conf test failed\n\n\n=====================\nEmail/SMTP Test Layer\n=====================\n\nThis layer starts and stops a smtp daemon on given port (default 1025)::\n\n >>> from lovely.testlayers import mail\n >>> layer = mail.SMTPServerLayer(port=1025)\n\nTo setup the layer call ``setUp()``::\n\n >>> layer.setUp()\n\nNow the Server can receive emails::\n\n >>> from email.mime.text import MIMEText\n >>> from email.utils import formatdate\n >>> from smtplib import SMTP\n >>> msg = MIMEText('testmessage', _charset='utf-8')\n >>> msg['Subject'] = 'first email'\n >>> msg['From'] = 'from@example.org'\n >>> msg['To'] = 'recipient@example.org'\n >>> msg['Date'] = formatdate(localtime=True)\n\n >>> s = SMTP()\n >>> _ = s.connect('localhost:1025')\n >>> _ = s.sendmail('from@example.org', 'recipient@example.com', msg.as_string())\n >>> msg['Subject'] = 'second email'\n >>> _ = s.sendmail('from@example.org', 'recipient@example.com', msg.as_string())\n\n >>> s.quit()\n (221, 'Bye')\n\nThe testlayer exposes a ``server`` property which can be used to access the\nreceived emails.\n\nUse the ``mbox(recipient)`` method to get the correct Mailbox::\n\n >>> mailbox = layer.server.mbox('recipient@example.com')\n\nUse ``is_empty()`` to verify that the mailbox isn't empty::\n\n >>> mailbox.is_empty()\n False\n\nIf the recipient didn't receive an email, an empty Mailbox is returned::\n\n >>> emptybox = layer.server.mbox('invalid@example.com')\n >>> emptybox.is_empty()\n True\n\nAnd ``popleft()`` to get the email that was received at first::\n\n >>> print(mailbox.popleft())\n Content-Type: text/plain; charset=\"utf-8\"\n MIME-Version: 1.0\n Content-Transfer-Encoding: base64\n Subject: first email\n From: from@example.org\n To: recipient@example.org\n ...\n \n ...\n\nThe layer can be shutdown using the tearDown method::\n\n >>> layer.tearDown()\n\nAfter tearDown() the server can't receive any more emails::\n\n >>> s = SMTP()\n >>> _ = s.connect('localhost:1025')\n Traceback (most recent call last):\n ...\n error: [Errno ...] Connection refused\n\nVerification that setUp() and tearDown() work for subsequent calls::\n\n >>> layer.setUp()\n\n >>> _ = s.connect('localhost:1025')\n >>> _ = s.sendmail('from@example.org', 'recipient@example.com', msg.as_string())\n >>> print(mailbox.popleft())\n Content-Type: text/plain; charset=\"utf-8\"\n MIME-Version: 1.0\n Content-Transfer-Encoding: base64\n Subject: first email\n From: from@example.org\n To: recipient@example.org\n ...\n \n ...\n\n >>> _ = s.quit()\n >>> layer.tearDown()\n >>> _ = s.connect('localhost:1025')\n Traceback (most recent call last):\n ...\n error: [Errno ...] Connection refused\n\nBefore setUp() is called the ``server`` property is None::\n\n >>> layer = mail.SMTPServerLayer(port=1025)\n >>> layer.server is None\n True\n\n====================\nCassandra test layer\n====================\n\nThis layer starts and stops a cassandra instance with a given storage\nconfiguration template. For information about cassandra see:\nhttp://en.wikipedia.org/wiki/Cassandra_(database)\n\n >>> from lovely.testlayers import cass\n\nAn example template exists in this directory which we now use for this\nexample.\n\n >>> import os\n >>> storage_conf_tmpl = os.path.join(os.path.dirname(__file__),\n ... 'storage-conf.xml.in')\n\nThe following keys are provided when the template gets evaluated. Let\nus look them up in the example file.\n\n >>> import re\n >>> tmpl_pat = re.compile(r'.*\\%\\(([^ \\)]+)\\)s.*')\n >>> conf_keys = set()\n >>> for l in file(storage_conf_tmpl).readlines():\n ... m = tmpl_pat.match(l)\n ... if m:\n ... conf_keys.add(m.group(1))\n\n\n >>> sorted(conf_keys)\n ['control_port', 'storage_port', 'thrift_port', 'var']\n\nWith the storage configuration path we can instantiate a new cassandra\nlayer. The thrift_port, storage_port, and control_port are optional\nkeyword arguments for the constructor and default to the standard port\n+10000.\n\n >>> l = cass.CassandraLayer('l', storage_conf=storage_conf_tmpl)\n >>> l.thrift_port\n 19160\n\nSo let us setup the server.\n\n >>> l.setUp()\n\nNow the cassandra server is up and running. We test this by connecting\nto the thrift port via telnet.\n\n >>> import telnetlib\n >>> tn = telnetlib.Telnet('localhost', l.thrift_port)\n >>> tn.close()\n\nThe connection is refused after teardown.\n\n >>> l.tearDown()\n\n >>> telnetlib.Telnet('localhost', l.thrift_port)\n Traceback (most recent call last):\n ...\n error:...Connection refused\n\n\n\n\n================\nmyserver control\n================\n\n >>> from lovely.testlayers import mysql\n >>> import tempfile, os\n >>> tmp = tempfile.mkdtemp()\n >>> dbDir = os.path.join(tmp, 'db')\n >>> dbDirFake = os.path.join(tmp, 'dbfake')\n\n >>> dbName = 'testing'\n\nLet us create a mysql server.\n\n >>> srv = mysql.Server(dbDir, port=17777)\n\nAnd init the db.\n\n >>> srv.initDB()\n >>> srv.start()\n\n >>> import time\n >>> time.sleep(3)\n\n >>> srv.createDB(dbName)\n\nNow we can get a list of databases.\n\n >>> sorted(srv.listDatabases())\n ['mysql', 'test', 'testing']\n\n\nIf no mysql server is installed on the system we will get an exception::\n\n >>> srv.orig_method = srv.mysqld_path\n >>> srv.mysqld_path = lambda: None\n\n >>> srv.start()\n Traceback (most recent call last):\n IOError: mysqld was not found. Is a MySQL server installed?\n\n >>> srv.mysqld_path = srv.orig_method\n\nRun SQL scripts\n================\n\nWe can run scripts from the filesystem.\n\n >>> script = os.path.join(tmp, 'ascript.sql')\n >>> f = file(script, 'w')\n >>> f.write(\"\"\"drop table if exists a; create table a (title varchar(64));\"\"\")\n >>> f.close()\n >>> srv.runScripts(dbName, [script])\n\n\nDump and Restore\n================\n\nLet us make a dump of our database\n\n >>> dumpA = os.path.join(tmp, 'a.sql')\n >>> srv.dump(dbName, dumpA)\n\nAnd now some changes\n\n >>> import _mysql\n >>> conn = _mysql.connect(host='127.0.0.1', port=17777, user='root', db=dbName)\n\n >>> for i in range(5):\n ... conn.query('insert into a values(%i)' % i)\n >>> conn.commit()\n\n >>> conn.close()\n\nAnother dump.\n\n >>> dumpB = os.path.join(tmp, 'b.sql')\n >>> srv.dump(dbName, dumpB)\n\nWe restore dumpA and the table is emtpy.\n\n >>> srv.restore(dbName, dumpA)\n\n >>> conn = _mysql.connect(host='127.0.0.1', port=17777, user='root', db=dbName)\n >>> conn.query('select count(*) from a')\n >>> conn.store_result().fetch_row()\n (('0',),)\n\n >>> conn.close()\n\nNow restore dumpB and we have our 5 rows back.\n\n >>> srv.restore(dbName, dumpB)\n\n >>> conn = _mysql.connect(host='127.0.0.1', port=17777, user='root', db=dbName)\n >>> conn.query('select count(*) from a')\n >>> conn.store_result().fetch_row()\n (('5',),)\n\n >>> conn.close()\n\nIf we try to restore a none existing file we gat a ValueError.\n\n >>> srv.restore(dbName, 'asdf')\n Traceback (most recent call last):\n ...\n ValueError: No such file '.../asdf'\n\n >>> srv.stop()\n\n\nMySQLDB Scripts\n===============\n\nWe can generate a control script for use as commandline script.\n\nThe simplest script is just to define a server.\n\n >>> dbDir2 = os.path.join(tmp, 'db2')\n >>> main = mysql.MySQLDBScript(dbDir2, port=17777)\n >>> main.start()\n >>> sorted(main.srv.listDatabases())\n ['mysql', 'test']\n >>> main.stop()\n\n\nWe can also define a database to be created upon startup.\n\n >>> main = mysql.MySQLDBScript(dbDir2, dbName='hoschi', port=17777)\n >>> main.start()\n >>> sorted(main.srv.listDatabases())\n ['hoschi', 'mysql', 'test']\n >>> main.stop()\n\nThe database is created only one time.\n\n >>> main.start()\n >>> main.stop()\n\nAnd also scripts to be executed.\n\n >>> main = mysql.MySQLDBScript(dbDir2, dbName='hoschi2',\n ... scripts=[script], port=17777)\n >>> main.start()\n\nNote that we used the same directory here so the other db is still there.\n\n >>> sorted(main.srv.listDatabases())\n ['hoschi', 'hoschi2', 'mysql', 'test']\n\nWe can run the scripts again. Note that scripts should always be\nnone-destructive. So if a schema update is due one just needs\nto run all scripts again.\n\n >>> main.runscripts()\n >>> main.stop()\n\n\nMySQLDatabaseLayer\n==================\n\nLet's create a layer::\n\n >>> layer = mysql.MySQLDatabaseLayer('testing')\n\nWe can get the store uri.\n\n >>> layer.storeURI()\n 'mysql://localhost:16543/testing'\n\n >>> layer.setUp()\n >>> layer.tearDown()\n\nThe second time the server ist started it takes the snapshot.\n\n >>> layer.setUp()\n >>> layer.tearDown()\n\nIf we try to run setup twice or the port is occupied, we get an error.\n\n >>> layer.setUp()\n >>> layer.setUp()\n Traceback (most recent call last):\n RuntimeError: Port already listening: 16543\n\n >>> layer.tearDown()\n\nWe can have appsetup definitions and sql scripts. There is also a\nconvinience class that let's us execute sql statements as setup.\n\n >>> setup = mysql.ExecuteSQL('create table testing (title varchar(32))')\n >>> layer = mysql.MySQLDatabaseLayer('testing', setup=setup)\n >>> layer.setUp()\n >>> layer.tearDown()\n >>> layer = mysql.MySQLDatabaseLayer('testing', setup=setup)\n >>> layer.setUp()\n >>> layer.tearDown()\n\nAlso if the database name is different, the same snapshots can be used.\n\n >>> layer2 = mysql.MySQLDatabaseLayer('testing2', setup=setup)\n >>> layer2.setUp()\n >>> layer2.tearDown()\n\nIf we do not provide the snapsotIdent the ident is built by using the\ndotted name of the setup callable and the hash of the arguments.\n\n >>> layer.snapshotIdent\n u'lovely.testlayers.mysql.ExecuteSQLe449d7734c67c100e0662d3319fe3f410e78ebcf'\n\nLet us provide an ident and scripts.\n\n >>> layer = mysql.MySQLDatabaseLayer('testing3', setup=setup,\n ... snapshotIdent='blah',\n ... scripts=[script])\n >>> layer.snapshotIdent\n 'blah'\n >>> layer.scripts\n ['/.../ascript.sql']\n\n\nOn setup the snapshot with the setup is created, therefore setup is\ncalled with the server as argument.\n\n >>> layer.setUp()\n\nUpon testSetUp this snapshot is now restored.\n\n >>> layer.testSetUp()\n\nSo now we should have the table there.\n\n >>> conn = _mysql.connect(host='127.0.0.1', port=16543, user='root', db=dbName)\n >>> conn.query('select * from testing')\n >>> conn.store_result().fetch_row()\n ()\n\n >>> conn.close()\n\nLet us add some data (we are now in a test):\n\n >>> conn = _mysql.connect(host='127.0.0.1', port=16543, user='root', db=dbName)\n\n >>> conn.query(\"insert into testing values('hoschi')\")\n >>> conn.commit()\n\n >>> conn.query('select * from testing')\n >>> conn.store_result().fetch_row()\n (('hoschi',),)\n\n >>> conn.close()\n\n >>> layer.testTearDown()\n >>> layer.tearDown()\n\nFinally do some cleanup::\n\n >>> import shutil\n >>> shutil.rmtree(tmp)\n\n================\npgserver control\n================\n\n >>> from lovely.testlayers import pgsql\n >>> import tempfile, os\n >>> tmp = tempfile.mkdtemp()\n >>> dbDir = os.path.join(tmp, 'db')\n >>> dbDirFake = os.path.join(tmp, 'dbfake')\n\n >>> dbName = 'testing'\n\nLet us create a postgres server. Note that we give the absolute path\nto the pg_config executable in order to use the postgresql\ninstallation from this project.\n\n >>> pgConfig = project_path('parts', 'postgres', 'bin', 'pg_config')\n >>> srv = pgsql.Server(dbDir, port=16666, pgConfig=pgConfig, verbose=True)\n\nOptional we could also define a path to a special postgresql.conf file\nto use, otherwise defaults are used.\n\n >>> srv.postgresqlConf\n '/.../lovely/testlayers/postgresql8....conf'\n\n >>> srvFake = pgsql.Server(dbDirFake, postgresqlConf=srv.postgresqlConf)\n >>> srvFake.postgresqlConf == srv.postgresqlConf\n True\n\nThe path needs to exist.\n\n >>> pgsql.Server(dbDirFake, postgresqlConf='/not/existing/path')\n Traceback (most recent call last):\n ...\n ValueError: postgresqlConf not found '/not/existing/path'\n\nWe can also specify the pg_config executable which defaults to\n'pg_config' and therefore needs to be in the path.\n\n >>> srv.pgConfig\n '/.../pg_config'\n\n >>> pgsql.Server(dbDirFake, pgConfig='notexistingcommand')\n Traceback (most recent call last):\n ...\n ValueError: pgConfig not found 'notexistingcommand'\n\nThe server is aware of its version, which is represented as a tuple of ints.\n\n >>> srv.pgVersion\n (8, ..., ...)\n\nAnd init the db.\n\n >>> srv.initDB()\n >>> srv.start()\n\n >>> srv.createDB(dbName)\n\nNow we can get a list of databases.\n\n >>> sorted(srv.listDatabases())\n ['postgres', 'template0', 'template1', 'testing']\n\n\nRun SQL scripts\n================\n\nWe can run scripts from the filesystem.\n\n >>> script = os.path.join(tmp, 'ascript.sql')\n >>> f = file(script, 'w')\n >>> f.write(\"\"\"create table a (title varchar);\"\"\")\n >>> f.close()\n >>> srv.runScripts(dbName, [script])\n\nOr from the shared directories by prefixing it with pg_config. So let\nus install tsearch2.\n\n >>> script = 'pg_config:share:system_views.sql'\n >>> srv.runScripts(dbName, [script])\n\n\nDump and Restore\n================\n\nLet us make a dump of our database\n\n >>> dumpA = os.path.join(tmp, 'a.sql')\n >>> srv.dump(dbName, dumpA)\n\nAnd now some changes\n\n >>> import psycopg2\n >>> cs = \"dbname='%s' host='127.0.0.1' port='16666'\" % dbName\n >>> conn = psycopg2.connect(cs)\n >>> cur = conn.cursor()\n\n >>> for i in range(5):\n ... cur.execute('insert into a values(%i)' % i)\n >>> conn.commit()\n\n >>> cur.close()\n >>> conn.close()\n\nAnother dump.\n\n >>> dumpB = os.path.join(tmp, 'b.sql')\n >>> srv.dump(dbName, dumpB)\n\nWe restore dumpA and the table is emtpy.\n\n >>> srv.restore(dbName, dumpA)\n\n >>> conn = psycopg2.connect(cs)\n >>> cur = conn.cursor()\n\n >>> cur.execute('select count(*) from a')\n >>> cur.fetchone()\n (0L,)\n\n >>> cur.close()\n >>> conn.close()\n\nNow restore dumpB and we have our 5 rows back.\n\n >>> srv.restore(dbName, dumpB)\n\n >>> conn = psycopg2.connect(cs)\n >>> cur = conn.cursor()\n\n >>> cur.execute('select count(*) from a')\n >>> cur.fetchone()\n (5L,)\n\n >>> cur.close()\n >>> conn.close()\n\nIf we try to restore a none existing file we gat a ValueError.\n\n >>> srv.restore(dbName, 'asdf')\n Traceback (most recent call last):\n ...\n ValueError: No such file '.../asdf'\n\n >>> srv.stop()\n\n\nPGDB Scripts\n============\n\nWe can generate a control script for use as commandline script.\n\nThe simplest script is just to define a server.\n\n >>> dbDir2 = os.path.join(tmp, 'db2')\n >>> main = pgsql.PGDBScript(dbDir2, port=16666, pgConfig=pgConfig)\n >>> main.start()\n >>> sorted(main.srv.listDatabases())\n ['postgres', 'template0', 'template1']\n >>> main.stop()\n\n\nWe can also define a database to be created upon startup.\n\n >>> main = pgsql.PGDBScript(dbDir2,\n ... pgConfig=pgConfig,\n ... dbName='hoschi', port=16666)\n >>> main.start()\n >>> sorted(main.srv.listDatabases())\n ['hoschi', 'postgres', 'template0', 'template1']\n >>> main.stop()\n\nThe database is created only one time.\n\n >>> main.start()\n >>> main.stop()\n\nAnd also scripts to be executed.\n\n >>> main = pgsql.PGDBScript(dbDir2, dbName='hoschi2',\n ... pgConfig=pgConfig,\n ... scripts=[script], port=16666)\n >>> main.start()\n\nNote that we used the same directory here so the other db is still there.\n\n >>> sorted(main.srv.listDatabases())\n ['hoschi', 'hoschi2', 'postgres', 'template0', 'template1']\n\nWe can run the scripts again. Note that scripts should always be\nnone-destructive. So if a schema update is due one just needs\nto run all scripts again.\n\n >>> main.runscripts()\n\n >>> main.stop()\n\n\nFinally do some cleanup::\n\n >>> import shutil\n >>> shutil.rmtree(tmp)\n\nPGDatabaseLayer\n===============\n\nLet's create a layer::\n\n >>> layer = pgsql.PGDatabaseLayer('testing', pgConfig=pgConfig)\n\nWe can get the store uri.\n\n >>> layer.storeURI()\n 'postgres://localhost:15432/testing'\n\n >>> layer.setUp()\n >>> layer.tearDown()\n\nThe second time the server ist started it takes the snapshot.\n\n >>> layer.setUp()\n >>> layer.tearDown()\n\nIf we try to run setup twice or the port is occupied, we get an error.\n\n >>> layer.setUp()\n >>> layer.setUp()\n Traceback (most recent call last):\n ...\n RuntimeError: Port already listening: 15432\n >>> layer.tearDown()\n\n\nWe can have appsetup definitions and sql scripts. There is also a\nconvinience class that let's us execute sql statements as setup.\n\n >>> setup = pgsql.ExecuteSQL('create table testing (title varchar)')\n >>> layer = pgsql.PGDatabaseLayer('testing', setup=setup, pgConfig=pgConfig)\n >>> layer.setUp()\n >>> layer.tearDown()\n >>> layer = pgsql.PGDatabaseLayer('testing', setup=setup, pgConfig=pgConfig)\n >>> layer.setUp()\n >>> layer.tearDown()\n\nAlso if the database name is different, the same snapshots can be used.\n\n >>> layer2 = pgsql.PGDatabaseLayer('testing2', setup=setup, pgConfig=pgConfig)\n >>> layer2.setUp()\n >>> layer2.tearDown()\n\nIf we do not provide the snapsotIdent the ident is built by using the\ndotted name of the setup callable and the hash of the arguments.\n\n >>> layer.snapshotIdent\n u'lovely.testlayers.pgsql.ExecuteSQLf9bb47b1baeff8d57f8f0dadfc91b99a3ee56991'\n\nLet us provide an ident and scripts.\n\n >>> layer = pgsql.PGDatabaseLayer('testing3', setup=setup,\n ... pgConfig=pgConfig,\n ... snapshotIdent='blah',\n ... scripts=['pg_config:share:system_views.sql'])\n >>> layer.snapshotIdent\n 'blah'\n >>> layer.scripts\n ['pg_config:share:system_views.sql']\n\n\nOn setup the snapshot with the setup is created, therefore setup is\ncalled with the server as argument.\n\n >>> layer.setUp()\n\nUpon testSetUp this snapshot is now restored.\n\n >>> layer.testSetUp()\n\nSo now we should have the table there.\n\n >>> cs = \"dbname='testing3' host='127.0.0.1' port='15432'\"\n >>> conn = psycopg2.connect(cs)\n >>> cur = conn.cursor()\n\n >>> cur.execute('select * from testing')\n >>> cur.fetchall()\n []\n\n >>> cur.close()\n >>> conn.close()\n\nLet us add some data (we are now in a test):\n\n >>> conn = psycopg2.connect(cs)\n >>> cur = conn.cursor()\n\n >>> cur.execute(\"insert into testing values('hoschi')\")\n >>> conn.commit()\n\n >>> cur.execute('select * from testing')\n >>> cur.fetchall()\n [('hoschi',)]\n\n >>> cur.close()\n >>> conn.close()\n\n >>> layer.testTearDown()\n\nNow the next test comes.\n\n >>> layer.testSetUp()\n\nMake sure we can abort a transaction. The storm synch needs to be\nremoved at this time.\n\n >>> import transaction\n >>> transaction.abort()\n\nAnd the data is gone but the table is still there.\n\n >>> conn = psycopg2.connect(cs)\n >>> cur = conn.cursor()\n\n >>> cur.execute('select * from testing')\n >>> cur.fetchall()\n []\n\n >>> cur.close()\n >>> conn.close()\n\n >>> layer.tearDown()\n\n\n\n========================================\nMongoDB test layer - single server setup\n========================================\n\n.. note::\n\n To run this test::\n\n bin/buildout install mongodb mongodb-test\n bin/test-mongodb --test=mongodb_single\n\n\nIntroduction\n============\n\n| For information about MongoDB see:\n| http://en.wikipedia.org/wiki/Mongodb\n\nThe ``MongoLayer`` starts and stops a single MongoDB instance.\n\n\n\nSingle server\n=============\n\nWarming up\n----------\nWe create a new MongoDB layer::\n\n >>> from lovely.testlayers import mongodb\n >>> mongo = mongodb.MongoLayer('mongodb.single', mongod_bin = project_path('bin', 'mongod'))\n >>> mongo.storage_port\n 37017\n\nSo let's bootstrap the server::\n\n >>> mongo.setUp()\n\n\nPre flight checks\n-----------------\nNow the MongoDB server is up and running. We test this by connecting\nto the storage port via telnet::\n\n >>> import telnetlib\n >>> tn = telnetlib.Telnet('localhost', mongo.storage_port)\n >>> tn.close()\n\n\nGetting real\n------------\n\nConnect to it using a real MongoDB client::\n\n >>> from pymongo import Connection\n >>> mongo_conn = Connection('localhost:37017', safe=True)\n >>> mongo_db = mongo_conn['foo-db']\n\nInsert some data::\n\n >>> document_id = mongo_db.foobar.insert({'hello': 'world'})\n >>> document_id\n ObjectId('...')\n\nAnd query it::\n\n >>> document = mongo_db.foobar.find_one(document_id)\n >>> document\n {u'_id': ObjectId('...'), u'hello': u'world'}\n\nAnother query::\n\n >>> mongo_db.foobar.find({'hello': 'world'})[0] == document\n True\n\n\nClean up\n--------\n\nDatabase\n________\n\n >>> mongo_conn.drop_database('foo-db')\n >>> mongo_conn.disconnect()\n >>> del mongo_conn\n >>> del mongo_db\n\n\nLayers\n______\n\nThe connection is refused after teardown::\n\n >>> mongo.tearDown()\n\n >>> telnetlib.Telnet('localhost', mongo.storage_port)\n Traceback (most recent call last):\n ...\n error:...Connection refused\n\n=======================================\nMongoDB test layer - master/slave setup\n=======================================\n\n.. note::\n\n To run this test::\n\n bin/buildout install mongodb mongodb-test\n bin/test-mongodb --test=mongodb_masterslave\n\n\nIntroduction\n============\n\n| For information about MongoDB see:\n| http://en.wikipedia.org/wiki/Mongodb\n\nThe ``MongoMasterSlaveLayer`` starts and stops multiple MongoDB\ninstances and configures a master-slave connection between them.\n\n\nMaster/Slave\n============\n\nWarming up\n----------\n\nWe create a new MongoDB layer::\n\n >>> from lovely.testlayers import mongodb\n >>> masterslave = mongodb.MongoMasterSlaveLayer('mongodb.masterslave', mongod_bin = project_path('bin', 'mongod'))\n >>> masterslave.storage_ports\n [37020, 37021, 37022]\n\nSo let's bootstrap the servers::\n\n >>> from zope.testrunner.runner import gather_layers\n >>> layers = []\n >>> gather_layers(masterslave, layers)\n >>> for layer in layers:\n ... layer.setUp()\n\n\nGetting real\n------------\n\nConnect to it using a real MongoDB client::\n\n >>> from pymongo import Connection, ReadPreference\n >>> from pymongo.master_slave_connection import MasterSlaveConnection\n >>> mongo_conn = MasterSlaveConnection(\n ... Connection('localhost:37020', safe=True, w=3),\n ... [\n ... Connection('localhost:37021', read_preference = ReadPreference.SECONDARY),\n ... Connection('localhost:37022', read_preference = ReadPreference.SECONDARY),\n ... ]\n ... )\n >>> mongo_db = mongo_conn['bar-db']\n\nQuery operation counters upfront to compare them later::\n\n >>> opcounters_before = masterslave.get_opcounters()['custom']\n\nInsert some data::\n\n >>> document_id = mongo_db.foobar.insert({'hello': 'world'})\n >>> document_id\n ObjectId('...')\n\nAnd query it::\n\n >>> document = mongo_db.foobar.find_one(document_id)\n >>> document\n {u'_id': ObjectId('...'), u'hello': u'world'}\n\nProve that the ``write`` operation was dispatched to the ``PRIMARY``,\nwhile the ``read`` operation was dispatched to any ``SECONDARY``::\n\n >>> opcounters_after = masterslave.get_opcounters()['custom']\n\n >>> opcounters_after['primary.insert'] == opcounters_before['primary.insert'] + 1\n True\n\n >>> assert \\\n ... opcounters_after['secondary.query'] == opcounters_before['secondary.query'] + 1, \\\n ... \"ERROR: expected 'after == before + 1', but got 'after=%s, before=%s'\" % \\\n ... (opcounters_after['secondary.query'], opcounters_before['secondary.query'])\n\n\n\nClean up\n--------\n\nDatabase\n________\n\n >>> mongo_conn.drop_database('bar-db')\n >>> mongo_conn.disconnect()\n >>> del mongo_conn\n >>> del mongo_db\n\n\nLayers\n______\n\nConnections are refused after teardown::\n\n >>> for layer in layers:\n ... layer.tearDown()\n\n >>> def check_down(*ports):\n ... for port in ports:\n ... try:\n ... tn = telnetlib.Telnet('localhost', port)\n ... tn.close()\n ... except:\n ... yield True\n\n >>> all(check_down(masterslave.storage_ports))\n True\n\n======================================\nMongoDB test layer - replica set setup\n======================================\n\n.. note::\n\n To run this test::\n\n bin/buildout install mongodb mongodb-test\n bin/test-mongodb --test=mongodb_replicaset\n\n\nIntroduction\n============\n\n| For information about MongoDB see:\n| http://en.wikipedia.org/wiki/Mongodb\n\nThe ``MongoReplicaSetLayer`` starts and stops multiple\nMongoDB instances and configures a replica set on top of them.\n\n\nReplica Set\n===========\n\n.. ifconfig:: False\n >>> from time import sleep\n\nWarming up\n----------\n\nWe create a new MongoDB layer::\n\n >>> from lovely.testlayers import mongodb\n >>> replicaset = mongodb.MongoReplicaSetLayer('mongodb.replicaset', mongod_bin = project_path('bin', 'mongod'))\n >>> #replicaset = mongodb.MongoReplicaSetLayer('mongodb.replicaset', mongod_bin = project_path('bin', 'mongod'), cleanup = False)\n >>> replicaset.storage_ports\n [37030, 37031, 37032]\n\n\nSo let's bootstrap the servers::\n\n >>> from zope.testrunner.runner import gather_layers\n >>> layers = []\n >>> gather_layers(replicaset, layers)\n >>> for layer in layers:\n ... layer.setUp()\n\n\nAnd check if the replica set got initiated properly::\n\n >>> from pymongo import Connection\n >>> mongo_conn = Connection('localhost:37030', safe=True)\n\n >>> mongo_conn.admin.command('replSetGetStatus').get('set')\n u'mongodb.replicaset'\n\n\nReady::\n\n >>> mongo_conn.disconnect()\n >>> del mongo_conn\n\n\nGetting real\n------------\n\nConnect to it using a real MongoDB client::\n\n >>> from pymongo import ReplicaSetConnection, ReadPreference\n >>> mongo_uri = 'mongodb://localhost:37030,localhost:37031,localhost:37032/?replicaSet=mongodb.replicaset'\n >>> mongo_conn = ReplicaSetConnection(mongo_uri, read_preference=ReadPreference.SECONDARY, safe=True, w=\"majority\")\n >>> mongo_db = mongo_conn['foobar-db']\n\nQuery operation counters upfront to compare them later::\n\n >>> sleep(1)\n >>> opcounters_before = replicaset.get_opcounters()['custom']\n\nInsert some data::\n\n >>> document_id = mongo_db.foobar.insert({'hello': 'world'})\n >>> document_id\n ObjectId('...')\n\nAnd query it::\n\n >>> document = mongo_db.foobar.find_one(document_id)\n >>> document\n {u'_id': ObjectId('...'), u'hello': u'world'}\n\nProve that the ``write`` operation was dispatched to the ``PRIMARY``,\nwhile the ``read`` operation was dispatched to any ``SECONDARY``::\n\n >>> sleep(1)\n >>> opcounters_after = replicaset.get_opcounters()['custom']\n\n >>> opcounters_after['primary.insert'] == opcounters_before['primary.insert'] + 1\n True\n\n >>> assert \\\n ... opcounters_after['secondary.query'] == opcounters_before['secondary.query'] + 1, \\\n ... \"ERROR: expected 'after == before + 1', but got 'after=%s, before=%s'\" % \\\n ... (opcounters_after['secondary.query'], opcounters_before['secondary.query'])\n\n\n\nClean up\n--------\n\nDatabase\n________\n\n >>> mongo_conn.drop_database('foobar-db')\n >>> mongo_conn.disconnect()\n >>> del mongo_conn\n >>> del mongo_db\n\n\nLayers\n______\n\nConnections are refused after teardown::\n\n >>> for layer in layers:\n ... layer.tearDown()\n\n >>> def check_down(*ports):\n ... for port in ports:\n ... try:\n ... tn = telnetlib.Telnet('localhost', port)\n ... tn.close()\n ... except:\n ... yield True\n\n >>> all(check_down(replicaset.storage_ports))\n True\n\n===================\nApacheDS test layer\n===================\n\n.. note::\n\n To run this test::\n\n bin/buildout install apacheds-test\n bin/test-apacheds --test=apacheds\n\n\nIntroduction\n============\n\n| For information about ApacheDS see:\n| https://directory.apache.org/apacheds/\n\nThe ``ApacheDSLayer`` starts and stops a single ApacheDS instance.\n\n\nSetup\n=====\nGo to https://directory.apache.org/apacheds/downloads.html\n\n\nSingle server\n=============\n\nWarming up\n----------\nWe create a new ApacheDS layer::\n\n >>> from lovely.testlayers import apacheds\n\n # Initialize layer object\n >>> server = apacheds.ApacheDSLayer('apacheds', port=10389)\n\n >>> server.port\n 10389\n\nSo let's bootstrap the server::\n\n >>> server.setUp()\n\n\nPre flight checks\n-----------------\nNow the OpenLDAP server is up and running. We test this by connecting\nto the storage port via telnet::\n\n >>> import telnetlib\n >>> tn = telnetlib.Telnet('localhost', server.port)\n >>> tn.close()\n\n\nGetting real\n------------\n\nConnect to it using a real OpenLDAP client::\n\n >>> import ldap\n >>> client = ldap.initialize('ldap://localhost:10389')\n >>> client.simple_bind_s('uid=admin,ou=system', 'secret')\n (97, [], 1, [])\n\nAn empty DIT is - empty::\n\n >>> client.search_s('dc=test,dc=example,dc=com', ldap.SCOPE_SUBTREE, '(cn=Hotzenplotz*)', ['cn','mail'])\n Traceback (most recent call last):\n ...\n NO_SUCH_OBJECT: {'info': \"NO_SUCH_OBJECT: failed for MessageType : SEARCH_REQUEST...\n\nInsert some data::\n\n Create DIT context for suffix\n >>> record = [('objectclass', ['dcObject', 'organization']), ('o', 'Test Organization'), ('dc', 'test')]\n >>> client.add_s('dc=test,dc=example,dc=com', record)\n (105, [])\n\n Create container for users\n >>> record = [('objectclass', ['top', 'organizationalUnit']), ('ou', 'users')]\n >>> client.add_s('ou=users,dc=test,dc=example,dc=com', record)\n (105, [])\n\n Create single user\n >>> record = [\n ... ('objectclass', ['top', 'person', 'organizationalPerson', 'inetOrgPerson']),\n ... ('cn', 'User 1'), ('sn', 'User 1'), ('uid', 'user1@test.example.com'),\n ... ('userPassword', '{SSHA}DnIz/2LWS6okrGYamkg3/R4smMu+h2gM')\n ... ]\n >>> client.add_s('cn=User 1,ou=users,dc=test,dc=example,dc=com', record)\n (105, [])\n\nAnd query it::\n\n >>> response = client.search_s('dc=test,dc=example,dc=com', ldap.SCOPE_SUBTREE, '(uid=user1@test.example.com)', ['cn', 'uid'])\n >>> response[0][0]\n 'cn=User 1,ou=users,dc=test,dc=example,dc=com'\n\n >>> response[0][1]['uid']\n ['user1@test.example.com']\n\n >>> response[0][1]['cn']\n ['User 1']\n\n\nClean up\n--------\n\nLayers\n______\n\nThe connection is refused after teardown::\n\n >>> server.tearDown()\n\n >>> telnetlib.Telnet('localhost', server.port)\n Traceback (most recent call last):\n ...\n error:...Connection refused\n\n===================\nOpenLDAP test layer\n===================\n\n.. note::\n\n To run this test::\n\n bin/buildout install openldap-test\n bin/test-openldap --test=openldap\n\n\nIntroduction\n============\n\n| For information about OpenLDAP see:\n| http://www.openldap.org/\n\nThe ``OpenLDAPLayer`` starts and stops a single OpenLDAP instance.\n\n\nSetup\n=====\nDebian Linux::\n\n aptitude install slapd\n\nCentOS Linux::\n\n yum install openldap-servers\n\nMac OS X, Macports::\n\n sudo port install openldap\n\n\n\nSingle server\n=============\n\nWarming up\n----------\nWe create a new OpenLDAP layer::\n\n >>> from lovely.testlayers import openldap\n\n # Initialize layer object\n >>> server = openldap.OpenLDAPLayer('openldap', port=3389)\n\n # Add essential schemas\n >>> server.add_schema('core.schema')\n >>> server.add_schema('cosine.schema')\n >>> server.add_schema('inetorgperson.schema')\n\n >>> server.port\n 3389\n\nSo let's bootstrap the server::\n\n >>> server.setUp()\n\n\nPre flight checks\n-----------------\nNow the OpenLDAP server is up and running. We test this by connecting\nto the storage port via telnet::\n\n >>> import telnetlib\n >>> tn = telnetlib.Telnet('localhost', server.port)\n >>> tn.close()\n\n\nGetting real\n------------\n\nConnect to it using a real OpenLDAP client::\n\n >>> import ldap\n >>> client = ldap.initialize('ldap://localhost:3389')\n >>> client.simple_bind_s('cn=admin,dc=test,dc=example,dc=com', 'secret')\n (97, [], 1, [])\n\nAn empty DIT is - empty::\n\n >>> client.search_s('dc=test,dc=example,dc=com', ldap.SCOPE_SUBTREE, '(cn=Hotzenplotz*)', ['cn','mail'])\n Traceback (most recent call last):\n ...\n NO_SUCH_OBJECT: {'desc': 'No such object'}\n\nInsert some data::\n\n Create DIT context for suffix\n >>> record = [('objectclass', ['dcObject', 'organization']), ('o', 'Test Organization'), ('dc', 'test')]\n >>> client.add_s('dc=test,dc=example,dc=com', record)\n (105, [])\n\n Create container for users\n >>> record = [('objectclass', ['top', 'organizationalUnit']), ('ou', 'users')]\n >>> client.add_s('ou=users,dc=test,dc=example,dc=com', record)\n (105, [])\n\n Create single user\n >>> record = [\n ... ('objectclass', ['top', 'person', 'organizationalPerson', 'inetOrgPerson']),\n ... ('cn', 'User 1'), ('sn', 'User 1'), ('uid', 'user1@test.example.com'),\n ... ('userPassword', '{SSHA}DnIz/2LWS6okrGYamkg3/R4smMu+h2gM')\n ... ]\n >>> client.add_s('cn=User 1,ou=users,dc=test,dc=example,dc=com', record)\n (105, [])\n\nAnd query it::\n\n >>> client.search_s('dc=test,dc=example,dc=com', ldap.SCOPE_SUBTREE, '(uid=user1@test.example.com)', ['cn', 'uid'])\n [('cn=User 1,ou=users,dc=test,dc=example,dc=com', {'cn': ['User 1'], 'uid': ['user1@test.example.com']})]\n\n\n\nClean up\n--------\n\nLayers\n______\n\nThe connection is refused after teardown::\n\n >>> server.tearDown()\n\n >>> telnetlib.Telnet('localhost', server.port)\n Traceback (most recent call last):\n ...\n error:...Connection refused\n\n==============\nChange History\n==============\n\nUnreleased\n==========\n\n2016/09/12 0.7.1\n================\n\n - Rename DEVELOP.txt into TESTS.rst to improve rendering on GitHub\n - Update README.rst\n - Python 2.6 / Java 1.8 compatibility for LDAP tests\n\n\n2016/09/07 0.7.0\n================\n\n - Refactor generic functionality from MongoLayer into WorkspaceLayer\n - Add server layers for OpenLDAP and ApacheDS LDAP servers\n\n2015/06/02 0.6.3\n================\n\n - call isUp with host localhost on setUp method of basesql layer\n\n2015/03/13 0.6.2\n================\n\n - fix: on SIGINT try to stop nginx silently\n\n - use only ascii characters in mongodb_* documents\n\n2015/03/12 0.6.1\n================\n\n - added SIGINT handling to nginx layer (KeyboardInterrupt)\n\n2013/09/06 0.6.0\n================\n\n - ServerLayer: is now compatible with python 3.3\n\n2013/07/01 0.5.3\n================\n\n - ServerLayer: reopen logfile in start instead of setUp\n\n2013/07/01 0.5.2\n================\n\n - ServerLayer: generate logfiles with correct file extension\n\n2013/07/01 0.5.1\n================\n\n - It's possible to specify logging of the ServerLayer\n\n - included memcached in buildout\n\n - use openresty instead of nginx\n\n - nailed versions of dependencies\n\n2013/06/19 0.5.0\n================\n\n - Add MongoLayer\n\n2013/04/03 0.4.3\n================\n\n - SMTPServerLayer's is now None before calling setUp()\n\n - add additional tests for SMTPServerLayer\n\n2013/04/03 0.4.2\n================\n\n - add missing __name__ to SMTPServerLayer\n\n2013/04/03 0.4.1\n================\n\n - add missing __bases__ to SMTPServerLayer\n\n2013/04/03 0.4.0\n================\n\n - added SMTPServerLayer\n\n - updated bootstrap.py and nginx/psql download location\n\n2012/11/23 0.3.5\n================\n\n - ServerLayer: add args for subprocess open\n\n2012/11/12 0.3.4\n================\n\n - set to zip_safe = False\n\n2012/11/12 0.3.3\n================\n\n - release without changes due to wrong distribution of previous version\n\n2011/12/06 0.3.2\n================\n\n - fixed #1 an endless loop in server layer\n\n2011/11/29 0.3.1\n================\n\n - added missing README to distro\n\n2011/11/29 0.3.0\n================\n\n - allow to set a snapshot directory in workdirectory-layer - this\n allows for generating non-temporary snapshots.\n\n - moved wait implementation for server start in server-layer into\n start, this is usefull when calling start and stop in tests, but\n might introduce incompatibilities when subclassed.\n\n - moved to github\n\n - postgresql 8.4 compat\n\n2011/05/18 0.2.3\n================\n\n - also dump routines for mysql\n\n2011/05/11 0.2.2\n================\n\n - try to run commands from the scripts dir (mysql 5.5)\n\n2011/05/10 0.2.1\n================\n\n - fixed the mysqld_path to work with newer mysql version\n\n2011/01/07 0.2.0\n================\n\n - fixed an UnboundLocalError in server layer\n\n - do not use shell option in server layer command and sanitize the\n command options.\n\n - reduced start/stop wait times in mysql layer\n\n - use modification times in layer sql script change checking\n additionally to the paths. this way the test dump is only used if\n the sql scripts have not been modified since the last test run.\n\n - stop sql servers when runscripts fails in layer setup because\n otherwise the server still runs after the testrunner exited.\n\n - allow to define a defaults file in mysql layer\n\n - fixed cassandra layer download url\n\n - removed dependency to ``zc.buildout`` which is now in an extra\n called ``cassandra`` because it is only needed for downloading\n cassandra.\n\n - removed dependency to ``zope.testing``\n\n - removed dependency to ``transaction``\n\n - do not pipe stderr in base server layer to prevent overflow because\n it never gets read\n\n2010/10/22 0.1.2\n================\n\n - look form mysqld in relative libexec dir in mysql layer\n\n2010/10/22 0.1.1\n================\n\n - allow setting the mysql_bin_dir in layer and server\n\n2010/07/14 0.1.0\n================\n\n - fix wait interval in isUp check in server layer\n\n - use hashlib instead of sha, to avoid deprecation warnings. Only\n works with python >= 2.5\n\n2010/03/08 0.1.0a7\n==================\n\n - made mysql layer aware to handle multiple instances of mysqld in parallel\n\n\n2010/02/03 0.1.0a6\n==================\n\n - added additional argument to set nginx configuration file. usefull if\n desired config is not located under given prefix\n\n\n2009/12/09 0.1.0a5\n==================\n\n - factored out the server part of the memcached layer, this could now\n be used for any server implementations, see ``memcached.py`` as an\n example how to use it.\n\n\n2009/11/02 0.1.0a4\n==================\n\n - raising a proper exception if mysqld was not found (fixes #3)\n\n - moved dependency for 'transaction' to extras[pgsql] (fixes #2)\n\n - fixed wrong path for dump databases in layer. (fixes #1)\n\n\n2009/10/30 0.1.0a3\n==================\n\n - the postgres and mysql client libs are now only defined as extra\n dependencies, so installation of this package is also possible\n without having those libs available\n\n - added nginx layer see nginx.txt\n\n\n2009/10/29 0.1.0a2\n==================\n\n - added coverage\n\n - added MySQLDatabaseLayer\n\n - added mysql server\n\n - added PGDatabaseLayer\n\n - added pgsql server\n\n\n2009/10/14 0.1.0a1\n==================\n\n- initial release", "description_content_type": null, "docs_url": null, "download_url": "UNKNOWN", "downloads": { "last_day": -1, "last_month": -1, "last_week": -1 }, "home_page": "https://github.com/lovelysystems/lovely.testlayers", "keywords": "testing zope layer test apacheds cassandra memcached mongodb mysql nginx openldap postgresql", "license": "Apache License 2.0", "maintainer": null, "maintainer_email": null, "name": "lovely.testlayers", "package_url": "https://pypi.org/project/lovely.testlayers/", "platform": "UNKNOWN", "project_url": "https://pypi.org/project/lovely.testlayers/", "project_urls": { "Download": "UNKNOWN", "Homepage": "https://github.com/lovelysystems/lovely.testlayers" }, "release_url": "https://pypi.org/project/lovely.testlayers/0.7.1/", "requires_dist": null, "requires_python": null, "summary": "test layers for use with zope.testrunner: apacheds, cassandra, memcached, mongodb, mysql, nginx, openldap, postgresql", "version": "0.7.1" }, "last_serial": 2337794, "releases": { "0.1.0": [ { "comment_text": "", "digests": { "md5": "8790816b1b55eb915f9c2f677d69a7bd", "sha256": "cced914d670239469f2c20bc77336a741246de2fb1154a5220593d11bde490a8" }, "downloads": -1, "filename": "lovely.testlayers-0.1.0.tar.gz", "has_sig": false, "md5_digest": "8790816b1b55eb915f9c2f677d69a7bd", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 29810, "upload_time": "2010-07-14T15:27:05", "url": "https://files.pythonhosted.org/packages/ac/ea/076b132691ef690f2c6e995fa605dea8baa86e26357bac35c05e2dc71c49/lovely.testlayers-0.1.0.tar.gz" } ], "0.1.0a1": [ { "comment_text": "", "digests": { "md5": "b5b3a4e78e932f92aab8efd197754079", "sha256": "c1908cd5819af6926101d1b6e1fd4f935a20955494f94e6ec18a15f582f41682" }, "downloads": -1, "filename": "lovely.testlayers-0.1.0a1.tar.gz", "has_sig": false, "md5_digest": "b5b3a4e78e932f92aab8efd197754079", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 9098, "upload_time": "2009-10-15T09:24:39", "url": "https://files.pythonhosted.org/packages/2f/1d/acaaa1f49d6a74f441ca001a8ce37d4ad5fbfae322d619a56fc338976315/lovely.testlayers-0.1.0a1.tar.gz" } ], "0.1.0a2": [ { "comment_text": "", "digests": { "md5": "3b0098a307688e4eb29c79d4cfcee4be", "sha256": "030bbd96be039bbadb856a8bc2b15c872c239bdfb1cffbe7bd5a3a0b5e2999c1" }, "downloads": -1, "filename": "lovely.testlayers-0.1.0a2.tar.gz", "has_sig": false, "md5_digest": "3b0098a307688e4eb29c79d4cfcee4be", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 17648, "upload_time": "2009-10-29T15:30:22", "url": "https://files.pythonhosted.org/packages/22/79/30f43b81c70f7f76d70c34d17ae482808e68b892005cd1a301479b3eba6d/lovely.testlayers-0.1.0a2.tar.gz" } ], "0.1.0a3": [ { "comment_text": "", "digests": { "md5": "c9edad28b9a69342ac85a561bb9249a6", "sha256": "5f01d5b11c976eccab44bb372cb66a50987cf5edf84741d9479a14c52138313e" }, "downloads": -1, "filename": "lovely.testlayers-0.1.0a3.tar.gz", "has_sig": false, "md5_digest": "c9edad28b9a69342ac85a561bb9249a6", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 19807, "upload_time": "2009-10-30T10:03:50", "url": "https://files.pythonhosted.org/packages/70/95/6588d8f1bc23154dfb9502e3c98d4a91f4c78e7209cc36914306080989d6/lovely.testlayers-0.1.0a3.tar.gz" } ], "0.1.0a4": [ { "comment_text": "", "digests": { "md5": "e30b10be628c5ce300abf69490c165e6", "sha256": "a2043f874a1d8bbd7f2e5c8148681830ffd40499e0f4365dfaf279af54aa0d21" }, "downloads": -1, "filename": "lovely.testlayers-0.1.0a4.tar.gz", "has_sig": false, "md5_digest": "e30b10be628c5ce300abf69490c165e6", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 29681, "upload_time": "2009-11-02T16:59:09", "url": "https://files.pythonhosted.org/packages/7c/67/2f3f248668f2ba506595d4e288eabb9682a5d70497679742fcb89ce5b472/lovely.testlayers-0.1.0a4.tar.gz" } ], "0.1.0a5": [ { "comment_text": "", "digests": { "md5": "904f8fd2554fda1d473df14b1f57d715", "sha256": "b8d2d70a51a13ee870c5cfb43aa688aad798dfcb22140fa3c3ccc039a7cdcd32" }, "downloads": -1, "filename": "lovely.testlayers-0.1.0a5.tar.gz", "has_sig": false, "md5_digest": "904f8fd2554fda1d473df14b1f57d715", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 29041, "upload_time": "2009-12-09T16:56:04", "url": "https://files.pythonhosted.org/packages/e2/ef/fa03a6d7d70c053997e2aad9a4abddb68267ec6f3745703fe089f4dc301b/lovely.testlayers-0.1.0a5.tar.gz" } ], "0.1.0a6": [ { "comment_text": "", "digests": { "md5": "858b5092924330aea8e065d65078809a", "sha256": "454a2a085e13a82b93ce2e8517f2a5945b8277de17d70d4d8e7660d2a23218b2" }, "downloads": -1, "filename": "lovely.testlayers-0.1.0a6.tar.gz", "has_sig": false, "md5_digest": "858b5092924330aea8e065d65078809a", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 29657, "upload_time": "2010-02-03T17:43:29", "url": "https://files.pythonhosted.org/packages/93/c6/d2ea7090b486c5b32876d5106c3e5d2e199901835daea64ed51bdcfb127a/lovely.testlayers-0.1.0a6.tar.gz" } ], "0.1.0a7": [ { "comment_text": "", "digests": { "md5": "0517788dd16280cf8670a373e2cad712", "sha256": "2c42ffdcc5511936e82f24190d45e44d5fd74892d78e1fff0469ad9ba2ceaa9a" }, "downloads": -1, "filename": "lovely.testlayers-0.1.0a7.tar.gz", "has_sig": false, "md5_digest": "0517788dd16280cf8670a373e2cad712", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 29718, "upload_time": "2010-03-08T12:35:04", "url": "https://files.pythonhosted.org/packages/93/c8/c1dae5556f9370035dce4ecc95c295038ba3e62a31c9380c5f42ec636a1f/lovely.testlayers-0.1.0a7.tar.gz" } ], "0.1.1": [ { "comment_text": "", "digests": { "md5": "e4c7a60b8c1dbc97ae2027de33a9d0d4", "sha256": "19459ebe4df42b15a239c66ccc79c6b8309d29eed11cf351d41f2eba26f6ac18" }, "downloads": -1, "filename": "lovely.testlayers-0.1.1.tar.gz", "has_sig": false, "md5_digest": "e4c7a60b8c1dbc97ae2027de33a9d0d4", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 30112, "upload_time": "2010-10-22T08:17:43", "url": "https://files.pythonhosted.org/packages/ca/87/acd81d66a3528d45f940b22f3882de230522aaee36dea03e66418a026594/lovely.testlayers-0.1.1.tar.gz" } ], "0.1.2": [ { "comment_text": "", "digests": { "md5": "954a4c312dac002fceb0aee673f14927", "sha256": "7c6bd9a2ecd1f13caeab0643f80ece717aed7f79e898d555d0611c9a885dfb1d" }, "downloads": -1, "filename": "lovely.testlayers-0.1.2.tar.gz", "has_sig": false, "md5_digest": "954a4c312dac002fceb0aee673f14927", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 30304, "upload_time": "2010-10-22T12:38:28", "url": "https://files.pythonhosted.org/packages/9f/65/22b99f968aa8e1a8754bd4672c521e00dc26c272f5c20268e5260fe9bf1a/lovely.testlayers-0.1.2.tar.gz" } ], "0.2.0": [ { "comment_text": "", "digests": { "md5": "67739399310d83970ec7e1adc56e2201", "sha256": "77ec0b3df5bbf167f391f5309cd244d5c8138089b56036f2fed80df391d4ccf5" }, "downloads": -1, "filename": "lovely.testlayers-0.2.0.tar.gz", "has_sig": false, "md5_digest": "67739399310d83970ec7e1adc56e2201", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 32170, "upload_time": "2011-01-07T14:50:43", "url": "https://files.pythonhosted.org/packages/6d/b8/82030d4957e7130749e10563f028b30477aa6db6f7ccc513740e37694b58/lovely.testlayers-0.2.0.tar.gz" } ], "0.2.1": [ { "comment_text": "", "digests": { "md5": "13dd2e5a0427251540257134f0b5e3bb", "sha256": "b23da1941b712ff739557efb4ab6357189abe6a97ed0678ea3d06b1e4e62ca02" }, "downloads": -1, "filename": "lovely.testlayers-0.2.1.tar.gz", "has_sig": false, "md5_digest": "13dd2e5a0427251540257134f0b5e3bb", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 32023, "upload_time": "2011-05-10T13:38:40", "url": "https://files.pythonhosted.org/packages/12/9d/78df0bb0ed85355a8f44932e76fa1e86e10a018362277e1efaa7486b7dd9/lovely.testlayers-0.2.1.tar.gz" } ], "0.2.2": [ { "comment_text": "", "digests": { "md5": "93b76b748ddc43468d3ad53c0469b369", "sha256": "ce4f362c34cb03d8fc3f77d388c51707c90b3789beb5c048a2156ed7c13c7f49" }, "downloads": -1, "filename": "lovely.testlayers-0.2.2.tar.gz", "has_sig": false, "md5_digest": "93b76b748ddc43468d3ad53c0469b369", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 32192, "upload_time": "2011-05-11T11:42:53", "url": "https://files.pythonhosted.org/packages/8c/98/5b372c685b066d80ee997d6452ce535a144e1c2bdb3ef4ba117d585d712b/lovely.testlayers-0.2.2.tar.gz" } ], "0.2.3": [ { "comment_text": "", "digests": { "md5": "32969714ed912bec758bbe103c922753", "sha256": "3033b7d4f43688ff2516c3401710402f7f00321c0806dacbb6c3d9a15757b3dd" }, "downloads": -1, "filename": "lovely.testlayers-0.2.3.tar.gz", "has_sig": false, "md5_digest": "32969714ed912bec758bbe103c922753", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 32265, "upload_time": "2011-05-18T15:34:39", "url": "https://files.pythonhosted.org/packages/64/cc/e7bf7e10d247c1041108cebe4c6b613667c8bffa46a6723515b91f5c43b2/lovely.testlayers-0.2.3.tar.gz" } ], "0.3.0": [ { "comment_text": "", "digests": { "md5": "1e2065c060a872faa104a42384f54ad6", "sha256": "32d2a96ab4b9dda6c4a6e5a243ef76327928ed2a441df844bb1df57e655d63f8" }, "downloads": -1, "filename": "lovely.testlayers-0.3.0.tar.gz", "has_sig": false, "md5_digest": "1e2065c060a872faa104a42384f54ad6", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 31829, "upload_time": "2011-11-29T22:22:41", "url": "https://files.pythonhosted.org/packages/4c/66/183789afd885ba352b6e00f1f6b661c0c23c5f9a73274507c93f6ce000ec/lovely.testlayers-0.3.0.tar.gz" } ], "0.3.1": [ { "comment_text": "", "digests": { "md5": "8f86519a83d8cf62917abcbdefbd9109", "sha256": "15d733ec9d755a859acc489329a6de4ad63b71fd574c062769034a4dd4240007" }, "downloads": -1, "filename": "lovely.testlayers-0.3.1.tar.gz", "has_sig": false, "md5_digest": "8f86519a83d8cf62917abcbdefbd9109", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 32015, "upload_time": "2011-11-29T22:28:18", "url": "https://files.pythonhosted.org/packages/3c/88/ba511f2e9c7a2956660ca94287632409fb32b6e006035f9e5eb0107b1c1f/lovely.testlayers-0.3.1.tar.gz" } ], "0.3.2": [ { "comment_text": "", "digests": { "md5": "23d21a43924f0bce62d2ea605b18a9c9", "sha256": "a77b14ed0d2ed282d6fbb318d79e5375f2b9322b4afd9e1665491863f1a2bc4d" }, "downloads": -1, "filename": "lovely.testlayers-0.3.2.tar.gz", "has_sig": false, "md5_digest": "23d21a43924f0bce62d2ea605b18a9c9", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 32796, "upload_time": "2011-12-06T07:21:31", "url": "https://files.pythonhosted.org/packages/c3/be/8b85bc3460e99d4ff54b5672189686d7127b87088457300bc137fef49184/lovely.testlayers-0.3.2.tar.gz" } ], "0.5.0": [ { "comment_text": "", "digests": { "md5": "12064e5bd89ac8e6e823ce0cc8fb17ba", "sha256": "89bce0f9fa84fc00fe5a12af9e30fa24940e9f7f388953e8f2906029963ffae7" }, "downloads": -1, "filename": "lovely.testlayers-0.5.0.tar.gz", "has_sig": false, "md5_digest": "12064e5bd89ac8e6e823ce0cc8fb17ba", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 47933, "upload_time": "2013-06-19T07:03:39", "url": "https://files.pythonhosted.org/packages/a4/bb/2c0d81567de23f5a62aecb575e40bd63486914bd6260715d76b09b1c180b/lovely.testlayers-0.5.0.tar.gz" } ], "0.5.1": [ { "comment_text": "", "digests": { "md5": "f7e04b1d48b0bd4dbfba09f5a8424329", "sha256": "fc1358118d05db30e74ed8bc80c1b17ceef6cb8e9ed4339ee26bb6144358cc8f" }, "downloads": -1, "filename": "lovely.testlayers-0.5.1.tar.gz", "has_sig": false, "md5_digest": "f7e04b1d48b0bd4dbfba09f5a8424329", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 49310, "upload_time": "2013-07-01T09:54:33", "url": "https://files.pythonhosted.org/packages/bd/c4/fe145167bf924dbb89ed32e88b5a81b219046d9f129b625b98feb1039043/lovely.testlayers-0.5.1.tar.gz" } ], "0.5.2": [ { "comment_text": "", "digests": { "md5": "a03abbda63acc07a67d6ac2fb2f4095a", "sha256": "6eb76fa7594cd9291bcfadb21b93a1f3096c9aed74eaa4df2ec7106355d21f84" }, "downloads": -1, "filename": "lovely.testlayers-0.5.2.tar.gz", "has_sig": false, "md5_digest": "a03abbda63acc07a67d6ac2fb2f4095a", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 49380, "upload_time": "2013-07-01T10:06:11", "url": "https://files.pythonhosted.org/packages/df/43/f3cca16db6ccdb7cb6cbcfc2760b2a3d5154e6e29a10002b48eba9c73bab/lovely.testlayers-0.5.2.tar.gz" } ], "0.5.3": [ { "comment_text": "", "digests": { "md5": "7f273b85928a3e53055627e0908d6d29", "sha256": "3593933123ee255a3896ad63ce49f1f650b9c68d21d78abbe1c4273cc9d51fe9" }, "downloads": -1, "filename": "lovely.testlayers-0.5.3.tar.gz", "has_sig": false, "md5_digest": "7f273b85928a3e53055627e0908d6d29", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 49443, "upload_time": "2013-07-01T13:17:01", "url": "https://files.pythonhosted.org/packages/c4/f0/b28fd12df3f1cf4e50ceb274156338d1dadd66e610106adab57e6e00097d/lovely.testlayers-0.5.3.tar.gz" } ], "0.6.0": [ { "comment_text": "", "digests": { "md5": "429e4cb89f73a6e4ce04dd712fdea45b", "sha256": "229bdf3d164959095e501db8e6492855e10a743299a27b7da5e57bf392746152" }, "downloads": -1, "filename": "lovely.testlayers-0.6.0.tar.gz", "has_sig": false, "md5_digest": "429e4cb89f73a6e4ce04dd712fdea45b", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 51355, "upload_time": "2013-09-06T06:54:19", "url": "https://files.pythonhosted.org/packages/cf/94/f3a869aad81e938dd6fa1a248f51d664cbc8f7daa0d3f05fe28e540559a8/lovely.testlayers-0.6.0.tar.gz" } ], "0.6.1": [ { "comment_text": "", "digests": { "md5": "bfb12444ee55ef4e9b353493259e6bea", "sha256": "c877dc0a2a73ef5f3aa0a0e5d865082576f1ad94b95db2fb2551b3af4d597d34" }, "downloads": -1, "filename": "lovely.testlayers-0.6.1.tar.gz", "has_sig": false, "md5_digest": "bfb12444ee55ef4e9b353493259e6bea", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 49836, "upload_time": "2015-03-12T11:27:29", "url": "https://files.pythonhosted.org/packages/fb/02/23d0ab5ffc9c62fa074321c0ff53bc143f6a1726689b4e1f5ca06f28061c/lovely.testlayers-0.6.1.tar.gz" } ], "0.6.2": [ { "comment_text": "", "digests": { "md5": "7bb1cf4c23c1e1f0b0437d9f2cd5e773", "sha256": "23b8407652bd7a3100617bafa40b95be24ab2676cf130ab55fd2b11b22687108" }, "downloads": -1, "filename": "lovely.testlayers-0.6.2.tar.gz", "has_sig": false, "md5_digest": "7bb1cf4c23c1e1f0b0437d9f2cd5e773", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 50044, "upload_time": "2015-03-13T08:14:13", "url": "https://files.pythonhosted.org/packages/5a/0e/4851023165ccf058997303688d81b74a1c4c6fa3fde030eccd1cc7714480/lovely.testlayers-0.6.2.tar.gz" } ], "0.6.3": [ { "comment_text": "", "digests": { "md5": "e525406843e11133bf54932772abb2b8", "sha256": "393f5046da4ee0ef93489b0ebc9cbfd957abeee85a25fc0d55a036bccd9c3159" }, "downloads": -1, "filename": "lovely.testlayers-0.6.3.tar.gz", "has_sig": false, "md5_digest": "e525406843e11133bf54932772abb2b8", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 50153, "upload_time": "2015-10-16T13:33:49", "url": "https://files.pythonhosted.org/packages/ca/cb/ef65d4c43c95542c2b553adea3c622e13fa681f961651b56a26d067ea4e9/lovely.testlayers-0.6.3.tar.gz" } ], "0.7.0": [ { "comment_text": "", "digests": { "md5": "98a6752c27ed6b4eda252e7ae539de01", "sha256": "a55737cfa4909ff34e256f91d442fa4690e4fb15c1b94390fdcafc8a1acbce3f" }, "downloads": -1, "filename": "lovely.testlayers-0.7.0.tar.gz", "has_sig": false, "md5_digest": "98a6752c27ed6b4eda252e7ae539de01", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 62606, "upload_time": "2016-09-07T07:22:01", "url": "https://files.pythonhosted.org/packages/26/ee/decbd99a6c32b2db1213a281d410d7421ecddf43d2a82ebc526b51628412/lovely.testlayers-0.7.0.tar.gz" } ], "0.7.1": [ { "comment_text": "", "digests": { "md5": "fa14d49fbd44f1de58717b322301ecc0", "sha256": "e81700f29becdbbd5b0c39bf02a5029a35ff1e95bfa0b141209d8a77a920ceb1" }, "downloads": -1, "filename": "lovely.testlayers-0.7.1.tar.gz", "has_sig": false, "md5_digest": "fa14d49fbd44f1de58717b322301ecc0", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 63841, "upload_time": "2016-09-12T12:39:19", "url": "https://files.pythonhosted.org/packages/97/89/1c9466c977f574302fd1ee2a1bbc018e7f3ad7b3e2edb00bfb897eb2dc80/lovely.testlayers-0.7.1.tar.gz" } ] }, "urls": [ { "comment_text": "", "digests": { "md5": "fa14d49fbd44f1de58717b322301ecc0", "sha256": "e81700f29becdbbd5b0c39bf02a5029a35ff1e95bfa0b141209d8a77a920ceb1" }, "downloads": -1, "filename": "lovely.testlayers-0.7.1.tar.gz", "has_sig": false, "md5_digest": "fa14d49fbd44f1de58717b322301ecc0", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 63841, "upload_time": "2016-09-12T12:39:19", "url": "https://files.pythonhosted.org/packages/97/89/1c9466c977f574302fd1ee2a1bbc018e7f3ad7b3e2edb00bfb897eb2dc80/lovely.testlayers-0.7.1.tar.gz" } ] }