From a7d56504be6c720bc4c02109d980f254cef54c2a Mon Sep 17 00:00:00 2001 From: Mike Kinney Date: Fri, 14 Jan 2022 16:36:53 -0800 Subject: [PATCH 1/5] handle snake_case or camelCase --- exampleConfig.yaml | 16 +++++ example_config.yaml | 1 + meshtastic/__main__.py | 107 ++++++++++++++++++++++++------- meshtastic/globals.py | 12 ++++ meshtastic/node.py | 2 - meshtastic/tests/test_main.py | 115 +++++++++++++++++++++++++++++++++- meshtastic/tests/test_util.py | 22 ++++++- meshtastic/util.py | 14 +++++ 8 files changed, 263 insertions(+), 26 deletions(-) create mode 100644 exampleConfig.yaml diff --git a/exampleConfig.yaml b/exampleConfig.yaml new file mode 100644 index 00000000..c4539fa5 --- /dev/null +++ b/exampleConfig.yaml @@ -0,0 +1,16 @@ +# example config using camelCase keys +owner: Bob TBeam + +channelUrl: https://www.meshtastic.org/d/#CgUYAyIBAQ + +location: + lat: 35.88888 + lon: -93.88888 + alt: 304 + +userPrefs: + region: 1 + isAlwaysPowered: 'true' + sendOwnerInterval: 2 + screenOnSecs: 31536000 + waitBluetoothSecs: 31536000 diff --git a/example_config.yaml b/example_config.yaml index 231b4651..90085125 100644 --- a/example_config.yaml +++ b/example_config.yaml @@ -1,3 +1,4 @@ +# example configuration file with snake_case keys owner: Bob TBeam channel_url: https://www.meshtastic.org/d/#CgUYAyIBAQ diff --git a/meshtastic/__main__.py b/meshtastic/__main__.py index 9ea52f4b..a4f2e119 100644 --- a/meshtastic/__main__.py +++ b/meshtastic/__main__.py @@ -20,7 +20,6 @@ from meshtastic.globals import Globals from meshtastic.__init__ import BROADCAST_ADDR - def onReceive(packet, interface): """Callback invoked when a packet arrives""" our_globals = Globals.getInstance() @@ -56,14 +55,26 @@ def onConnection(interface, topic=pub.AUTO_TOPIC): # pylint: disable=W0613 def getPref(attributes, name): """Get a channel or preferences value""" + camel_name = meshtastic.util.snake_to_camel(name) + # Note: protobufs has the keys in snake_case, so snake internally + snake_name = meshtastic.util.camel_to_snake(name) + logging.debug(f'snake_name:{snake_name} camel_name:{camel_name}') + logging.debug(f'use camel:{Globals.getInstance().get_camel_case()}') + objDesc = attributes.DESCRIPTOR - field = objDesc.fields_by_name.get(name) + field = objDesc.fields_by_name.get(snake_name) if not field: - print(f"{attributes.__class__.__name__} does not have an attribute called {name}, so you can not get it.") + if Globals.getInstance().get_camel_case(): + print(f"{attributes.__class__.__name__} does not have an attribute called {camel_name}, so you can not get it.") + else: + print(f"{attributes.__class__.__name__} does not have an attribute called {snake_name}, so you can not get it.") print(f"Choices in sorted order are:") names = [] for f in objDesc.fields: - names.append(f'{f.name}') + tmp_name = f'{f.name}' + if Globals.getInstance().get_camel_case(): + tmp_name = meshtastic.util.snake_to_camel(tmp_name) + names.append(tmp_name) for temp_name in sorted(names): print(f" {temp_name}") return @@ -71,28 +82,45 @@ def getPref(attributes, name): # okay - try to read the value try: try: - val = getattr(attributes, name) + val = getattr(attributes, snake_name) except TypeError: # The getter didn't like our arg type guess try again as a string - val = getattr(attributes, name) + val = getattr(attributes, snake_name) # succeeded! - print(f"{name}: {str(val)}") + if Globals.getInstance().get_camel_case(): + print(f"{camel_name}: {str(val)}") + logging.debug(f"{camel_name}: {str(val)}") + else: + print(f"{snake_name}: {str(val)}") + logging.debug(f"{snake_name}: {str(val)}") except Exception as ex: - print(f"Can't get {name} due to {ex}") + if Globals.getInstance().get_camel_case(): + print(f"Can't get {camel_name} due to {ex}") + else: + print(f"Can't get {snake_name} due to {ex}") def setPref(attributes, name, valStr): """Set a channel or preferences value""" + snake_name = meshtastic.util.camel_to_snake(name) + camel_name = meshtastic.util.snake_to_camel(name) + objDesc = attributes.DESCRIPTOR - field = objDesc.fields_by_name.get(name) + field = objDesc.fields_by_name.get(snake_name) if not field: - print(f"{attributes.__class__.__name__} does not have an attribute called {name}, so you can not set it.") + if Globals.getInstance().get_camel_case(): + print(f"{attributes.__class__.__name__} does not have an attribute called {camel_name}, so you can not set it.") + else: + print(f"{attributes.__class__.__name__} does not have an attribute called {snake_name}, so you can not set it.") print(f"Choices in sorted order are:") names = [] for f in objDesc.fields: - names.append(f'{f.name}') + tmp_name = f'{f.name}' + if Globals.getInstance().get_camel_case(): + tmp_name = meshtastic.util.snake_to_camel(tmp_name) + names.append(tmp_name) for temp_name in sorted(names): print(f" {temp_name}") return @@ -107,11 +135,17 @@ def setPref(attributes, name, valStr): if e: val = e.number else: - print(f"{name} does not have an enum called {val}, so you can not set it.") + if Globals.getInstance().get_camel_case(): + print(f"{snake_name} does not have an enum called {val}, so you can not set it.") + else: + print(f"{camel_name} does not have an enum called {val}, so you can not set it.") print(f"Choices in sorted order are:") names = [] for f in enumType.values: - names.append(f'{f.name}') + tmp_name = f'{f.name}' + if Globals.getInstance().get_camel_case(): + tmp_name = meshtastic.util.snake_to_camel(tmp_name) + names.append(name) for temp_name in sorted(names): print(f" {temp_name}") return @@ -119,15 +153,21 @@ def setPref(attributes, name, valStr): # okay - try to read the value try: try: - setattr(attributes, name, val) + setattr(attributes, snake_name, val) except TypeError: # The setter didn't like our arg type guess try again as a string - setattr(attributes, name, valStr) + setattr(attributes, snake_name, valStr) # succeeded! - print(f"Set {name} to {valStr}") + if Globals.getInstance().get_camel_case(): + print(f"Set {camel_name} to {valStr}") + else: + print(f"Set {snake_name} to {valStr}") except Exception as ex: - print(f"Can't set {name} due to {ex}") + if Globals.getInstance().get_camel_case(): + print(f"Can't set {camel_name} due to {ex}") + else: + print(f"Can't set {snake_name} due to {ex}") def onConnected(interface): @@ -311,6 +351,10 @@ def onConnected(interface): print("Setting channel url to", configuration['channel_url']) interface.getNode(args.dest).setURL(configuration['channel_url']) + if 'channelUrl' in configuration: + print("Setting channel url to", configuration['channelUrl']) + interface.getNode(args.dest).setURL(configuration['channelUrl']) + if 'location' in configuration: alt = 0 lat = 0.0 @@ -340,6 +384,13 @@ def onConnected(interface): print("Writing modified preferences to device") interface.getNode(args.dest).writeConfig() + if 'userPrefs' in configuration: + prefs = interface.getNode(args.dest).radioConfig.preferences + for pref in configuration['userPrefs']: + setPref(prefs, pref, str(configuration['userPrefs'][pref])) + print("Writing modified preferences to device") + interface.getNode(args.dest).writeConfig() + if args.export_config: # export the configuration (the opposite of '--configure') closeNow = True @@ -548,7 +599,10 @@ def export_config(interface): if owner: config += f"owner: {owner}\n\n" if channel_url: - config += f"channel_url: {channel_url}\n\n" + if Globals.getInstance().get_camel_case(): + config += f"channelUrl: {channel_url}\n\n" + else: + config += f"channel_url: {channel_url}\n\n" if lat or lon or alt: config += "location:\n" if lat: @@ -561,9 +615,16 @@ def export_config(interface): preferences = f'{interface.localNode.radioConfig.preferences}' prefs = preferences.splitlines() if prefs: - config += "user_prefs:\n" + if Globals.getInstance().get_camel_case(): + config += "userPrefs:\n" + else: + config += "user_prefs:\n" for pref in prefs: - config += f" {meshtastic.util.quoteBooleans(pref)}\n" + if Globals.getInstance().get_camel_case(): + # Note: This may not work if the value has '_' + config += f" {meshtastic.util.snake_to_camel(meshtastic.util.quoteBooleans(pref))}\n" + else: + config += f" {meshtastic.util.quoteBooleans(pref)}\n" print(config) return config @@ -692,10 +753,12 @@ def initParser(): action="store_true") parser.add_argument( - "--get", help="Get a preferences field. Use an invalid field such as '0' to get a list of all fields.", nargs=1, action='append') + "--get", help=("Get a preferences field. Use an invalid field such as '0' to get a list of all fields." + " Can use either snake_case or camelCase format. (ex: 'ls_secs' or 'lsSecs')"), + nargs=1, action='append') parser.add_argument( - "--set", help="Set a preferences field", nargs=2, action='append') + "--set", help="Set a preferences field. Can use either snake_case or camelCase format. (ex: 'ls_secs' or 'lsSecs')", nargs=2, action='append') parser.add_argument( "--seturl", help="Set a channel URL", action="store") diff --git a/meshtastic/globals.py b/meshtastic/globals.py index 6e5125b6..a8a2bc74 100644 --- a/meshtastic/globals.py +++ b/meshtastic/globals.py @@ -30,6 +30,8 @@ def __init__(self): self.channel_index = None self.logfile = None self.tunnelInstance = None + # TODO: to migrate to camel_case for v1.3 change this value to True + self.camel_case = False def reset(self): """Reset all of our globals. If you add a member, add it to this method, too.""" @@ -38,6 +40,8 @@ def reset(self): self.channel_index = None self.logfile = None self.tunnelInstance = None + # TODO: to migrate to camel_case for v1.3 change this value to True + self.camel_case = False # setters def set_args(self, args): @@ -60,6 +64,10 @@ def set_tunnelInstance(self, tunnelInstance): """Set the tunnelInstance""" self.tunnelInstance = tunnelInstance + def set_camel_case(self): + """Force using camelCase for things like prefs/set/set""" + self.camel_case = True + # getters def get_args(self): """Get args""" @@ -80,3 +88,7 @@ def get_logfile(self): def get_tunnelInstance(self): """Get tunnelInstance""" return self.tunnelInstance + + def get_camel_case(self): + """Get whether or not to use camelCase""" + return self.camel_case diff --git a/meshtastic/node.py b/meshtastic/node.py index 2c9cbd0e..a08523af 100644 --- a/meshtastic/node.py +++ b/meshtastic/node.py @@ -8,8 +8,6 @@ from meshtastic.util import pskToString, stripnl, Timeout, our_exit, fromPSK - - class Node: """A model of a (local or remote) node in the mesh diff --git a/meshtastic/tests/test_main.py b/meshtastic/tests/test_main.py index 84721f32..d28fbd85 100644 --- a/meshtastic/tests/test_main.py +++ b/meshtastic/tests/test_main.py @@ -781,6 +781,28 @@ def test_main_set_valid(capsys): mo.assert_called() +@pytest.mark.unit +@pytest.mark.usefixtures("reset_globals") +def test_main_set_valid_camel_case(capsys): + """Test --set with valid field""" + sys.argv = ['', '--set', 'wifi_ssid', 'foo'] + Globals.getInstance().set_args(sys.argv) + Globals.getInstance().set_camel_case() + + mocked_node = MagicMock(autospec=Node) + + iface = MagicMock(autospec=SerialInterface) + iface.getNode.return_value = mocked_node + + with patch('meshtastic.serial_interface.SerialInterface', return_value=iface) as mo: + main() + out, err = capsys.readouterr() + assert re.search(r'Connected to radio', out, re.MULTILINE) + assert re.search(r'Set wifiSsid to foo', out, re.MULTILINE) + assert err == '' + mo.assert_called() + + @pytest.mark.unit @pytest.mark.usefixtures("reset_globals") def test_main_set_with_invalid(capsys): @@ -809,7 +831,7 @@ def test_main_set_with_invalid(capsys): # TODO: write some negative --configure tests @pytest.mark.unit @pytest.mark.usefixtures("reset_globals") -def test_main_configure(capsys): +def test_main_configure_with_snake_case(capsys): """Test --configure with valid file""" sys.argv = ['', '--configure', 'example_config.yaml'] Globals.getInstance().set_args(sys.argv) @@ -833,6 +855,31 @@ def test_main_configure(capsys): mo.assert_called() +@pytest.mark.unit +@pytest.mark.usefixtures("reset_globals") +def test_main_configure_with_camel_case_keys(capsys): + """Test --configure with valid file""" + sys.argv = ['', '--configure', 'exampleConfig.yaml'] + Globals.getInstance().set_args(sys.argv) + + mocked_node = MagicMock(autospec=Node) + + iface = MagicMock(autospec=SerialInterface) + iface.getNode.return_value = mocked_node + + with patch('meshtastic.serial_interface.SerialInterface', return_value=iface) as mo: + main() + out, err = capsys.readouterr() + assert re.search(r'Connected to radio', out, re.MULTILINE) + assert re.search(r'Setting device owner', out, re.MULTILINE) + assert re.search(r'Setting channel url', out, re.MULTILINE) + assert re.search(r'Fixing altitude', out, re.MULTILINE) + assert re.search(r'Fixing latitude', out, re.MULTILINE) + assert re.search(r'Fixing longitude', out, re.MULTILINE) + assert re.search(r'Writing modified preferences', out, re.MULTILINE) + assert err == '' + mo.assert_called() + @pytest.mark.unit @pytest.mark.usefixtures("reset_globals") def test_main_ch_add_valid(capsys): @@ -1289,6 +1336,33 @@ def test_main_get_with_valid_values(capsys): assert err == '' +@pytest.mark.unit +@pytest.mark.usefixtures("reset_globals") +def test_main_get_with_valid_values_camel(capsys, caplog): + """Test --get with valid values (with string, number, boolean)""" + sys.argv = ['', '--get', 'lsSecs', '--get', 'wifiSsid', '--get', 'fixedPosition'] + Globals.getInstance().set_args(sys.argv) + Globals.getInstance().set_camel_case() + + with caplog.at_level(logging.DEBUG): + with patch('meshtastic.serial_interface.SerialInterface') as mo: + + mo().getNode().radioConfig.preferences.wifi_ssid = 'foo' + mo().getNode().radioConfig.preferences.ls_secs = 300 + mo().getNode().radioConfig.preferences.fixed_position = False + + main() + + mo.assert_called() + + out, err = capsys.readouterr() + assert re.search(r'Connected to radio', out, re.MULTILINE) + assert re.search(r'lsSecs: 300', out, re.MULTILINE) + assert re.search(r'wifiSsid: foo', out, re.MULTILINE) + assert re.search(r'fixedPosition: False', out, re.MULTILINE) + assert err == '' + + @pytest.mark.unit @pytest.mark.usefixtures("reset_globals") def test_main_get_with_invalid(capsys): @@ -1492,6 +1566,45 @@ def test_main_export_config(capsys): assert err == '' +@pytest.mark.unit +@pytest.mark.usefixtures("reset_globals") +def test_main_export_config_use_camel(capsys): + """Test export_config() function directly""" + Globals.getInstance().set_camel_case() + iface = MagicMock(autospec=SerialInterface) + with patch('meshtastic.serial_interface.SerialInterface', return_value=iface) as mo: + mo.getLongName.return_value = 'foo' + mo.localNode.getURL.return_value = 'bar' + mo.getMyNodeInfo().get.return_value = { 'latitudeI': 1100000000, 'longitudeI': 1200000000, + 'altitude': 100, 'batteryLevel': 34, 'latitude': 110.0, + 'longitude': 120.0} + mo.localNode.radioConfig.preferences = """phone_timeout_secs: 900 +ls_secs: 300 +position_broadcast_smart: true +fixed_position: true +position_flags: 35""" + export_config(mo) + out, err = capsys.readouterr() + + # ensure we do not output this line + assert not re.search(r'Connected to radio', out, re.MULTILINE) + + assert re.search(r'owner: foo', out, re.MULTILINE) + assert re.search(r'channelUrl: bar', out, re.MULTILINE) + assert re.search(r'location:', out, re.MULTILINE) + assert re.search(r'lat: 110.0', out, re.MULTILINE) + assert re.search(r'lon: 120.0', out, re.MULTILINE) + assert re.search(r'alt: 100', out, re.MULTILINE) + assert re.search(r'userPrefs:', out, re.MULTILINE) + assert re.search(r'phoneTimeoutSecs: 900', out, re.MULTILINE) + assert re.search(r'lsSecs: 300', out, re.MULTILINE) + # TODO: should True be capitalized here? + assert re.search(r"positionBroadcastSmart: 'True'", out, re.MULTILINE) + assert re.search(r"fixedPosition: 'True'", out, re.MULTILINE) + assert re.search(r"positionFlags: 35", out, re.MULTILINE) + assert err == '' + + @pytest.mark.unit @pytest.mark.usefixtures("reset_globals") def test_main_export_config_called_from_main(capsys): diff --git a/meshtastic/tests/test_util.py b/meshtastic/tests/test_util.py index 03cf4c92..515fc762 100644 --- a/meshtastic/tests/test_util.py +++ b/meshtastic/tests/test_util.py @@ -10,7 +10,8 @@ support_info, genPSK256, fromStr, fromPSK, quoteBooleans, catchAndIgnore, remove_keys_from_dict, Timeout, hexstr, - ipstr, readnet_u16, findPorts, convert_mac_addr) + ipstr, readnet_u16, findPorts, convert_mac_addr, + snake_to_camel, camel_to_snake) @pytest.mark.unit @@ -251,3 +252,22 @@ def test_convert_mac_addr(): assert convert_mac_addr('/c0gFyhb') == 'fd:cd:20:17:28:5b' assert convert_mac_addr('fd:cd:20:17:28:5b') == 'fd:cd:20:17:28:5b' assert convert_mac_addr('') == '' + + +@pytest.mark.unit +def test_snake_to_camel(): + """Test snake_to_camel""" + assert snake_to_camel('') == '' + assert snake_to_camel('foo') == 'foo' + assert snake_to_camel('foo_bar') == 'fooBar' + assert snake_to_camel('fooBar') == 'fooBar' + + +@pytest.mark.unit +def test_camel_to_snake(): + """Test camel_to_snake""" + assert camel_to_snake('') == '' + assert camel_to_snake('foo') == 'foo' + assert camel_to_snake('Foo') == 'foo' + assert camel_to_snake('fooBar') == 'foo_bar' + assert camel_to_snake('fooBarBaz') == 'foo_bar_baz' diff --git a/meshtastic/util.py b/meshtastic/util.py index 8abbea98..a2fa57c6 100644 --- a/meshtastic/util.py +++ b/meshtastic/util.py @@ -243,3 +243,17 @@ def convert_mac_addr(val): val_as_bytes = base64.b64decode(val) return hexstr(val_as_bytes) return val + + +def snake_to_camel(a_string): + """convert snake_case to camelCase""" + # split underscore using split + temp = a_string.split('_') + # joining result + result = temp[0] + ''.join(ele.title() for ele in temp[1:]) + return result + + +def camel_to_snake(a_string): + """convert camelCase to snake_case""" + return ''.join(['_'+i.lower() if i.isupper() else i for i in a_string]).lstrip('_') From afb21c6dc3b968c3b4c2231d57aa7db67b90529e Mon Sep 17 00:00:00 2001 From: Mike Kinney Date: Fri, 14 Jan 2022 20:15:25 -0800 Subject: [PATCH 2/5] remove code not needed --- meshtastic/__main__.py | 27 +++----- meshtastic/tests/test_main.py | 117 +++++++++++++++++++++++++++++++++- 2 files changed, 124 insertions(+), 20 deletions(-) diff --git a/meshtastic/__main__.py b/meshtastic/__main__.py index a4f2e119..f71567cb 100644 --- a/meshtastic/__main__.py +++ b/meshtastic/__main__.py @@ -79,26 +79,15 @@ def getPref(attributes, name): print(f" {temp_name}") return - # okay - try to read the value - try: - try: - val = getattr(attributes, snake_name) - except TypeError: - # The getter didn't like our arg type guess try again as a string - val = getattr(attributes, snake_name) + # read the value + val = getattr(attributes, snake_name) - # succeeded! - if Globals.getInstance().get_camel_case(): - print(f"{camel_name}: {str(val)}") - logging.debug(f"{camel_name}: {str(val)}") - else: - print(f"{snake_name}: {str(val)}") - logging.debug(f"{snake_name}: {str(val)}") - except Exception as ex: - if Globals.getInstance().get_camel_case(): - print(f"Can't get {camel_name} due to {ex}") - else: - print(f"Can't get {snake_name} due to {ex}") + if Globals.getInstance().get_camel_case(): + print(f"{camel_name}: {str(val)}") + logging.debug(f"{camel_name}: {str(val)}") + else: + print(f"{snake_name}: {str(val)}") + logging.debug(f"{snake_name}: {str(val)}") def setPref(attributes, name, valStr): diff --git a/meshtastic/tests/test_main.py b/meshtastic/tests/test_main.py index d28fbd85..4448fef7 100644 --- a/meshtastic/tests/test_main.py +++ b/meshtastic/tests/test_main.py @@ -1738,6 +1738,22 @@ def test_main_getPref_valid_field(capsys): assert err == '' +@pytest.mark.unit +@pytest.mark.usefixtures("reset_globals") +def test_main_getPref_valid_field_camel(capsys): + """Test getPref() with a valid field""" + Globals.getInstance().set_camel_case() + prefs = MagicMock() + prefs.DESCRIPTOR.fields_by_name.get.return_value = 'ls_secs' + prefs.wifi_ssid = 'foo' + prefs.ls_secs = 300 + prefs.fixed_position = False + + getPref(prefs, 'ls_secs') + out, err = capsys.readouterr() + assert re.search(r'lsSecs: 300', out, re.MULTILINE) + assert err == '' + @pytest.mark.unit @pytest.mark.usefixtures("reset_globals") def test_main_getPref_valid_field_string(capsys): @@ -1754,6 +1770,23 @@ def test_main_getPref_valid_field_string(capsys): assert err == '' +@pytest.mark.unit +@pytest.mark.usefixtures("reset_globals") +def test_main_getPref_valid_field_string_camel(capsys): + """Test getPref() with a valid field and value as a string""" + Globals.getInstance().set_camel_case() + prefs = MagicMock() + prefs.DESCRIPTOR.fields_by_name.get.return_value = 'wifi_ssid' + prefs.wifi_ssid = 'foo' + prefs.ls_secs = 300 + prefs.fixed_position = False + + getPref(prefs, 'wifi_ssid') + out, err = capsys.readouterr() + assert re.search(r'wifiSsid: foo', out, re.MULTILINE) + assert err == '' + + @pytest.mark.unit @pytest.mark.usefixtures("reset_globals") def test_main_getPref_valid_field_bool(capsys): @@ -1770,6 +1803,23 @@ def test_main_getPref_valid_field_bool(capsys): assert err == '' +@pytest.mark.unit +@pytest.mark.usefixtures("reset_globals") +def test_main_getPref_valid_field_bool_camel(capsys): + """Test getPref() with a valid field and value as a bool""" + Globals.getInstance().set_camel_case() + prefs = MagicMock() + prefs.DESCRIPTOR.fields_by_name.get.return_value = 'fixed_position' + prefs.wifi_ssid = 'foo' + prefs.ls_secs = 300 + prefs.fixed_position = False + + getPref(prefs, 'fixed_position') + out, err = capsys.readouterr() + assert re.search(r'fixedPosition: False', out, re.MULTILINE) + assert err == '' + + @pytest.mark.unit @pytest.mark.usefixtures("reset_globals") def test_main_getPref_invalid_field(capsys): @@ -1804,7 +1854,40 @@ def __init__(self, name): @pytest.mark.unit @pytest.mark.usefixtures("reset_globals") -def test_main_setPref_valid_field_int(capsys): +def test_main_getPref_invalid_field_camel(capsys): + """Test getPref() with an invalid field""" + Globals.getInstance().set_camel_case() + + class Field: + """Simple class for testing.""" + + def __init__(self, name): + """constructor""" + self.name = name + + prefs = MagicMock() + prefs.DESCRIPTOR.fields_by_name.get.return_value = None + + # Note: This is a subset of the real fields + ls_secs_field = Field('ls_secs') + is_router = Field('is_router') + fixed_position = Field('fixed_position') + + fields = [ ls_secs_field, is_router, fixed_position ] + prefs.DESCRIPTOR.fields = fields + + getPref(prefs, 'foo') + + out, err = capsys.readouterr() + assert re.search(r'does not have an attribute called foo', out, re.MULTILINE) + # ensure they are sorted + assert re.search(r'fixedPosition\s+isRouter\s+lsSecs', out, re.MULTILINE) + assert err == '' + + +@pytest.mark.unit +@pytest.mark.usefixtures("reset_globals") +def test_main_setPref_valid_field_int_as_string(capsys): """Test setPref() with a valid field""" class Field: @@ -1857,6 +1940,38 @@ def __init__(self, name): assert err == '' +@pytest.mark.unit +@pytest.mark.usefixtures("reset_globals") +def test_main_setPref_invalid_field_camel(capsys): + """Test setPref() with a invalid field""" + Globals.getInstance().set_camel_case() + + class Field: + """Simple class for testing.""" + + def __init__(self, name): + """constructor""" + self.name = name + + prefs = MagicMock() + prefs.DESCRIPTOR.fields_by_name.get.return_value = None + + # Note: This is a subset of the real fields + ls_secs_field = Field('ls_secs') + is_router = Field('is_router') + fixed_position = Field('fixed_position') + + fields = [ ls_secs_field, is_router, fixed_position ] + prefs.DESCRIPTOR.fields = fields + + setPref(prefs, 'foo', '300') + out, err = capsys.readouterr() + assert re.search(r'does not have an attribute called foo', out, re.MULTILINE) + # ensure they are sorted + assert re.search(r'fixedPosition\s+isRouter\s+lsSecs', out, re.MULTILINE) + assert err == '' + + @pytest.mark.unit @pytest.mark.usefixtures("reset_globals") def test_main_ch_set_psk_no_ch_index(capsys): From 7921db007b2f01bcb64fc44bddccdbfbac7e2b65 Mon Sep 17 00:00:00 2001 From: Mike Kinney Date: Sat, 15 Jan 2022 00:01:44 -0800 Subject: [PATCH 3/5] add some coverage to getPref() and setPref() --- meshtastic/__main__.py | 6 ++- meshtastic/tests/test_main.py | 69 +++++++++++++++++++++++++++++++++++ 2 files changed, 73 insertions(+), 2 deletions(-) diff --git a/meshtastic/__main__.py b/meshtastic/__main__.py index f71567cb..a4b15601 100644 --- a/meshtastic/__main__.py +++ b/meshtastic/__main__.py @@ -95,6 +95,8 @@ def setPref(attributes, name, valStr): snake_name = meshtastic.util.camel_to_snake(name) camel_name = meshtastic.util.snake_to_camel(name) + logging.debug(f'snake_name:{snake_name}') + logging.debug(f'camel_name:{camel_name}') objDesc = attributes.DESCRIPTOR field = objDesc.fields_by_name.get(snake_name) @@ -125,9 +127,9 @@ def setPref(attributes, name, valStr): val = e.number else: if Globals.getInstance().get_camel_case(): - print(f"{snake_name} does not have an enum called {val}, so you can not set it.") - else: print(f"{camel_name} does not have an enum called {val}, so you can not set it.") + else: + print(f"{snake_name} does not have an enum called {val}, so you can not set it.") print(f"Choices in sorted order are:") names = [] for f in enumType.values: diff --git a/meshtastic/tests/test_main.py b/meshtastic/tests/test_main.py index 4448fef7..6adba2b4 100644 --- a/meshtastic/tests/test_main.py +++ b/meshtastic/tests/test_main.py @@ -19,6 +19,7 @@ from ..node import Node from ..channel_pb2 import Channel from ..remote_hardware import onGPIOreceive +from ..radioconfig_pb2 import RadioConfig @pytest.mark.unit @@ -1908,6 +1909,74 @@ def __init__(self, name, enum_type): assert err == '' +@pytest.mark.unit +@pytest.mark.usefixtures("reset_globals") +def test_main_setPref_valid_field_invalid_enum(capsys, caplog): + """Test setPref() with a valid field but invalid enum value""" + + radioConfig = RadioConfig() + prefs = radioConfig.preferences + + with caplog.at_level(logging.DEBUG): + setPref(prefs, 'charge_current', 'foo') + out, err = capsys.readouterr() + assert re.search(r'charge_current does not have an enum called foo', out, re.MULTILINE) + assert err == '' + + +@pytest.mark.unit +@pytest.mark.usefixtures("reset_globals") +def test_main_setPref_valid_field_invalid_enum_camel(capsys, caplog): + """Test setPref() with a valid field but invalid enum value""" + Globals.getInstance().set_camel_case() + + radioConfig = RadioConfig() + prefs = radioConfig.preferences + + with caplog.at_level(logging.DEBUG): + setPref(prefs, 'charge_current', 'foo') + out, err = capsys.readouterr() + assert re.search(r'chargeCurrent does not have an enum called foo', out, re.MULTILINE) + assert err == '' + + +@pytest.mark.unit +@pytest.mark.usefixtures("reset_globals") +def test_main_setPref_valid_field_valid_enum(capsys, caplog): + """Test setPref() with a valid field and valid enum value""" + + # charge_current + # some valid values: MA100 MA1000 MA1080 + + radioConfig = RadioConfig() + prefs = radioConfig.preferences + + with caplog.at_level(logging.DEBUG): + setPref(prefs, 'charge_current', 'MA100') + out, err = capsys.readouterr() + assert re.search(r'Set charge_current to MA100', out, re.MULTILINE) + assert err == '' + + +@pytest.mark.unit +@pytest.mark.usefixtures("reset_globals") +def test_main_setPref_valid_field_valid_enum_camel(capsys, caplog): + """Test setPref() with a valid field and valid enum value""" + Globals.getInstance().set_camel_case() + + # charge_current + # some valid values: MA100 MA1000 MA1080 + + radioConfig = RadioConfig() + prefs = radioConfig.preferences + + with caplog.at_level(logging.DEBUG): + setPref(prefs, 'charge_current', 'MA100') + out, err = capsys.readouterr() + assert re.search(r'Set chargeCurrent to MA100', out, re.MULTILINE) + assert err == '' + + @pytest.mark.unit @pytest.mark.usefixtures("reset_globals") def test_main_setPref_invalid_field(capsys): From ae9ae91af56d010201e27e81685c3abb2424f25b Mon Sep 17 00:00:00 2001 From: Mike Kinney Date: Sat, 15 Jan 2022 00:24:41 -0800 Subject: [PATCH 4/5] remove dead code --- meshtastic/__main__.py | 22 +++++----------------- 1 file changed, 5 insertions(+), 17 deletions(-) diff --git a/meshtastic/__main__.py b/meshtastic/__main__.py index a4b15601..042b3aae 100644 --- a/meshtastic/__main__.py +++ b/meshtastic/__main__.py @@ -141,24 +141,12 @@ def setPref(attributes, name, valStr): print(f" {temp_name}") return - # okay - try to read the value - try: - try: - setattr(attributes, snake_name, val) - except TypeError: - # The setter didn't like our arg type guess try again as a string - setattr(attributes, snake_name, valStr) + setattr(attributes, snake_name, val) - # succeeded! - if Globals.getInstance().get_camel_case(): - print(f"Set {camel_name} to {valStr}") - else: - print(f"Set {snake_name} to {valStr}") - except Exception as ex: - if Globals.getInstance().get_camel_case(): - print(f"Can't set {camel_name} due to {ex}") - else: - print(f"Can't set {snake_name} due to {ex}") + if Globals.getInstance().get_camel_case(): + print(f"Set {camel_name} to {valStr}") + else: + print(f"Set {snake_name} to {valStr}") def onConnected(interface): From db09b4718dcbc0ff54e7db9c2a84d22a8a527ff6 Mon Sep 17 00:00:00 2001 From: Mike Kinney Date: Sat, 15 Jan 2022 10:21:24 -0800 Subject: [PATCH 5/5] add two more lines to code coverage --- meshtastic/tests/test_main.py | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/meshtastic/tests/test_main.py b/meshtastic/tests/test_main.py index 6adba2b4..4521a727 100644 --- a/meshtastic/tests/test_main.py +++ b/meshtastic/tests/test_main.py @@ -367,6 +367,27 @@ def test_main_qr(capsys): mo.assert_called() +@pytest.mark.unit +@pytest.mark.usefixtures("reset_globals") +def test_main_onConnected_exception(capsys): + """Test the exception in onConnected""" + sys.argv = ['', '--qr'] + Globals.getInstance().set_args(sys.argv) + + def throw_an_exception(junk): + raise Exception("Fake exception.") + + iface = MagicMock(autospec=SerialInterface) + with patch('meshtastic.serial_interface.SerialInterface', return_value=iface): + with patch('pyqrcode.create', side_effect=throw_an_exception): + with pytest.raises(Exception) as pytest_wrapped_e: + main() + out, err = capsys.readouterr() + assert re.search('Aborting due to: Fake exception', out, re.MULTILINE) + assert err == '' + assert pytest_wrapped_e.type == Exception + + @pytest.mark.unit @pytest.mark.usefixtures("reset_globals") def test_main_nodes(capsys):