def setUp(self): self.host = 'foobar.baz' self.port = 8080 self.device_id = 'DFDS44' self.meta = ThreadSafeDict() self.meta['foo'] = mock.Mock(units="F", sampleRate="10", min=0, max=100) self.meta['bar'] = mock.Mock(units="C", sampleRate="1", min=-100, max=200) self.meta['baz'] = mock.Mock(units="C", sampleRate="1", min=-100, max=200) # Mock's constructor treats 'name' in a special way, it won't return a string self.meta['foo'].configure_mock(name='foo') self.meta['bar'].configure_mock(name='bar') self.meta['baz'].configure_mock(name='baz') self.data_bus = None self.telemetry_connection = None self.status_function = mock.Mock() self.data_bus = mock.Mock() self.telemetry_connection = TelemetryConnection(self.host, self.port, self.device_id, self.meta, self.data_bus, self.status_function) # We mock these functions because we don't want to really connect to anything # This is a unit test, not integration! self.telemetry_connection.create_socket = mock.Mock() self.telemetry_connection.connect = mock.Mock() self.telemetry_connection.push = mock.Mock()
class TelemetryConnectionTest(unittest.TestCase): def setUp(self): self.host = 'foobar.baz' self.port = 8080 self.device_id = 'DFDS44' self.meta = ThreadSafeDict() self.meta['foo'] = mock.Mock(units="F", sampleRate="10", min=0, max=100) self.meta['bar'] = mock.Mock(units="C", sampleRate="1", min=-100, max=200) self.meta['baz'] = mock.Mock(units="C", sampleRate="1", min=-100, max=200) # Mock's constructor treats 'name' in a special way, it won't return a string self.meta['foo'].configure_mock(name='foo') self.meta['bar'].configure_mock(name='bar') self.meta['baz'].configure_mock(name='baz') self.data_bus = None self.telemetry_connection = None self.status_function = mock.Mock() self.data_bus = mock.Mock() self.telemetry_connection = TelemetryConnection(self.host, self.port, self.device_id, self.meta, self.data_bus, self.status_function) # We mock these functions because we don't want to really connect to anything # This is a unit test, not integration! self.telemetry_connection.create_socket = mock.Mock() self.telemetry_connection.connect = mock.Mock() self.telemetry_connection.push = mock.Mock() def test_run(self, asyncore_loop_mock): self.telemetry_connection.run() self.telemetry_connection.create_socket.assert_called_with(socket.AF_INET, socket.SOCK_STREAM) self.telemetry_connection.connect.assert_called_with((self.host, self.port)) self.assertTrue(asyncore_loop_mock.called) self.assertTrue(self.data_bus.add_sample_listener.called) self.assertTrue(self.data_bus.addMetaListener.called) def test_sends_auth(self, asyncore_loop_mock): self.telemetry_connection.run() self.telemetry_connection.handle_connect() self.telemetry_connection.push.assert_called_with('{"cmd":{"schemaVer":2,"auth":{"deviceId":"' + self.device_id + '"}}}' + "\n") def test_sends_authorized_status(self, asyncore_loop_mock): self.telemetry_connection.run() self.telemetry_connection.handle_connect() args, kwargs = self.status_function.call_args status, message, status_code = args self.assertEqual(status_code, TelemetryConnection.STATUS_CONNECTED) self.telemetry_connection.collect_incoming_data('{"status":"ok"}') self.telemetry_connection.found_terminator() args, kwargs = self.status_function.call_args status, message, status_code = args self.assertEqual(status_code, TelemetryConnection.STATUS_STREAMING) def test_sends_error_authenticating_status(self, asyncore_loop_mock): self.telemetry_connection.run() self.telemetry_connection.handle_connect() self.telemetry_connection.collect_incoming_data('{"status":"error","message":"invalid serial number"}') self.telemetry_connection.found_terminator() args, kwargs = self.status_function.call_args status, message, status_code = args self.assertEqual(status_code, TelemetryConnection.ERROR_AUTHENTICATING) def test_sends_unknown_error(self, asyncore_loop_mock): self.telemetry_connection.run() self.telemetry_connection.handle_connect() self.telemetry_connection.collect_incoming_data('{"foo":"bar"}') self.telemetry_connection.found_terminator() args, kwargs = self.status_function.call_args status, message, status_code = args self.assertEqual(status_code, TelemetryConnection.ERROR_UNKNOWN_MESSAGE) def test_sends_meta(self, asyncore_loop_mock): self.telemetry_connection.run() self.telemetry_connection.handle_connect() args, kwargs = self.status_function.call_args status, message, status_code = args self.assertEqual(status_code, TelemetryConnection.STATUS_CONNECTED) self.telemetry_connection.collect_incoming_data('{"status":"ok"}') self.telemetry_connection.found_terminator() args, kwargs = self.telemetry_connection.push.call_args message, = args meta_json = json.loads(message) self.assertTrue("s" in meta_json, "Sends 's'") self.assertTrue("meta" in meta_json["s"], "Sends meta property") for name, info in self.meta.iteritems(): # Verify all the channels were sent with correct info channel_info = next((item for item in meta_json["s"]["meta"] if item["nm"] == name), None) self.assertIsNotNone(channel_info, "Channel found") self.assertEqual(channel_info["ut"], info.units, "Units are sent") self.assertEqual(channel_info["sr"], info.sampleRate, "Sample rate is sent") self.assertEqual(channel_info["min"], info.min, "Min is sent") self.assertEqual(channel_info["max"], info.max, "Max is sent") def test_sends_line_break(self, asyncore_loop_mock): self.telemetry_connection.send_msg("foo") args, kwargs = self.telemetry_connection.push.call_args message, = args self.assertEqual(message, "foo\n", "Newline added to end of message") @patch('threading.Timer') def test_sends_bitmask(self, timer_mock, asyncore_loop_mock): sample = ThreadSafeDict() sample['bar'] = 3.0 bitmask = '' # Compute our own bitmask to verify for channel_name, value in self.meta.iteritems(): if channel_name == 'bar': bitmask = "1" + bitmask else: bitmask = "0" + bitmask bitmask = int(bitmask, 2) self.telemetry_connection._on_sample(sample) self.telemetry_connection._send_sample() args, kwargs = self.telemetry_connection.push.call_args message, = args message_json = json.loads(message) self.assertEqual(bitmask, message_json["s"]["d"][len(message_json["s"]["d"])-1]) @patch('threading.Timer') def test_sends_multiple_bitmasks(self, timer_mock, asyncore_loop_mock): sample = ThreadSafeDict() meta = ThreadSafeDict() for i in range(1, 40): meta['sensor' + str(i)] = mock.Mock(units="F", sampleRate="10", min=random.randint(-100, 0), max=random.randint(0, 100)) meta['sensor' + str(i)].configure_mock(name='sensor' + str(i)) # Manually doing this to aid debugging if this test breaks sample['sensor2'] = random.randint(-50, 50) sample['sensor3'] = random.randint(-50, 50) sample['sensor4'] = random.randint(-50, 50) sample['sensor5'] = random.randint(-50, 50) sample['sensor6'] = random.randint(-50, 50) sample['sensor7'] = random.randint(-50, 50) sample['sensor8'] = random.randint(-50, 50) sample['sensor10'] = random.randint(-50, 50) sample['sensor11'] = random.randint(-50, 50) sample['sensor12'] = random.randint(-50, 50) sample['sensor21'] = random.randint(-50, 50) sample['sensor25'] = random.randint(-50, 50) sample['sensor26'] = random.randint(-50, 50) sample['sensor29'] = random.randint(-50, 50) sample['sensor33'] = random.randint(-50, 50) sample['sensor35'] = random.randint(-50, 50) sample['sensor37'] = random.randint(-50, 50) sample['sensor38'] = random.randint(-50, 50) sample['sensor39'] = random.randint(-50, 50) self.telemetry_connection.authorized = True self.telemetry_connection._on_meta(meta) self.telemetry_connection._on_sample(sample) self.telemetry_connection._send_sample() args, kwargs = self.telemetry_connection.push.call_args message, = args message_json = json.loads(message) self.assertEqual(19, message_json["s"]["d"][len(message_json["s"]["d"])-1]) self.assertEqual(1479378921, message_json["s"]["d"][len(message_json["s"]["d"])-2]) @patch('threading.Timer') def resends_meta(self, timer_mock, asyncore_loop_mock): meta = {} for i in range(1, 5): meta['sensor'+str(i)] = mock.Mock(units="F", sampleRate="10", min=random.randint(-100,0), max=random.randint(0,100)) meta['sensor'+str(i)].configure_mock(name='sensor'+str(i)) self.telemetry_connection._on_meta(meta) args, kwargs = self.telemetry_connection.push.call_args message, = args message_json = json.loads(message) self.assertEqual(len(message_json["s"]["meta"]), 5)
class DataBus(object): """Central hub for current sample data. Receives data from DataBusPump Also contains the periodic updater for listeners. Updates occur in the UI thread via Clock.schedule_interval Architecture: (DATA SOURCE) => DataBus => (LISTENERS) Typical use: (CHANNEL LISTENERS) => DataBus.addChannelListener() -- listeners receive updates with a particular channel's value (META LISTENERS) => DataBus.addMetaListener() -- Listeners receive updates with meta data Note: DataBus must be started via start_update before any data flows """ channel_metas = ThreadSafeDict() channel_data = ThreadSafeDict() sample = None channel_listeners = {} meta_listeners = [] meta_updated = False data_filters = [] sample_listeners = [] _polling = False rcp_meta_read = False def __init__(self, **kwargs): super(DataBus, self).__init__(**kwargs) def start_update(self, interval=DEFAULT_DATABUS_UPDATE_INTERVAL): if self._polling: return Clock.schedule_interval(self.notify_listeners, interval) self._polling = True def stop_update(self): Clock.unschedule(self.notify_listeners) self._polling = False def _update_datafilter_meta(self, datafilter): channel_metas = self.channel_metas metas = datafilter.get_channel_meta(channel_metas) with self.channel_metas as cm: for channel, meta in metas.iteritems(): cm[channel] = meta def update_channel_meta(self, metas): """update channel metadata information This should be called when the channel information has changed """ # update channel metadata with self.channel_metas as cm, self.channel_data as cd: # clear our list of channel data values, in case channels # were removed on this metadata update cd.clear() # clear and reload our channel metas cm.clear() for meta in metas.channel_metas: cm[meta.name] = meta # add channel meta for existing filters for f in self.data_filters: self._update_datafilter_meta(f) self.meta_updated = True self.rcp_meta_read = True def addSampleListener(self, callback): self.sample_listeners.append(callback) def update_samples(self, sample): """Update channel data with new samples """ with self.channel_data as cd: for sample_item in sample.samples: channel = sample_item.channelMeta.name value = sample_item.value cd[channel] = value # apply filters to updated data for f in self.data_filters: f.filter(cd) def notify_listeners(self, dt): if self.meta_updated: with self.channel_metas as cm: self.notify_meta_listeners(cm) self.meta_updated = False with self.channel_data as cd: for channel, value in cd.iteritems(): self.notify_channel_listeners(channel, value) for listener in self.sample_listeners: listener(cd) def notify_channel_listeners(self, channel, value): listeners = self.channel_listeners.get(str(channel)) if listeners: for listener in listeners: listener(value) def notify_meta_listeners(self, channelMeta): for listener in self.meta_listeners: listener(channelMeta) def addChannelListener(self, channel, callback): listeners = self.channel_listeners.get(channel) if listeners == None: listeners = [callback] self.channel_listeners[channel] = listeners else: listeners.append(callback) def removeChannelListener(self, channel, callback): try: listeners = self.channel_listeners.get(channel) if listeners: listeners.remove(callback) except: pass def remove_sample_listener(self, listener): ''' Remove the specified sample listener :param listener :type object / callback function ''' try: self.sample_listeners.remove(listener) except Exception as e: Logger.debug('Could not remove sample listener {}: {}'.format( listener, e)) def remove_meta_listener(self, listener): ''' Remove the specified meta listener :param listener :type object / callback function ''' try: self.meta_listeners.remove(listener) except Exception as e: Logger.debug('Could not remove meta listener {}: {}'.format( listener, e)) def add_sample_listener(self, callback): self.sample_listeners.append(callback) def addMetaListener(self, callback): self.meta_listeners.append(callback) def add_data_filter(self, datafilter): self.data_filters.append(datafilter) self._update_datafilter_meta(datafilter) def getMeta(self): return self.channel_metas def getData(self, channel): return self.channel_data[channel]