diff --git a/.codespellignorelines b/.codespellignorelines index 460b8f21b0..10832fd44e 100644 --- a/.codespellignorelines +++ b/.codespellignorelines @@ -17,7 +17,6 @@ /(?:([0-9]{1,3})(?:\s+THRU\s+([0-9]{0,3}))?)(?:\s+@\s+([0-9]{0,3}))?$/); str = str.replace('>', 'THRU'); ' THRU ' + ola.common.DmxConstants.MAX_CHANNEL_NUMBER); - ' THRU ' + ola.common.DmxConstants.MAX_CHANNEL_NUMBER); // If it's the T or > keys, autocomplete 'THRU' case 'U': // THRU var values = ['7', '8', '9', ' THRU ', '4', '5', '6', ' @ ', '1', '2', '3', @@ -118,9 +117,6 @@ class AsyncronousLibUsbAdaptor : public BaseLibUsbAdaptor { OLA_ASSERT_EQ(expected, JsonWriter::AsString(uint_value)); * Test the uint item " \"type\": \"uint\",\n" - " \"type\": \"uint\",\n" - " \"type\": \"uint\",\n" - " \"type\": \"uint\",\n" std::map m_uint_map_variables; if (message.uint_offset < MAX_UINT_FIELDS) { message.uint16_fields[message.uint_offset++] = field->Value(); @@ -131,10 +127,8 @@ class AsyncronousLibUsbAdaptor : public BaseLibUsbAdaptor { status_message() : uint_offset(0), int_offset(0), status_type(0), std::string Type() const { return "uint"; } if (items[i]['type'] == 'uint') { - if (items[i]['type'] == 'uint') { if (type == 'string' || type == 'uint' || type == 'hidden') { const char RDMHTTPModule::GENERIC_UINT_FIELD[] = "int"; - section.AddItem(new HiddenItem("1", GENERIC_UINT_FIELD)); section.AddItem(new HiddenItem("1", GENERIC_UINT_FIELD)); SelectItem *item = new SelectItem("Personality", GENERIC_UINT_FIELD); string personality_str = request->GetParameter(GENERIC_UINT_FIELD); @@ -198,4 +192,6 @@ import java.nio.ByteOrder; "{'a': 'caf\\\\xe9'}") # self.assertEqual('%s' % rtf._EscapeData({"caf\xe9": "bar"}), # "{'caf\xe9': 'bar'}") + self.assertEqual('caf\\xe9', StringEscape(u'caf\xe9')) + self.assertEqual('caf\\xe9', ("%s" % StringEscape(u'caf\xe9'))) "forin": true, diff --git a/.gitignore b/.gitignore index bdbd6e9b20..169ab16a4f 100644 --- a/.gitignore +++ b/.gitignore @@ -196,6 +196,8 @@ python/ola/PidStoreTest.sh python/ola/RDMTest.sh python/ola/Version.py python/ola/rpc/SimpleRpcControllerTest.sh +python/ola/testing/ +python/ola/testing/__init__.py slp/slp_client slp/slp_client.exe slp/slp_server @@ -228,6 +230,7 @@ tools/rdm/DataLocation.py tools/rdm/ExpectedResultsTest.sh tools/rdm/ResponderTestTest.sh tools/rdm/TestHelpersTest.sh +tools/rdm/TestRunnerTest.sh tools/rdm/TestStateTest.sh tools/rdmpro/rdmpro_sniffer tools/rdmpro/rdmpro_sniffer.exe diff --git a/NEWS b/NEWS index e0d0e7706c..8011a16f95 100644 --- a/NEWS +++ b/NEWS @@ -1,12 +1,14 @@ x/y/2023 ola-0.10.9 Features: - * Further improvements on Python 3 compatibility #1506 + * Python 3 compatibility across the board (including the RDM Responder Tests)! + #1506 * Support for the JMS USB2DMX PRO V2.1 device via the FTDI plugin #1728 API: * Python: Add a fetch current DMX example. RDM Tests: + * Python 3 compatibility of the RDM Tests #1599 * Fix a longstanding bug in the GetMaxPacketSize RDM test around timeouts Bugs: diff --git a/configure.ac b/configure.ac index 8315010b98..0252ce69a1 100644 --- a/configure.ac +++ b/configure.ac @@ -973,18 +973,27 @@ AM_CONDITIONAL([FOUND_CPPLINT], [test "x$cpplint" = xyes]) # srcdir and set PYTHONPATH=${top_builddir}/python in data/rdm/Makefile.am AC_CONFIG_LINKS([python/ola/__init__.py:python/ola/__init__.py python/ola/ClientWrapper.py:python/ola/ClientWrapper.py + python/ola/DMXConstants.py:python/ola/DMXConstants.py + python/ola/DUBDecoder.py:python/ola/DUBDecoder.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/StringUtils.py:python/ola/StringUtils.py python/ola/TestUtils.py:python/ola/TestUtils.py python/ola/UID.py:python/ola/UID.py python/ola/rpc/__init__.py:python/ola/rpc/__init__.py python/ola/rpc/SimpleRpcController.py:python/ola/rpc/SimpleRpcController.py python/ola/rpc/StreamRpcChannel.py:python/ola/rpc/StreamRpcChannel.py + tools/rdm/__init__.py:tools/rdm/__init__.py tools/rdm/ExpectedResults.py:tools/rdm/ExpectedResults.py + tools/rdm/ResponderTest.py:tools/rdm/ResponderTest.py tools/rdm/TestCategory.py:tools/rdm/TestCategory.py + tools/rdm/TestDefinitions.py:tools/rdm/TestDefinitions.py + tools/rdm/TestHelpers.py:tools/rdm/TestHelpers.py + tools/rdm/TestMixins.py:tools/rdm/TestMixins.py + tools/rdm/TestRunner.py:tools/rdm/TestRunner.py tools/rdm/TestState.py:tools/rdm/TestState.py tools/rdm/TimingStats.py:tools/rdm/TimingStats.py]) diff --git a/python/ola/Makefile.mk b/python/ola/Makefile.mk index e282ef187b..19d7e395da 100644 --- a/python/ola/Makefile.mk +++ b/python/ola/Makefile.mk @@ -85,6 +85,7 @@ dist_check_SCRIPTS += \ python/ola/OlaClientTest.py \ python/ola/PidStoreTest.py \ python/ola/RDMTest.py \ + python/ola/StringUtilsTest.py \ python/ola/TestUtils.py \ python/ola/UIDTest.py @@ -96,6 +97,7 @@ test_scripts += \ python/ola/OlaClientTest.sh \ python/ola/PidStoreTest.sh \ python/ola/RDMTest.sh \ + python/ola/StringUtilsTest.py \ python/ola/UIDTest.py endif diff --git a/python/ola/PidStore.py b/python/ola/PidStore.py index bf50dd616b..3028af709f 100644 --- a/python/ola/PidStore.py +++ b/python/ola/PidStore.py @@ -45,7 +45,7 @@ MAX_VALID_SUB_DEVICE = 0x0200 ALL_SUB_DEVICES = 0xffff -# The two types of commands classes +# The different types of commands classes RDM_GET, RDM_SET, RDM_DISCOVERY = range(3) @@ -637,6 +637,12 @@ def Pack(self, args): arg = args[0] arg_size = len(arg) + # Handle the fact a UTF-8 character could be multi-byte + if sys.version_info >= (3, 2): + arg_size = max(arg_size, len(bytes(arg, 'utf-8'))) + else: + arg_size = max(arg_size, len(arg.encode('utf-8'))) + if self.max is not None and arg_size > self.max: raise ArgsValidationError('%s can be at most %d,' % (self.name, self.max)) @@ -647,9 +653,9 @@ def Pack(self, args): try: if sys.version_info >= (3, 2): - data = struct.unpack('%ds' % arg_size, bytes(arg, 'utf8')) + data = struct.unpack('%ds' % arg_size, bytes(arg, 'utf-8')) else: - data = struct.unpack('%ds' % arg_size, arg) + data = struct.unpack('%ds' % arg_size, arg.encode('utf-8')) except struct.error as e: raise ArgsValidationError("Can't pack data: %s" % e) return data[0], 1 @@ -669,10 +675,12 @@ def Unpack(self, data): except struct.error as e: raise UnpackException(e) - if sys.version_info >= (3, 2): - return value[0].rstrip(b'\x00').decode('utf-8') - else: - return value[0].rstrip(b'\x00') + try: + value = value[0].rstrip(b'\x00').decode('utf-8') + except UnicodeDecodeError as e: + raise UnpackException(e) + + return value def GetDescription(self, indent=0): indent = ' ' * indent @@ -878,7 +886,7 @@ def Unpack(self, data): 'Too many repeated group_count for %s, limit is %d, found %d' % (self.name, self.max, group_count)) - if self.max is not None and group_count < self.min: + if self.min is not None and group_count < self.min: raise UnpackException( 'Too few repeated group_count for %s, limit is %d, found %d' % (self.name, self.min, group_count)) diff --git a/python/ola/PidStoreTest.py b/python/ola/PidStoreTest.py index b928024648..600f1dd389 100755 --- a/python/ola/PidStoreTest.py +++ b/python/ola/PidStoreTest.py @@ -222,6 +222,22 @@ def testPackUnpack(self): self.assertEqual(decoded['slots_required'], 7) self.assertEqual(decoded['name'], "UnpackTest") + # Test null handling, trailing null should be truncated on the way back in + args = ["42", "7", "Foo\0"] + blob = pid._responses.get(PidStore.RDM_GET).Pack(args)[0] + # Not truncated here + self.assertEqual(blob, binascii.unhexlify("2a0007466f6f00")) + decoded = pid.Unpack(blob, PidStore.RDM_GET) + self.assertEqual(decoded['personality'], 42) + self.assertEqual(decoded['slots_required'], 7) + self.assertEqual(decoded['name'], "Foo") + + # Confirm we raise an error if we try and unpack a non-ASCII, non-UTF-8 + # containing packet (0xc0) + with self.assertRaises(PidStore.UnpackException): + blob = binascii.unhexlify("2a0007556e7061636bc054657374") + decoded = pid.Unpack(blob, PidStore.RDM_GET) + def testPackRanges(self): store = PidStore.PidStore() store.Load([os.path.join(path, "test_pids.proto")]) @@ -279,6 +295,28 @@ def testPackRanges(self): args = ["enx"] blob = pid._responses.get(PidStore.RDM_GET).Pack(args)[0] + # test packing some non-printable characters + args = ["\x0d\x7f"] + blob = pid._responses.get(PidStore.RDM_GET).Pack(args)[0] + self.assertEqual(blob, binascii.unhexlify("0d7f")) + decoded = pid.Unpack(blob, PidStore.RDM_GET) + self.assertEqual(decoded, {'languages': [{'language': '\x0d\x7f'}]}) + + # test packing some non-ascii characters, as the + # LATIN CAPITAL LETTER A WITH GRAVE, unicode U+00C0 gets encoded as two + # bytes (\xc3\x80) the total length is three bytes and it doesn't fit! + with self.assertRaises(PidStore.ArgsValidationError): + args = [u"\x0d\xc0"] + blob = pid._responses.get(PidStore.RDM_GET).Pack(args)[0] + + # It works on it's own as it's short enough... + args = [u"\u00c0"] + blob = pid._responses.get(PidStore.RDM_GET).Pack(args)[0] + self.assertEqual(blob, binascii.unhexlify("c380")) + decoded = pid.Unpack(blob, PidStore.RDM_GET) + # This is the unicode code point for it + self.assertEqual(decoded, {'languages': [{'language': u'\u00c0'}]}) + # valid empty string pid = store.GetName("STATUS_ID_DESCRIPTION") args = [""] diff --git a/python/ola/StringUtils.py b/python/ola/StringUtils.py new file mode 100644 index 0000000000..a0f51cfafd --- /dev/null +++ b/python/ola/StringUtils.py @@ -0,0 +1,45 @@ +# 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 +# +# StringUtils.py +# Copyright (C) 2022 Peter Newman + +"""Common utils for OLA Python string handling""" + +import sys + +if sys.version_info >= (3, 0): + try: + unicode + except NameError: + unicode = str + +__author__ = 'nomis52@gmail.com (Simon Newton)' + + +def StringEscape(s): + """Escape unprintable characters in a string.""" + # TODO(Peter): How does this interact with the E1.20 Unicode flag? + # We don't use sys.version_info.major to support Python 2.6. + if sys.version_info[0] == 2 and type(s) == str: + return s.encode('string-escape') + elif sys.version_info[0] == 2 and type(s) == unicode: + return s.encode('unicode-escape') + elif type(s) == str: + # All strings in Python 3 are unicode + # This encode/decode pair gets us an escaped string + return s.encode('unicode-escape').decode(encoding="ascii", + errors="backslashreplace") + else: + raise TypeError('Only strings are supported not %s' % type(s)) diff --git a/python/ola/StringUtilsTest.py b/python/ola/StringUtilsTest.py new file mode 100755 index 0000000000..b7191111c1 --- /dev/null +++ b/python/ola/StringUtilsTest.py @@ -0,0 +1,57 @@ +#!/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 +# +# StringUtilsTest.py +# Copyright (C) 2022 Peter Newman + +import unittest + +from ola.StringUtils import StringEscape + +"""Test cases for StringUtils utilities.""" + +__author__ = 'nomis52@gmail.com (Simon Newton)' + + +class StringUtilsTest(unittest.TestCase): + def testStringEscape(self): + # Test we escape properly + self.assertEqual('foo', StringEscape("foo")) + self.assertEqual('bar', StringEscape("bar")) + self.assertEqual('bar[]', StringEscape("bar[]")) + self.assertEqual('foo-bar', StringEscape(u'foo-bar')) + self.assertEqual('foo\\x00bar', StringEscape("foo\x00bar")) + # TODO(Peter): How does this interact with the E1.20 Unicode flag? + self.assertEqual('caf\\xe9', StringEscape(u'caf\xe9')) + self.assertEqual('foo\\u2014bar', StringEscape(u'foo\u2014bar')) + + # Test that we display nicely in a string + self.assertEqual('foo', ("%s" % StringEscape("foo"))) + self.assertEqual('bar[]', ("%s" % StringEscape("bar[]"))) + self.assertEqual('foo-bar', ("%s" % StringEscape(u'foo-bar'))) + self.assertEqual('foo\\x00bar', ("%s" % StringEscape("foo\x00bar"))) + # TODO(Peter): How does this interact with the E1.20 Unicode flag? + self.assertEqual('caf\\xe9', ("%s" % StringEscape(u'caf\xe9'))) + self.assertEqual('foo\\u2014bar', ("%s" % StringEscape(u'foo\u2014bar'))) + + # Confirm we throw an exception if we pass in a number or something else + # that's not a string + with self.assertRaises(TypeError): + result = StringEscape(42) + self.assertNone(result) + + +if __name__ == '__main__': + unittest.main() diff --git a/tools/rdm/ExpectedResultsTest.py b/tools/rdm/ExpectedResultsTest.py index 27b6512853..9fb1ab997a 100644 --- a/tools/rdm/ExpectedResultsTest.py +++ b/tools/rdm/ExpectedResultsTest.py @@ -19,6 +19,7 @@ import unittest from collections import namedtuple +# Keep this import relative to simplify the testing from ExpectedResults import (BroadcastResult, DUBResult, InvalidResponse, SuccessfulResult, TimeoutResult, UnsupportedResult) diff --git a/tools/rdm/Makefile.mk b/tools/rdm/Makefile.mk index d81b5911cc..afc6b72ba6 100644 --- a/tools/rdm/Makefile.mk +++ b/tools/rdm/Makefile.mk @@ -61,7 +61,10 @@ tools/rdm/ExpectedResultsTest.sh: tools/rdm/Makefile.mk chmod +x $(top_builddir)/tools/rdm/ExpectedResultsTest.sh tools/rdm/ResponderTestTest.sh: tools/rdm/Makefile.mk - mkdir -p $(top_builddir)/python/ola + mkdir -p $(top_builddir)/python/ola/testing + touch $(top_builddir)/python/ola/testing/__init__.py + # This link is relative within the builddir + $(LN_S) -f ../../../tools/rdm $(top_builddir)/python/ola/testing/rdm echo "PYTHONPATH=${top_builddir}/python $(PYTHON) ${srcdir}/tools/rdm/ResponderTestTest.py; exit \$$?" > $(top_builddir)/tools/rdm/ResponderTestTest.sh chmod +x $(top_builddir)/tools/rdm/ResponderTestTest.sh @@ -70,6 +73,14 @@ tools/rdm/TestHelpersTest.sh: tools/rdm/Makefile.mk echo "PYTHONPATH=${top_builddir}/python $(PYTHON) ${srcdir}/tools/rdm/TestHelpersTest.py; exit \$$?" > $(top_builddir)/tools/rdm/TestHelpersTest.sh chmod +x $(top_builddir)/tools/rdm/TestHelpersTest.sh +tools/rdm/TestRunnerTest.sh: tools/rdm/Makefile.mk + mkdir -p $(top_builddir)/python/ola/testing + touch $(top_builddir)/python/ola/testing/__init__.py + # This link is relative within the builddir + $(LN_S) -f ../../../tools/rdm $(top_builddir)/python/ola/testing/rdm + echo "PYTHONPATH=${top_builddir}/python $(PYTHON) ${srcdir}/tools/rdm/TestRunnerTest.py; exit \$$?" > $(top_builddir)/tools/rdm/TestRunnerTest.sh + chmod +x $(top_builddir)/tools/rdm/TestRunnerTest.sh + tools/rdm/TestStateTest.sh: tools/rdm/Makefile.mk mkdir -p $(top_builddir)/python/ola echo "PYTHONPATH=${top_builddir}/python $(PYTHON) ${srcdir}/tools/rdm/TestStateTest.py; exit \$$?" > $(top_builddir)/tools/rdm/TestStateTest.sh @@ -79,6 +90,7 @@ dist_check_SCRIPTS += \ tools/rdm/ExpectedResultsTest.py \ tools/rdm/ResponderTestTest.py \ tools/rdm/TestHelpersTest.py \ + tools/rdm/TestRunnerTest.py \ tools/rdm/TestStateTest.py if BUILD_PYTHON_LIBS @@ -86,14 +98,19 @@ test_scripts += \ tools/rdm/ExpectedResultsTest.sh \ tools/rdm/ResponderTestTest.sh \ tools/rdm/TestHelpersTest.sh \ + tools/rdm/TestRunnerTest.sh \ tools/rdm/TestStateTest.sh endif CLEANFILES += \ + python/ola/testing/*.pyc \ + python/ola/testing/__pycache__/* \ + python/ola/testing/__init__.py \ tools/rdm/*.pyc \ tools/rdm/ExpectedResultsTest.sh \ tools/rdm/ResponderTestTest.sh \ tools/rdm/TestHelpersTest.sh \ + tools/rdm/TestRunnerTest.sh \ tools/rdm/TestStateTest.sh \ tools/rdm/__pycache__/* diff --git a/tools/rdm/ResponderTest.py b/tools/rdm/ResponderTest.py index a059a03185..93490306ff 100644 --- a/tools/rdm/ResponderTest.py +++ b/tools/rdm/ResponderTest.py @@ -28,12 +28,14 @@ import sys import time -from ExpectedResults import (AckDiscoveryResult, AckGetResult, AckSetResult, - NackDiscoveryResult, NackGetResult, NackSetResult) from ola.OlaClient import OlaClient, RDMNack -from TestCategory import TestCategory -from TestState import TestState -from TimingStats import TimingStats +from ola.StringUtils import StringEscape +from ola.testing.rdm.ExpectedResults import (AckDiscoveryResult, AckGetResult, + AckSetResult, NackDiscoveryResult, + NackGetResult, NackSetResult) +from ola.testing.rdm.TestCategory import TestCategory +from ola.testing.rdm.TestState import TestState +from ola.testing.rdm.TimingStats import TimingStats from ola import PidStore @@ -376,7 +378,7 @@ def SendDirectedDiscovery(self, uid, sub_device, pid, args=[]): args, include_frames=True) - def SendRawDiscovery(self, sub_device, pid, data=""): + def SendRawDiscovery(self, sub_device, pid, data=b''): """Send a raw Discovery request. Args: @@ -426,7 +428,7 @@ def SendDirectedGet(self, uid, sub_device, pid, args=[]): include_frames=True) return ret_code - def SendRawGet(self, sub_device, pid, data=""): + def SendRawGet(self, sub_device, pid, data=b''): """Send a raw GET request. Args: @@ -478,7 +480,7 @@ def SendDirectedSet(self, uid, sub_device, pid, args=[]): self.SleepAfterBroadcastSet() return ret_code - def SendRawSet(self, sub_device, pid, data=""): + def SendRawSet(self, sub_device, pid, data=b''): """Send a raw SET request. Args: @@ -618,17 +620,8 @@ def _EscapeData(self, data): # We can't escape the key as then it may become a new key d[k] = self._EscapeData(v) return d - # TODO(Peter): How does this interact with the E1.20 Unicode flag? - # We don't use sys.version_info.major to support Python 2.6. - elif sys.version_info[0] == 2 and type(data) == str: - return data.encode('string-escape') - elif sys.version_info[0] == 2 and type(data) == unicode: - return data.encode('unicode-escape') - elif type(data) == str: - # All strings in Python 3 are unicode - # This encode/decode pair gets us an escaped string - return data.encode('unicode-escape').decode(encoding="ascii", - errors="backslashreplace") + elif type(data) == str or type(data) == unicode: + return StringEscape(data) else: return data diff --git a/tools/rdm/ResponderTestTest.py b/tools/rdm/ResponderTestTest.py index 0caa499c3a..96fd2fa949 100644 --- a/tools/rdm/ResponderTestTest.py +++ b/tools/rdm/ResponderTestTest.py @@ -18,7 +18,7 @@ import unittest -from ResponderTest import ResponderTestFixture, TestFixture +from ola.testing.rdm.ResponderTest import ResponderTestFixture, TestFixture """Test cases for sorting TestFixtures.""" diff --git a/tools/rdm/TestDefinitions.py b/tools/rdm/TestDefinitions.py index a76c6dbe4e..08b3688fab 100644 --- a/tools/rdm/TestDefinitions.py +++ b/tools/rdm/TestDefinitions.py @@ -19,10 +19,6 @@ import operator import struct -import TestMixins -from ExpectedResults import (RDM_GET, RDM_SET, AckGetResult, BroadcastResult, - InvalidResponse, NackGetResult, TimeoutResult, - UnsupportedResult) from ola.OlaClient import OlaClient, RDMNack from ola.PidStore import ROOT_DEVICE from ola.RDMConstants import (INTERFACE_HARDWARE_TYPE_ETHERNET, @@ -33,12 +29,18 @@ RDM_MAX_DOMAIN_NAME_LENGTH, RDM_MAX_HOSTNAME_LENGTH, RDM_MIN_HOSTNAME_LENGTH, RDM_ZERO_FOOTPRINT_DMX_ADDRESS) +from ola.StringUtils import StringEscape +from ola.testing.rdm import TestMixins +from ola.testing.rdm.ExpectedResults import (RDM_GET, RDM_SET, AckGetResult, + BroadcastResult, InvalidResponse, + NackGetResult, TimeoutResult, + UnsupportedResult) +from ola.testing.rdm.ResponderTest import (OptionalParameterTestFixture, + ResponderTestFixture, TestFixture) +from ola.testing.rdm.TestCategory import TestCategory +from ola.testing.rdm.TestHelpers import ContainsUnprintable +from ola.testing.rdm.TestMixins import MAX_DMX_ADDRESS from ola.UID import UID -from ResponderTest import (OptionalParameterTestFixture, ResponderTestFixture, - TestFixture) -from TestCategory import TestCategory -from TestHelpers import ContainsUnprintable -from TestMixins import MAX_DMX_ADDRESS from ola import PidStore, RDMConstants @@ -95,7 +97,7 @@ def Test(self): TimeoutResult(), UnsupportedResult() ]) - self.SendRawDiscovery(ROOT_DEVICE, self.pid, 'x') + self.SendRawDiscovery(ROOT_DEVICE, self.pid, b'x') class UnMuteDevice(ResponderTestFixture): @@ -134,7 +136,7 @@ def Test(self): TimeoutResult(), UnsupportedResult() ]) - self.SendRawDiscovery(ROOT_DEVICE, self.pid, 'x') + self.SendRawDiscovery(ROOT_DEVICE, self.pid, b'x') class RequestsWhileUnmuted(ResponderTestFixture): @@ -584,7 +586,7 @@ def Test(self): field_values=self.FIELD_VALUES, warning='Get %s with data returned an ack' % self.pid.name) ]) - self.SendRawGet(ROOT_DEVICE, self.pid, 'x') + self.SendRawGet(ROOT_DEVICE, self.pid, b'x') def VerifyResult(self, response, fields): self.SetProperty('supports_over_sized_pdl', True) @@ -609,7 +611,7 @@ def Test(self): advisory='Responder timed out to a command with PDL of %d' % self.MAX_PDL), ]) - self.SendRawGet(ROOT_DEVICE, self.pid, 'x' * self.MAX_PDL) + self.SendRawGet(ROOT_DEVICE, self.pid, b'x' * self.MAX_PDL) def VerifyResult(self, response, fields): ok = response.response_code not in [OlaClient.RDM_INVALID_RESPONSE, @@ -646,7 +648,7 @@ def SendGet(self): InvalidResponse(action=self.GetFailed), TimeoutResult(action=self.GetFailed), ]) - self.SendRawGet(ROOT_DEVICE, self.pid, 'x' * self._current) + self.SendRawGet(ROOT_DEVICE, self.pid, b'x' * self._current) def GetPassed(self): self._lower = self._current @@ -856,7 +858,7 @@ def Test(self): ]) else: self.AddExpectedResults(self.NackGetResult(RDMNack.NR_UNKNOWN_PID)) - self.SendRawGet(ROOT_DEVICE, self.pid, 'foo') + self.SendRawGet(ROOT_DEVICE, self.pid, b'foo') class SetSupportedParameters(ResponderTestFixture): @@ -890,7 +892,7 @@ class GetSubDeviceSupportedParameters(ResponderTestFixture): 'IDENTIFY_DEVICE'] def Test(self): - self._sub_devices = self.Property('sub_device_addresses').keys() + self._sub_devices = list(self.Property('sub_device_addresses').keys()) self._sub_devices.reverse() self._params = {} self._GetSupportedParams() @@ -1063,7 +1065,7 @@ def VerifyResult(self, response, fields): if ContainsUnprintable(fields['description']): self.AddAdvisory( 'Description field in %s contains unprintable characters, was %s' % - (self.PID, fields['description'].encode('string-escape'))) + (self.PID, StringEscape(fields['description']))) class GetParamDescriptionForNonManufacturerPid(ResponderTestFixture): @@ -1105,7 +1107,7 @@ def Test(self): if self.Property('manufacturer_parameters'): results = self.NackGetResult(RDMNack.NR_FORMAT_ERROR) self.AddExpectedResults(results) - self.SendRawGet(ROOT_DEVICE, self.pid, 'foo') + self.SendRawGet(ROOT_DEVICE, self.pid, b'foo') class SetParamDescription(TestMixins.UnsupportedSetMixin, @@ -1318,7 +1320,7 @@ class SetDeviceModelDescriptionWithData(TestMixins.UnsupportedSetMixin, """SET the device model description with data.""" CATEGORY = TestCategory.ERROR_CONDITIONS PID = 'DEVICE_MODEL_DESCRIPTION' - DATA = 'FOO BAR' + DATA = b'FOO BAR' class AllSubDevicesGetModelDescription(TestMixins.AllSubDevicesGetMixin, @@ -1358,7 +1360,7 @@ class SetManufacturerLabelWithData(TestMixins.UnsupportedSetMixin, """SET the manufacturer label with data.""" CATEGORY = TestCategory.ERROR_CONDITIONS PID = 'MANUFACTURER_LABEL' - DATA = 'FOO BAR' + DATA = b'FOO BAR' class AllSubDevicesGetManufacturerLabel(TestMixins.AllSubDevicesGetMixin, @@ -1447,13 +1449,15 @@ def OldValue(self): return self.Property('device_label') -class SetNonAsciiDeviceLabel(TestMixins.SetLabelMixin, - OptionalParameterTestFixture): - """SET the device label to something that contains non-ascii data.""" +class SetNonPrintableAsciiDeviceLabel(TestMixins.SetLabelMixin, + OptionalParameterTestFixture): + """SET the device label to something that contains non-printable ASCII + characters. + """ CATEGORY = TestCategory.PRODUCT_INFORMATION PID = 'DEVICE_LABEL' REQUIRES = ['device_label'] - TEST_LABEL = 'string with\x0d non ascii\xc0' + TEST_LABEL = 'str w\x0d non\x1bprint ASCII\x7f' def ExpectedResults(self): return [ @@ -1467,6 +1471,38 @@ def OldValue(self): return self.Property('device_label') +# TODO(Peter): Get this test to work where we just compare the returned string +# as bytes so we don't try and fail to decode it as ASCII/UTF-8 +# class SetNonAsciiDeviceLabel(TestMixins.SetLabelMixin, +# OptionalParameterTestFixture): +# """SET the device label to something that contains non-ASCII data.""" +# CATEGORY = TestCategory.PRODUCT_INFORMATION +# PID = 'DEVICE_LABEL' +# REQUIRES = ['device_label'] +# # Store directly as bytes so we don't try and decode as UTF-8 +# TEST_LABEL = b'string with\x0d non ASCII\xc0' +# +# def ExpectedResults(self): +# return [ +# self.NackSetResult(RDMNack.NR_DATA_OUT_OF_RANGE), +# self.NackSetResult(RDMNack.NR_FORMAT_ERROR), +# self.NackSetResult(RDMNack.NR_UNSUPPORTED_COMMAND_CLASS), +# self.AckSetResult(action=self.VerifySet) +# ] +# +# def OldValue(self): +# return self.Property('device_label') +# +# def Test(self): +# # We have to override test here as this has to be raw as we can't encode +# # it +# # on Python 3 as it turns it to UTF-8 or escapes it +# # It's also technically out of spec for E1.20 unless it's sent as UTF-8 +# self._test_state = self.SET +# self.AddIfSetSupported(self.ExpectedResults()) +# self.SendRawSet(PidStore.ROOT_DEVICE, self.pid, self.TEST_LABEL) + + class SetEmptyDeviceLabel(TestMixins.SetLabelMixin, OptionalParameterTestFixture): """SET the device label with no data.""" @@ -1521,7 +1557,7 @@ def VerifyResult(self, response, fields): if ContainsUnprintable(language): self.AddAdvisory( 'Language name in language capabilities contains unprintable ' - 'characters, was %s' % language.encode('string-escape')) + 'characters, was %s' % StringEscape(language)) self.SetProperty('languages_capabilities', language_set) @@ -1591,14 +1627,48 @@ def VerifySet(self): self.SendGet(ROOT_DEVICE, self.pid) +class SetNumericLanguage(OptionalParameterTestFixture): + """Try to set the language to ASCII numeric characters.""" + CATEGORY = TestCategory.PRODUCT_INFORMATION + PID = 'LANGUAGE' + + def Test(self): + self.AddIfSetSupported(self.NackSetResult(RDMNack.NR_DATA_OUT_OF_RANGE)) + self.SendSet(ROOT_DEVICE, self.pid, ['01']) + + +class SetNullLanguage(OptionalParameterTestFixture): + """Try to set the language to two null ASCII characters.""" + CATEGORY = TestCategory.PRODUCT_INFORMATION + PID = 'LANGUAGE' + + def Test(self): + self.AddIfSetSupported(self.NackSetResult(RDMNack.NR_DATA_OUT_OF_RANGE)) + self.SendSet(ROOT_DEVICE, self.pid, ['\x00\x00']) + + +class SetNonPrintableAsciiLanguage(OptionalParameterTestFixture): + """Try to set the language to non-printable ASCII characters.""" + CATEGORY = TestCategory.PRODUCT_INFORMATION + PID = 'LANGUAGE' + + def Test(self): + self.AddIfSetSupported(self.NackSetResult(RDMNack.NR_DATA_OUT_OF_RANGE)) + self.SendSet(ROOT_DEVICE, self.pid, ['\x1b\x7f']) + + class SetNonAsciiLanguage(OptionalParameterTestFixture): - """Try to set the language to non-ascii characters.""" + """Try to set the language to non-ASCII characters.""" CATEGORY = TestCategory.PRODUCT_INFORMATION PID = 'LANGUAGE' def Test(self): self.AddIfSetSupported(self.NackSetResult(RDMNack.NR_DATA_OUT_OF_RANGE)) - self.SendSet(ROOT_DEVICE, self.pid, ['\x0d\xc0']) + # This has to be raw as we can't encode it on Python 3 as it turns it to + # UTF-8 and too many characters + # It's also technically out of spec for E1.20 unless it's sent as UTF-8 at + # which point we're back at square one + self.SendRawSet(ROOT_DEVICE, self.pid, b'\x0d\xc0') class SetUnsupportedLanguage(OptionalParameterTestFixture): @@ -1665,7 +1735,7 @@ class GetSubDeviceSoftwareVersionLabel(ResponderTestFixture): REQUIRES = ['sub_device_addresses'] def Test(self): - self._sub_devices = self.Property('sub_device_addresses').keys() + self._sub_devices = list(self.Property('sub_device_addresses').keys()) self._sub_devices.reverse() self._GetSoftwareVersion() @@ -1798,7 +1868,7 @@ def VerifyResult(self, response, fields): if ContainsUnprintable(fields['name']): self.AddAdvisory( 'Name field in %s contains unprintable characters, was %s' % - (self.PID, fields['name'].encode('string-escape'))) + (self.PID, StringEscape(fields['name']))) class GetPersonality(OptionalParameterTestFixture): @@ -1879,7 +1949,7 @@ def VerifyResult(self, response, fields): if ContainsUnprintable(fields['name']): self.AddAdvisory( 'Name field in %s contains unprintable characters, was %s' % - (self.PID, fields['name'].encode('string-escape'))) + (self.PID, StringEscape(fields['name']))) class SetPersonality(OptionalParameterTestFixture): @@ -1998,7 +2068,7 @@ class SetOversizedPersonality(OptionalParameterTestFixture): def Test(self): self.AddIfSetSupported(self.NackSetResult(RDMNack.NR_FORMAT_ERROR)) - self.SendRawSet(ROOT_DEVICE, self.pid, 'foo') + self.SendRawSet(ROOT_DEVICE, self.pid, b'foo') class AllSubDevicesGetPersonality(TestMixins.AllSubDevicesGetMixin, @@ -2067,7 +2137,7 @@ def Test(self): warning='Get %s with data returned an ack' % self.pid.name), ] self.AddExpectedResults(results) - self.SendRawGet(PidStore.ROOT_DEVICE, self.pid, 'foo') + self.SendRawGet(PidStore.ROOT_DEVICE, self.pid, b'foo') class SetStartAddress(TestMixins.SetStartAddressMixin, ResponderTestFixture): @@ -2182,7 +2252,7 @@ def Test(self): self.NackSetResult(RDMNack.NR_UNSUPPORTED_COMMAND_CLASS), self.NackSetResult(RDMNack.NR_FORMAT_ERROR), ]) - self.SendRawSet(ROOT_DEVICE, self.pid, 'foo') + self.SendRawSet(ROOT_DEVICE, self.pid, b'foo') class AllSubDevicesGetStartAddress(TestMixins.AllSubDevicesGetMixin, @@ -2295,7 +2365,7 @@ class GetSlotDescriptionWithTooMuchData(OptionalParameterTestFixture): def Test(self): self.AddIfGetSupported(self.NackGetResult(RDMNack.NR_FORMAT_ERROR)) - self.SendRawGet(ROOT_DEVICE, self.pid, 'foo') + self.SendRawGet(ROOT_DEVICE, self.pid, b'foo') class GetUndefinedSlotDefinitionDescriptions(OptionalParameterTestFixture): @@ -2621,7 +2691,7 @@ def VerifyResult(self, response, fields): self.AddAdvisory( 'Name field in sensor definition for sensor %d contains unprintable' ' characters, was %s' % (self._current_index, - fields['name'].encode('string-escape'))) + StringEscape(fields['name']))) def CheckCondition(self, sensor_number, fields, lhs, predicate_str, rhs): """Check for a condition and add a warning if it isn't true.""" @@ -2646,7 +2716,7 @@ class GetSensorDefinitionWithTooMuchData(OptionalParameterTestFixture): def Test(self): self.AddIfGetSupported(self.NackGetResult(RDMNack.NR_FORMAT_ERROR)) - self.SendRawGet(ROOT_DEVICE, self.pid, 'foo') + self.SendRawGet(ROOT_DEVICE, self.pid, b'foo') class GetInvalidSensorDefinition(OptionalParameterTestFixture): @@ -2689,7 +2759,7 @@ class GetSensorValues(OptionalParameterTestFixture): def Test(self): # The head of the list is the current sensor we're querying - self._sensors = self.Property('sensor_definitions').values() + self._sensors = list(self.Property('sensor_definitions').values()) self._sensor_values = [] if self._sensors: @@ -2827,7 +2897,7 @@ class ResetSensorValue(OptionalParameterTestFixture): def Test(self): # The head of the list is the current sensor we're querying - self._sensors = self.Property('sensor_definitions').values() + self._sensors = list(self.Property('sensor_definitions').values()) self._sensor_values = [] if self._sensors: @@ -2923,7 +2993,7 @@ def Test(self): self.NackSetResult(RDMNack.NR_FORMAT_ERROR), self.NackSetResult(RDMNack.NR_UNSUPPORTED_COMMAND_CLASS), ]) - self.SendRawSet(ROOT_DEVICE, self.pid, '') + self.SendRawSet(ROOT_DEVICE, self.pid, b'') class AllSubDevicesGetSensorValue(TestMixins.AllSubDevicesGetMixin, @@ -2953,7 +3023,7 @@ class RecordSensorValues(OptionalParameterTestFixture): def Test(self): # The head of the list is the current sensor we're querying - self._sensors = self.Property('sensor_definitions').values() + self._sensors = list(self.Property('sensor_definitions').values()) self._sensor_values = [] if self._sensors: @@ -3070,7 +3140,7 @@ def Test(self): else: expected_result = RDMNack.NR_UNSUPPORTED_COMMAND_CLASS self.AddIfSetSupported(self.NackSetResult(expected_result)) - self.SendRawSet(ROOT_DEVICE, self.pid, '') + self.SendRawSet(ROOT_DEVICE, self.pid, b'') class AllSubDevicesGetDeviceHours(TestMixins.AllSubDevicesGetMixin, @@ -3129,7 +3199,7 @@ def Test(self): else: expected_result = RDMNack.NR_UNSUPPORTED_COMMAND_CLASS self.AddIfSetSupported(self.NackSetResult(expected_result)) - self.SendRawSet(ROOT_DEVICE, self.pid, '') + self.SendRawSet(ROOT_DEVICE, self.pid, b'') class AllSubDevicesGetLampHours(TestMixins.AllSubDevicesGetMixin, @@ -3187,7 +3257,7 @@ def Test(self): else: expected_result = RDMNack.NR_UNSUPPORTED_COMMAND_CLASS self.AddIfSetSupported(self.NackSetResult(expected_result)) - self.SendRawSet(ROOT_DEVICE, self.pid, '') + self.SendRawSet(ROOT_DEVICE, self.pid, b'') class AllSubDevicesGetLampStrikes(TestMixins.AllSubDevicesGetMixin, @@ -3370,7 +3440,7 @@ def Test(self): else: expected_result = RDMNack.NR_UNSUPPORTED_COMMAND_CLASS self.AddIfSetSupported(self.NackSetResult(expected_result)) - self.SendRawSet(ROOT_DEVICE, self.pid, '') + self.SendRawSet(ROOT_DEVICE, self.pid, b'') class AllSubDevicesGetDevicePowerCycles(TestMixins.AllSubDevicesGetMixin, @@ -3625,7 +3695,8 @@ class GetRealTimeClock(OptionalParameterTestFixture): def Test(self): self.AddIfGetSupported( - self.AckGetResult(field_names=self.ALLOWED_RANGES.keys() + ['second'])) + self.AckGetResult(field_names=list(self.ALLOWED_RANGES.keys()) + + ['second'])) self.SendGet(ROOT_DEVICE, self.pid) def VerifyResult(self, response, fields): @@ -3672,7 +3743,7 @@ def Test(self): self.NackSetResult(RDMNack.NR_UNSUPPORTED_COMMAND_CLASS), self.NackSetResult(RDMNack.NR_FORMAT_ERROR), ]) - self.SendRawSet(ROOT_DEVICE, self.pid, '') + self.SendRawSet(ROOT_DEVICE, self.pid, b'') class AllSubDevicesGetRealTimeClock(TestMixins.AllSubDevicesGetMixin, @@ -3796,7 +3867,7 @@ class SetIdentifyDeviceWithNoData(ResponderTestFixture): def Test(self): self.AddExpectedResults(self.NackSetResult(RDMNack.NR_FORMAT_ERROR)) - self.SendRawSet(ROOT_DEVICE, self.pid, '') + self.SendRawSet(ROOT_DEVICE, self.pid, b'') def ResetState(self): self.SendSet(ROOT_DEVICE, self.pid, [self.Property('identify_state')]) @@ -3817,7 +3888,7 @@ class GetSubDeviceIdentifyDevice(ResponderTestFixture): REQUIRES = ['sub_device_addresses'] def Test(self): - self._sub_devices = self.Property('sub_device_addresses').keys() + self._sub_devices = list(self.Property('sub_device_addresses').keys()) self._sub_devices.reverse() self._GetIdentifyDevice() @@ -3972,7 +4043,7 @@ def VerifyResult(self, response, fields): self.AddAdvisory( 'Description field in self test description for test number %d ' 'contains unprintable characters, was %s' % - (1, fields['description'].encode('string-escape'))) + (1, StringEscape(fields['description']))) class GetSelfTestDescriptionWithNoData(TestMixins.GetWithNoDataMixin, @@ -4025,8 +4096,7 @@ def VerifyResult(self, response, fields): self.AddAdvisory( 'Description field in self test description for test number %d ' 'contains unprintable characters, was %s' % - (fields['test_number'], - fields['description'].encode('string-escape'))) + (fields['test_number'], StringEscape(fields['description']))) class AllSubDevicesGetSelfTestDescription(TestMixins.AllSubDevicesGetMixin, @@ -5117,7 +5187,7 @@ class GetLockPinWithData(TestMixins.GetWithDataMixin, """Get LOCK_PIN with data.""" CATEGORY = TestCategory.ERROR_CONDITIONS PID = 'LOCK_PIN' - DATA = 'foo' + DATA = b'foo' # Some responders may not let you GET the pin. ALLOWED_NACKS = [RDMNack.NR_UNSUPPORTED_COMMAND_CLASS] @@ -6818,7 +6888,7 @@ class GetIPv4DefaultRouteWithData(TestMixins.GetWithDataMixin, # """SET the IPv4 default route with data.""" # CATEGORY = TestCategory.ERROR_CONDITIONS # PID = 'IPV4_DEFAULT_ROUTE' -# DATA = 'FOOBAR' +# DATA = b'FOOBAR' class AllSubDevicesGetIPv4DefaultRoute(TestMixins.AllSubDevicesGetMixin, @@ -6854,7 +6924,7 @@ class GetInterfaceLabelWithTooMuchData(OptionalParameterTestFixture): def Test(self): self.AddIfGetSupported(self.NackGetResult(RDMNack.NR_FORMAT_ERROR)) - self.SendRawGet(ROOT_DEVICE, self.pid, 'foobar') + self.SendRawGet(ROOT_DEVICE, self.pid, b'foobar') class GetZeroInterfaceLabel(TestMixins.GetZeroUInt32Mixin, @@ -6875,7 +6945,7 @@ class SetInterfaceLabelWithData(TestMixins.UnsupportedSetMixin, """SET the interface label with data.""" CATEGORY = TestCategory.ERROR_CONDITIONS PID = 'INTERFACE_LABEL' - DATA = 'FOO BAR' + DATA = b'FOO BAR' # Cross check the control fields with various other properties diff --git a/tools/rdm/TestHelpers.py b/tools/rdm/TestHelpers.py index daa12c2099..51eb606de7 100644 --- a/tools/rdm/TestHelpers.py +++ b/tools/rdm/TestHelpers.py @@ -17,6 +17,8 @@ import sys +from ola.StringUtils import StringEscape + if sys.version_info >= (3, 0): try: unicode @@ -29,13 +31,8 @@ def ContainsUnprintable(s): """Check if a string s contain unprintable characters.""" # TODO(Peter): How does this interact with the E1.20 Unicode flag? - # We don't use sys.version_info.major to support Python 2.6. - if sys.version_info[0] == 2 and type(s) == str: - return s != s.encode('string-escape') - elif sys.version_info[0] == 2 and type(s) == unicode: - return s != s.encode('unicode-escape') - elif type(s) == str: - # All strings in Python 3 are unicode - return s.encode() != s.encode('unicode-escape') + if type(s) == str or type(s) == unicode: + # All strings in Python 3 are unicode, Python 2 ones might not be + return s != StringEscape(s) else: - return False + raise TypeError('Only strings are supported not %s' % type(s)) diff --git a/tools/rdm/TestHelpersTest.py b/tools/rdm/TestHelpersTest.py index 61d734fcde..f02805577a 100644 --- a/tools/rdm/TestHelpersTest.py +++ b/tools/rdm/TestHelpersTest.py @@ -18,6 +18,7 @@ import unittest +# Keep this import relative to simplify the testing from TestHelpers import ContainsUnprintable """Test cases for TestHelpers utilities.""" @@ -32,10 +33,15 @@ def testContainsUnprintable(self): self.assertFalse(ContainsUnprintable("bar[]")) self.assertFalse(ContainsUnprintable(u'foo-bar')) self.assertTrue(ContainsUnprintable("foo\x00bar")) - # TODO(Peter): How does this interact with the E1.20 Unicode flag? + # TODO(Peter): How do these interact with the E1.20 Unicode flag? self.assertTrue(ContainsUnprintable(u'caf\xe9')) + self.assertTrue(ContainsUnprintable(u'c\xc0f\xe9')) self.assertTrue(ContainsUnprintable(u'foo\u2014bar')) + with self.assertRaises(TypeError): + result = ContainsUnprintable(42) + self.assertNone(result) + if __name__ == '__main__': unittest.main() diff --git a/tools/rdm/TestLogger.py b/tools/rdm/TestLogger.py index e1a7269e90..dbbae91ff2 100644 --- a/tools/rdm/TestLogger.py +++ b/tools/rdm/TestLogger.py @@ -20,6 +20,7 @@ import pickle import re +from ola.StringUtils import StringEscape from ola.testing.rdm.TestState import TestState from ola import Version @@ -93,7 +94,9 @@ def SaveLog(self, uid, timestamp, end_time, tests, device, test_parameters): filename = os.path.join(self._log_dir, filename) try: - log_file = open(filename, 'w') + # We need to write as binary because pickle on Python 3 generates binary + # data + log_file = open(filename, 'wb') except IOError as e: raise TestLoggerException( 'Failed to write to %s: %s' % (filename, e.message)) @@ -211,13 +214,12 @@ def _FormatData(self, test_data, requested_category, requested_test_state, manufacturer_label = test_data['properties'].get('manufacturer_label', None) if manufacturer_label: - results_log.append('Manufacturer: %s' % - manufacturer_label.encode('string-escape')) + results_log.append('Manufacturer: %s' % StringEscape(manufacturer_label)) model_description = test_data['properties'].get('model_description', None) if model_description: results_log.append('Model Description: %s' % - model_description.encode('string-escape')) + StringEscape(model_description)) software_version = test_data['properties'].get('software_version', None) if software_version: diff --git a/tools/rdm/TestMixins.py b/tools/rdm/TestMixins.py index 625d2ddadf..1762713935 100644 --- a/tools/rdm/TestMixins.py +++ b/tools/rdm/TestMixins.py @@ -17,18 +17,20 @@ import struct -from ExpectedResults import (AckDiscoveryResult, AckGetResult, BroadcastResult, - DUBResult, NackSetResult, TimeoutResult, - UnsupportedResult) from ola.DMXConstants import DMX_UNIVERSE_SIZE from ola.DUBDecoder import DecodeResponse from ola.OlaClient import OlaClient, RDMNack from ola.PidStore import ROOT_DEVICE from ola.RDMConstants import RDM_MAX_STRING_LENGTH +from ola.StringUtils import StringEscape +from ola.testing.rdm.ExpectedResults import (AckDiscoveryResult, AckGetResult, + BroadcastResult, DUBResult, + NackSetResult, TimeoutResult, + UnsupportedResult) +from ola.testing.rdm.ResponderTest import ResponderTestFixture +from ola.testing.rdm.TestCategory import TestCategory +from ola.testing.rdm.TestHelpers import ContainsUnprintable from ola.UID import UID -from ResponderTest import ResponderTestFixture -from TestCategory import TestCategory -from TestHelpers import ContainsUnprintable from ola import PidStore @@ -110,7 +112,7 @@ def VerifyResult(self, response, fields): self.AddAdvisory( '%s field in %s contains unprintable characters, was %s' % (self.EXPECTED_FIELDS[0].capitalize(), self.PID, - string_field.encode('string-escape'))) + StringEscape(string_field))) if self.MIN_LENGTH and len(string_field) < self.MIN_LENGTH: self.SetFailed( @@ -166,7 +168,7 @@ def VerifyResult(self, response, fields): self.AddAdvisory( '%s field in %s contains unprintable characters, was %s' % (self.EXPECTED_FIELDS[0].capitalize(), self.PID, - string_field.encode('string-escape'))) + StringEscape(string_field))) if self.MIN_LENGTH and len(string_field) < self.MIN_LENGTH: self.SetFailed( @@ -187,7 +189,7 @@ class GetWithDataMixin(object): If ALLOWED_NACKS is non-empty, this adds a custom NackGetResult to the list of allowed results for each entry. """ - DATA = 'foo' + DATA = b'foo' ALLOWED_NACKS = [] def Test(self): @@ -204,7 +206,7 @@ def Test(self): class GetMandatoryPIDWithDataMixin(object): """GET a mandatory PID with junk param data.""" - DATA = 'foo' + DATA = b'foo' def Test(self): # PID must return something as this PID is required (can't return @@ -250,7 +252,7 @@ def Test(self): class SetWithDataMixin(ResponderTestFixture): """SET a PID with random param data.""" - DATA = 'foo' + DATA = b'foo' def Test(self): self.AddIfSetSupported([ @@ -265,7 +267,7 @@ class SetWithNoDataMixin(object): """Attempt a set with no data.""" def Test(self): self.AddIfSetSupported(self.NackSetResult(RDMNack.NR_FORMAT_ERROR)) - self.SendRawSet(PidStore.ROOT_DEVICE, self.pid, '') + self.SendRawSet(PidStore.ROOT_DEVICE, self.pid, b'') # TODO(simon): add a method to check this didn't change the value @@ -319,8 +321,8 @@ def VerifyResult(self, response, fields): (self.pid, len(new_label))) else: self.SetFailed('Labels didn\'t match, expected "%s", got "%s"' % - (self.TEST_LABEL.encode('string-escape'), - new_label.encode('string-escape'))) + (StringEscape(self.TEST_LABEL), + StringEscape(new_label))) def ResetState(self): if not self.OldValue(): @@ -347,7 +349,7 @@ def Test(self): class SetOversizedLabelMixin(object): """Send an over-sized SET label command.""" - LONG_STRING = 'this is a string which is more than 32 characters' + LONG_STRING = b'this is a string which is more than 32 characters' def Test(self): self.verify_result = False @@ -1025,7 +1027,7 @@ def VerifyResult(self, response, fields): self.PID, self.DESCRIPTION_FIELD, self.current_item, - fields[self.DESCRIPTION_FIELD].encode('string-escape'))) + StringEscape(fields[self.DESCRIPTION_FIELD]))) class GetSettingDescriptionsRangeMixin(GetSettingDescriptionsMixin): @@ -1044,8 +1046,8 @@ def ListOfSettings(self): if self.NumberOfSettings() is None: return [] else: - return range(self.FIRST_INDEX_OFFSET, - self.NumberOfSettings() + self.FIRST_INDEX_OFFSET) + return list(range(self.FIRST_INDEX_OFFSET, + self.NumberOfSettings() + self.FIRST_INDEX_OFFSET)) class GetSettingDescriptionsListMixin(GetSettingDescriptionsMixin): diff --git a/tools/rdm/TestRunner.py b/tools/rdm/TestRunner.py index 1e76908dfc..b4399884e4 100644 --- a/tools/rdm/TestRunner.py +++ b/tools/rdm/TestRunner.py @@ -22,8 +22,9 @@ from ola.OlaClient import OlaClient, RDMNack from ola.RDMAPI import RDMAPI -from ola.testing.rdm import ResponderTest -from TimingStats import TimingStats +from ola.testing.rdm.ResponderTest import (OptionalParameterTestFixture, + ResponderTestFixture, TestFixture) +from ola.testing.rdm.TimingStats import TimingStats from ola import PidStore @@ -197,14 +198,17 @@ def GetTestClasses(module): if not inspect.isclass(cls): continue base_classes = [ - ResponderTest.OptionalParameterTestFixture, - ResponderTest.ResponderTestFixture, - ResponderTest.TestFixture + OptionalParameterTestFixture, + ResponderTestFixture, + TestFixture ] if cls in base_classes: continue - if issubclass(cls, ResponderTest.TestFixture): + # This seems to confuse Python 3 if we compare it to + # ResponderTest.TestFixture, some sort of diamond inheritance issue? + # So test for the base version of it instead + if issubclass(cls, TestFixture): classes.append(cls) return classes diff --git a/tools/rdm/TestRunnerTest.py b/tools/rdm/TestRunnerTest.py new file mode 100644 index 0000000000..436e1278f0 --- /dev/null +++ b/tools/rdm/TestRunnerTest.py @@ -0,0 +1,59 @@ +#!/usr/bin/env python +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program 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 Library General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +# +# TestRunnerTest.py +# Copyright (C) 2022 Peter Newman + +import unittest + +from ola.testing.rdm import ResponderTest, TestDefinitions, TestRunner +from ola.testing.rdm.ResponderTest import (OptionalParameterTestFixture, + ResponderTestFixture, TestFixture) +from ola.testing.rdm.TestDefinitions import GetDeviceInfo + +"""Test cases for TestRunner utilities.""" + +__author__ = 'nomis52@gmail.com (Simon Newton)' + + +class TestRunnerGetTestClasses(unittest.TestCase): + def testGetTestClasses(self): + self.assertTrue(len(TestRunner.GetTestClasses(TestDefinitions)) > 0, + "Didn't find any test classes") + self.assertTrue(len(TestRunner.GetTestClasses(TestDefinitions)) > 100, + "Didn't find a realistic number of test classes") + # Check for a common test + self.assertTrue(GetDeviceInfo in + TestRunner.GetTestClasses(TestDefinitions), + "GetDeviceInfo missing from list of test classes") + # Check we don't contain the base classes: + # Test for various versions of them due to potential issues with Python 3 + # The static versions are probably a better test for the test run + for classname in [OptionalParameterTestFixture, + ResponderTestFixture, + TestFixture, + ResponderTest.OptionalParameterTestFixture, + ResponderTest.ResponderTestFixture, + ResponderTest.TestFixture, + TestDefinitions.OptionalParameterTestFixture, + TestDefinitions.ResponderTestFixture, + TestDefinitions.TestFixture]: + self.assertTrue(classname not in + TestRunner.GetTestClasses(TestDefinitions), + "Class %s found in list of test classes" % classname) + + +if __name__ == '__main__': + unittest.main() diff --git a/tools/rdm/TestStateTest.py b/tools/rdm/TestStateTest.py index 9e3aa26962..c2b37a149a 100644 --- a/tools/rdm/TestStateTest.py +++ b/tools/rdm/TestStateTest.py @@ -19,6 +19,7 @@ import unittest from ola.TestUtils import allHashNotEqual, allNotEqual +# Keep this import relative to simplify the testing from TestState import TestState """Test cases for sorting TestState.""" diff --git a/tools/rdm/rdm_responder_test.py b/tools/rdm/rdm_responder_test.py index f9bc5882f8..6433499d04 100755 --- a/tools/rdm/rdm_responder_test.py +++ b/tools/rdm/rdm_responder_test.py @@ -27,6 +27,7 @@ from optparse import OptionParser from ola.ClientWrapper import ClientWrapper +from ola.StringUtils import StringEscape from ola.testing.rdm import TestDefinitions, TestRunner from ola.testing.rdm.DMXSender import DMXSender from ola.testing.rdm.TestState import TestState @@ -202,13 +203,11 @@ def DisplaySummary(options, runner, tests, device): manufacturer_label = getattr(device, 'manufacturer_label', None) if manufacturer_label: - logging.info('Manufacturer: %s' % - manufacturer_label.encode('string-escape')) + logging.info('Manufacturer: %s' % StringEscape(manufacturer_label)) model_description = getattr(device, 'model_description', None) if model_description: - logging.info('Model Description: %s' % - model_description.encode('string-escape')) + logging.info('Model Description: %s' % StringEscape(model_description)) software_version = getattr(device, 'software_version', None) if software_version: @@ -250,6 +249,9 @@ def main(): options = ParseOptions() test_classes = TestRunner.GetTestClasses(TestDefinitions) + if len(test_classes) <= 0: + print('Failed to find any tests to run') + sys.exit(2) if options.list_tests: for test_name in sorted(c.__name__ for c in test_classes): print(test_name) diff --git a/tools/rdm/rdm_test_server.py b/tools/rdm/rdm_test_server.py index 1235e66cd0..742768d991 100755 --- a/tools/rdm/rdm_test_server.py +++ b/tools/rdm/rdm_test_server.py @@ -403,7 +403,10 @@ def PostParam(self, param, default=None): request_body = self._environ['wsgi.input'].read(request_body_size) post_params = urlparse.parse_qs(request_body) for p in post_params: - self._post_params[p] = post_params[p][0] + # In Python 3, the param name and value is a bytestring not a string, + # so convert it for backwards compatibility and sanity + self._post_params[p.decode('utf-8')] = ( + post_params[p][0].decode('utf-8')) return self._post_params.get(param, default) @@ -632,7 +635,6 @@ class DownloadModelDataHandler(RequestHandler): """Take the data in the form and return it as a downloadable file.""" def HandleRequest(self, request, response): - print(dir(request)) model_data = request.PostParam('model_data') or '' logging.info(model_data) @@ -642,7 +644,7 @@ def HandleRequest(self, request, response): 'attachment; filename="%s"' % filename) response.SetHeader('Content-type', 'text/plain') response.SetHeader('Content-length', '%d' % len(model_data)) - response.AppendData(model_data) + response.AppendData(model_data.encode()) class DownloadResultsHandler(RequestHandler): @@ -678,7 +680,7 @@ def HandleRequest(self, request, response): 'attachment; filename="%s"' % filename) response.SetHeader('Content-type', 'text/plain') response.SetHeader('Content-length', '%d' % len(output)) - response.AppendData(output) + response.AppendData(output.encode()) class RunTestsHandler(OLAServerRequestHandler):