diff --git a/.gitignore b/.gitignore index d478671e1f..e7931c3b68 100644 --- a/.gitignore +++ b/.gitignore @@ -186,11 +186,13 @@ protoc/ola_protoc protoc/ola_protoc.exe protoc/ola_protoc_plugin protoc/ola_protoc_plugin.exe +python/PyCompileTest.sh python/examples/ola_rdm_set.py python/ola/ClientWrapperTest.sh python/ola/OlaClientTest.sh python/ola/PidStoreLocation.py python/ola/PidStoreTest.sh +python/ola/RDMTest.sh python/ola/Version.py python/ola/rpc/SimpleRpcControllerTest.sh slp/slp_client diff --git a/.travis.yml b/.travis.yml index 1d458c687c..c82317ef11 100644 --- a/.travis.yml +++ b/.travis.yml @@ -90,8 +90,9 @@ matrix: - os: linux dist: xenial compiler: clang - env: TASK='compile' - python: '2.7' + env: + - TASK='compile' + - PYTHON='python3' addons: apt: packages: @@ -104,7 +105,6 @@ matrix: dist: xenial compiler: gcc env: TASK='compile' - python: '2.7' addons: apt: packages: @@ -178,7 +178,7 @@ matrix: dist: xenial env: - TASK='codespell' - - PATH=/opt/python/3.7.1/bin:$PATH + - PYTHON='python3' addons: apt: packages: @@ -314,10 +314,11 @@ install: - if [ "$TRAVIS_OS_NAME" == "linux" ]; then pip install --user numpy; fi - if [ "$TASK" = "coverage" ]; then pip install --user cpp-coveralls; fi - if [ "$TASK" = "flake8" ]; then pip install --user flake8; fi - - if [ "$TASK" = "codespell" ]; then pip3 install --user git+https://github.com/codespell-project/codespell.git; fi + - if [ "$TASK" = "codespell" ]; then pip install --user git+https://github.com/codespell-project/codespell.git; fi - if [ "$TASK" = "jshint" ]; then npm install -g grunt-cli; fi before_install: + - if [ "$PYTHON" == "python3" ]; then pyenv global 3.7.1 ; fi #Fix permissions for unbound (and possibly others) - if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then if [ ! -d /usr/local/sbin ]; then sudo mkdir -p /usr/local/sbin && sudo chown -R $(whoami) /usr/local/sbin; fi; fi #Add a missing gnupg folder @@ -355,12 +356,11 @@ before_install: - if [ "$TASK" == "spellintian" -o "$TASK" == "spellintian-duplicates" ]; then wget "http://old-releases.ubuntu.com/ubuntu/pool/main/l/lintian/lintian_2.5.104_all.deb"; sudo dpkg -i lintian_*.deb; sudo apt-get install -f -y; fi # Install a later lintian after_failure: -# Disabled as otherwise the logfile is too big -# - if [ -f ${TRAVIS_BUILD_DIR}/config.log ]; then cat ${TRAVIS_BUILD_DIR}/config.log; fi - - if [ -f ${TRAVIS_BUILD_DIR}/ola-*/_build/config.log ]; then cat ${TRAVIS_BUILD_DIR}/ola-*/_build/config.log; fi - - if [ -f ${TRAVIS_BUILD_DIR}/ola-*/_build/sub/config.log ]; then cat ${TRAVIS_BUILD_DIR}/ola-*/_build/sub/config.log; fi - if [ -f ${TRAVIS_BUILD_DIR}/ola-*/_build/test-suite.log ]; then cat ${TRAVIS_BUILD_DIR}/ola-*/_build/test-suite.log; fi - if [ -f ${TRAVIS_BUILD_DIR}/ola-*/_build/sub/test-suite.log ]; then cat ${TRAVIS_BUILD_DIR}/ola-*/_build/sub/test-suite.log; fi + - if [ -f ${TRAVIS_BUILD_DIR}/config.log ]; then cat ${TRAVIS_BUILD_DIR}/config.log; fi + - if [ -f ${TRAVIS_BUILD_DIR}/ola-*/_build/config.log ]; then cat ${TRAVIS_BUILD_DIR}/ola-*/_build/config.log; fi + - if [ -f ${TRAVIS_BUILD_DIR}/ola-*/_build/sub/config.log ]; then cat ${TRAVIS_BUILD_DIR}/ola-*/_build/sub/config.log; fi after_success: - if [ "$TASK" = "coverage" ]; then coveralls --gcov /usr/bin/gcov-8 -b . -E '.*Test\.cpp$' -E '.*\.pb\.cc$' -E '.*\.pb\.cpp$' -E '.*\.pb\.h$' -E '.*\.yy\.cpp$' -E '.*\.tab\.cpp$' -E '.*\.tab\.h$' -E '.*/doxygen/examples.*$' --gcov-options '\-lp' > /dev/null; fi diff --git a/Makefile.am b/Makefile.am index c03a99070e..130bbb001a 100644 --- a/Makefile.am +++ b/Makefile.am @@ -149,6 +149,9 @@ built_sources = # Test scripts are run if BUILD_TESTS is true. test_scripts = +# directories with python code that should be test compiled during the build +PYTHON_BUILD_DIRS = + # The includes # ----------------------------------------------------------------------------- @@ -172,6 +175,7 @@ include plugins/Makefile.mk include olad/Makefile.mk include protoc/Makefile.mk include python/Makefile.mk +include scripts/Makefile.mk include tools/Makefile.mk # ----------------------------------------------------------------------------- diff --git a/configure.ac b/configure.ac index e1c9ed54fc..0d5fa6bcb6 100644 --- a/configure.ac +++ b/configure.ac @@ -965,6 +965,7 @@ AC_CONFIG_LINKS([python/ola/__init__.py:python/ola/__init__.py python/ola/MACAddress.py:python/ola/MACAddress.py python/ola/OlaClient.py:python/ola/OlaClient.py python/ola/PidStore.py:python/ola/PidStore.py + python/ola/RDMAPI.py:python/ola/RDMAPI.py python/ola/RDMConstants.py:python/ola/RDMConstants.py python/ola/TestUtils.py:python/ola/TestUtils.py python/ola/UID.py:python/ola/UID.py diff --git a/data/Makefile.mk b/data/Makefile.mk index cc6afb63f3..fae7e38891 100644 --- a/data/Makefile.mk +++ b/data/Makefile.mk @@ -1 +1,3 @@ include data/rdm/Makefile.mk + +PYTHON_BUILD_DIRS += data diff --git a/include/Makefile.mk b/include/Makefile.mk index c42383c800..08797660e1 100644 --- a/include/Makefile.mk +++ b/include/Makefile.mk @@ -1,2 +1,6 @@ include include/ola/Makefile.mk include include/olad/Makefile.mk + +# uncomment when include is py3 compatible +# PYTHON_BUILD_DIRS += include + diff --git a/python/Makefile.mk b/python/Makefile.mk index 748c323bd7..1682171414 100644 --- a/python/Makefile.mk +++ b/python/Makefile.mk @@ -1,2 +1,17 @@ include python/examples/Makefile.mk include python/ola/Makefile.mk + +python/PyCompileTest.sh: python/Makefile.mk + mkdir -p $(top_builddir)/python + echo "$(PYTHON) -m compileall $(PYTHON_BUILD_DIRS); exit \$$?" > $(top_builddir)/python/PyCompileTest.sh + chmod +x $(top_builddir)/python/PyCompileTest.sh + +PYTHON_BUILD_DIRS += python + +if BUILD_PYTHON_LIBS +test_scripts += \ + python/PyCompileTest.sh +endif + +CLEANFILES += \ + python/PyCompileTest.sh diff --git a/python/examples/ola_rdm_get.py b/python/examples/ola_rdm_get.py index 06a2542592..009ea7da8e 100755 --- a/python/examples/ola_rdm_get.py +++ b/python/examples/ola_rdm_get.py @@ -18,6 +18,7 @@ '''Get a PID from a UID.''' +from __future__ import print_function import cmd import getopt import os.path @@ -459,7 +460,7 @@ def main(): try: PidStore.GetStore(pid_location) except PidStore.MissingPLASAPIDs as e: - print e + print(e) sys.exit() controller = InteractiveModeController(universe, diff --git a/python/ola/ClientWrapperTest.py b/python/ola/ClientWrapperTest.py index 8983bef4ff..cd1e5cf2dc 100644 --- a/python/ola/ClientWrapperTest.py +++ b/python/ola/ClientWrapperTest.py @@ -17,6 +17,7 @@ # Copyright (C) 2019 Bruce Lowekamp import array +import binascii import datetime import socket # import timeout_decorator @@ -208,7 +209,16 @@ class results: def DataCallback(self): data = sockets[1].recv(4096) - self.assertTrue(len(data) > 100) + expected = binascii.unhexlify( + "7d000010080110001a0d557064617465446d784461746122680801126400000" + "000000000000000000000000000000000000000000000000000000000000000" + "000000000000000000000000000000000000000000000000000000000000000" + "000000000000000000000000000000000000000000000000000000000000000" + "000000") + self.assertEqual(data, expected, + msg="Regression check failed. If protocol change " + "was intended set expected to: " + + str(binascii.hexlify(data))) results.gotdata = True wrapper.AddEvent(0, wrapper.Stop) diff --git a/python/ola/Makefile.mk b/python/ola/Makefile.mk index c671cdea66..e282ef187b 100644 --- a/python/ola/Makefile.mk +++ b/python/ola/Makefile.mk @@ -73,12 +73,18 @@ python/ola/PidStoreTest.sh: python/ola/Makefile.mk echo "PYTHONPATH=${top_builddir}/python TESTDATADIR=$(srcdir)/common/rdm/testdata $(PYTHON) ${srcdir}/python/ola/PidStoreTest.py; exit \$$?" > $(top_builddir)/python/ola/PidStoreTest.sh chmod +x $(top_builddir)/python/ola/PidStoreTest.sh +python/ola/RDMTest.sh: python/ola/Makefile.mk + mkdir -p $(top_builddir)/python/ola + echo "PYTHONPATH=${top_builddir}/python PIDSTOREDIR=$(srcdir)/data/rdm $(PYTHON) ${srcdir}/python/ola/RDMTest.py; exit \$$?" > $(top_builddir)/python/ola/RDMTest.sh + chmod +x $(top_builddir)/python/ola/RDMTest.sh + dist_check_SCRIPTS += \ python/ola/DUBDecoderTest.py \ python/ola/ClientWrapperTest.py \ python/ola/MACAddressTest.py \ python/ola/OlaClientTest.py \ python/ola/PidStoreTest.py \ + python/ola/RDMTest.py \ python/ola/TestUtils.py \ python/ola/UIDTest.py @@ -89,6 +95,7 @@ test_scripts += \ python/ola/MACAddressTest.py \ python/ola/OlaClientTest.sh \ python/ola/PidStoreTest.sh \ + python/ola/RDMTest.sh \ python/ola/UIDTest.py endif @@ -96,4 +103,6 @@ CLEANFILES += \ python/ola/*.pyc \ python/ola/ClientWrapperTest.sh \ python/ola/OlaClientTest.sh \ - python/ola/PidStoreTest.sh + python/ola/PidStoreTest.sh \ + python/ola/RDMTest.sh \ + python/ola/__pycache__/* diff --git a/python/ola/OlaClient.py b/python/ola/OlaClient.py index c1fb7935ca..20856bdfc1 100644 --- a/python/ola/OlaClient.py +++ b/python/ola/OlaClient.py @@ -429,6 +429,12 @@ def message(self): class RDMNack(object): + """Nack response to a request. + + Individual NACK response reasons can be access as attrs, e.g. + RMDNack.NR_FORMAT_ERROR + """ + NACK_SYMBOLS_TO_VALUES = { 'NR_UNKNOWN_PID': (0, 'Unknown PID'), 'NR_FORMAT_ERROR': (1, 'Format Error'), @@ -1224,7 +1230,7 @@ def RunRDMDiscovery(self, universe, full, callback): raise OLADNotRunningException() return True - def RDMGet(self, universe, uid, sub_device, param_id, callback, data='', + def RDMGet(self, universe, uid, sub_device, param_id, callback, data=b'', include_frames=False): """Send an RDM get command. @@ -1246,7 +1252,7 @@ def RDMGet(self, universe, uid, sub_device, param_id, callback, data='', return self._RDMMessage(universe, uid, sub_device, param_id, callback, data, include_frames) - def RDMSet(self, universe, uid, sub_device, param_id, callback, data='', + def RDMSet(self, universe, uid, sub_device, param_id, callback, data=b'', include_frames=False): """Send an RDM set command. @@ -1274,7 +1280,7 @@ def SendRawRDMDiscovery(self, sub_device, param_id, callback, - data='', + data=b'', include_frames=False): """Send an RDM Discovery command. Unless you're writing RDM tests you shouldn't need to use this. diff --git a/python/ola/PidStore.py b/python/ola/PidStore.py index 7c8a424b21..bdf64e411b 100644 --- a/python/ola/PidStore.py +++ b/python/ola/PidStore.py @@ -53,7 +53,7 @@ class Error(Exception): class InvalidPidFormat(Error): - "Indicates the PID data file was invalid.""" + """Indicates the PID data file was invalid.""" class PidStructureException(Error): @@ -458,7 +458,7 @@ def _AccountForMultiplierPack(self, value): raise ArgsValidationError( 'Conversion will lose data: %d -> %d' % (new_value, (new_value / multiplier * multiplier))) - new_value = new_value / multiplier + new_value = int(new_value / multiplier) else: try: @@ -645,7 +645,10 @@ def Pack(self, args): (self.name, self.min)) try: - data = struct.unpack('%ds' % arg_size, arg) + if sys.version >= '3.2': + data = struct.unpack('%ds' % arg_size, bytes(arg, 'utf8')) + else: + data = struct.unpack('%ds' % arg_size, arg) except struct.error as e: raise ArgsValidationError("Can't pack data: %s" % e) return data[0], 1 @@ -665,7 +668,10 @@ def Unpack(self, data): except struct.error as e: raise UnpackException(e) - return value[0].rstrip('\x00') + if sys.version >= '3.2': + return value[0].rstrip(b'\x00').decode('utf-8') + else: + return value[0].rstrip(b'\x00') def GetDescription(self, indent=0): indent = ' ' * indent @@ -807,10 +813,10 @@ def Pack(self, args): raise ArgsValidationError('Too many arguments, expected %d, got %d' % (arg_offset, len(args))) - return ''.join(data), arg_offset + return b''.join(data), arg_offset elif self._group_size == 0: - return '', 0 + return b'', 0 else: # this could be groups of fields, but we don't support that yet data = [] @@ -823,7 +829,7 @@ def Pack(self, args): if arg_offset < len(args): raise ArgsValidationError('Too many arguments, expected %d, got %d' % (arg_offset, len(args))) - return ''.join(data), arg_offset + return b''.join(data), arg_offset def Unpack(self, data): """Unpack binary data. diff --git a/python/ola/PidStoreTest.py b/python/ola/PidStoreTest.py index 5a6d92b0d3..8f93c82d29 100755 --- a/python/ola/PidStoreTest.py +++ b/python/ola/PidStoreTest.py @@ -16,6 +16,7 @@ # PidStoreTest.py # Copyright (C) 2020 Bruce Lowekamp +import binascii import os import unittest import ola.PidStore as PidStore @@ -198,6 +199,95 @@ def testCmp(self): self.assertNotEqual(hash(p1b), hash(p2)) self.assertNotEqual(hash(p1a), hash(p3)) + def testPackUnpack(self): + store = PidStore.PidStore() + store.Load([os.path.join(path, "test_pids.proto")]) + + pid = store.GetName("DMX_PERSONALITY_DESCRIPTION") + + # Pid.Pack only packs requests and Pid.Unpack only unpacks responses + # so test in two halves + args = ["42"] + blob = pid.Pack(args, PidStore.RDM_GET) + decoded = pid._requests.get(PidStore.RDM_GET).Unpack(blob)[0] + self.assertEqual(decoded['personality'], 42) + + args = ["42", "7", "UnpackTest"] + blob = pid._responses.get(PidStore.RDM_GET).Pack(args)[0] + decoded = pid.Unpack(blob, PidStore.RDM_GET) + self.assertEqual(decoded['personality'], 42) + self.assertEqual(decoded['slots_required'], 7) + self.assertEqual(decoded['name'], "UnpackTest") + + def testPackRanges(self): + store = PidStore.PidStore() + store.Load([os.path.join(path, "test_pids.proto")]) + + pid = store.GetName("REAL_TIME_CLOCK") + + # first check encoding of valid RTC data + args = ["2020", "6", "20", "21", "22", "23"] + blob = pid.Pack(args, PidStore.RDM_SET) + self.assertEqual(blob, binascii.unhexlify("07e40614151617")) + decoded = pid._requests.get(PidStore.RDM_SET).Unpack(blob)[0] + self.assertEqual(decoded, {'year': 2020, 'month': 6, + 'day': 20, 'hour': 21, + 'minute': 22, 'second': 23}) + + # next check that ranges are being enforced properly + # invalid year (2002 < 2003) + with self.assertRaises(PidStore.ArgsValidationError): + args = ["2002", "6", "20", "20", "20", "20"] + blob = pid.Pack(args, PidStore.RDM_SET) + + # invalid month < 1 + with self.assertRaises(PidStore.ArgsValidationError): + args = ["2020", "0", "20", "20", "20", "20"] + blob = pid.Pack(args, PidStore.RDM_SET) + + # invalid month > 12 + with self.assertRaises(PidStore.ArgsValidationError): + args = ["2020", "13", "20", "20", "20", "20"] + blob = pid.Pack(args, PidStore.RDM_SET) + + # invalid month > 255 + with self.assertRaises(PidStore.ArgsValidationError): + args = ["2020", "256", "20", "20", "20", "20"] + blob = pid.Pack(args, PidStore.RDM_SET) + + # invalid negative month + with self.assertRaises(PidStore.ArgsValidationError): + args = ["2020", "-1", "20", "20", "20", "20"] + blob = pid.Pack(args, PidStore.RDM_SET) + + # tests for string with min=max=2 + pid = store.GetName("LANGUAGE_CAPABILITIES") + args = ["en"] + blob = pid._responses.get(PidStore.RDM_GET).Pack(args)[0] + decoded = pid.Unpack(blob, PidStore.RDM_GET) + self.assertEqual(decoded, {'languages': [{'language': 'en'}]}) + + with self.assertRaises(PidStore.ArgsValidationError): + args = ["e"] + blob = pid._responses.get(PidStore.RDM_GET).Pack(args)[0] + + with self.assertRaises(PidStore.ArgsValidationError): + args = ["enx"] + blob = pid._responses.get(PidStore.RDM_GET).Pack(args)[0] + + # valid empty string + pid = store.GetName("STATUS_ID_DESCRIPTION") + args = [""] + blob = pid._responses.get(PidStore.RDM_GET).Pack(args)[0] + self.assertEqual(len(blob), 0) + decoded = pid.Unpack(blob, PidStore.RDM_GET) + self.assertEqual(decoded['label'], "") + + # string too long + with self.assertRaises(PidStore.ArgsValidationError): + args = ["123456789012345678901234567890123"] + blob = pid._responses.get(PidStore.RDM_GET).Pack(args)[0] + if __name__ == '__main__': path = (os.environ.get('TESTDATADIR', "../common/rdm/testdata")) diff --git a/python/ola/RDMTest.py b/python/ola/RDMTest.py new file mode 100644 index 0000000000..773612124b --- /dev/null +++ b/python/ola/RDMTest.py @@ -0,0 +1,226 @@ +#!/usr/bin/env python +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA +# +# RDMTest.py +# Copyright (C) 2019 Bruce Lowekamp + +import binascii +import os +import socket +# import timeout_decorator +import unittest +from ola import PidStore +from ola.ClientWrapper import ClientWrapper +from ola.OlaClient import RDMNack +from ola.RDMAPI import RDMAPI +from ola.UID import UID + + +"""Test cases for RDM device commands.""" + +__author__ = 'bruce@lowekamp.net (Bruce Lowekamp)' + +global pid_store_path + + +class RDMTest(unittest.TestCase): + # @timeout_decorator.timeout(2) + def testGetWithResponse(self): + """uses client to send an RDM get with mocked olad. + Regression test that confirms sent message is correct and + sends fixed response message.""" + sockets = socket.socketpair() + wrapper = ClientWrapper(sockets[0]) + pid_store = PidStore.GetStore(pid_store_path) + client = wrapper.Client() + rdm_api = RDMAPI(client, pid_store) + + class results: + got_request = False + got_response = False + + def DataCallback(self): + # request and response for + # ola_rdm_get.py -u 1 --uid 7a70:ffffff00 device_info + # against olad dummy plugin + # enable logging in rpc/StreamRpcChannel.py + data = sockets[1].recv(4096) + expected = binascii.unhexlify( + "29000010080110001a0a52444d436f6d6d616e6422170801120908f0f4011500" + "ffffff180020602a0030003800") + self.assertEqual(data, expected, + msg="Regression check failed. If protocol change " + "was intended set expected to: " + + str(binascii.hexlify(data))) + results.got_request = True + response = binascii.unhexlify( + "3f0000100802100022390800100018002213010000017fff0000000300050204" + "00010000032860300038004a0908f0f4011500ffffff520908f0f40115ac1100" + "02580a") + sent_bytes = sockets[1].send(response) + self.assertEqual(sent_bytes, len(response)) + + def ResponseCallback(self, response, data, unpack_exception): + results.got_response = True + self.assertEqual(response.response_type, client.RDM_ACK) + self.assertEqual(response.pid, 0x60) + self.assertEqual(data["dmx_footprint"], 5) + self.assertEqual(data["software_version"], 3) + self.assertEqual(data["personality_count"], 4) + self.assertEqual(data["device_model"], 1) + self.assertEqual(data["current_personality"], 2) + self.assertEqual(data["protocol_major"], 1) + self.assertEqual(data["protocol_minor"], 0) + self.assertEqual(data["product_category"], 32767) + self.assertEqual(data["dmx_start_address"], 1) + self.assertEqual(data["sub_device_count"], 0) + self.assertEqual(data["sensor_count"], 3) + wrapper.AddEvent(0, wrapper.Stop) + + wrapper._ss.AddReadDescriptor(sockets[1], lambda: DataCallback(self)) + + uid = UID.FromString("7a70:ffffff00") + pid = pid_store.GetName("DEVICE_INFO") + rdm_api.Get(1, uid, 0, pid, lambda x, y, z: ResponseCallback(self, x, y, z)) + + wrapper.Run() + + sockets[0].close() + sockets[1].close() + + self.assertTrue(results.got_request) + self.assertTrue(results.got_response) + + # @timeout_decorator.timeout(2) + def testGetParamsWithResponse(self): + """uses client to send an RDM get with mocked olad. + Regression test that confirms sent message is correct and + sends fixed response message.""" + sockets = socket.socketpair() + wrapper = ClientWrapper(sockets[0]) + pid_store = PidStore.GetStore(pid_store_path) + client = wrapper.Client() + rdm_api = RDMAPI(client, pid_store) + + class results: + got_request = False + got_response = False + + def DataCallback(self): + # request and response for + # ola_rdm_get.py -u 1 --uid 7a70:ffffff00 DMX_PERSONALITY_DESCRIPTION 2 + # against olad dummy plugin + # enable logging in rpc/StreamRpcChannel.py + data = sockets[1].recv(4096) + expected = binascii.unhexlify( + "2b000010080110001a0a52444d436f6d6d616e6422190801120908f0f4011500" + "ffffff180020e1012a010230003800") + self.assertEqual(data, expected, + msg="Regression check failed. If protocol change " + "was intended set expected to: " + + str(binascii.hexlify(data))) + results.got_request = True + response = binascii.unhexlify( + "3d0000100802100022370800100018002210020005506572736f6e616c697479" + "203228e101300038004a0908f0f4011500ffffff520908f0f40115ac107de058" + "29") + sent_bytes = sockets[1].send(response) + self.assertEqual(sent_bytes, len(response)) + + def ResponseCallback(self, response, data, unpack_exception): + results.got_response = True + self.assertEqual(response.response_type, client.RDM_ACK) + self.assertEqual(response.pid, 0xe1) + self.assertEqual(data['personality'], 2) + self.assertEqual(data['slots_required'], 5) + self.assertEqual(data['name'], "Personality 2") + wrapper.AddEvent(0, wrapper.Stop) + + wrapper._ss.AddReadDescriptor(sockets[1], lambda: DataCallback(self)) + + uid = UID.FromString("7a70:ffffff00") + pid = pid_store.GetName("DMX_PERSONALITY_DESCRIPTION") + rdm_api.Get(1, uid, 0, pid, + lambda x, y, z: ResponseCallback(self, x, y, z), args=["2"]) + + wrapper.Run() + + sockets[0].close() + sockets[1].close() + + self.assertTrue(results.got_request) + self.assertTrue(results.got_response) + + # @timeout_decorator.timeout(2) + def testSetParamsWithNack(self): + """uses client to send an RDM set with mocked olad. + Regression test that confirms sent message is correct and + sends fixed response message.""" + sockets = socket.socketpair() + wrapper = ClientWrapper(sockets[0]) + pid_store = PidStore.GetStore(pid_store_path) + client = wrapper.Client() + rdm_api = RDMAPI(client, pid_store) + + class results: + got_request = False + got_response = False + + def DataCallback(self): + # request and response for + # ola_rdm_set.py -u 1 --uid 7a70:ffffff00 DMX_PERSONALITY 10 + # against olad dummy plugin + # enable logging in rpc/StreamRpcChannel.py + data = sockets[1].recv(4096) + expected = binascii.unhexlify( + "2b000010080110001a0a52444d436f6d6d616e6422190801120908f0f401150" + "0ffffff180020e0012a010a30013800") + self.assertEqual(data, expected, + msg="Regression check failed. If protocol change " + "was intended set expected to: " + + str(binascii.hexlify(data))) + results.got_request = True + response = binascii.unhexlify( + "2f0000100802100022290800100218002202000628e001300138004a0908f0f" + "4011500ffffff520908f0f40115ac107de05831") + sent_bytes = sockets[1].send(response) + self.assertEqual(sent_bytes, len(response)) + + def ResponseCallback(self, response, data, unpack_exception): + results.got_response = True + self.assertEqual(response.response_type, client.RDM_NACK_REASON) + self.assertEqual(response.pid, 0xe0) + self.assertEqual(response.nack_reason, RDMNack.NR_DATA_OUT_OF_RANGE) + wrapper.AddEvent(0, wrapper.Stop) + + wrapper._ss.AddReadDescriptor(sockets[1], lambda: DataCallback(self)) + + uid = UID.FromString("7a70:ffffff00") + pid = pid_store.GetName("DMX_PERSONALITY") + rdm_api.Set(1, uid, 0, pid, + lambda x, y, z: ResponseCallback(self, x, y, z), args=["10"]) + + wrapper.Run() + + sockets[0].close() + sockets[1].close() + + self.assertTrue(results.got_request) + self.assertTrue(results.got_response) + + +if __name__ == '__main__': + pid_store_path = (os.environ.get('PIDSTOREDIR', "../data/rdm")) + unittest.main() diff --git a/python/ola/rpc/Makefile.mk b/python/ola/rpc/Makefile.mk index 16e1e0354a..79d8443bf2 100644 --- a/python/ola/rpc/Makefile.mk +++ b/python/ola/rpc/Makefile.mk @@ -29,4 +29,5 @@ python/ola/rpc/SimpleRpcControllerTest.sh: python/ola/rpc/Makefile.mk chmod +x $(top_builddir)/python/ola/rpc/SimpleRpcControllerTest.sh CLEANFILES += python/ola/rpc/SimpleRpcControllerTest.sh \ - python/ola/rpc/*.pyc + python/ola/rpc/*.pyc \ + python/ola/rpc/__pycache__/* diff --git a/python/ola/rpc/StreamRpcChannel.py b/python/ola/rpc/StreamRpcChannel.py index e414708be6..d4ddd9178b 100644 --- a/python/ola/rpc/StreamRpcChannel.py +++ b/python/ola/rpc/StreamRpcChannel.py @@ -15,6 +15,7 @@ # StreamRpcChannel.py # Copyright (C) 2005 Simon Newton +import binascii import logging import struct from google.protobuf import service @@ -66,6 +67,9 @@ def __init__(self, socket, service_impl, close_callback=None): self._expected_size = None # The size of the message we're receiving self._skip_message = False # Skip the current message self._close_callback = close_callback + self._log_msgs = False # set to enable wire message logging + if self._log_msgs: + logging.basicConfig(level=logging.DEBUG) def SocketReady(self): """Read data from the socket and handle when we get a full message. @@ -170,6 +174,9 @@ def _SendMessage(self, message): data = message.SerializeToString() # combine into one buffer to send so we avoid sending two packets data = self._EncodeHeader(len(data)) + data + # this log is useful for building mock regression tests + if self._log_msgs: + logging.debug("send->" + str(binascii.hexlify(data))) sent_bytes = self._socket.send(data) if sent_bytes != len(data): @@ -240,6 +247,8 @@ def _ProcessIncomingData(self): if not raw_header: # not enough data yet return + if self._log_msgs: + logging.debug("recvhdr<-" + str(binascii.hexlify(raw_header))) header = struct.unpack('=L', raw_header)[0] version, size = self._DecodeHeader(header) @@ -254,6 +263,8 @@ def _ProcessIncomingData(self): # not enough data yet return + if self._log_msgs: + logging.debug("recvmsg<-" + str(binascii.hexlify(data))) if not self._skip_message: self._HandleNewMessage(data) self._expected_size = 0 diff --git a/scripts/Makefile.mk b/scripts/Makefile.mk new file mode 100644 index 0000000000..199f0f9e58 --- /dev/null +++ b/scripts/Makefile.mk @@ -0,0 +1,2 @@ +# uncomment when scripts is py3 compatible +# PYTHON_BUILD_DIRS += scripts diff --git a/tools/Makefile.mk b/tools/Makefile.mk index 4a30fa4dfc..afa5742817 100644 --- a/tools/Makefile.mk +++ b/tools/Makefile.mk @@ -13,3 +13,6 @@ dist_noinst_DATA += \ tools/ola_mon/index.html \ tools/ola_mon/ola_mon.conf \ tools/ola_mon/ola_mon.py + +# uncomment when tools is py3 compatible +# PYTHON_BUILD_DIRS += tools