Skip to content

Commit

Permalink
Fix parsing and repr for OP_UPDATE and OP_DELETE.
Browse files Browse the repository at this point in the history
  • Loading branch information
ajdavis committed Mar 23, 2015
1 parent 0e38a4e commit 0c6bce2
Show file tree
Hide file tree
Showing 2 changed files with 113 additions and 23 deletions.
52 changes: 44 additions & 8 deletions mockupdb/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -327,6 +327,7 @@ class Request(object):
"""
opcode = None
is_command = None
_flags_map = None

def __init__(self, *args, **kwargs):
self._flags = kwargs.pop('flags', None)
Expand Down Expand Up @@ -478,9 +479,12 @@ def __repr__(self):
parts.append(docs_repr(*self.docs))

if self._flags:
parts.append('flags=%s' % (
'|'.join(name for name, value in QUERY_FLAGS.items()
if self._flags & value)))
if self._flags_map:
parts.append('flags=%s' % (
'|'.join(name for name, value in self._flags_map.items()
if self._flags & value)))
else:
parts.append('flags=%d' % self._flags)

if self._namespace:
parts.append('namespace="%s"' % self._namespace)
Expand All @@ -492,6 +496,7 @@ class OpQuery(Request):
"""A query (besides a command) the client executes on the server."""
opcode = OP_QUERY
is_command = False
_flags_map = QUERY_FLAGS

@classmethod
def unpack(cls, msg, client, server, request_id):
Expand Down Expand Up @@ -665,6 +670,12 @@ def __repr__(self):
class _LegacyWrite(Request):
is_command = False


class OpInsert(_LegacyWrite):
"""A legacy OP_INSERT the client executes on the server."""
opcode = OP_INSERT
_flags_map = INSERT_FLAGS

@classmethod
def unpack(cls, msg, client, server, request_id):
"""Parse message and return an `OpInsert`.
Expand All @@ -679,19 +690,44 @@ def unpack(cls, msg, client, server, request_id):
request_id=request_id, server=server)


class OpInsert(_LegacyWrite):
"""A legacy OP_INSERT the client executes on the server."""
opcode = OP_INSERT


class OpUpdate(_LegacyWrite):
"""A legacy OP_UPDATE the client executes on the server."""
opcode = OP_UPDATE
_flags_map = UPDATE_FLAGS

@classmethod
def unpack(cls, msg, client, server, request_id):
"""Parse message and return an `OpUpdate`.
Takes the client message as bytes, the client and server socket objects,
and the client request id.
"""
# First 4 bytes of OP_UPDATE are "reserved".
namespace, pos = _get_c_string(msg, 4)
flags, = _UNPACK_INT(msg[pos:pos + 4])
docs = bson.decode_all(msg[pos+4:], CODEC_OPTIONS)
return cls(*docs, namespace=namespace, flags=flags, client=client,
request_id=request_id, server=server)


class OpDelete(_LegacyWrite):
"""A legacy OP_DELETE the client executes on the server."""
opcode = OP_DELETE
_flags_map = DELETE_FLAGS

@classmethod
def unpack(cls, msg, client, server, request_id):
"""Parse message and return an `OpDelete`.
Takes the client message as bytes, the client and server socket objects,
and the client request id.
"""
# First 4 bytes of OP_DELETE are "reserved".
namespace, pos = _get_c_string(msg, 4)
flags, = _UNPACK_INT(msg[pos:pos + 4])
docs = bson.decode_all(msg[pos+4:], CODEC_OPTIONS)
return cls(*docs, namespace=namespace, flags=flags, client=client,
request_id=request_id, server=server)


class OpReply(object):
Expand Down
84 changes: 69 additions & 15 deletions tests/test_mockupdb.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,18 +21,9 @@
from bson.codec_options import CodecOptions
from pymongo.errors import ConnectionFailure
from pymongo.topology_description import TOPOLOGY_TYPE
from pymongo import MongoClient, ReadPreference, message

from mockupdb import (Command,
Future,
go,
going,
MockupDB,
OpInsert,
OpReply,
OpQuery,
Request,
wait_until)
from pymongo import MongoClient, ReadPreference, message, WriteConcern

from mockupdb import *
from tests import unittest # unittest2 on Python 2.6.


Expand Down Expand Up @@ -105,16 +96,16 @@ def test_repr(self):
self.assertEqual('Request()', repr(Request()))
self.assertEqual('Request({})', repr(Request({})))
self.assertEqual('Request({})', repr(Request([{}])))
self.assertEqual('Request(flags=SlaveOkay)', repr(Request(flags=4)))
self.assertEqual('Request({}, flags=TailableCursor|AwaitData)',
repr(Request({}, flags=34)))
self.assertEqual('Request(flags=4)', repr(Request(flags=4)))

self.assertEqual('OpQuery({})', repr(OpQuery()))
self.assertEqual('OpQuery({})', repr(OpQuery({})))
self.assertEqual('OpQuery({})', repr(OpQuery([{}])))
self.assertEqual('OpQuery({}, flags=SlaveOkay)', repr(OpQuery(flags=4)))
self.assertEqual('OpQuery({}, flags=SlaveOkay)',
repr(OpQuery({}, flags=4)))
self.assertEqual('OpQuery({}, flags=TailableCursor|AwaitData)',
repr(OpQuery({}, flags=34)))

self.assertEqual('Command({})', repr(Command()))
self.assertEqual('Command({"foo": 1})', repr(Command('foo')))
Expand All @@ -127,6 +118,69 @@ def test_repr(self):
self.assertEqual('OpInsert({}, {})', repr(OpInsert({}, {})))


class TestLegacyWrites(unittest.TestCase):
def setUp(self):
self.server = MockupDB(auto_ismaster=True)
self.server.run()
self.addCleanup(self.server.stop)
self.client = MongoClient(self.server.uri)
self.collection = self.client.db.collection

def test_insert_one(self):
with going(self.collection.insert_one, {'_id': 1}) as future:
self.server.receives(OpInsert({'_id': 1}, flags=0))
self.server.receives(Command('getlasterror')).replies_to_gle()

self.assertEqual(1, future().inserted_id)

def test_insert_many(self):
collection = self.collection.with_options(write_concern=WriteConcern(0))
flags = INSERT_FLAGS['ContinueOnError']
docs = [{'_id': 1}, {'_id': 2}]
with going(collection.insert_many, docs, ordered=False) as future:
request = self.server.receives(OpInsert(docs, flags=flags))
self.assertEqual(1, request.flags)

self.assertEqual([1, 2], future().inserted_ids)

def test_replace_one(self):
with going(self.collection.replace_one, {}, {}) as future:
self.server.receives(OpUpdate({}, {}, flags=0))
request = self.server.receives(Command('getlasterror'))
request.replies_to_gle(upserted=1)

self.assertEqual(1, future().upserted_id)

def test_update_many(self):
flags = UPDATE_FLAGS['MultiUpdate']
with going(self.collection.update_many, {}, {'$unset': 'a'}) as future:
update = self.server.receives(OpUpdate({}, {}, flags=flags))
self.assertEqual(2, update.flags)
gle = self.server.receives(Command('getlasterror'))
gle.replies_to_gle(upserted=1)

self.assertEqual(1, future().upserted_id)

def test_delete_one(self):
flags = DELETE_FLAGS['SingleRemove']
with going(self.collection.delete_one, {}) as future:
delete = self.server.receives(OpDelete({}, flags=flags))
self.assertEqual(1, delete.flags)
gle = self.server.receives(Command('getlasterror'))
gle.replies_to_gle(n=1)

self.assertEqual(1, future().deleted_count)

def test_delete_many(self):
with going(self.collection.delete_many, {}) as future:
delete = self.server.receives(OpDelete({}, flags=0))
self.assertEqual(0, delete.flags)
gle = self.server.receives(Command('getlasterror'))
gle.replies_to_gle(n=2)

self.assertEqual(2, future().deleted_count)


# TODO: Move to pymongo-mockup-tests
class TestIsMasterFrequency(unittest.TestCase):
def test_server_selection(self):
Expand Down

0 comments on commit 0c6bce2

Please sign in to comment.