Skip to content

Commit

Permalink
Fix Matcher for PyMongo bson types
Browse files Browse the repository at this point in the history
Before, something like {'_id': ObjectId('...')} matched no request
because MockupDB's vendored bson classes, such as ObjectId, failed
isinstance checks comparing themselves to PyMongo objects. Now, we
reimplement equality-checking for all such objects.

Fixes #13
  • Loading branch information
ajdavis committed Feb 24, 2018
1 parent 0638171 commit 27f29b0
Show file tree
Hide file tree
Showing 2 changed files with 80 additions and 6 deletions.
45 changes: 43 additions & 2 deletions mockupdb/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -273,6 +273,47 @@ def _get_c_string(data, position):
return _utf_8_decode(data[position:end], None, True)[0], end + 1


def _bson_values_equal(a, b):
# Check if values are either from our vendored bson or PyMongo's bson.
for value in (a, b):
if not hasattr(value, '_type_marker'):
# Normal equality.
return a == b

def marker(obj):
return getattr(obj, '_type_marker', None)

if marker(a) != marker(b):
return a == b

# Instances of Binary, ObjectId, etc. from our vendored bson don't equal
# instances of PyMongo's bson classes that users pass in as message specs,
# since isinstance() fails. Reimplement equality checks for each class.
key_fn = {
# Binary.
5: lambda obj: (obj.subtype, bytes(obj)),
# ObjectId.
7: lambda obj: obj.binary,
# Regex.
11: lambda obj: (obj.pattern, obj.flags),
# Code.
13: lambda obj: (obj.scope, str(obj)),
# Timestamp.
17: lambda obj: (obj.time, obj.inc),
# DBRef.
100: lambda obj: (obj.database, obj.collection, obj.id),
# MaxKey.
127: lambda obj: 127,
# MinKey.
255: lambda obj: 255,
}.get(marker(a))

if key_fn:
return key_fn(a) == key_fn(b)

return a == b


class _PeekableQueue(Queue):
"""Only safe from one consumer thread at a time."""
_NO_ITEM = object()
Expand Down Expand Up @@ -457,7 +498,7 @@ def _matches_docs(self, docs, other_docs):
if value is absent:
if key in other_doc:
return False
elif other_doc.get(key, None) != value:
elif not _bson_values_equal(value, other_doc.get(key, None)):
return False
if isinstance(doc, (OrderedDict, _bson.SON)):
if not isinstance(other_doc, (OrderedDict, _bson.SON)):
Expand Down Expand Up @@ -622,7 +663,7 @@ def _matches_docs(self, docs, other_docs):
if items and other_items:
if items[0][0].lower() != other_items[0][0].lower():
return False
if items[0][1] != other_items[0][1]:
if not _bson_values_equal(items[0][1], other_items[0][1]):
return False
return super(Command, self)._matches_docs(
[OrderedDict(items[1:])],
Expand Down
41 changes: 37 additions & 4 deletions tests/test_mockupdb.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
"""Test MockupDB."""

import contextlib
import os
import ssl
import sys

Expand All @@ -19,15 +18,15 @@
from Queue import Queue

# Tests depend on PyMongo's BSON implementation, but MockupDB itself does not.
from bson import SON
from bson import (SON, ObjectId, Binary, Regex, Code, Timestamp, DBRef,
MaxKey, MinKey)
from bson.codec_options import CodecOptions
from pymongo import MongoClient, message, WriteConcern

from mockupdb import (go, going,
from mockupdb import (_bson as mockup_bson, go, going,
Command, Matcher, MockupDB, Request,
OpDelete, OpInsert, OpQuery, OpUpdate,
DELETE_FLAGS, INSERT_FLAGS, UPDATE_FLAGS, QUERY_FLAGS)

from tests import unittest # unittest2 on Python 2.6.


Expand Down Expand Up @@ -196,6 +195,40 @@ def test_command_fields(self):
self.assertFalse(
Matcher(Command('a', b=1)).matches(Command('a', b=2)))

def test_bson_classes(self):
_id = '5a918f9fa08bff9c7688d3e1'

for a, b in [
(Binary(b'foo'), mockup_bson.Binary(b'foo')),
(ObjectId(_id), mockup_bson.ObjectId(_id)),
(Regex('foo', 'i'), mockup_bson.Regex('foo', 'i')),
(Code('foo'), mockup_bson.Code('foo')),
(Code('foo', {'x': 1}), mockup_bson.Code('foo', {'x': 1})),
(Timestamp(1, 2), mockup_bson.Timestamp(1, 2)),
(DBRef('coll', 1), mockup_bson.DBRef('coll', 1)),
(DBRef('coll', 1, 'db'), mockup_bson.DBRef('coll', 1, 'db')),
(MaxKey(), mockup_bson.MaxKey()),
(MinKey(), mockup_bson.MinKey()),
]:
# Basic case.
self.assertTrue(
Matcher(Command(y=b)).matches(Command(y=b)),
"MockupDB %r doesn't equal itself" % (b,))

# First Command argument is special, try comparing the second also.
self.assertTrue(
Matcher(Command('x', y=b)).matches(Command('x', y=b)),
"MockupDB %r doesn't equal itself" % (b,))

# In practice, users pass PyMongo classes in message specs.
self.assertTrue(
Matcher(Command(y=b)).matches(Command(y=a)),
"PyMongo %r != MockupDB %r" % (a, b))

self.assertTrue(
Matcher(Command('x', y=b)).matches(Command('x', y=a)),
"PyMongo %r != MockupDB %r" % (a, b))


class TestAutoresponds(unittest.TestCase):
def test_auto_dequeue(self):
Expand Down

0 comments on commit 27f29b0

Please sign in to comment.