--- a/devtools/__init__.py Thu Aug 26 15:05:03 2010 +0200
+++ b/devtools/__init__.py Wed Sep 01 15:56:21 2010 +0200
@@ -96,6 +96,7 @@
set_language = False
read_instance_schema = False
init_repository = True
+ db_require_setup = True
options = cwconfig.merge_options(ServerConfiguration.options + (
('anonymous-user',
{'type' : 'string',
@@ -195,6 +196,27 @@
self.init_repository = sourcefile is None
self.sourcefile = sourcefile
+class RealDatabaseConfiguration(ApptestConfiguration):
+ """configuration class for tests to run on a real database.
+
+ The intialization is done by specifying a source file path.
+
+ Important note: init_test_database / reset_test_database steps are
+ skipped. It's thus up to the test developer to implement setUp/tearDown
+ accordingly.
+
+ Example usage::
+
+ class MyTests(CubicWebTC):
+ _config = RealDatabseConfiguration('myapp',
+ sourcefile='/path/to/sources')
+ def test_something(self):
+ rset = self.execute('Any X WHERE X is CWUser')
+ self.view('foaf', rset)
+
+ """
+ db_require_setup = False # skip init_db / reset_db steps
+ read_instance_schema = True # read schema from database
# test database handling #######################################################
@@ -204,12 +226,13 @@
config = config or TestServerConfiguration(configdir)
sources = config.sources()
driver = sources['system']['db-driver']
- if driver == 'sqlite':
- init_test_database_sqlite(config)
- elif driver == 'postgres':
- init_test_database_postgres(config)
- else:
- raise ValueError('no initialization function for driver %r' % driver)
+ if config.db_require_setup:
+ if driver == 'sqlite':
+ init_test_database_sqlite(config)
+ elif driver == 'postgres':
+ init_test_database_postgres(config)
+ else:
+ raise ValueError('no initialization function for driver %r' % driver)
config._cubes = None # avoid assertion error
repo, cnx = in_memory_cnx(config, unicode(sources['admin']['login']),
password=sources['admin']['password'] or 'xxx')
@@ -220,6 +243,8 @@
def reset_test_database(config):
"""init a test database for a specific driver"""
+ if not config.db_require_setup:
+ return
driver = config.sources()['system']['db-driver']
if driver == 'sqlite':
reset_test_database_sqlite(config)
--- a/devtools/testlib.py Thu Aug 26 15:05:03 2010 +0200
+++ b/devtools/testlib.py Wed Sep 01 15:56:21 2010 +0200
@@ -25,6 +25,7 @@
import sys
import re
from urllib import unquote
+import urlparse
from math import log
from contextlib import contextmanager
from warnings import warn
@@ -142,6 +143,29 @@
cwconfig.SMTP = MockSMTP
+class TestCaseConnectionProxy(object):
+ """thin wrapper around `cubicweb.dbapi.Connection` context-manager
+ used in CubicWebTC (cf. `cubicweb.devtools.testlib.CubicWebTC.login` method)
+
+ It just proxies to the default connection context manager but
+ restores the original connection on exit.
+ """
+ def __init__(self, testcase, cnx):
+ self.testcase = testcase
+ self.cnx = cnx
+
+ def __getattr__(self, attrname):
+ return getattr(self.cnx, attrname)
+
+ def __enter__(self):
+ return self.cnx.__enter__()
+
+ def __exit__(self, exctype, exc, tb):
+ try:
+ return self.cnx.__exit__(exctype, exc, tb)
+ finally:
+ self.cnx.close()
+ self.testcase.restore_connection()
# base class for cubicweb tests requiring a full cw environments ###############
@@ -331,7 +355,7 @@
self._cnxs.append(self.cnx)
if login == self.vreg.config.anonymous_user()[0]:
self.cnx.anonymous_connection = True
- return self.cnx
+ return TestCaseConnectionProxy(self, self.cnx)
def restore_connection(self):
if not self.cnx is self._orig_cnx[0]:
@@ -528,6 +552,30 @@
raise
return result
+ def req_from_url(self, url):
+ """parses `url` and builds the corresponding CW-web request
+
+ req.form will be setup using the url's query string
+ """
+ req = self.request()
+ if isinstance(url, unicode):
+ url = url.encode(req.encoding) # req.setup_params() expects encoded strings
+ querystring = urlparse.urlparse(url)[-2]
+ params = urlparse.parse_qs(querystring)
+ req.setup_params(params)
+ return req
+
+ def url_publish(self, url):
+ """takes `url`, uses application's app_resolver to find the
+ appropriate controller, and publishes the result.
+
+ This should pretty much correspond to what occurs in a real CW server
+ except the apache-rewriter component is not called.
+ """
+ req = self.req_from_url(url)
+ ctrlid, rset = self.app.url_resolver.process(req, req.relative_path(False))
+ return self.ctrl_publish(req, ctrlid)
+
def expect_redirect(self, callback, req):
"""call the given callback with req as argument, expecting to get a
Redirect exception
--- a/doc/book/en/devrepo/testing.rst Thu Aug 26 15:05:03 2010 +0200
+++ b/doc/book/en/devrepo/testing.rst Wed Sep 01 15:56:21 2010 +0200
@@ -212,6 +212,37 @@
auto_populate cannot guess by itself; these must yield resultsets
against which views may be selected.
+Testing on a real-life database
+-------------------------------
+
+The ``CubicWebTC`` class uses the `cubicweb.devtools.ApptestConfiguration`
+configuration class to setup its testing environment (database driver,
+user password, application home, and so on). The `cubicweb.devtools`
+module also provides a `RealDatabaseConfiguration`
+class that will read a regular cubicweb sources file to fetch all
+this information but will also prevent the database to be initalized
+and reset between tests.
+
+For a test class to use a specific configuration, you have to set
+the `_config` class attribute on the class as in:
+
+.. sourcecode:: python
+
+ from cubicweb.devtools import RealDatabaseConfiguration
+ from cubicweb.devtools.testlib import CubicWebTC
+
+ class BlogRealDatabaseTC(CubicWebTC):
+ _config = RealDatabaseConfiguration('blog',
+ sourcefile='/path/to/realdb_sources')
+
+ def test_blog_rss(self):
+ req = self.request()
+ rset = req.execute('Any B ORDERBY D DESC WHERE B is BlogEntry, '
+ 'B created_by U, U login "logilab", B creation_date D')
+ self.view('rss', rset)
+
+
+
Testing with other cubes
------------------------
--- a/hooks/syncschema.py Thu Aug 26 15:05:03 2010 +0200
+++ b/hooks/syncschema.py Wed Sep 01 15:56:21 2010 +0200
@@ -521,10 +521,16 @@
insert_rdef_on_subclasses(session, eschema, rschema, rdefdef,
{'composite': entity.composite})
else:
+ if rschema.symmetric:
+ # for symmetric relations, rdefs will store relation definitions
+ # in both ways (i.e. (subj -> obj) and (obj -> subj))
+ relation_already_defined = len(rschema.rdefs) > 2
+ else:
+ relation_already_defined = len(rschema.rdefs) > 1
# need to create the relation if no relation definition in the
# schema and if it has not been added during other event of the same
# transaction
- if not (len(rschema.rdefs) > 1 or
+ if not (relation_already_defined or
rtype in session.transaction_data.get('createdtables', ())):
rschema = schema.rschema(rtype)
# create the necessary table
--- a/server/test/data/migratedapp/schema.py Thu Aug 26 15:05:03 2010 +0200
+++ b/server/test/data/migratedapp/schema.py Wed Sep 01 15:56:21 2010 +0200
@@ -138,6 +138,9 @@
cp = String(maxsize=12)
ville= String(maxsize=32)
+class same_as(RelationDefinition):
+ subject = ('Societe',)
+ object = 'ExternalUri'
class evaluee(RelationDefinition):
subject = ('Personne', 'CWUser', 'Societe')
--- a/server/test/unittest_migractions.py Thu Aug 26 15:05:03 2010 +0200
+++ b/server/test/unittest_migractions.py Wed Sep 01 15:56:21 2010 +0200
@@ -545,5 +545,15 @@
self.assertEquals(self.schema['Note'].specializes(), None)
self.assertEquals(self.schema['Text'].specializes(), None)
+
+ def test_add_symmetric_relation_type(self):
+ same_as_sql = self.mh.sqlexec("SELECT sql FROM sqlite_master WHERE type='table' "
+ "and name='same_as_relation'")
+ self.failIf(same_as_sql)
+ self.mh.cmd_add_relation_type('same_as')
+ same_as_sql = self.mh.sqlexec("SELECT sql FROM sqlite_master WHERE type='table' "
+ "and name='same_as_relation'")
+ self.failUnless(same_as_sql)
+
if __name__ == '__main__':
unittest_main()
--- a/test/unittest_entity.py Thu Aug 26 15:05:03 2010 +0200
+++ b/test/unittest_entity.py Wed Sep 01 15:56:21 2010 +0200
@@ -329,7 +329,7 @@
e['content'] = 'du *texte*'
e['content_format'] = 'text/plain'
self.assertEquals(e.printable_value('content'),
- '<p>\ndu *texte*\n</p>')
+ '<p>\ndu *texte*<br/>\n</p>')
e['title'] = 'zou'
e['content'] = '''\
a title