| 
 # Copyright (C) 2011  Internet Systems Consortium, Inc. ("ISC")  
#  
# Permission to use, copy, modify, and distribute this software for any  
# purpose with or without fee is hereby granted, provided that the above  
# copyright notice and this permission notice appear in all copies.  
#  
# THE SOFTWARE IS PROVIDED "AS IS" AND INTERNET SYSTEMS CONSORTIUM  
# DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL  
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL  
# INTERNET SYSTEMS CONSORTIUM BE LIABLE FOR ANY SPECIAL, DIRECT,  
# INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING  
# FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT,  
# NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION  
# WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.  
  
import unittest  
import isc.log  
import isc.bind10.socket_cache  
import isc.bind10.sockcreator  
from isc.net.addr import IPAddr  
import os  
  
class Test(unittest.TestCase):  
    """  
    Base for the tests here. It replaces the os.close method.  
    """  
    def setUp(self):  
        self._closes = []  
        isc.bind10.socket_cache.os.close = self.__close  
  
    def tearDown(self):  
        # This is not very clean solution. But when the test stops  
        # to exist, the method must not be used to destroy the  
        # object any more. And we can't restore the os.close here  
        # as we never work with real sockets here.  
exit        isc.bind10.socket_cache.os.close = lambda fd: None  
  
    def __close(self, fd):  
        """  
        Just log a close was called.  
        """  
        self._closes.append(fd)  
  
class SocketTest(Test):  
    """  
    Test for the Socket class.  
    """  
    def setUp(self):  
        """  
        Creates the socket to be tested.  
  
        It also creates other useful test variables.  
        """  
        Test.setUp(self)  
        self.__address = IPAddr("192.0.2.1")  
        self.__socket = isc.bind10.socket_cache.Socket('UDP', self.__address,  
                                                       1024, 42)  
  
    def test_init(self):  
        """  
        Checks the intrnals of the cache just after the creation.  
        """  
        self.assertEqual('UDP', self.__socket.protocol)  
        self.assertEqual(self.__address, self.__socket.address)  
        self.assertEqual(1024, self.__socket.port)  
        self.assertEqual(42, self.__socket.fileno)  
        self.assertEqual({}, self.__socket.active_tokens)  
        self.assertEqual({}, self.__socket.shares)  
        self.assertEqual(set(), self.__socket.waiting_tokens)  
  
    def test_del(self):  
        """  
        Check it closes the socket when removed.  
        """  
        # This should make the refcount 0 and call the descructor  
        # right away  
        self.__socket = None  
        self.assertEqual([42], self._closes)  
  
    def test_share_modes(self):  
        """  
        Test the share mode compatibility check function.  
        """  
        modes = ['NO', 'SAMEAPP', 'ANY']  
        # If there are no shares, it is compatible with everything.  
        for mode in modes:  
            self.assertTrue(self.__socket.share_compatible(mode, 'anything'))  
  
        # There's an NO already, so it is incompatible with everything.  
        self.__socket.shares = {'token': ('NO', 'anything')}  
        for mode in modes:  
            self.assertFalse(self.__socket.share_compatible(mode, 'anything'))  
  
        # If there's SAMEAPP, it is compatible with ANY and SAMEAPP with the  
        # same name.  
        self.__socket.shares = {'token': ('SAMEAPP', 'app')}  
        self.assertFalse(self.__socket.share_compatible('NO', 'app'))  
        self.assertFalse(self.__socket.share_compatible('SAMEAPP',  
                                                        'something'))  
        self.assertTrue(self.__socket.share_compatible('SAMEAPP', 'app'))  
        self.assertTrue(self.__socket.share_compatible('ANY', 'app'))  
        self.assertFalse(self.__socket.share_compatible('ANY', 'something'))  
  
        # If there's ANY, then ANY and SAMEAPP with the same name is compatible  
        self.__socket.shares = {'token': ('ANY', 'app')}  
        self.assertFalse(self.__socket.share_compatible('NO', 'app'))  
        self.assertFalse(self.__socket.share_compatible('SAMEAPP',  
                                                        'something'))  
        self.assertTrue(self.__socket.share_compatible('SAMEAPP', 'app'))  
        self.assertTrue(self.__socket.share_compatible('ANY', 'something'))  
  
        # In case there are multiple already inside  
        self.__socket.shares = {  
            'token': ('ANY', 'app'),  
            'another': ('SAMEAPP', 'app')  
        }  
        self.assertFalse(self.__socket.share_compatible('NO', 'app'))  
        self.assertFalse(self.__socket.share_compatible('SAMEAPP',  
                                                        'something'))  
        self.assertTrue(self.__socket.share_compatible('SAMEAPP', 'app'))  
        self.assertFalse(self.__socket.share_compatible('ANY', 'something'))  
        self.assertTrue(self.__socket.share_compatible('ANY', 'app'))  
  
        # Invalid inputs are rejected  
        self.assertRaises(ValueError, self.__socket.share_compatible, 'bad',  
                          'bad')  
  
class SocketCacheTest(Test):  
    """  
    Some tests for the isc.bind10.socket_cache.Cache.  
  
    This class, as well as being the testcase, pretends to be the  
    socket creator so it can hijack all the requests for sockets.  
    """  
    def setUp(self):  
        """  
        Creates the cache for tests with us being the socket creator.  
  
        Also creates some more variables for testing.  
        """  
        Test.setUp(self)  
        self.__cache = isc.bind10.socket_cache.Cache(self)  
        self.__address = IPAddr("192.0.2.1")  
        self.__socket = isc.bind10.socket_cache.Socket('UDP', self.__address,  
                                                       1024, 42)  
        self.__get_socket_called = False  
  
    def test_init(self):  
        """  
        Checks the internals of the cache just after the creation.  
        """  
        self.assertEqual(self, self.__cache._creator)  
        self.assertEqual({}, self.__cache._waiting_tokens)  
        self.assertEqual({}, self.__cache._active_tokens)  
        self.assertEqual({}, self.__cache._active_apps)  
        self.assertEqual({}, self.__cache._sockets)  
        self.assertEqual(set(), self.__cache._live_tokens)  
  
    def get_socket(self, address, port, socktype):  
        """  
        Pretend to be a socket creator.  
  
        This expects to be called with the _address, port 1024 and 'UDP'.  
  
        Returns 42 and notes down it was called.  
        """  
        self.assertEqual(self.__address, address)  
        self.assertEqual(1024, port)  
        self.assertEqual('UDP', socktype)  
        self.__get_socket_called = True  
        return 42  
  
    def test_get_token_cached(self):  
        """  
        Check the behaviour of get_token when the requested socket is already  
        cached inside.  
        """  
        self.__cache._sockets = {  
            'UDP': {'192.0.2.1': {1024: self.__socket}}  
        }  
        token = self.__cache.get_token('UDP', self.__address, 1024, 'ANY',  
                                       'test')  
        # It didn't call get_socket  
        self.assertFalse(self.__get_socket_called)  
        # It returned something  
        self.assertIsNotNone(token)  
        # The token is both in the waiting sockets and the live tokens  
        self.assertEqual({token: self.__socket}, self.__cache._waiting_tokens)  
        self.assertEqual(set([token]), self.__cache._live_tokens)  
        # The token got the new share to block any relevant queries  
        self.assertEqual({token: ('ANY', 'test')}, self.__socket.shares)  
        # The socket knows the token is waiting in it  
        self.assertEqual(set([token]), self.__socket.waiting_tokens)  
  
        # If we request one more, with incompatible share, it is rejected  
        self.assertRaises(isc.bind10.socket_cache.ShareError,  
                          self.__cache.get_token, 'UDP', self.__address, 1024,  
                          'NO', 'test')  
        # The internals are not changed, so the same checks  
        self.assertEqual({token: self.__socket}, self.__cache._waiting_tokens)  
        self.assertEqual(set([token]), self.__cache._live_tokens)  
        self.assertEqual({token: ('ANY', 'test')}, self.__socket.shares)  
        self.assertEqual(set([token]), self.__socket.waiting_tokens)  
  
    def test_get_token_uncached(self):  
        """  
        Check a new socket is created when a corresponding one is missing.  
        """  
        token = self.__cache.get_token('UDP', self.__address, 1024, 'ANY',  
                                       'test')  
        # The get_socket was called  
        self.assertTrue(self.__get_socket_called)  
        # It returned something  
        self.assertIsNotNone(token)  
        # Get the socket and check it looks OK  
        socket = self.__cache._waiting_tokens[token]  
        self.assertEqual(self.__address, socket.address)  
        self.assertEqual(1024, socket.port)  
        self.assertEqual(42, socket.fileno)  
        self.assertEqual('UDP', socket.protocol)  
        # The socket is properly cached  
        self.assertEqual({  
            'UDP': {'192.0.2.1': {1024: socket}}  
        }, self.__cache._sockets)  
        # The token is both in the waiting sockets and the live tokens  
        self.assertEqual({token: socket}, self.__cache._waiting_tokens)  
        self.assertEqual(set([token]), self.__cache._live_tokens)  
        # The token got the new share to block any relevant queries  
        self.assertEqual({token: ('ANY', 'test')}, socket.shares)  
        # The socket knows the token is waiting in it  
        self.assertEqual(set([token]), socket.waiting_tokens)  
  
    def test_get_token_excs(self):  
        """  
        Test that it is handled properly if the socket creator raises  
        some exceptions.  
        """  
        def raiseCreatorError(fatal):  
            raise isc.bind10.sockcreator.CreatorError('test error', fatal)  
        # First, fatal socket creator errors are passed through  
        self.get_socket = lambda addr, port, proto: raiseCreatorError(True)  
        self.assertRaises(isc.bind10.sockcreator.CreatorError,  
                          self.__cache.get_token, 'UDP', self.__address, 1024,  
                          'NO', 'test')  
        # And nonfatal are converted to SocketError  
        self.get_socket = lambda addr, port, proto: raiseCreatorError(False)  
        self.assertRaises(isc.bind10.socket_cache.SocketError,  
                          self.__cache.get_token, 'UDP', self.__address, 1024,  
                          'NO', 'test')  
  
    def test_get_socket(self):  
        """  
        Test that we can pickup a socket if we know a token.  
        """  
        token = "token"  
        app = 13  
        # No socket prepared there  
        self.assertRaises(ValueError, self.__cache.get_socket, token, app)  
        # Not changed  
        self.assertEqual({}, self.__cache._active_tokens)  
        self.assertEqual({}, self.__cache._active_apps)  
        self.assertEqual({}, self.__cache._sockets)  
        self.assertEqual(set(), self.__cache._live_tokens)  
        # Prepare a token there  
        self.__socket.waiting_tokens = set([token])  
        self.__socket.shares = {token: ('ANY', 'app')}  
        self.__cache._waiting_tokens = {token: self.__socket}  
        self.__cache._sockets = {'UDP': {'192.0.2.1': {1024: self.__socket}}}  
        self.__cache._live_tokens = set([token])  
        socket = self.__cache.get_socket(token, app)  
        # Received the fileno  
        self.assertEqual(42, socket)  
        # It moved from waiting to active ones  
        self.assertEqual({}, self.__cache._waiting_tokens)  
        self.assertEqual({token: self.__socket}, self.__cache._active_tokens)  
        self.assertEqual({13: set([token])}, self.__cache._active_apps)  
        self.assertEqual(set([token]), self.__cache._live_tokens)  
        self.assertEqual(set(), self.__socket.waiting_tokens)  
        self.assertEqual({token: 13}, self.__socket.active_tokens)  
        # Trying to get it again fails  
        self.assertRaises(ValueError, self.__cache.get_socket, token, app)  
  
    def test_drop_application(self):  
        """  
        Test that a drop_application calls drop_socket on all the sockets  
        held by the application.  
        """  
        sockets = set()  
        def drop_socket(token):  
            sockets.add(token)  
        # Mock the drop_socket so we know it is called  
        self.__cache.drop_socket = drop_socket  
        self.assertRaises(ValueError, self.__cache.drop_application,  
                          13)  
        self.assertEqual(set(), sockets)  
        # Put the tokens into active_apps. Nothing else should be touched  
        # by this call, so leave it alone.  
        self.__cache._active_apps = {  
            1: set(['t1', 't2']),  
            2: set(['t3'])  
        }  
        self.__cache.drop_application(1)  
        # We don't check the _active_apps, as it would be cleaned by  
        # drop_socket and we removed it.  
        self.assertEqual(set(['t1', 't2']), sockets)  
  
    def test_drop_socket(self):  
        """  
        Test the drop_socket call. It tests:  
        * That a socket that still has something to keep it alive is left alive  
          (both waiting and active).  
        * If not, it is deleted.  
        * All bookkeeping data around are properly removed.  
        * Of course the exception.  
        """  
        self.assertRaises(ValueError, self.__cache.drop_socket, "bad token")  
        self.__socket.active_tokens = {'t1': 1}  
        self.__socket.waiting_tokens = set(['t2'])  
        self.__socket.shares = {'t1': ('ANY', 'app1'), 't2': ('ANY', 'app2')}  
        self.__cache._waiting_tokens = {'t2': self.__socket}  
        self.__cache._active_tokens = {'t1': self.__socket}  
        self.__cache._sockets = {'UDP': {'192.0.2.1': {1024: self.__socket}}}  
        self.__cache._live_tokens = set(['t1', 't2'])  
        self.__cache._active_apps = {1: set(['t1'])}  
        # We can't drop what wasn't picket up yet  
        self.assertRaises(ValueError, self.__cache.drop_socket, 't2')  
        self.assertEqual({'t1': 1}, self.__socket.active_tokens)  
        self.assertEqual(set(['t2']), self.__socket.waiting_tokens)  
        self.assertEqual({'t1': ('ANY', 'app1'), 't2': ('ANY', 'app2')},  
                         self.__socket.shares)  
        self.assertEqual({'t2': self.__socket}, self.__cache._waiting_tokens)  
        self.assertEqual({'t1': self.__socket}, self.__cache._active_tokens)  
        self.assertEqual({'UDP': {'192.0.2.1': {1024: self.__socket}}},  
                         self.__cache._sockets)  
        self.assertEqual(set(['t1', 't2']), self.__cache._live_tokens)  
        self.assertEqual({1: set(['t1'])}, self.__cache._active_apps)  
        self.assertEqual([], self._closes)  
        # If we drop this, it survives because it waits for being picked up  
        self.__cache.drop_socket('t1')  
        self.assertEqual({}, self.__socket.active_tokens)  
        self.assertEqual(set(['t2']), self.__socket.waiting_tokens)  
        self.assertEqual({'t2': ('ANY', 'app2')}, self.__socket.shares)  
        self.assertEqual({}, self.__cache._active_tokens)  
        self.assertEqual({'UDP': {'192.0.2.1': {1024: self.__socket}}},  
                         self.__cache._sockets)  
        self.assertEqual(set(['t2']), self.__cache._live_tokens)  
        self.assertEqual({}, self.__cache._active_apps)  
        self.assertEqual([], self._closes)  
        # Fill it again, now two applications having the same socket  
        self.__socket.active_tokens = {'t1': 1, 't2': 2}  
        self.__socket.waiting_tokens = set()  
        self.__socket.shares = {'t1': ('ANY', 'app1'), 't2': ('ANY', 'app2')}  
        self.__cache._waiting_tokens = {}  
        self.__cache._active_tokens = {  
            't1': self.__socket,  
            't2': self.__socket  
        }  
        self.__cache._live_tokens = set(['t1', 't2', 't3'])  
        self.assertEqual([], self._closes)  
        # We cheat here little bit, the t3 doesn't exist enywhere else, but  
        # we need to check the app isn't removed too soon and it shouldn't  
        # matter anywhere else, so we just avoid the tiresome filling in  
        self.__cache._active_apps = {1: set(['t1', 't3']), 2: set(['t2'])}  
        # Drop it as t1. It should still live.  
        self.__cache.drop_socket('t1')  
        self.assertEqual({'t2': 2}, self.__socket.active_tokens)  
        self.assertEqual(set(), self.__socket.waiting_tokens)  
        self.assertEqual({'t2': ('ANY', 'app2')}, self.__socket.shares)  
        self.assertEqual({}, self.__cache._waiting_tokens)  
        self.assertEqual({'t2': self.__socket}, self.__cache._active_tokens)  
        self.assertEqual({'UDP': {'192.0.2.1': {1024: self.__socket}}},  
                         self.__cache._sockets)  
        self.assertEqual(set(['t3', 't2']), self.__cache._live_tokens)  
        self.assertEqual({1: set(['t3']), 2: set(['t2'])},  
                         self.__cache._active_apps)  
        self.assertEqual([], self._closes)  
        # Drop it again, from the other application. It should get removed  
        # and closed.  
        self.__cache.drop_socket('t2')  
        self.assertEqual({}, self.__socket.active_tokens)  
        self.assertEqual(set(), self.__socket.waiting_tokens)  
        self.assertEqual({}, self.__socket.shares)  
        self.assertEqual({}, self.__cache._waiting_tokens)  
        self.assertEqual({}, self.__cache._active_tokens)  
        self.assertEqual({}, self.__cache._sockets)  
        self.assertEqual(set(['t3']), self.__cache._live_tokens)  
        self.assertEqual({1: set(['t3'])}, self.__cache._active_apps)  
        # The cache doesn't hold the socket. So when we remove it ourself,  
        # it should get closed.  
        self.__socket = None  
        self.assertEqual([42], self._closes)  
  
exitif __name__ == '__main__':  
    isc.log.init("bind10")  
    isc.log.resetUnitTestRootLogger()  
    unittest.main()  
                
             |