Skip to content

Commit

Permalink
Update tests: PyMongo uses OP_MSG for w=0 writes
Browse files Browse the repository at this point in the history
  • Loading branch information
ajdavis committed Jun 29, 2018
1 parent d3a3d2f commit e350691
Show file tree
Hide file tree
Showing 3 changed files with 74 additions and 77 deletions.
73 changes: 23 additions & 50 deletions docs/tutorial.rst
Original file line number Diff line number Diff line change
Expand Up @@ -49,18 +49,6 @@ does *not* match "ismaster".
(Notice that `~Request.replies` returns True. This makes more advanced uses of
`~MockupDB.autoresponds` easier, see the reference document.)

Reply To Legacy Writes
----------------------

Send an unacknowledged OP_INSERT:

>>> from pymongo.write_concern import WriteConcern
>>> w0 = WriteConcern(w=0)
>>> collection = client.db.coll.with_options(write_concern=w0)
>>> collection.insert_one({'_id': 1}) # doctest: +ELLIPSIS
<pymongo.results.InsertOneResult object at ...>
>>> server.receives()
OpInsert({"_id": 1}, namespace="db.coll")

Reply To Write Commands
-----------------------
Expand All @@ -82,7 +70,7 @@ client's request to arrive on the main thread:

>>> cmd = server.receives()
>>> cmd
Command({"insert": "coll", "ordered": true, "documents": [{"_id": 1}]}, namespace="db")
OpMsg({"insert": "coll", "ordered": true, "$db": "db", "$readPreference": {"mode": "primary"}, "documents": [{"_id": 1}]}, namespace="db")

(Note how MockupDB renders requests and replies as JSON, not Python.
The chief differences are that "true" and "false" are lower-case, and the order
Expand Down Expand Up @@ -158,12 +146,12 @@ little loop:
... try:
... while server.running:
... # Match queries most restrictive first.
... if server.got(Command('find', 'coll', filter={'a': {'$gt': 1}})):
... if server.got(OpMsg('find', 'coll', filter={'a': {'$gt': 1}})):
... server.reply(cursor={'id': 0, 'firstBatch':[{'a': 2}]})
... elif server.got('break'):
... server.ok()
... break
... elif server.got(Command('find', 'coll')):
... elif server.got(OpMsg('find', 'coll')):
... server.reply(
... cursor={'id': 0, 'firstBatch':[{'a': 1}, {'a': 2}]})
... else:
Expand Down Expand Up @@ -247,14 +235,14 @@ can assert that your application's requests match a particular pattern:
>>> client = MongoClient(server.uri)
>>> future = go(client.db.collection.insert, {'_id': 1})
>>> # Assert the command name is "insert" and its parameter is "collection".
>>> request = server.receives(Command('insert', 'collection'))
>>> request = server.receives(OpMsg('insert', 'collection'))
>>> request.ok()
True
>>> assert future()

If the request did not match, MockupDB would raise an `AssertionError`.

The arguments to `Command` above are an example of a message spec. The
The arguments to `OpMsg` above are an example of a message spec. The
pattern-matching rules are implemented in `Matcher`.
Here are
some more examples.
Expand Down Expand Up @@ -321,21 +309,10 @@ You can specify what request opcode to match:
>>> m.matches(OpQuery, {'_id': 1})
True

Commands are queries on some database's "database.$cmd" namespace.
They are specially prohibited from matching regular queries:

>>> Matcher(OpQuery).matches(Command)
False
>>> Matcher(Command).matches(Command)
True
>>> Matcher(OpQuery).matches(OpQuery)
True
>>> Matcher(Command).matches(OpQuery)
False

Commands in MongoDB 3.6 and later use the OP_MSG wire protocol message.
The command name is matched case-insensitively:

>>> Matcher(Command('ismaster')).matches(Command('IsMaster'))
>>> Matcher(OpMsg('ismaster')).matches(OpMsg('IsMaster'))
True

You can match properties specific to certain opcodes:
Expand Down Expand Up @@ -405,31 +382,27 @@ value if it is a number, otherwise interprets it as the first field of the reply
document and assumes the value is 1:

>>> import mockupdb
>>> mockupdb.make_reply()
OpReply()
>>> mockupdb.make_reply(0)
OpReply({"ok": 0})
>>> mockupdb.make_reply("foo")
OpReply({"foo": 1})
>>> mockupdb.make_op_msg_reply()
OpMsgReply()
>>> mockupdb.make_op_msg_reply(0)
OpMsgReply({"ok": 0})
>>> mockupdb.make_op_msg_reply("foo")
OpMsgReply({"foo": 1})

You can pass a dict or OrderedDict of fields instead of using keyword arguments.
This is best for fieldnames that are not valid Python identifiers:

>>> mockupdb.make_reply(OrderedDict([('ok', 0), ('$err', 'bad')]))
OpReply({"ok": 0, "$err": "bad"})
>>> mockupdb.make_op_msg_reply(OrderedDict([('ok', 0), ('$err', 'bad')]))
OpMsgReply({"ok": 0, "$err": "bad"})

You can customize the OP_REPLY header flags with the "flags" keyword argument:

>>> r = mockupdb.make_reply(OrderedDict([('ok', 0), ('$err', 'bad')]),
... flags=REPLY_FLAGS['QueryFailure'])
>>> r = mockupdb.make_op_msg_reply(OrderedDict([('ok', 0), ('$err', 'bad')]),
... flags=OP_MSG_FLAGS['checksumPresent'])
>>> repr(r)
'OpReply({"ok": 0, "$err": "bad"}, flags=QueryFailure)'

The above logic, which simulates a query error in MongoDB before 3.2, is
provided conveniently in `~Request.fail()`. This protocol is obsolete in MongoDB
3.2+, which uses commands for all operations.
'OpMsgReply({"ok": 0, "$err": "bad"}, flags=checksumPresent)'

Although these examples call `make_reply` explicitly, this is only to
Although these examples call `make_op_msg_reply` explicitly, this is only to
illustrate how replies are specified. Your code will pass these arguments to a
`Request` method like `~Request.replies`.

Expand All @@ -455,24 +428,24 @@ Test what happens when a query fails:

>>> cursor = collection.find().batch_size(1)
>>> future = go(next, cursor)
>>> server.receives(Command('find', 'coll')).fail()
>>> server.receives(OpMsg('find', 'coll')).command_err()
True
>>> future()
Traceback (most recent call last):
...
OperationFailure: database error: MockupDB query failure
OperationFailure: database error: MockupDB command failure

You can simulate normal querying, too:

>>> cursor = collection.find().batch_size(2)
>>> future = go(list, cursor)
>>> documents = [{'_id': 1}, {'x': 2}, {'foo': 'bar'}, {'beauty': True}]
>>> request = server.receives(Command('find', 'coll'))
>>> request = server.receives(OpMsg('find', 'coll'))
>>> n = request['batchSize']
>>> request.replies(cursor={'id': 123, 'firstBatch': documents[:n]})
True
>>> while True:
... getmore = server.receives(Command('getMore', 123))
... getmore = server.receives(OpMsg('getMore', 123))
... n = getmore['batchSize']
... if documents:
... cursor_id = 123
Expand Down
41 changes: 27 additions & 14 deletions mockupdb/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -352,7 +352,7 @@ def get(self, block=True, timeout=None):


class Request(object):
"""Base class for `Command`, `OpInsert`, and so on.
"""Base class for `Command`, `OpMsg`, and so on.
Some useful asserts you can do in tests:
Expand All @@ -364,13 +364,13 @@ class Request(object):
True
>>> {'_id': 1} == OpInsert([{'_id': 0}, {'_id': 1}])[1]
True
>>> 'field' in Command(field=1)
>>> 'field' in OpMsg(field=1)
True
>>> 'field' in Command()
>>> 'field' in OpMsg()
False
>>> 'field' in Command('ismaster')
>>> 'field' in OpMsg('ismaster')
False
>>> Command(ismaster=False)['ismaster'] is False
>>> OpMsg(ismaster=False)['ismaster'] is False
True
"""
opcode = None
Expand Down Expand Up @@ -576,9 +576,9 @@ class CommandBase(Request):
def command_name(self):
"""The command name or None.
>>> Command({'count': 'collection'}).command_name
>>> OpMsg({'count': 'collection'}).command_name
'count'
>>> Command('aggregate', 'collection', cursor=absent).command_name
>>> OpMsg('aggregate', 'collection', cursor=absent).command_name
'aggregate'
"""
if self.docs and self.docs[0]:
Expand Down Expand Up @@ -1041,6 +1041,15 @@ def reply_bytes(self, request):
"<iiii", 16 + len(data), reply_id, response_to, OP_MSG)
return header + data

def __repr__(self):
rep = '%s(%s' % (self.__class__.__name__, self)
if self._flags:
rep += ', flags=' + '|'.join(
name for name, value in OP_MSG_FLAGS.items()
if self._flags & value)

return rep + ')'


absent = {'absent': 1}

Expand Down Expand Up @@ -1299,9 +1308,9 @@ def got(self, *args, **kwargs):
>>> future = go(client.db.command, 'foo')
>>> s.got('foo')
True
>>> s.got(Command('foo', namespace='db'))
>>> s.got(OpMsg('foo', namespace='db'))
True
>>> s.got(Command('foo', key='value'))
>>> s.got(OpMsg('foo', key='value'))
False
>>> s.ok()
>>> future() == {'ok': 1}
Expand Down Expand Up @@ -1365,17 +1374,20 @@ def autoresponds(self, matcher, *args, **kwargs):
The remaining arguments are a :ref:`message spec <message spec>`:
>>> # ok
>>> responder = s.autoresponds('bar', ok=0, errmsg='err')
>>> client.db.command('bar')
Traceback (most recent call last):
...
OperationFailure: command SON([('bar', 1)]) on namespace db.$cmd failed: err
>>> responder = s.autoresponds(Command('find', 'collection'),
>>> responder = s.autoresponds(OpMsg('find', 'collection'),
... {'cursor': {'id': 0, 'firstBatch': [{'_id': 1}, {'_id': 2}]}})
>>> # ok
>>> list(client.db.collection.find()) == [{'_id': 1}, {'_id': 2}]
True
>>> responder = s.autoresponds(Command('find', 'collection'),
>>> responder = s.autoresponds(OpMsg('find', 'collection'),
... {'cursor': {'id': 0, 'firstBatch': [{'a': 1}, {'a': 2}]}})
>>> # bad
>>> list(client.db.collection.find()) == [{'a': 1}, {'a': 2}]
True
Expand All @@ -1387,6 +1399,7 @@ def autoresponds(self, matcher, *args, **kwargs):
and replied to. Future matching requests skip the queue.
>>> future = go(client.db.command, 'baz')
>>> # bad
>>> responder = s.autoresponds('baz', {'key': 'value'})
>>> future() == {'ok': 1, 'key': 'value'}
True
Expand Down Expand Up @@ -1426,7 +1439,7 @@ def autoresponds(self, matcher, *args, **kwargs):
... print('logging: %r' % request)
>>> responder = s.autoresponds(logger)
>>> client.db.command('baz') == {'ok': 1, 'a': 2}
logging: Command({"baz": 1}, flags=SlaveOkay, namespace="db")
logging: OpMsg({"baz": 1, "$db": "db", "$readPreference": {"mode": "primaryPreferred"}}, namespace="db")
True
The synonym `subscribe` better expresses your intent if your handler
Expand Down Expand Up @@ -1929,7 +1942,7 @@ def interactive_server(port=27017, verbose=True, all_ok=False, name='MockupDB',
server.autoresponds('whatsmyuri', you='localhost:12345')
server.autoresponds({'getLog': 'startupWarnings'},
log=['hello from %s!' % name])
server.autoresponds(Command('buildInfo'), version='MockupDB ' + __version__)
server.autoresponds(Command('listCollections'))
server.autoresponds(OpMsg('buildInfo'), version='MockupDB ' + __version__)
server.autoresponds(OpMsg('listCollections'))
server.autoresponds('replSetGetStatus', ok=0)
return server
37 changes: 24 additions & 13 deletions tests/test_mockupdb.py
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,7 @@ def test_assert_matches(self):
request.assert_matches(Command('foo'))


class TestLegacyWrites(unittest.TestCase):
class TestUnacknowledgedWrites(unittest.TestCase):
def setUp(self):
self.server = MockupDB(auto_ismaster=True)
self.server.run()
Expand All @@ -146,37 +146,48 @@ def setUp(self):

def test_insert_one(self):
with going(self.collection.insert_one, {'_id': 1}):
self.server.receives(OpInsert({'_id': 1}, flags=0))
# The moreToCome flag = 2.
self.server.receives(
OpMsg('insert', 'collection', writeConcern={'w': 0}, flags=2))

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):
self.server.receives(OpInsert(docs, flags=flags))
self.server.receives(OpMsg(SON([
('insert', 'collection'),
('ordered', False),
('writeConcern', {'w': 0})]), flags=2))

def test_replace_one(self):
with going(self.collection.replace_one, {}, {}):
self.server.receives(OpUpdate({}, {}, flags=0))
self.server.receives(OpMsg(SON([
('update', 'collection'),
('writeConcern', {'w': 0})
]), flags=2))

def test_update_many(self):
flags = UPDATE_FLAGS['MultiUpdate']
with going(self.collection.update_many, {}, {'$unset': 'a'}):
update = self.server.receives(OpUpdate({}, {}, flags=flags))
self.assertEqual(2, update.flags)
self.server.receives(OpMsg(SON([
('update', 'collection'),
('ordered', True),
('writeConcern', {'w': 0})
]), flags=2))

def test_delete_one(self):
flags = DELETE_FLAGS['SingleRemove']
with going(self.collection.delete_one, {}):
delete = self.server.receives(OpDelete({}, flags=flags))
self.assertEqual(1, delete.flags)
self.server.receives(OpMsg(SON([
('delete', 'collection'),
('writeConcern', {'w': 0})
]), flags=2))

def test_delete_many(self):
with going(self.collection.delete_many, {}):
delete = self.server.receives(OpDelete({}, flags=0))
self.assertEqual(0, delete.flags)
self.server.receives(OpMsg(SON([
('delete', 'collection'),
('writeConcern', {'w': 0})]), flags=2))


class TestMatcher(unittest.TestCase):
Expand Down

0 comments on commit e350691

Please sign in to comment.