Esempio n. 1
0
class ShellHandler(ZMQStreamHandler):

    def initialize(self, *args, **kwargs):
        self.shell_stream = None

    def open(self, kernel_id):
        km = self.application.kernel_manager
        self.max_msg_size = km.max_msg_size
        self.kernel_id = kernel_id
        try:
            self.shell_stream = km.create_shell_stream(kernel_id)
        except web.HTTPError:
            # WebSockets don't response to traditional error codes so we
            # close the connection.
            if not self.stream.closed():
                self.stream.close()
        else:
            self.session = Session()
            self.shell_stream.on_recv(self._on_zmq_reply)

    def on_message(self, msg):
        if len(msg) < self.max_msg_size:
            msg = jsonapi.loads(msg)
            self.session.send(self.shell_stream, msg)

    def on_close(self):
        # Make sure the stream exists and is not already closed.
        if self.shell_stream is not None and not self.shell_stream.closed():
            self.shell_stream.close()
Esempio n. 2
0
def main(connection_file):
    """watch iopub channel, and print messages"""

    ctx = zmq.Context.instance()

    with open(connection_file) as f:
        cfg = json.loads(f.read())

    location = cfg["location"]
    reg_url = cfg["url"]
    session = Session(key=str_to_bytes(cfg["exec_key"]))

    query = ctx.socket(zmq.DEALER)
    query.connect(disambiguate_url(cfg["url"], location))
    session.send(query, "connection_request")
    idents, msg = session.recv(query, mode=0)
    c = msg["content"]
    iopub_url = disambiguate_url(c["iopub"], location)
    sub = ctx.socket(zmq.SUB)
    # This will subscribe to all messages:
    sub.setsockopt(zmq.SUBSCRIBE, b"")
    # replace with b'' with b'engine.1.stdout' to subscribe only to engine 1's stdout
    # 0MQ subscriptions are simple 'foo*' matches, so 'engine.1.' subscribes
    # to everything from engine 1, but there is no way to subscribe to
    # just stdout from everyone.
    # multiple calls to subscribe will add subscriptions, e.g. to subscribe to
    # engine 1's stderr and engine 2's stdout:
    # sub.setsockopt(zmq.SUBSCRIBE, b'engine.1.stderr')
    # sub.setsockopt(zmq.SUBSCRIBE, b'engine.2.stdout')
    sub.connect(iopub_url)
    while True:
        try:
            idents, msg = session.recv(sub, mode=0)
        except KeyboardInterrupt:
            return
        # ident always length 1 here
        topic = idents[0]
        if msg["msg_type"] == "stream":
            # stdout/stderr
            # stream names are in msg['content']['name'], if you want to handle
            # them differently
            print "%s: %s" % (topic, msg["content"]["data"])
        elif msg["msg_type"] == "pyerr":
            # Python traceback
            c = msg["content"]
            print topic + ":"
            for line in c["traceback"]:
                # indent lines
                print "    " + line
Esempio n. 3
0
    def new_session(self):
        """ Starts a new kernel on an open computer. 

        :returns: kernel id assigned to the newly created kernel
        :rtype: string
        """
        comp_id = self._find_open_computer()
        resource_limits = self._comps[comp_id].get("resource_limits")
        reply = self._sender.send_msg(
            {
                "type": "start_kernel",
                "content": {
                    "resource_limits": resource_limits
                }
            }, comp_id)
        if reply["type"] == "success":
            reply_content = reply["content"]
            kernel_id = reply_content["kernel_id"]
            kernel_connection = reply_content["connection"]
            self._kernels[kernel_id] = {
                "comp_id": comp_id,
                "connection": kernel_connection,
                "executing": False,
                "timeout": time.time() + self.kernel_timeout
            }
            self._comps[comp_id]["kernels"][kernel_id] = None
            self._sessions[kernel_id] = Session(key=kernel_connection["key"])
            return kernel_id
        else:
            return False
Esempio n. 4
0
 def open(self, kernel_id):
     self.kernel_id = kernel_id.decode('ascii')
     try:
         cfg = self.application.ipython_app.config
     except AttributeError:
         # protect from the case where this is run from something other than
         # the notebook app:
         cfg = None
     self.session = Session(config=cfg)
     self.save_on_message = self.on_message
     self.on_message = self.on_first_message
Esempio n. 5
0
def make_starter(up_addr, down_addr, *args, **kwargs):
    """entry point function for launching a kernelstarter in a subprocess"""
    loop = ioloop.IOLoop.instance()
    ctx = zmq.Context()
    session = Session()
    upstream = zmqstream.ZMQStream(ctx.socket(zmq.XREQ), loop)
    upstream.connect(up_addr)
    downstream = zmqstream.ZMQStream(ctx.socket(zmq.XREQ), loop)
    downstream.connect(down_addr)

    starter = KernelStarter(session, upstream, downstream, *args, **kwargs)
    starter.start()
    loop.start()
Esempio n. 6
0
 def open(self, kernel_id):
     km = self.application.kernel_manager
     self.max_msg_size = km.max_msg_size
     self.kernel_id = kernel_id
     try:
         self.shell_stream = km.create_shell_stream(kernel_id)
     except web.HTTPError:
         # WebSockets don't response to traditional error codes so we
         # close the connection.
         if not self.stream.closed():
             self.stream.close()
     else:
         self.session = Session()
         self.shell_stream.on_recv(self._on_zmq_reply)
Esempio n. 7
0
def main(connection_file):
    """watch iopub channel, and print messages"""

    ctx = zmq.Context.instance()

    with open(connection_file) as f:
        cfg = json.loads(f.read())

    location = cfg['location']
    reg_url = cfg['url']
    session = Session(key=str_to_bytes(cfg['exec_key']))

    query = ctx.socket(zmq.DEALER)
    query.connect(disambiguate_url(cfg['url'], location))
    session.send(query, "connection_request")
    idents, msg = session.recv(query, mode=0)
    c = msg['content']
    iopub_url = disambiguate_url(c['iopub'], location)
    sub = ctx.socket(zmq.SUB)
    # This will subscribe to all messages:
    sub.setsockopt(zmq.SUBSCRIBE, b'')
    # replace with b'' with b'engine.1.stdout' to subscribe only to engine 1's stdout
    # 0MQ subscriptions are simple 'foo*' matches, so 'engine.1.' subscribes
    # to everything from engine 1, but there is no way to subscribe to
    # just stdout from everyone.
    # multiple calls to subscribe will add subscriptions, e.g. to subscribe to
    # engine 1's stderr and engine 2's stdout:
    # sub.setsockopt(zmq.SUBSCRIBE, b'engine.1.stderr')
    # sub.setsockopt(zmq.SUBSCRIBE, b'engine.2.stdout')
    sub.connect(iopub_url)
    while True:
        try:
            idents, msg = session.recv(sub, mode=0)
        except KeyboardInterrupt:
            return
        # ident always length 1 here
        topic = idents[0]
        if msg['msg_type'] == 'stream':
            # stdout/stderr
            # stream names are in msg['content']['name'], if you want to handle
            # them differently
            print("%s: %s" % (topic, msg['content']['data']))
        elif msg['msg_type'] == 'pyerr':
            # Python traceback
            c = msg['content']
            print(topic + ':')
            for line in c['traceback']:
                # indent lines
                print('    ' + line)
Esempio n. 8
0
 def setUp(self):
     self.session = Session()
     self.db = self.create_db()
     self.load_records(16)
Esempio n. 9
0
class TestDictBackend(TestCase):
    def setUp(self):
        self.session = Session()
        self.db = self.create_db()
        self.load_records(16)

    def create_db(self):
        return DictDB()

    def load_records(self, n=1):
        """load n records for testing"""
        #sleep 1/10 s, to ensure timestamp is different to previous calls
        time.sleep(0.1)
        msg_ids = []
        for i in range(n):
            msg = self.session.msg('apply_request', content=dict(a=5))
            msg['buffers'] = []
            rec = init_record(msg)
            msg_id = msg['header']['msg_id']
            msg_ids.append(msg_id)
            self.db.add_record(msg_id, rec)
        return msg_ids

    def test_add_record(self):
        before = self.db.get_history()
        self.load_records(5)
        after = self.db.get_history()
        self.assertEqual(len(after), len(before) + 5)
        self.assertEqual(after[:-5], before)

    def test_drop_record(self):
        msg_id = self.load_records()[-1]
        rec = self.db.get_record(msg_id)
        self.db.drop_record(msg_id)
        self.assertRaises(KeyError, self.db.get_record, msg_id)

    def _round_to_millisecond(self, dt):
        """necessary because mongodb rounds microseconds"""
        micro = dt.microsecond
        extra = int(str(micro)[-3:])
        return dt - timedelta(microseconds=extra)

    def test_update_record(self):
        now = self._round_to_millisecond(datetime.now())
        #
        msg_id = self.db.get_history()[-1]
        rec1 = self.db.get_record(msg_id)
        data = {'stdout': 'hello there', 'completed': now}
        self.db.update_record(msg_id, data)
        rec2 = self.db.get_record(msg_id)
        self.assertEqual(rec2['stdout'], 'hello there')
        self.assertEqual(rec2['completed'], now)
        rec1.update(data)
        self.assertEqual(rec1, rec2)

    # def test_update_record_bad(self):
    #     """test updating nonexistant records"""
    #     msg_id = str(uuid.uuid4())
    #     data = {'stdout': 'hello there'}
    #     self.assertRaises(KeyError, self.db.update_record, msg_id, data)

    def test_find_records_dt(self):
        """test finding records by date"""
        hist = self.db.get_history()
        middle = self.db.get_record(hist[len(hist) // 2])
        tic = middle['submitted']
        before = self.db.find_records({'submitted': {'$lt': tic}})
        after = self.db.find_records({'submitted': {'$gte': tic}})
        self.assertEqual(len(before) + len(after), len(hist))
        for b in before:
            self.assertTrue(b['submitted'] < tic)
        for a in after:
            self.assertTrue(a['submitted'] >= tic)
        same = self.db.find_records({'submitted': tic})
        for s in same:
            self.assertTrue(s['submitted'] == tic)

    def test_find_records_keys(self):
        """test extracting subset of record keys"""
        found = self.db.find_records({'msg_id': {
            '$ne': ''
        }},
                                     keys=['submitted', 'completed'])
        for rec in found:
            self.assertEqual(set(rec.keys()),
                             set(['msg_id', 'submitted', 'completed']))

    def test_find_records_msg_id(self):
        """ensure msg_id is always in found records"""
        found = self.db.find_records({'msg_id': {
            '$ne': ''
        }},
                                     keys=['submitted', 'completed'])
        for rec in found:
            self.assertTrue('msg_id' in rec.keys())
        found = self.db.find_records({'msg_id': {
            '$ne': ''
        }},
                                     keys=['submitted'])
        for rec in found:
            self.assertTrue('msg_id' in rec.keys())
        found = self.db.find_records({'msg_id': {'$ne': ''}}, keys=['msg_id'])
        for rec in found:
            self.assertTrue('msg_id' in rec.keys())

    def test_find_records_in(self):
        """test finding records with '$in','$nin' operators"""
        hist = self.db.get_history()
        even = hist[::2]
        odd = hist[1::2]
        recs = self.db.find_records({'msg_id': {'$in': even}})
        found = [r['msg_id'] for r in recs]
        self.assertEqual(set(even), set(found))
        recs = self.db.find_records({'msg_id': {'$nin': even}})
        found = [r['msg_id'] for r in recs]
        self.assertEqual(set(odd), set(found))

    def test_get_history(self):
        msg_ids = self.db.get_history()
        latest = datetime(1984, 1, 1)
        for msg_id in msg_ids:
            rec = self.db.get_record(msg_id)
            newt = rec['submitted']
            self.assertTrue(newt >= latest)
            latest = newt
        msg_id = self.load_records(1)[-1]
        self.assertEqual(self.db.get_history()[-1], msg_id)

    def test_datetime(self):
        """get/set timestamps with datetime objects"""
        msg_id = self.db.get_history()[-1]
        rec = self.db.get_record(msg_id)
        self.assertTrue(isinstance(rec['submitted'], datetime))
        self.db.update_record(msg_id, dict(completed=datetime.now()))
        rec = self.db.get_record(msg_id)
        self.assertTrue(isinstance(rec['completed'], datetime))

    def test_drop_matching(self):
        msg_ids = self.load_records(10)
        query = {'msg_id': {'$in': msg_ids}}
        self.db.drop_matching_records(query)
        recs = self.db.find_records(query)
        self.assertEqual(len(recs), 0)

    def test_null(self):
        """test None comparison queries"""
        msg_ids = self.load_records(10)

        query = {'msg_id': None}
        recs = self.db.find_records(query)
        self.assertEqual(len(recs), 0)

        query = {'msg_id': {'$ne': None}}
        recs = self.db.find_records(query)
        self.assertTrue(len(recs) >= 10)

    def test_pop_safe_get(self):
        """editing query results shouldn't affect record [get]"""
        msg_id = self.db.get_history()[-1]
        rec = self.db.get_record(msg_id)
        rec.pop('buffers')
        rec['garbage'] = 'hello'
        rec['header']['msg_id'] = 'fubar'
        rec2 = self.db.get_record(msg_id)
        self.assertTrue('buffers' in rec2)
        self.assertFalse('garbage' in rec2)
        self.assertEqual(rec2['header']['msg_id'], msg_id)

    def test_pop_safe_find(self):
        """editing query results shouldn't affect record [find]"""
        msg_id = self.db.get_history()[-1]
        rec = self.db.find_records({'msg_id': msg_id})[0]
        rec.pop('buffers')
        rec['garbage'] = 'hello'
        rec['header']['msg_id'] = 'fubar'
        rec2 = self.db.find_records({'msg_id': msg_id})[0]
        self.assertTrue('buffers' in rec2)
        self.assertFalse('garbage' in rec2)
        self.assertEqual(rec2['header']['msg_id'], msg_id)

    def test_pop_safe_find_keys(self):
        """editing query results shouldn't affect record [find+keys]"""
        msg_id = self.db.get_history()[-1]
        rec = self.db.find_records({'msg_id': msg_id},
                                   keys=['buffers', 'header'])[0]
        rec.pop('buffers')
        rec['garbage'] = 'hello'
        rec['header']['msg_id'] = 'fubar'
        rec2 = self.db.find_records({'msg_id': msg_id})[0]
        self.assertTrue('buffers' in rec2)
        self.assertFalse('garbage' in rec2)
        self.assertEqual(rec2['header']['msg_id'], msg_id)
Esempio n. 10
0
 def setUp(self):
     self.session = Session()
     self.db = self.create_db()
     self.load_records(16)
Esempio n. 11
0
class TestDictBackend(TestCase):
    def setUp(self):
        self.session = Session()
        self.db = self.create_db()
        self.load_records(16)
    
    def create_db(self):
        return DictDB()
    
    def load_records(self, n=1):
        """load n records for testing"""
        #sleep 1/10 s, to ensure timestamp is different to previous calls
        time.sleep(0.1)
        msg_ids = []
        for i in range(n):
            msg = self.session.msg('apply_request', content=dict(a=5))
            msg['buffers'] = []
            rec = init_record(msg)
            msg_id = msg['header']['msg_id']
            msg_ids.append(msg_id)
            self.db.add_record(msg_id, rec)
        return msg_ids
    
    def test_add_record(self):
        before = self.db.get_history()
        self.load_records(5)
        after = self.db.get_history()
        self.assertEquals(len(after), len(before)+5)
        self.assertEquals(after[:-5],before)
        
    def test_drop_record(self):
        msg_id = self.load_records()[-1]
        rec = self.db.get_record(msg_id)
        self.db.drop_record(msg_id)
        self.assertRaises(KeyError,self.db.get_record, msg_id)
    
    def _round_to_millisecond(self, dt):
        """necessary because mongodb rounds microseconds"""
        micro = dt.microsecond
        extra = int(str(micro)[-3:])
        return dt - timedelta(microseconds=extra)
    
    def test_update_record(self):
        now = self._round_to_millisecond(datetime.now())
        # 
        msg_id = self.db.get_history()[-1]
        rec1 = self.db.get_record(msg_id)
        data = {'stdout': 'hello there', 'completed' : now}
        self.db.update_record(msg_id, data)
        rec2 = self.db.get_record(msg_id)
        self.assertEquals(rec2['stdout'], 'hello there')
        self.assertEquals(rec2['completed'], now)
        rec1.update(data)
        self.assertEquals(rec1, rec2)
    
    # def test_update_record_bad(self):
    #     """test updating nonexistant records"""
    #     msg_id = str(uuid.uuid4())
    #     data = {'stdout': 'hello there'}
    #     self.assertRaises(KeyError, self.db.update_record, msg_id, data)

    def test_find_records_dt(self):
        """test finding records by date"""
        hist = self.db.get_history()
        middle = self.db.get_record(hist[len(hist)//2])
        tic = middle['submitted']
        before = self.db.find_records({'submitted' : {'$lt' : tic}})
        after = self.db.find_records({'submitted' : {'$gte' : tic}})
        self.assertEquals(len(before)+len(after),len(hist))
        for b in before:
            self.assertTrue(b['submitted'] < tic)
        for a in after:
            self.assertTrue(a['submitted'] >= tic)
        same = self.db.find_records({'submitted' : tic})
        for s in same:
            self.assertTrue(s['submitted'] == tic)
    
    def test_find_records_keys(self):
        """test extracting subset of record keys"""
        found = self.db.find_records({'msg_id': {'$ne' : ''}},keys=['submitted', 'completed'])
        for rec in found:
            self.assertEquals(set(rec.keys()), set(['msg_id', 'submitted', 'completed']))
    
    def test_find_records_msg_id(self):
        """ensure msg_id is always in found records"""
        found = self.db.find_records({'msg_id': {'$ne' : ''}},keys=['submitted', 'completed'])
        for rec in found:
            self.assertTrue('msg_id' in rec.keys())
        found = self.db.find_records({'msg_id': {'$ne' : ''}},keys=['submitted'])
        for rec in found:
            self.assertTrue('msg_id' in rec.keys())
        found = self.db.find_records({'msg_id': {'$ne' : ''}},keys=['msg_id'])
        for rec in found:
            self.assertTrue('msg_id' in rec.keys())
    
    def test_find_records_in(self):
        """test finding records with '$in','$nin' operators"""
        hist = self.db.get_history()
        even = hist[::2]
        odd = hist[1::2]
        recs = self.db.find_records({ 'msg_id' : {'$in' : even}})
        found = [ r['msg_id'] for r in recs ]
        self.assertEquals(set(even), set(found))
        recs = self.db.find_records({ 'msg_id' : {'$nin' : even}})
        found = [ r['msg_id'] for r in recs ]
        self.assertEquals(set(odd), set(found))
    
    def test_get_history(self):
        msg_ids = self.db.get_history()
        latest = datetime(1984,1,1)
        for msg_id in msg_ids:
            rec = self.db.get_record(msg_id)
            newt = rec['submitted']
            self.assertTrue(newt >= latest)
            latest = newt
        msg_id = self.load_records(1)[-1]
        self.assertEquals(self.db.get_history()[-1],msg_id)
    
    def test_datetime(self):
        """get/set timestamps with datetime objects"""
        msg_id = self.db.get_history()[-1]
        rec = self.db.get_record(msg_id)
        self.assertTrue(isinstance(rec['submitted'], datetime))
        self.db.update_record(msg_id, dict(completed=datetime.now()))
        rec = self.db.get_record(msg_id)
        self.assertTrue(isinstance(rec['completed'], datetime))

    def test_drop_matching(self):
        msg_ids = self.load_records(10)
        query = {'msg_id' : {'$in':msg_ids}}
        self.db.drop_matching_records(query)
        recs = self.db.find_records(query)
        self.assertEquals(len(recs), 0)
    
    def test_null(self):
        """test None comparison queries"""
        msg_ids = self.load_records(10)

        query = {'msg_id' : None}
        recs = self.db.find_records(query)
        self.assertEquals(len(recs), 0)

        query = {'msg_id' : {'$ne' : None}}
        recs = self.db.find_records(query)
        self.assertTrue(len(recs) >= 10)
    
    def test_pop_safe_get(self):
        """editing query results shouldn't affect record [get]"""
        msg_id = self.db.get_history()[-1]
        rec = self.db.get_record(msg_id)
        rec.pop('buffers')
        rec['garbage'] = 'hello'
        rec2 = self.db.get_record(msg_id)
        self.assertTrue('buffers' in rec2)
        self.assertFalse('garbage' in rec2)
    
    def test_pop_safe_find(self):
        """editing query results shouldn't affect record [find]"""
        msg_id = self.db.get_history()[-1]
        rec = self.db.find_records({'msg_id' : msg_id})[0]
        rec.pop('buffers')
        rec['garbage'] = 'hello'
        rec2 = self.db.find_records({'msg_id' : msg_id})[0]
        self.assertTrue('buffers' in rec2)
        self.assertFalse('garbage' in rec2)

    def test_pop_safe_find_keys(self):
        """editing query results shouldn't affect record [find+keys]"""
        msg_id = self.db.get_history()[-1]
        rec = self.db.find_records({'msg_id' : msg_id}, keys=['buffers'])[0]
        rec.pop('buffers')
        rec['garbage'] = 'hello'
        rec2 = self.db.find_records({'msg_id' : msg_id})[0]
        self.assertTrue('buffers' in rec2)
        self.assertFalse('garbage' in rec2)
Esempio n. 12
0
 def init_session(self):
     """create our session object"""
     default_secure(self.config)
     self.session = Session(config=self.config, username=u'kernel')
Esempio n. 13
0
class TaskDBTest:
    def setUp(self):
        self.session = Session()
        self.db = self.create_db()
        self.load_records(16)

    def create_db(self):
        raise NotImplementedError

    def load_records(self, n=1, buffer_size=100):
        """load n records for testing"""
        # sleep 1/10 s, to ensure timestamp is different to previous calls
        time.sleep(0.1)
        msg_ids = []
        for i in range(n):
            msg = self.session.msg("apply_request", content=dict(a=5))
            msg["buffers"] = [os.urandom(buffer_size)]
            rec = init_record(msg)
            msg_id = msg["header"]["msg_id"]
            msg_ids.append(msg_id)
            self.db.add_record(msg_id, rec)
        return msg_ids

    def test_add_record(self):
        before = self.db.get_history()
        self.load_records(5)
        after = self.db.get_history()
        self.assertEqual(len(after), len(before) + 5)
        self.assertEqual(after[:-5], before)

    def test_drop_record(self):
        msg_id = self.load_records()[-1]
        rec = self.db.get_record(msg_id)
        self.db.drop_record(msg_id)
        self.assertRaises(KeyError, self.db.get_record, msg_id)

    def _round_to_millisecond(self, dt):
        """necessary because mongodb rounds microseconds"""
        micro = dt.microsecond
        extra = int(str(micro)[-3:])
        return dt - timedelta(microseconds=extra)

    def test_update_record(self):
        now = self._round_to_millisecond(datetime.now())
        #
        msg_id = self.db.get_history()[-1]
        rec1 = self.db.get_record(msg_id)
        data = {"stdout": "hello there", "completed": now}
        self.db.update_record(msg_id, data)
        rec2 = self.db.get_record(msg_id)
        self.assertEqual(rec2["stdout"], "hello there")
        self.assertEqual(rec2["completed"], now)
        rec1.update(data)
        self.assertEqual(rec1, rec2)

    # def test_update_record_bad(self):
    #     """test updating nonexistant records"""
    #     msg_id = str(uuid.uuid4())
    #     data = {'stdout': 'hello there'}
    #     self.assertRaises(KeyError, self.db.update_record, msg_id, data)

    def test_find_records_dt(self):
        """test finding records by date"""
        hist = self.db.get_history()
        middle = self.db.get_record(hist[len(hist) // 2])
        tic = middle["submitted"]
        before = self.db.find_records({"submitted": {"$lt": tic}})
        after = self.db.find_records({"submitted": {"$gte": tic}})
        self.assertEqual(len(before) + len(after), len(hist))
        for b in before:
            self.assertTrue(b["submitted"] < tic)
        for a in after:
            self.assertTrue(a["submitted"] >= tic)
        same = self.db.find_records({"submitted": tic})
        for s in same:
            self.assertTrue(s["submitted"] == tic)

    def test_find_records_keys(self):
        """test extracting subset of record keys"""
        found = self.db.find_records({"msg_id": {"$ne": ""}}, keys=["submitted", "completed"])
        for rec in found:
            self.assertEqual(set(rec.keys()), set(["msg_id", "submitted", "completed"]))

    def test_find_records_msg_id(self):
        """ensure msg_id is always in found records"""
        found = self.db.find_records({"msg_id": {"$ne": ""}}, keys=["submitted", "completed"])
        for rec in found:
            self.assertTrue("msg_id" in rec.keys())
        found = self.db.find_records({"msg_id": {"$ne": ""}}, keys=["submitted"])
        for rec in found:
            self.assertTrue("msg_id" in rec.keys())
        found = self.db.find_records({"msg_id": {"$ne": ""}}, keys=["msg_id"])
        for rec in found:
            self.assertTrue("msg_id" in rec.keys())

    def test_find_records_in(self):
        """test finding records with '$in','$nin' operators"""
        hist = self.db.get_history()
        even = hist[::2]
        odd = hist[1::2]
        recs = self.db.find_records({"msg_id": {"$in": even}})
        found = [r["msg_id"] for r in recs]
        self.assertEqual(set(even), set(found))
        recs = self.db.find_records({"msg_id": {"$nin": even}})
        found = [r["msg_id"] for r in recs]
        self.assertEqual(set(odd), set(found))

    def test_get_history(self):
        msg_ids = self.db.get_history()
        latest = datetime(1984, 1, 1)
        for msg_id in msg_ids:
            rec = self.db.get_record(msg_id)
            newt = rec["submitted"]
            self.assertTrue(newt >= latest)
            latest = newt
        msg_id = self.load_records(1)[-1]
        self.assertEqual(self.db.get_history()[-1], msg_id)

    def test_datetime(self):
        """get/set timestamps with datetime objects"""
        msg_id = self.db.get_history()[-1]
        rec = self.db.get_record(msg_id)
        self.assertTrue(isinstance(rec["submitted"], datetime))
        self.db.update_record(msg_id, dict(completed=datetime.now()))
        rec = self.db.get_record(msg_id)
        self.assertTrue(isinstance(rec["completed"], datetime))

    def test_drop_matching(self):
        msg_ids = self.load_records(10)
        query = {"msg_id": {"$in": msg_ids}}
        self.db.drop_matching_records(query)
        recs = self.db.find_records(query)
        self.assertEqual(len(recs), 0)

    def test_null(self):
        """test None comparison queries"""
        msg_ids = self.load_records(10)

        query = {"msg_id": None}
        recs = self.db.find_records(query)
        self.assertEqual(len(recs), 0)

        query = {"msg_id": {"$ne": None}}
        recs = self.db.find_records(query)
        self.assertTrue(len(recs) >= 10)

    def test_pop_safe_get(self):
        """editing query results shouldn't affect record [get]"""
        msg_id = self.db.get_history()[-1]
        rec = self.db.get_record(msg_id)
        rec.pop("buffers")
        rec["garbage"] = "hello"
        rec["header"]["msg_id"] = "fubar"
        rec2 = self.db.get_record(msg_id)
        self.assertTrue("buffers" in rec2)
        self.assertFalse("garbage" in rec2)
        self.assertEqual(rec2["header"]["msg_id"], msg_id)

    def test_pop_safe_find(self):
        """editing query results shouldn't affect record [find]"""
        msg_id = self.db.get_history()[-1]
        rec = self.db.find_records({"msg_id": msg_id})[0]
        rec.pop("buffers")
        rec["garbage"] = "hello"
        rec["header"]["msg_id"] = "fubar"
        rec2 = self.db.find_records({"msg_id": msg_id})[0]
        self.assertTrue("buffers" in rec2)
        self.assertFalse("garbage" in rec2)
        self.assertEqual(rec2["header"]["msg_id"], msg_id)

    def test_pop_safe_find_keys(self):
        """editing query results shouldn't affect record [find+keys]"""
        msg_id = self.db.get_history()[-1]
        rec = self.db.find_records({"msg_id": msg_id}, keys=["buffers", "header"])[0]
        rec.pop("buffers")
        rec["garbage"] = "hello"
        rec["header"]["msg_id"] = "fubar"
        rec2 = self.db.find_records({"msg_id": msg_id})[0]
        self.assertTrue("buffers" in rec2)
        self.assertFalse("garbage" in rec2)
        self.assertEqual(rec2["header"]["msg_id"], msg_id)