def setUp(self): self.receiver = MockPositioningReceiver() self.adapter = nmea.NMEAAdapter(self.receiver) self.protocol = nmea.NMEAProtocol(self.adapter)
class NMEAReceiverTests(TestCase): """ Tests for the NMEA receiver. """ def setUp(self): self.receiver = MockPositioningReceiver() self.adapter = nmea.NMEAAdapter(self.receiver) self.protocol = nmea.NMEAProtocol(self.adapter) def test_onlyFireWhenCurrentSentenceHasNewInformation(self): """ If the current sentence does not contain any new fields for a particular callback, that callback is not called; even if all necessary information is still in the state from one or more previous messages. """ self.protocol.lineReceived(GPGGA) gpggaCallbacks = set( ["positionReceived", "positionErrorReceived", "altitudeReceived"] ) self.assertEqual(set(self.receiver.called.keys()), gpggaCallbacks) self.receiver.clear() self.assertNotEqual(self.adapter._state, {}) # GPHDT contains heading information but not position, # altitude or anything like that; but that information is # still in the state. self.protocol.lineReceived(GPHDT) gphdtCallbacks = set(["headingReceived"]) self.assertEqual(set(self.receiver.called.keys()), gphdtCallbacks) def _receiverTest(self, sentences, expectedFired=(), extraTest=None): """ A generic test for NMEA receiver behavior. @param sentences: The sequence of sentences to simulate receiving. @type sentences: iterable of C{str} @param expectedFired: The names of the callbacks expected to fire. @type expectedFired: iterable of C{str} @param extraTest: An optional extra test hook. @type extraTest: nullary callable """ for sentence in sentences: self.protocol.lineReceived(sentence) actuallyFired = self.receiver.called.keys() self.assertEqual(set(actuallyFired), set(expectedFired)) if extraTest is not None: extraTest() self.receiver.clear() self.adapter.clear() def test_positionErrorUpdateAcrossStates(self): """ The positioning error is updated across multiple states. """ sentences = [GPGSA] + GPGSV_SEQ callbacksFired = ["positionErrorReceived", "beaconInformationReceived"] def _getIdentifiers(beacons): return sorted(map(attrgetter("identifier"), beacons)) def checkBeaconInformation(): beaconInformation = self.adapter._state["beaconInformation"] seenIdentifiers = _getIdentifiers(beaconInformation.seenBeacons) expected = [3, 4, 6, 13, 14, 16, 18, 19, 22, 24, 27] self.assertEqual(seenIdentifiers, expected) usedIdentifiers = _getIdentifiers(beaconInformation.usedBeacons) # These are not actually all the PRNs in the sample GPGSA: # only the ones also reported by the GPGSV sequence. This # is just because the sample data doesn't come from the # same reporting cycle of a GPS device. self.assertEqual(usedIdentifiers, [14, 18, 19, 22, 27]) self._receiverTest(sentences, callbacksFired, checkBeaconInformation) def test_emptyMiddleGSV(self): """ A GSV sentence with empty entries in any position does not mean that entries in subsequent positions of the same GSV sentence are ignored. """ sentences = [GPGSV_EMPTY_MIDDLE] callbacksFired = ["beaconInformationReceived"] def checkBeaconInformation(): beaconInformation = self.adapter._state["beaconInformation"] seenBeacons = beaconInformation.seenBeacons self.assertEqual(len(seenBeacons), 2) self.assertIn(13, [b.identifier for b in seenBeacons]) self._receiverTest(sentences, callbacksFired, checkBeaconInformation) def test_GGASentences(self): """ A sequence of GGA sentences fires C{positionReceived}, C{positionErrorReceived} and C{altitudeReceived}. """ sentences = [GPGGA] callbacksFired = [ "positionReceived", "positionErrorReceived", "altitudeReceived", ] self._receiverTest(sentences, callbacksFired) def test_GGAWithDateInState(self): """ When receiving a GPGGA sentence and a date was already in the state, the new time (from the GPGGA sentence) is combined with that date. """ self.adapter._state["_date"] = datetime.date(2014, 1, 1) sentences = [GPGGA] callbacksFired = [ "positionReceived", "positionErrorReceived", "altitudeReceived", "timeReceived", ] self._receiverTest(sentences, callbacksFired) def test_RMCSentences(self): """ A sequence of RMC sentences fires C{positionReceived}, C{speedReceived}, C{headingReceived} and C{timeReceived}. """ sentences = [GPRMC] callbacksFired = [ "headingReceived", "speedReceived", "positionReceived", "timeReceived", ] self._receiverTest(sentences, callbacksFired) def test_GSVSentences(self): """ A complete sequence of GSV sentences fires C{beaconInformationReceived}. """ sentences = [GPGSV_FIRST, GPGSV_MIDDLE, GPGSV_LAST] callbacksFired = ["beaconInformationReceived"] def checkPartialInformation(): self.assertNotIn("_partialBeaconInformation", self.adapter._state) self._receiverTest(sentences, callbacksFired, checkPartialInformation) def test_emptyMiddleEntriesGSVSequence(self): """ A complete sequence of GSV sentences with empty entries in the middle still fires C{beaconInformationReceived}. """ sentences = [GPGSV_EMPTY_MIDDLE] self._receiverTest(sentences, ["beaconInformationReceived"]) def test_incompleteGSVSequence(self): """ An incomplete sequence of GSV sentences does not fire any callbacks. """ sentences = [GPGSV_FIRST] self._receiverTest(sentences) def test_singleSentenceGSVSequence(self): """ The parser does not fail badly when the sequence consists of only one sentence (but is otherwise complete). """ sentences = [GPGSV_SINGLE] self._receiverTest(sentences, ["beaconInformationReceived"]) def test_GLLSentences(self): """ GLL sentences fire C{positionReceived}. """ sentences = [GPGLL_PARTIAL, GPGLL] self._receiverTest(sentences, ["positionReceived"]) def test_HDTSentences(self): """ HDT sentences fire C{headingReceived}. """ sentences = [GPHDT] self._receiverTest(sentences, ["headingReceived"]) def test_mixedSentences(self): """ A mix of sentences fires the correct callbacks. """ sentences = [GPRMC, GPGGA] callbacksFired = [ "altitudeReceived", "speedReceived", "positionReceived", "positionErrorReceived", "timeReceived", "headingReceived", ] def checkTime(): expectedDateTime = datetime.datetime(1994, 3, 23, 12, 35, 19) self.assertEqual(self.adapter._state["time"], expectedDateTime) self._receiverTest(sentences, callbacksFired, checkTime) def test_lotsOfMixedSentences(self): """ Sends an entire gamut of sentences and verifies the appropriate callbacks fire. These are more than you'd expect from your average consumer GPS device. They have most of the important information, including beacon information and visibility. """ sentences = [GPGSA] + GPGSV_SEQ + [GPRMC, GPGGA, GPGLL] callbacksFired = [ "headingReceived", "beaconInformationReceived", "speedReceived", "positionReceived", "timeReceived", "altitudeReceived", "positionErrorReceived", ] self._receiverTest(sentences, callbacksFired)
class NMEAReceiverTest(TestCase): """ Tests for the NMEA receiver. """ def setUp(self): self.receiver = MockPositioningReceiver() self.adapter = nmea.NMEAAdapter(self.receiver) self.protocol = nmea.NMEAProtocol(self.adapter) def test_onlyFireWhenCurrentSentenceHasNewInformation(self): """ If the current sentence does not contain any new fields for a particular callback, that callback is not called; even if all necessary information is still in the state from one or more previous messages. """ self.protocol.lineReceived(GPGGA) GPGGACallbacks = set(['positionReceived', 'positionErrorReceived', 'altitudeReceived']) self.assertEqual(set(self.receiver.called.keys()), GPGGACallbacks) self.receiver.clear() self.assertNotEqual(self.adapter._state, {}) # GPHDT contains heading infromation but not position, # altitude or anything like that; but that information is # still in the state. self.protocol.lineReceived(GPHDT) GPHDTCallbacks = set(['headingReceived']) self.assertEqual(set(self.receiver.called.keys()), GPHDTCallbacks) def _receiverTest(self, sentences, expectedFired=(), extraTest=None): """ A generic test for NMEA receiver behavior. @param sentences: The sequence of sentences to simulate receiving. @type sentences: iterable of C{str} @param expectedFired: The names of the callbacks expected to fire. @type expectedFired: iterable of C{str} @param extraTest: An optional extra test hook. @type extraTest: nullary callable """ for sentence in sentences: self.protocol.lineReceived(sentence) actuallyFired = self.receiver.called.keys() self.assertEqual(set(actuallyFired), set(expectedFired)) if extraTest is not None: extraTest() self.receiver.clear() self.adapter.clear() def test_positionErrorUpdateAcrossStates(self): """ The positioning error is updated across multiple states. """ sentences = [GPGSA] + GPGSV_SEQ callbacksFired = ['positionErrorReceived', 'beaconInformationReceived'] def _getIdentifiers(beacons): return sorted(map(attrgetter("identifier"), beacons)) def checkBeaconInformation(): beaconInformation = self.adapter._state['beaconInformation'] seenIdentifiers = _getIdentifiers(beaconInformation.seenBeacons) expected = [3, 4, 6, 13, 14, 16, 18, 19, 22, 24, 27] self.assertEqual(seenIdentifiers, expected) usedIdentifiers = _getIdentifiers(beaconInformation.usedBeacons) # These are not actually all the PRNs in the sample GPGSA: # only the ones also reported by the GPGSV sequence. This # is just because the sample data doesn't come from the # same reporting cycle of a GPS device. self.assertEqual(usedIdentifiers, [14, 18, 19, 22, 27]) self._receiverTest(sentences, callbacksFired, checkBeaconInformation) def test_emptyMiddleGSV(self): """ A GSV sentence with empty entries in any position does not mean that entries in subsequent positions of the same GSV sentence are ignored. """ sentences = [GPGSV_EMPTY_MIDDLE] callbacksFired = ['beaconInformationReceived'] def checkBeaconInformation(): beaconInformation = self.adapter._state['beaconInformation'] seenBeacons = beaconInformation.seenBeacons self.assertEqual(len(seenBeacons), 2) self.assertIn(13, [b.identifier for b in seenBeacons]) self._receiverTest(sentences, callbacksFired, checkBeaconInformation) def test_GGASentences(self): """ A sequence of GGA sentences fires C{positionReceived}, C{positionErrorReceived} and C{altitudeReceived}. """ sentences = [GPGGA] callbacksFired = ['positionReceived', 'positionErrorReceived', 'altitudeReceived'] self._receiverTest(sentences, callbacksFired) def test_GGAWithDateInState(self): """ When receiving a GPGGA sentence and a date was already in the state, the new time (from the GPGGA sentence) is combined with that date. """ self.adapter._state["_date"] = datetime.date(2014, 1, 1) sentences = [GPGGA] callbacksFired = ['positionReceived', 'positionErrorReceived', 'altitudeReceived', 'timeReceived'] self._receiverTest(sentences, callbacksFired) def test_RMCSentences(self): """ A sequence of RMC sentences fires C{positionReceived}, C{speedReceived}, C{headingReceived} and C{timeReceived}. """ sentences = [GPRMC] callbacksFired = ['headingReceived', 'speedReceived', 'positionReceived', 'timeReceived'] self._receiverTest(sentences, callbacksFired) def test_GSVSentences(self): """ A complete sequence of GSV sentences fires C{beaconInformationReceived}. """ sentences = [GPGSV_FIRST, GPGSV_MIDDLE, GPGSV_LAST] callbacksFired = ['beaconInformationReceived'] def checkPartialInformation(): self.assertNotIn('_partialBeaconInformation', self.adapter._state) self._receiverTest(sentences, callbacksFired, checkPartialInformation) def test_emptyMiddleEntriesGSVSequence(self): """ A complete sequence of GSV sentences with empty entries in the middle still fires C{beaconInformationReceived}. """ sentences = [GPGSV_EMPTY_MIDDLE] self._receiverTest(sentences, ["beaconInformationReceived"]) def test_incompleteGSVSequence(self): """ An incomplete sequence of GSV sentences does not fire any callbacks. """ sentences = [GPGSV_FIRST] self._receiverTest(sentences) def test_singleSentenceGSVSequence(self): """ The parser does not fail badly when the sequence consists of only one sentence (but is otherwise complete). """ sentences = [GPGSV_SINGLE] self._receiverTest(sentences, ["beaconInformationReceived"]) def test_GLLSentences(self): """ GLL sentences fire C{positionReceived}. """ sentences = [GPGLL_PARTIAL, GPGLL] self._receiverTest(sentences, ['positionReceived']) def test_HDTSentences(self): """ HDT sentences fire C{headingReceived}. """ sentences = [GPHDT] self._receiverTest(sentences, ['headingReceived']) def test_mixedSentences(self): """ A mix of sentences fires the correct callbacks. """ sentences = [GPRMC, GPGGA] callbacksFired = ['altitudeReceived', 'speedReceived', 'positionReceived', 'positionErrorReceived', 'timeReceived', 'headingReceived'] def checkTime(): expectedDateTime = datetime.datetime(1994, 3, 23, 12, 35, 19) self.assertEqual(self.adapter._state['time'], expectedDateTime) self._receiverTest(sentences, callbacksFired, checkTime) def test_lotsOfMixedSentences(self): """ Sends an entire gamut of sentences and verifies the appropriate callbacks fire. These are more than you'd expect from your average consumer GPS device. They have most of the important information, including beacon information and visibility. """ sentences = [GPGSA] + GPGSV_SEQ + [GPRMC, GPGGA, GPGLL] callbacksFired = ['headingReceived', 'beaconInformationReceived', 'speedReceived', 'positionReceived', 'timeReceived', 'altitudeReceived', 'positionErrorReceived'] self._receiverTest(sentences, callbacksFired)