[Checkins] SVN: m01.mongofake/trunk/ - include m01.stub for testing mongofake components and compare with real

Roger Ineichen cvs-admin at zope.org
Sun Dec 16 07:48:54 UTC 2012


Log message for revision 128680:
  - include m01.stub for testing mongofake components and compare with real
    pymongo output
  - implemented some basic tests and run them with m01.stub and m01.mongofake
    libraries
  - implemented correct response for collection update method based on new
    pymongo 2.4. More will follow if more tests get implemented
  - switch from lovely.importchecker to p01.recipe.setup importchecker which
    doesn't warn about bad svn url during buildout process
  - update TODO.txt
  - prepare release

Changed:
  U   m01.mongofake/trunk/CHANGES.txt
  U   m01.mongofake/trunk/TODO.txt
  U   m01.mongofake/trunk/buildout.cfg
  U   m01.mongofake/trunk/setup.py
  U   m01.mongofake/trunk/src/m01/mongofake/__init__.py
  A   m01.mongofake/trunk/src/m01/mongofake/testing/
  A   m01.mongofake/trunk/src/m01/mongofake/testing/__init__.py
  A   m01.mongofake/trunk/src/m01/mongofake/testing.txt
  U   m01.mongofake/trunk/src/m01/mongofake/tests.py

-=-
Modified: m01.mongofake/trunk/CHANGES.txt
===================================================================
--- m01.mongofake/trunk/CHANGES.txt	2012-12-15 19:28:08 UTC (rev 128679)
+++ m01.mongofake/trunk/CHANGES.txt	2012-12-16 07:48:53 UTC (rev 128680)
@@ -2,12 +2,21 @@
 CHANGES
 =======
 
-0.2.0 (unreleased)
+0.2.0 (2012-12-16)
 ------------------
 
 - feature: support pymongo 2.4 API and support new safe write concern
 
+- implemented some basic tests and run them with m01.stub and m01.mongofake
+  libraries
 
+- implemented correct response for collection update method based on new
+  pymongo 2.4. More will follow if more tests get implemented
+
+- switch from lovely.importchecker to p01.recipe.setup importchecker which
+  doesn't warn about bad svn url during buildout process
+
+
 0.1.1 (2012-12-10)
 ------------------
 

Modified: m01.mongofake/trunk/TODO.txt
===================================================================
--- m01.mongofake/trunk/TODO.txt	2012-12-15 19:28:08 UTC (rev 128679)
+++ m01.mongofake/trunk/TODO.txt	2012-12-16 07:48:53 UTC (rev 128680)
@@ -2,4 +2,8 @@
 TODO
 ====
 
-- add some fake mongodb tests
\ No newline at end of file
+- implement more pymongo methods
+
+- implement m01.mongofake response similar then given from pymongo
+
+- add more tests and compare output from m01.mongofake and pymongo

Modified: m01.mongofake/trunk/buildout.cfg
===================================================================
--- m01.mongofake/trunk/buildout.cfg	2012-12-15 19:28:08 UTC (rev 128679)
+++ m01.mongofake/trunk/buildout.cfg	2012-12-16 07:48:53 UTC (rev 128680)
@@ -6,17 +6,17 @@
 
 [test]
 recipe = zc.recipe.testrunner
-eggs = m01.mongofake
+eggs = m01.mongofake [test]
 
 
 [checker]
-recipe = lovely.recipe:importchecker
+recipe = p01.recipe.setup:importchecker
 path = src/m01.mongofake
 
 
 [coverage]
 recipe = zc.recipe.testrunner
-eggs = m01.mongofake
+eggs = m01.mongofake [test]
 defaults = ['--all', '--coverage', '../../coverage']
 
 

Modified: m01.mongofake/trunk/setup.py
===================================================================
--- m01.mongofake/trunk/setup.py	2012-12-15 19:28:08 UTC (rev 128679)
+++ m01.mongofake/trunk/setup.py	2012-12-16 07:48:53 UTC (rev 128680)
@@ -21,7 +21,7 @@
 
 setup(
     name='m01.mongofake',
-    version='0.2.0dev',
+    version='0.2.0',
     author='Zope Foundation and Contributors',
     author_email='zope-dev at zope.org',
     description="Fake MongoDB implementation",
@@ -45,7 +45,11 @@
     packages=find_packages('src'),
     include_package_data=True,
     package_dir={'': 'src'},
-    extras_require=dict(),
+    extras_require=dict(
+        test=[
+            'm01.stub',
+            'zope.testing',
+        ]),
     install_requires=[
         'setuptools',
         'pymongo',

Modified: m01.mongofake/trunk/src/m01/mongofake/__init__.py
===================================================================
--- m01.mongofake/trunk/src/m01/mongofake/__init__.py	2012-12-15 19:28:08 UTC (rev 128679)
+++ m01.mongofake/trunk/src/m01/mongofake/__init__.py	2012-12-16 07:48:53 UTC (rev 128680)
@@ -16,14 +16,18 @@
 """
 __docformat__ = "reStructuredText"
 
+import calendar
 import copy
+import pprint as pp
 import re
+import struct
+import time
 import types
-import pprint as pp
 
 import bson.objectid
 import bson.son
-import pymongo.cursor 
+import pymongo.cursor
+import pymongo.database
 
 
 ###############################################################################
@@ -118,6 +122,31 @@
    ])
 
 
+def getObjectId(secs=0):
+    """Knows how to generate similar ObjectId based on integer (counter)
+
+    Note: this method can get used if you need to define similar ObjectId
+    in a non persistent environment if need to bootstrap mongo containers.
+    """
+    time_tuple = time.gmtime(secs)
+    ts = calendar.timegm(time_tuple)
+    oid = struct.pack(">i", int(ts)) + "\x00" * 8
+    return bson.objectid.ObjectId(oid)
+
+
+def getObjectIdByTimeStr(tStr, format="%Y-%m-%d %H:%M:%S"):
+    """Knows how to generate similar ObjectId based on a time string
+
+    The time string format used by default is ``%Y-%m-%d %H:%M:%S``.
+    Use the current development time which could prevent duplicated
+    ObjectId. At least some kind of ;-)
+    """
+    time.strptime(tStr, "%Y-%m-%d %H:%M:%S")
+    ts = time.mktime(tStr)
+    oid = struct.pack(">i", int(ts)) + "\x00" * 8
+    return bson.objectid.ObjectId(oid)
+
+
 ###############################################################################
 #
 # fake MongoDB
@@ -329,7 +358,7 @@
 
     def __init__(self, database, name):
         self.database = database
-        self.name = name
+        self.name = unicode(name)
         self.full_name = '%s.%s' % (database, name)
         self.docs = OrderedData()
 
@@ -353,18 +382,37 @@
         if not isinstance(upsert, types.BooleanType):
             raise TypeError("upsert must be an instance of bool")
 
+        existing = False
+        counter = 0
         for key, doc in list(self.docs.items()):
+            if (counter > 0 and not multi):
+                break 
             for k, v in spec.items():
                 if k in doc and v == doc[k]:
                     setData = document.get('$set')
                     if setData is not None:
                         # do a partial update based on $set data
                         for pk, pv in setData.items():
-                            doc[pk] = pv
+                            doc[unicode(pk)] = pv
+                        counter += 1
+                        existing = True
                     else:
-                        self.docs[key] = document
+                        d = {}
+                        for k, v in list(document.items()):
+                            # use unicode keys as mongodb does
+                            d[unicode(k)] = v
+                        self.docs[unicode(key)] = d
+                        existing = True
+                        counter += 1
                     break
 
+        cid = 42
+        ok = 1.0
+        err = None
+        return {u'updatedExisting': existing, u'connectionId': cid, u'ok': ok,
+                u'err': err, u'n': counter}
+
+
     def save(self, to_save, manipulate=True, safe=None, check_keys=True,
         **kwargs):
         if not isinstance(to_save, types.DictType):
@@ -470,20 +518,36 @@
             as_dict[field] = 1
         return as_dict
 
+    def __repr__(self):
+        return "%s(%r, %r)" % (self.__class__.__name__, self.database,
+            self.name)
 
+
 class FakeDatabase(object):
     """Fake mongoDB database."""
 
     def __init__(self, connection, name):
-        self.connection = connection
-        self.name = name
+        pymongo.database._check_name(name)
+        self.__name = unicode(name)
+        self.__connection = connection
         self.cols = {}
 
+    @property
+    def connection(self):
+        return self.__connection
+
+    @property
+    def name(self):
+        return self.__name
+
     def clear(self):
         for k, col in self.cols.items():
             col.clear()
             del self.cols[k]
 
+    def create_collection(self, name, **kw):
+        return True
+
     def collection_names(self):
         return list(self.cols.keys())
 
@@ -497,33 +561,163 @@
     def __getitem__(self, name):
         return self.__getattr__(name)
 
-    def create_collection(self, name, **kw):
-        return True
+    def __iter__(self):
+        return self
 
+    def next(self):
+        raise TypeError("'Database' object is not iterable")
 
+    def __call__(self, *args, **kwargs):
+        """This is only here so that some API misusages are easier to debug.
+        """
+        raise TypeError("'Database' object is not callable. If you meant to "
+                        "call the '%s' method on a '%s' object it is "
+                        "failing because no such method exists." % (
+                            self.__name, self.__connection.__class__.__name__))
+
+    def __repr__(self):
+        return "%s(%r, %r)" % (self.__class__.__name__, self.__connection,
+            self.__name)
+
+
 class FakeMongoClient(object):
     """Fake MongoDB MongoClient."""
 
+    HOST = 'localhost'
+    POST = 27017
+
+    __max_bson_size = 4 * 1024 * 1024
+
     def __init__(self):
-        self.dbs = {}
+        self.__dbs = {}
+        self.__host = None
+        self.__port = None
+        self.__max_pool_size = 10
+        self.__document_class = {}
+        self.__tz_aware = False
+        self.__nodes = []
 
-    def __call__(self, host='localhost', port=27017, tz_aware=True):
+    @property
+    def dbs(self):
+        return self.__dbs
+
+    def __call__(self, host=None, port=None, max_pool_size=10,
+        document_class=dict, tz_aware=False, _connect=True, **kwargs):
+        if host is None:
+            host = self.HOST
+        if isinstance(host, basestring):
+            host = [host]
+        if port is None:
+            port = self.PORT
+        if not isinstance(port, int):
+            raise TypeError("port must be an instance of int")
+
+        self.__max_pool_size = max_pool_size
+        self.__document_class = document_class
+        self.__tz_aware = tz_aware
+
+        seeds = set()
+        username = None
+        password = None
+        db = None
+        opts = {}
+        for entity in host:
+            if "://" in entity:
+                if entity.startswith("mongodb://"):
+                    res = pymongo.uri_parser.parse_uri(entity, port)
+                    seeds.update(res["nodelist"])
+                    username = res["username"] or username
+                    password = res["password"] or password
+                    db = res["database"] or db
+                    opts = res["options"]
+                else:
+                    idx = entity.find("://")
+                    raise pymongo.errors.InvalidURI("Invalid URI scheme: %s" % (
+                        entity[:idx],))
+            else:
+                seeds.update(pymongo.uri_parser.split_hosts(entity, port))
+        if not seeds:
+            raise pymongo.errors.ConfigurationError(
+                "need to specify at least one host")
+
+        self.__nodes = seeds
+        self.__host = None
+        self.__port = None
+
+        if _connect:
+            # _connect=False is not supported yet because we need to implement
+            # some fake host, port setup concept first
+            try:
+                self.__find_node(seeds)
+            except pymongo.errors.AutoReconnect, e:
+                # ConnectionFailure makes more sense here than AutoReconnect
+                raise pymongo.errors.ConnectionFailure(str(e))
+
         return self
 
+    def __find_node(self, seeds=None):
+        # very simple find node implementation
+        errors = []
+        mongos_candidates = []
+        candidates = seeds or self.__nodes.copy()
+        for candidate in candidates:
+            node, ismaster, isdbgrid, res_time = self.__try_node(candidate)
+            return node
+
+        # couldn't find a suitable host.
+        self.disconnect()
+        raise pymongo.errors.AutoReconnect(', '.join(errors))
+
+    def __try_node(self, node):
+        self.disconnect()
+        self.__host, self.__port = node
+        # return node and some fake data
+        ismaster = True
+        isdbgrid = False
+        res_time = None
+        return node, ismaster, isdbgrid, res_time
+
+    @property
+    def host(self):
+        return self.__host
+
+    @property
+    def port(self):
+        return self.__port
+
+    @property
+    def tz_aware(self):
+        return self.__tz_aware
+
+    @property
+    def max_bson_size(self):
+        return self.__max_bson_size
+
+    @property
+    def nodes(self):
+        """List of all known nodes."""
+        return self.__nodes
+
     def drop_database(self, name):
-        db = self.dbs.get(name)
+        db = self.__dbs.get(name)
         if db is not None:
             db.clear()
-            del self.dbs[name]
+            del self.__dbs[name]
 
+    def database_names(self):
+        return list(self.__dbs.keys())
+
     def disconnect(self):
         pass
 
-    def database_names(self):
-        return list(self.dbs.keys())
+    def close(self):
+        self.disconnect()
 
+    def alive(self):
+        return True
+
     def __getattr__(self, name):
-        db = self.dbs.get(name)
+        db = self.__dbs.get(name)
         if db is None:
             db = FakeDatabase(self, name)
             self.dbs[name] = db
@@ -532,6 +726,26 @@
     def __getitem__(self, name):
         return self.__getattr__(name)
 
+    def __enter__(self):
+        return self
+
+    def __exit__(self, exc_type, exc_val, exc_tb):
+        self.disconnect()
+
+    def __iter__(self):
+        return self
+
+    def next(self):
+        raise TypeError("'%s' object is not iterable" % self.__class__.__name__)
+
+    def __repr__(self):
+        if len(self.__nodes) == 1:
+            return "%s(%r, %r)" % (self.__class__.__name__, self.__host, self.__port)
+        else:
+            nodes = ["%s:%d" % n for n in self.__nodes]
+            return "%s(%r)" % (self.__class__.__name__, nodes)
+
+
 class FakeMongoConnection(FakeMongoClient):
     """BBB: support old FakeMongoConnection class"""
 
@@ -540,7 +754,7 @@
 fakeMongoClient = FakeMongoClient()
 
 # BBB: support
-fakeMongoConnection = fakeMongoClient
+fakeMongoConnection = FakeMongoConnection()
 
 
 class FakeMongoConnectionPool(object):


Property changes on: m01.mongofake/trunk/src/m01/mongofake/testing
___________________________________________________________________
Added: svn:ignore
   + sandbox


Added: m01.mongofake/trunk/src/m01/mongofake/testing/__init__.py
===================================================================
--- m01.mongofake/trunk/src/m01/mongofake/testing/__init__.py	                        (rev 0)
+++ m01.mongofake/trunk/src/m01/mongofake/testing/__init__.py	2012-12-16 07:48:53 UTC (rev 128680)
@@ -0,0 +1,97 @@
+##############################################################################
+#
+# Copyright (c) 2012 Zope Foundation and Contributors.
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+##############################################################################
+"""
+$Id$
+"""
+__docformat__ = "reStructuredText"
+
+import os
+import unittest
+
+import pymongo
+
+import m01.stub.testing
+
+import m01.mongofake
+
+
+# mongo db name used for testing
+TEST_DB_NAME = 'm01_mongofake_database'
+
+
+###############################################################################
+#
+# test helper methods
+#
+###############################################################################
+
+_testClient = None
+
+def getTestClient():
+    return _testClient
+
+
+def getTestDatabase():
+    client = getTestClient()
+    return client[TEST_DB_NAME]
+
+
+def getTestCollection(collectionName='test'):
+    client = getTestClient()
+    db = client[TEST_DB_NAME]
+    return db[collectionName]
+
+
+def dropTestDatabase():
+    client = getTestClient()
+    client.drop_database(TEST_DB_NAME)
+
+
+###############################################################################
+#
+# test setup methods
+#
+###############################################################################
+
+# fake mongodb setup
+def setUpFakeMongo(test=None):
+    """Setup fake (singleton) mongo client"""
+    global _testClient
+    host = 'localhost'
+    port = 45017
+    _testClient = m01.mongofake.fakeMongoClient(host, port)
+
+
+def tearDownFakeMongo(test=None):
+    """Tear down fake mongo client"""
+    global _testClient
+    _testClient = None
+
+
+# stub mongodb server
+def setUpStubMongo(test=None):
+    """Setup real empty mongodb"""
+    host = 'localhost'
+    port = 45017
+    sandBoxDir = os.path.join(os.path.dirname(__file__), 'sandbox')
+    m01.stub.testing.startMongoDBServer(host, port, sandBoxDir=sandBoxDir)
+    # ensure that we use a a real MongoClient
+    global _testClient
+    _testClient = pymongo.MongoClient(host, port)
+
+
+def tearDownStubMongo(test=None):
+    """Tear down real mongodb"""
+    sleep = 0.5
+    m01.stub.testing.stopMongoDBServer(sleep)


Property changes on: m01.mongofake/trunk/src/m01/mongofake/testing/__init__.py
___________________________________________________________________
Added: svn:keywords
   + Date Author Id Revision
Added: svn:eol-style
   + native

Added: m01.mongofake/trunk/src/m01/mongofake/testing.txt
===================================================================
--- m01.mongofake/trunk/src/m01/mongofake/testing.txt	                        (rev 0)
+++ m01.mongofake/trunk/src/m01/mongofake/testing.txt	2012-12-16 07:48:53 UTC (rev 128680)
@@ -0,0 +1,112 @@
+=======
+pymongo
+=======
+
+This test, if running with --all or level 2 will donaload and start a real
+mongodb instance on localhost:45017. See testing setup for more information.
+
+  >>> from m01.mongofake import getObjectId
+  >>> from m01.mongofake import pprint
+
+helper
+------
+
+We can simply use our test helper methods for get the current mongodb test
+client, database or collection.
+
+  >>> from m01.mongofake.testing import getTestClient
+  >>> getTestClient()
+  MongoClient('localhost', 45017)
+
+  >>> from m01.mongofake.testing import getTestDatabase
+  >>> getTestDatabase()
+  Database(MongoClient('localhost', 45017), u'm01_mongofake_database')
+
+  >>> from m01.mongofake.testing import getTestCollection
+  >>> getTestCollection()
+  Collection(Database(MongoClient('localhost', 45017), u'm01_mongofake_database'), u'test')
+
+
+pymongo
+-------
+
+Let's test some simple pymongo methods. Setup up some objects and get them
+back. First add a new collection:
+
+  >>> client = getTestClient()
+  >>> db = client.m01_mongofake_database
+  >>> collection = db.fruits
+  >>> data = {'_id': getObjectId(1),
+  ...         'name': u'apple',
+  ...         'color': u'green'} 
+  >>> oid = collection.insert(data)
+  >>> oid
+  ObjectId('000000010000000000000000')
+
+find_one:
+
+  >>> data = client.m01_mongofake_database.fruits.find_one({'_id': oid})
+  >>> pprint(data)
+  {u'_id': ObjectId('000000010000000000000000'),
+   u'color': u'green',
+   u'name': u'apple'}
+
+update:
+
+  >>> data['fresh'] = True
+  >>> client.m01_mongofake_database.fruits.update({'_id': oid}, data)
+  {u'updatedExisting': True, u'connectionId': 2, u'ok': 1.0, u'err': None, u'n': 1}
+
+find:
+
+  >>> for doc in client.m01_mongofake_database.fruits.find({'_id': oid}):
+  ...     pprint(doc)
+  {u'_id': ObjectId('000000010000000000000000'),
+   u'color': u'green',
+   u'fresh': True,
+   u'name': u'apple'}
+
+
+add more apples:
+
+  >>> data = {'_id': getObjectId(2),
+  ...         'name': u'apple',
+  ...         'color': u'red',
+  ...         'fresh': True,} 
+  >>> db.fruits.insert(data)
+  ObjectId('000000020000000000000000')
+
+find (all):
+
+  >>> for doc in client.m01_mongofake_database.fruits.find({'fresh': True}):
+  ...     pprint(doc)
+  {u'_id': ObjectId('000000010000000000000000'),
+   u'color': u'green',
+   u'fresh': True,
+   u'name': u'apple'}
+  {u'_id': ObjectId('000000020000000000000000'),
+   u'color': u'red',
+   u'fresh': True,
+   u'name': u'apple'}
+
+update multi (update response):
+
+  >>> data = {'$set': {'fresh': False}}
+  >>> client.m01_mongofake_database.fruits.update({'fresh': True}, data,
+  ...    multi=True)
+  {u'updatedExisting': True, u'connectionId': 2, u'ok': 1.0, u'err': None, u'n': 2}
+
+  >>> cursor = client.m01_mongofake_database.fruits.find({'fresh': False})
+  >>> cursor.count()
+  2
+
+  >>> for doc in client.m01_mongofake_database.fruits.find({'fresh': False}):
+  ...     pprint(doc)
+  {u'_id': ObjectId('000000010000000000000000'),
+   u'color': u'green',
+   u'fresh': False,
+   u'name': u'apple'}
+  {u'_id': ObjectId('000000020000000000000000'),
+   u'color': u'red',
+   u'fresh': False,
+   u'name': u'apple'}


Property changes on: m01.mongofake/trunk/src/m01/mongofake/testing.txt
___________________________________________________________________
Added: svn:keywords
   + Date Author Id Revision
Added: svn:eol-style
   + native

Modified: m01.mongofake/trunk/src/m01/mongofake/tests.py
===================================================================
--- m01.mongofake/trunk/src/m01/mongofake/tests.py	2012-12-15 19:28:08 UTC (rev 128679)
+++ m01.mongofake/trunk/src/m01/mongofake/tests.py	2012-12-16 07:48:53 UTC (rev 128680)
@@ -16,17 +16,68 @@
 """
 __docformat__ = "reStructuredText"
 
+import re
 import unittest
 import doctest
 
+from zope.testing.renormalizing import RENormalizing
 
+import m01.mongofake.testing
+
+
+CHECKER = RENormalizing([
+   (re.compile('connectionId'), '...'),
+   ])
+
+
+FAKE_CHECKER = RENormalizing([
+   (re.compile('FakeMongoClient'), 'MongoClient'),
+   (re.compile('FakeDatabase'), 'Database'),
+   (re.compile('FakeCollection'), 'Collection'),
+   (re.compile("'connectionId': [0-9]+"), r"'connectionId': ..."),
+   ])
+
+
 def test_suite():
-    return unittest.TestSuite((
+    """This test suite will run the tests with the fake and a real mongodb and
+    make sure both output are the same.
+    """
+    suites = []
+    append = suites.append
+
+    # real mongo database tests using m01.stub using level 2 tests (--all)
+    testNames = ['testing.txt',
+                 ]
+    for name in testNames:
+        suite = unittest.TestSuite((
+            doctest.DocFileSuite(name,
+                setUp=m01.mongofake.testing.setUpStubMongo,
+                tearDown=m01.mongofake.testing.tearDownStubMongo,
+                optionflags=doctest.NORMALIZE_WHITESPACE|doctest.ELLIPSIS,
+                checker=CHECKER),
+            ))
+        suite.level = 2
+        append(suite)
+
+    # fake mongo database tests using FakeMongoClient
+    for name in testNames:
+        append(
+            doctest.DocFileSuite(name,
+                setUp=m01.mongofake.testing.setUpFakeMongo,
+                tearDown=m01.mongofake.testing.tearDownFakeMongo,
+                optionflags=doctest.NORMALIZE_WHITESPACE|doctest.ELLIPSIS,
+                checker=FAKE_CHECKER),
+        )
+
+    # additional non mongodb tests
+    append(
         doctest.DocFileSuite('README.txt',
-            optionflags=doctest.NORMALIZE_WHITESPACE|doctest.ELLIPSIS,
-            ),
-        ))
+            optionflags=doctest.NORMALIZE_WHITESPACE|doctest.ELLIPSIS),
+    )
 
+    # return test suite
+    return unittest.TestSuite(suites)
 
+
 if __name__=='__main__':
     unittest.main(defaultTest='test_suite')



More information about the checkins mailing list