class TestICAClass(tornado.testing.AsyncTestCase):

    def setUp(self):
        super(TestICAClass, self).setUp()
        self.client = InspectingClientAsync('', 0, initial_inspection=False,
                                            ioloop=self.io_loop)

        self.client.set_state_callback(self._cb_state)

    def test_initial_state(self):
        self.assertEqual(self.client.state,
                         inspecting_client.InspectingClientStateType(
            connected=False, synced=False, model_changed=False, data_synced=False))

    def _cb_state(self, state, model_changes):
        """A callback used in the test."""
        self.stop((state, model_changes))

    def test_util_method_difference_add(self):
        """Test the _difference utility method on add."""

        name = None
        original_keys = ['B', 'C']
        updated_keys = ['B', 'C', 'D']
        self.client._sensors_index = {}
        for sen in original_keys:
            data = {'description': "This is {0}.".format(sen)}
            self.client._update_index(self.client._sensors_index, sen, data)
        added, removed = self.client._difference(
            original_keys, updated_keys, name, self.client._sensors_index)
        self.assertIn('D', added)
        self.assertIn('B', self.client.sensors)
        self.assertIn('C', self.client.sensors)

    def test_util_method_difference_rem(self):
        """Test the _difference utility method on remove."""

        name = None
        original_keys = ['A', 'B', 'C']
        updated_keys = ['B', 'C']
        self.client._sensors_index = {}
        for sen in original_keys:
            data = {'description': "This is {0}.".format(sen)}
            self.client._update_index(self.client._sensors_index, sen, data)

        added, removed = self.client._difference(
            original_keys, updated_keys, name, self.client._sensors_index)
        self.assertIn('A', removed)
        self.assertNotIn('A', self.client.sensors)
        self.assertIn('B', self.client.sensors)
        self.assertIn('C', self.client.sensors)

    def test_util_method_difference_rem_named(self):
        """Test the _difference utility method on remove with name set."""

        name = 'A'
        original_keys = ['B', 'C']
        updated_keys = []
        self.client._sensors_index = {}
        for sen in original_keys:
            data = {'description': "This is {0}.".format(sen)}
            self.client._update_index(self.client._sensors_index, sen, data)
        added, removed = self.client._difference(
            original_keys, updated_keys, name, self.client._sensors_index)
        self.assertNotIn('A', removed)
        self.assertNotIn('A', self.client.sensors)
        self.assertIn('B', self.client.sensors)
        self.assertIn('C', self.client.sensors)

    def test_util_method_difference_changed(self):
        """Test the _difference utility method on changed."""

        name = None
        original_keys = ['B']
        updated_keys = ['B']

        self.client._sensors_index = {}
        for sen in original_keys:
            data = {'description': "This is {0}.".format(sen), '_changed': True}
            self.client._update_index(self.client._sensors_index, sen, data)

        added, removed = self.client._difference(original_keys, updated_keys,
                                                 name, self.client._sensors_index)
        self.assertIn('B', self.client.sensors)

    def test_update_index(self):
        """Test the update_index method."""

        index = self.client._sensors_index = {}

        data = {'description': "This is {0}.".format('A')}
        self.client._update_index(index, 'A', data)

        data = {'description': "This is {0}.".format('B')}
        self.client._update_index(index, 'B', data)

        data = {'description': "This is {0}.".format('A')}
        self.client._update_index(index, 'A', data)

        data = {'description': "This is new {0}.".format('B')}
        self.client._update_index(index, 'B', data)

        self.assertIn('new', index['B'].get('description'))
        self.assertFalse(index['A'].get('_changed', False))
        self.assertTrue(index['B'].get('_changed'))
class TestInspectingClientAsyncStateCallback(tornado.testing.AsyncTestCase):
    longMessage = True
    maxDiff = None

    def setUp(self):
        super(TestInspectingClientAsyncStateCallback, self).setUp()
        self.server = DeviceTestServer('', 0)
        start_thread_with_cleanup(self, self.server, start_timeout=1)
        self.host, self.port = self.server.bind_address
        self.state_cb_future = tornado.concurrent.Future()
        self.client = InspectingClientAsync(self.host, self.port,
                                            ioloop=self.io_loop)
        # Set a short initial_resync timeout to make resync tests quick
        self.client.initial_resync_timeout = 0.001
        self.client.set_state_callback(self._test_state_cb)
        self.done_state_cb_futures = []
        self.cnt_state_cb_futures = collections.defaultdict(tornado.concurrent.Future)

    def _test_state_cb(self, state, model_changes):
        f = self.state_cb_future
        self.state_cb_future = tornado.concurrent.Future()
        self.done_state_cb_futures.append(f)
        num_calls = len(self.done_state_cb_futures)
        f.set_result((state, model_changes))
        self.cnt_state_cb_futures[num_calls].set_result(None)

    @tornado.gen.coroutine
    def _check_cb_count(self, expected_count):
        """Let the ioloop run and assert that the callback has been called
        the expected number of times"""
        yield tornado.gen.moment
        self.assertEqual(len(self.done_state_cb_futures), expected_count)

    @tornado.testing.gen_test(timeout=1)
    def test_from_connect(self):
        # Hold back #version-connect informs
        num_calls_before = len(self.done_state_cb_futures)
        logger.debug('before starting client, num_calls_before:{}'.format(num_calls_before))

        self.server.proceed_on_client_connect.clear()
        self.client.connect()

        state, model_changes = yield self.state_cb_future
        self.assertEqual(state, inspecting_client.InspectingClientStateType(
            connected=False, synced=False, model_changed=False, data_synced=False))

        state, model_changes = yield self.state_cb_future
        self.assertEqual(state, inspecting_client.InspectingClientStateType(
            connected=True, synced=False, model_changed=False, data_synced=False))
        self.assertIs(model_changes, None)
        # Due to the structure of the state loop the initial state may be sent
        # twice before we get here, and was the case for the initial
        # implementation. Changes made on 2015-01-26 caused it to happen only
        # once, hence + 1. If the implementation changes having + 2 would also
        # be OK. As, indeed, changes made on 2016-11-03 caused again :)
        yield self._check_cb_count(num_calls_before + 2)

        # Now let the server send #version-connect informs
        num_calls_before = len(self.done_state_cb_futures)
        self.server.ioloop.add_callback(self.server.proceed_on_client_connect.set)
        # We're expecting two calls hard on each other's heels, so lets wait for them
        yield self.cnt_state_cb_futures[num_calls_before + 2]
        # We expected two status callbacks, and no more after
        yield self._check_cb_count(num_calls_before + 2)
        state, model_changes = yield self.done_state_cb_futures[-2]
        state2, model_changes2 = yield self.done_state_cb_futures[-1]
        self.assertEqual(state, inspecting_client.InspectingClientStateType(
            connected=True, synced=False, model_changed=True, data_synced=True))
        # Check that the expected model changes came from the callback
        self._test_expected_model_changes(model_changes)
        self.assertEqual(state2, inspecting_client.InspectingClientStateType(
            connected=True, synced=True, model_changed=False, data_synced=True))
        self.assertEqual(model_changes2, None)

    def _test_expected_model_changes(self, model_changes):
        # Check that the model_changes reflect the sensors and requests of the
        # test sever (self.server)
        server_sensors = self.server._sensors.keys()
        server_requests = self.server._request_handlers.keys()
        self.assertEqual(model_changes, dict(
            sensors=dict(added=set(server_sensors), removed=set()),
            requests=dict(added=set(server_requests), removed=set())))


    @tornado.testing.gen_test(timeout=1)
    def test_reconnect(self):
        self.client.connect()
        yield self.client.until_synced()
        yield tornado.gen.moment   # Make sure the ioloop is 'caught up'

        # cause a disconnection and check that the callback is called
        num_calls_before = len(self.done_state_cb_futures)
        self.server.stop()
        self.server.join()
        state, model_changes = yield self.state_cb_future
        self.assertEqual(state, inspecting_client.InspectingClientStateType(
            connected=False, synced=False, model_changed=False, data_synced=False))
        self.assertIs(model_changes, None)
        yield self._check_cb_count(num_calls_before + 1)

    @tornado.gen.coroutine
    def _test_inspection_error(self, break_var, break_message):
        # Test that the client retries if there is an error in the inspection
        # process
        setattr(self.server, break_var, break_message)

        self.client.connect()
        # Wait for the client to be connected
        yield self.client.until_connected()
        # Wait for the state loop to send another update or 2
        yield self.state_cb_future
        state, _ = yield self.state_cb_future
        # Check that data is still not synced
        self.assertFalse(state.synced)
        self.assertFalse(state.data_synced)

        # Now fix the inspection request, client should sync up.
        setattr(self.server, break_var, False)
        # Check that the server's sensors and request are reflected in the model
        # changes.
        #
        changes_state = inspecting_client.InspectingClientStateType(
            connected=True, synced=False, model_changed=True, data_synced=True)
        yield self.client.until_state(changes_state)
        state, model_changes = self.done_state_cb_futures[-1].result()
        assert state == changes_state
        self._test_expected_model_changes(model_changes)
        yield self.client.until_synced()
        self.assertTrue(self.client.synced)



    @tornado.testing.gen_test(timeout=1)
    def test_help_inspection_error(self):
        yield self._test_inspection_error('break_help', 'Help is broken')

    @tornado.testing.gen_test(timeout=1)
    def test_sensor_list_inspection_error(self):
        yield self._test_inspection_error(
            'break_sensor_list', 'Sensor-list is broken')
class TestICAClass(tornado.testing.AsyncTestCase):

    def setUp(self):
        super(TestICAClass, self).setUp()
        self.client = InspectingClientAsync('', 0, initial_inspection=False,
                                            ioloop=self.io_loop)

        self.client.set_state_callback(self._cb_state)

    def test_initial_state(self):
        self.assertEqual(self.client.state,
                         inspecting_client.InspectingClientStateType(
            connected=False, synced=False, model_changed=False, data_synced=False))

    def _cb_state(self, state, model_changes):
        """A callback used in the test."""
        self.stop((state, model_changes))

    def test_util_method_difference_add(self):
        """Test the _difference utility method on add."""

        name = None
        original_keys = ['B', 'C']
        updated_keys = ['B', 'C', 'D']
        self.client._sensors_index = {}
        for sen in original_keys:
            data = {'description': "This is {0}.".format(sen)}
            self.client._update_index(self.client._sensors_index, sen, data)
        added, removed = self.client._difference(
            original_keys, updated_keys, name, self.client._sensors_index)
        self.assertIn('D', added)
        self.assertIn('B', self.client.sensors)
        self.assertIn('C', self.client.sensors)

    def test_util_method_difference_rem(self):
        """Test the _difference utility method on remove."""

        name = None
        original_keys = ['A', 'B', 'C']
        updated_keys = ['B', 'C']
        self.client._sensors_index = {}
        for sen in original_keys:
            data = {'description': "This is {0}.".format(sen)}
            self.client._update_index(self.client._sensors_index, sen, data)

        added, removed = self.client._difference(
            original_keys, updated_keys, name, self.client._sensors_index)
        self.assertIn('A', removed)
        self.assertNotIn('A', self.client.sensors)
        self.assertIn('B', self.client.sensors)
        self.assertIn('C', self.client.sensors)

    def test_util_method_difference_rem_named(self):
        """Test the _difference utility method on remove with name set."""

        name = 'A'
        original_keys = ['B', 'C']
        updated_keys = []
        self.client._sensors_index = {}
        for sen in original_keys:
            data = {'description': "This is {0}.".format(sen)}
            self.client._update_index(self.client._sensors_index, sen, data)
        added, removed = self.client._difference(
            original_keys, updated_keys, name, self.client._sensors_index)
        self.assertNotIn('A', removed)
        self.assertNotIn('A', self.client.sensors)
        self.assertIn('B', self.client.sensors)
        self.assertIn('C', self.client.sensors)

    def test_util_method_difference_changed(self):
        """Test the _difference utility method on changed."""

        name = None
        original_keys = ['B']
        updated_keys = ['B']

        self.client._sensors_index = {}
        for sen in original_keys:
            data = {'description': "This is {0}.".format(sen), '_changed': True}
            self.client._update_index(self.client._sensors_index, sen, data)

        added, removed = self.client._difference(original_keys, updated_keys,
                                                 name, self.client._sensors_index)
        self.assertIn('B', self.client.sensors)

    def test_update_index(self):
        """Test the update_index method."""

        index = self.client._sensors_index = {}

        data = {'description': "This is {0}.".format('A')}
        self.client._update_index(index, 'A', data)

        data = {'description': "This is {0}.".format('B')}
        self.client._update_index(index, 'B', data)

        data = {'description': "This is {0}.".format('A')}
        self.client._update_index(index, 'A', data)

        data = {'description': "This is new {0}.".format('B')}
        self.client._update_index(index, 'B', data)

        self.assertIn('new', index['B'].get('description'))
        self.assertFalse(index['A'].get('_changed', False))
        self.assertTrue(index['B'].get('_changed'))
class TestInspectingClientAsyncStateCallback(tornado.testing.AsyncTestCase):

    def setUp(self):
        super(TestInspectingClientAsyncStateCallback, self).setUp()
        self.server = DeviceTestServer('', 0)
        start_thread_with_cleanup(self, self.server, start_timeout=1)
        self.host, self.port = self.server.bind_address
        self.state_cb_future = tornado.concurrent.Future()
        self.client = InspectingClientAsync(self.host, self.port,
                                            ioloop=self.io_loop)
        self.client.set_state_callback(self._test_state_cb)
        self.done_state_cb_futures = []
        self.cnt_state_cb_futures = collections.defaultdict(tornado.concurrent.Future)

    def _test_state_cb(self, state, model_changes):
        f = self.state_cb_future
        self.state_cb_future = tornado.concurrent.Future()
        self.done_state_cb_futures.append(f)
        num_calls = len(self.done_state_cb_futures)
        f.set_result((state, model_changes))
        self.cnt_state_cb_futures[num_calls].set_result(None)

    @tornado.gen.coroutine
    def _check_no_cb(self, no_expected):
        """Let the ioloop run and assert that the callback was not called"""
        yield tornado.gen.moment
        self.assertEqual(len(self.done_state_cb_futures), no_expected)

    @tornado.testing.gen_test(timeout=1)
    def test_from_connect(self):
        # Hold back #version-connect informs
        num_calls_before = len(self.done_state_cb_futures)
        logger.debug('before starting client, num_calls_before:{}'.format(num_calls_before))

        self.server.proceed_on_client_connect.clear()
        self.client.connect()

        state, model_changes = yield self.state_cb_future
        self.assertEqual(state, inspecting_client.InspectingClientStateType(
            connected=True, synced=False, model_changed=False, data_synced=False))
        self.assertIs(model_changes, None)
        # Due to the structure of the state loop the initial state may be sent twice
        # before we get her, and was the case for the initial implementation. Changes made
        # on 2015-01-26 caused it to happy only once, hence + 1. If the implementation
        # changes having + 2 would also be OK.
        yield self._check_no_cb(num_calls_before + 1)

        # Now let the server send #version-connect informs
        num_calls_before = len(self.done_state_cb_futures)
        self.server.ioloop.add_callback(self.server.proceed_on_client_connect.set)
        # We're expecting two calls hard on each other's heels, so lets wait for them
        yield self.cnt_state_cb_futures[num_calls_before + 2]
        # We expected two status callbacks, and no more after
        yield self._check_no_cb(num_calls_before + 2)
        state, model_changes = yield self.done_state_cb_futures[-2]
        state2, model_changes2 = yield self.done_state_cb_futures[-1]
        self.assertEqual(state, inspecting_client.InspectingClientStateType(
            connected=True, synced=False, model_changed=True, data_synced=True))
        server_sensors = self.server._sensors.keys()
        server_requests = self.server._request_handlers.keys()
        self.assertEqual(model_changes, dict(
            sensors=dict(added=set(server_sensors), removed=set()),
            requests=dict(added=set(server_requests), removed=set())))
        self.assertEqual(state2, inspecting_client.InspectingClientStateType(
            connected=True, synced=True, model_changed=False, data_synced=True))
        self.assertEqual(model_changes2, None)

    @tornado.testing.gen_test(timeout=1)
    def test_reconnect(self):
        self.client.connect()
        yield self.client.until_synced()
        yield tornado.gen.moment   # Make sure the ioloop is 'caught up'

        # cause a disconnection and check that the callback is called
        num_calls_before = len(self.done_state_cb_futures)
        self.server.stop()
        self.server.join()
        state, model_changes = yield self.state_cb_future
        self.assertEqual(state, inspecting_client.InspectingClientStateType(
            connected=False, synced=False, model_changed=False, data_synced=False))
        self.assertIs(model_changes, None)
        yield self._check_no_cb(num_calls_before + 1)
class TestInspectingClientAsyncStateCallback(tornado.testing.AsyncTestCase):
    longMessage = True
    maxDiff = None

    def setUp(self):
        super(TestInspectingClientAsyncStateCallback, self).setUp()
        self.server = DeviceTestServer('', 0)
        start_thread_with_cleanup(self, self.server, start_timeout=1)
        self.host, self.port = self.server.bind_address
        self.state_cb_future = tornado.concurrent.Future()
        self.client = InspectingClientAsync(self.host, self.port,
                                            ioloop=self.io_loop)
        # Set a short initial_resync timeout to make resync tests quick
        self.client.initial_resync_timeout = 0.001
        self.client.set_state_callback(self._test_state_cb)
        self.done_state_cb_futures = []
        self.cnt_state_cb_futures = collections.defaultdict(tornado.concurrent.Future)

    def _test_state_cb(self, state, model_changes):
        f = self.state_cb_future
        self.state_cb_future = tornado.concurrent.Future()
        self.done_state_cb_futures.append(f)
        num_calls = len(self.done_state_cb_futures)
        f.set_result((state, model_changes))
        self.cnt_state_cb_futures[num_calls].set_result(None)

    @tornado.gen.coroutine
    def _check_cb_count(self, expected_count):
        """Let the ioloop run and assert that the callback has been called
        the expected number of times"""
        yield tornado.gen.moment
        self.assertEqual(len(self.done_state_cb_futures), expected_count)

    @tornado.testing.gen_test(timeout=1)
    def test_from_connect(self):
        # Hold back #version-connect informs
        num_calls_before = len(self.done_state_cb_futures)
        logger.debug('before starting client, num_calls_before:{}'.format(num_calls_before))

        self.server.proceed_on_client_connect.clear()
        self.client.connect()

        state, model_changes = yield self.state_cb_future
        self.assertEqual(state, inspecting_client.InspectingClientStateType(
            connected=False, synced=False, model_changed=False, data_synced=False))

        state, model_changes = yield self.state_cb_future
        self.assertEqual(state, inspecting_client.InspectingClientStateType(
            connected=True, synced=False, model_changed=False, data_synced=False))
        self.assertIs(model_changes, None)
        # Due to the structure of the state loop the initial state may be sent
        # twice before we get here, and was the case for the initial
        # implementation. Changes made on 2015-01-26 caused it to happen only
        # once, hence + 1. If the implementation changes having + 2 would also
        # be OK. As, indeed, changes made on 2016-11-03 caused again :)
        yield self._check_cb_count(num_calls_before + 2)

        # Now let the server send #version-connect informs
        num_calls_before = len(self.done_state_cb_futures)
        self.server.ioloop.add_callback(self.server.proceed_on_client_connect.set)
        # We're expecting two calls hard on each other's heels, so lets wait for them
        yield self.cnt_state_cb_futures[num_calls_before + 2]
        # We expected two status callbacks, and no more after
        yield self._check_cb_count(num_calls_before + 2)
        state, model_changes = yield self.done_state_cb_futures[-2]
        state2, model_changes2 = yield self.done_state_cb_futures[-1]
        self.assertEqual(state, inspecting_client.InspectingClientStateType(
            connected=True, synced=False, model_changed=True, data_synced=True))
        # Check that the expected model changes came from the callback
        self._test_expected_model_changes(model_changes)
        self.assertEqual(state2, inspecting_client.InspectingClientStateType(
            connected=True, synced=True, model_changed=False, data_synced=True))
        self.assertEqual(model_changes2, None)

    def _test_expected_model_changes(self, model_changes):
        # Check that the model_changes reflect the sensors and requests of the
        # test sever (self.server)
        server_sensors = self.server._sensors.keys()
        server_requests = self.server._request_handlers.keys()
        self.assertEqual(model_changes, dict(
            sensors=dict(added=set(server_sensors), removed=set()),
            requests=dict(added=set(server_requests), removed=set())))


    @tornado.testing.gen_test(timeout=1)
    def test_reconnect(self):
        self.client.connect()
        yield self.client.until_synced()
        yield tornado.gen.moment   # Make sure the ioloop is 'caught up'

        # cause a disconnection and check that the callback is called
        num_calls_before = len(self.done_state_cb_futures)
        self.server.stop()
        self.server.join()
        state, model_changes = yield self.state_cb_future
        self.assertEqual(state, inspecting_client.InspectingClientStateType(
            connected=False, synced=False, model_changed=False, data_synced=False))
        self.assertIs(model_changes, None)
        yield self._check_cb_count(num_calls_before + 1)

    @tornado.gen.coroutine
    def _test_inspection_error(self, break_var, break_message):
        # Test that the client retries if there is an error in the inspection
        # process
        setattr(self.server, break_var, break_message)

        self.client.connect()
        # Wait for the client to be connected
        yield self.client.until_connected()
        # Wait for the state loop to send another update or 2
        yield self.state_cb_future
        state, _ = yield self.state_cb_future
        # Check that data is still not synced
        self.assertFalse(state.synced)
        self.assertFalse(state.data_synced)

        # Now fix the inspection request, client should sync up.
        setattr(self.server, break_var, False)
        # Check that the server's sensors and request are reflected in the model
        # changes.
        #
        changes_state = inspecting_client.InspectingClientStateType(
            connected=True, synced=False, model_changed=True, data_synced=True)
        yield self.client.until_state(changes_state)
        state, model_changes = self.done_state_cb_futures[-1].result()
        assert state == changes_state
        self._test_expected_model_changes(model_changes)
        yield self.client.until_synced()
        self.assertTrue(self.client.synced)



    @tornado.testing.gen_test(timeout=1)
    def test_help_inspection_error(self):
        yield self._test_inspection_error('break_help', 'Help is broken')

    @tornado.testing.gen_test(timeout=1)
    def test_sensor_list_inspection_error(self):
        yield self._test_inspection_error(
            'break_sensor_list', 'Sensor-list is broken')