From 276b2762c883c1ededef90b5f7bd2859453a1580 Mon Sep 17 00:00:00 2001 From: Mike Kinney Date: Thu, 23 Dec 2021 00:03:32 -0800 Subject: [PATCH 1/2] add more info/checking on --sendtext and --ch-index; wrote helper method and tests --- meshtastic/__main__.py | 19 ++++++----- meshtastic/node.py | 10 ++++++ meshtastic/tests/test_main.py | 60 ++++++++++++++++++++++++++++++++++- meshtastic/tests/test_node.py | 30 +++++++++++++++++- 4 files changed, 109 insertions(+), 10 deletions(-) diff --git a/meshtastic/__main__.py b/meshtastic/__main__.py index 6fe4da65..83911baf 100644 --- a/meshtastic/__main__.py +++ b/meshtastic/__main__.py @@ -27,6 +27,7 @@ def onReceive(packet, interface): args = our_globals.get_args() try: d = packet.get('decoded') + logging.debug(f'd:{d}') # Exit once we receive a reply if args and args.sendtext and packet["to"] == interface.myInfo.my_node_num and d["portnum"] == portnums_pb2.PortNum.TEXT_MESSAGE_APP: @@ -243,8 +244,12 @@ def getNode(): channelIndex = 0 if args.ch_index is not None: channelIndex = int(args.ch_index) - print(f"Sending text message {args.sendtext} to {args.destOrAll}") - interface.sendText(args.sendtext, args.destOrAll, wantAck=True, channelIndex=channelIndex) + ch = interface.getChannelByChannelIndex(channelIndex) + if ch and ch.role != channel_pb2.Channel.Role.DISABLED: + print(f"Sending text message {args.sendtext} to {args.destOrAll} on channelIndex:{channelIndex}") + interface.sendText(args.sendtext, args.destOrAll, wantAck=True, channelIndex=channelIndex) + else: + meshtastic.util.our_exit(f"Warning: {channelIndex} is not a valid channel. Channel must not be DISABLED.") if args.sendping: payload = str.encode("test string") @@ -565,7 +570,8 @@ def common(): our_globals = Globals.getInstance() args = our_globals.get_args() parser = our_globals.get_parser() - logging.basicConfig(level=logging.DEBUG if args.debug else logging.INFO) + logging.basicConfig(level=logging.DEBUG if args.debug else logging.INFO, + format='%(levelname)s file:%(filename)s %(funcName)s line:%(lineno)s %(message)s') if len(sys.argv) == 1: parser.print_help(sys.stderr) @@ -688,7 +694,7 @@ def initParser(): "--seturl", help="Set a channel URL", action="store") parser.add_argument( - "--ch-index", help="Set the specified channel index", action="store") + "--ch-index", help="Set the specified channel index. Channels start at 0 (0 is the PRIMARY channel).", action="store") parser.add_argument( "--ch-add", help="Add a secondary channel, you must specify a channel name", default=None) @@ -738,7 +744,7 @@ def initParser(): "--dest", help="The destination node id for any sent commands, if not set '^all' or '^local' is assumed as appropriate", default=None) parser.add_argument( - "--sendtext", help="Send a text message") + "--sendtext", help="Send a text message. Can specify a destination '--dest' and/or channel index '--ch-index'.") parser.add_argument( "--sendping", help="Send a ping message (which requests a reply)", action="store_true") @@ -746,9 +752,6 @@ def initParser(): parser.add_argument( "--reboot", help="Tell the destination node to reboot", action="store_true") - # parser.add_argument( - # "--repeat", help="Normally the send commands send only one message, use this option to request repeated sends") - parser.add_argument( "--reply", help="Reply to received messages", action="store_true") diff --git a/meshtastic/node.py b/meshtastic/node.py index 3158d577..003c9450 100644 --- a/meshtastic/node.py +++ b/meshtastic/node.py @@ -90,6 +90,16 @@ def writeChannel(self, channelIndex, adminIndex=0): self._sendAdmin(p, adminIndex=adminIndex) logging.debug(f"Wrote channel {channelIndex}") + def getChannelByChannelIndex(self, channelIndex): + """Get channel by channelIndex + channelIndex: number, typically 0-7; based on max number channels + returns: None if there is no channel found + """ + ch = None + if self.channels and 0 <= channelIndex < len(self.channels): + ch = self.channels[channelIndex] + return ch + def deleteChannel(self, channelIndex): """Delete the specifed channelIndex and shift other channels up""" ch = self.channels[channelIndex] diff --git a/meshtastic/tests/test_main.py b/meshtastic/tests/test_main.py index f8a67341..c83cbe55 100644 --- a/meshtastic/tests/test_main.py +++ b/meshtastic/tests/test_main.py @@ -418,6 +418,48 @@ def mock_sendText(text, dest, wantAck, channelIndex): mo.assert_called() +@pytest.mark.unit +def test_main_sendtext_with_channel(capsys, reset_globals): + """Test --sendtext""" + sys.argv = ['', '--sendtext', 'hello', '--ch-index', '1'] + Globals.getInstance().set_args(sys.argv) + + iface = MagicMock(autospec=SerialInterface) + def mock_sendText(text, dest, wantAck, channelIndex): + print('inside mocked sendText') + iface.sendText.side_effect = mock_sendText + + 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'Sending text message', out, re.MULTILINE) + assert re.search(r'on channelIndex:1', out, re.MULTILINE) + assert re.search(r'inside mocked sendText', out, re.MULTILINE) + assert err == '' + mo.assert_called() + + +@pytest.mark.unit +def test_main_sendtext_with_invalid_channel(capsys, reset_globals): + """Test --sendtext""" + sys.argv = ['', '--sendtext', 'hello', '--ch-index', '-1'] + Globals.getInstance().set_args(sys.argv) + + iface = MagicMock(autospec=SerialInterface) + iface.getChannelByChannelIndex.return_value = None + with patch('meshtastic.serial_interface.SerialInterface', return_value=iface) as mo: + #mo.getChannelByChannelIndex.return_value = None + with pytest.raises(SystemExit) as pytest_wrapped_e: + main() + assert pytest_wrapped_e.type == SystemExit + assert pytest_wrapped_e.value.code == 1 + out, err = capsys.readouterr() + assert re.search(r'is not a valid channel', out, re.MULTILINE) + assert err == '' + mo.assert_called() + + @pytest.mark.unit def test_main_sendtext_with_dest(capsys, reset_globals): """Test --sendtext with --dest""" @@ -1201,7 +1243,7 @@ def getName(self): @pytest.mark.unit def test_main_export_config(reset_globals, capsys): - """Test export_config""" + """Test export_config() function directly""" iface = MagicMock(autospec=SerialInterface) with patch('meshtastic.serial_interface.SerialInterface', return_value=iface) as mo: mo.getLongName.return_value = 'foo' @@ -1229,3 +1271,19 @@ def test_main_export_config(reset_globals, capsys): assert re.search(r"fixed_position: 'true'", out, re.MULTILINE) assert re.search(r"position_flags: 35", out, re.MULTILINE) assert err == '' + + +@pytest.mark.unit +def test_main_export_config_called_from_main(capsys, reset_globals): + """Test --export-config""" + sys.argv = ['', '--export-config'] + Globals.getInstance().set_args(sys.argv) + + iface = MagicMock(autospec=SerialInterface) + 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'# start of Meshtastic configure yaml', out, re.MULTILINE) + assert err == '' + mo.assert_called() diff --git a/meshtastic/tests/test_node.py b/meshtastic/tests/test_node.py index e9b2ff8d..02e0f194 100644 --- a/meshtastic/tests/test_node.py +++ b/meshtastic/tests/test_node.py @@ -186,6 +186,35 @@ def test_showChannels(capsys): assert err == '' +@pytest.mark.unit +def test_getChannelByChannelIndex(): + """Test getChannelByChannelIndex()""" + anode = Node('foo', 'bar') + + channel1 = Channel(index=1, role=1) # primary channel + channel2 = Channel(index=2, role=2) # secondary channel + channel3 = Channel(index=3, role=0) + channel4 = Channel(index=4, role=0) + channel5 = Channel(index=5, role=0) + channel6 = Channel(index=6, role=0) + channel7 = Channel(index=7, role=0) + channel8 = Channel(index=8, role=0) + + channels = [ channel1, channel2, channel3, channel4, channel5, channel6, channel7, channel8 ] + + anode.channels = channels + + # test primary + assert anode.getChannelByChannelIndex(0) is not None + # test secondary + assert anode.getChannelByChannelIndex(1) is not None + # test disabled + assert anode.getChannelByChannelIndex(2) is not None + # test invalid values + assert anode.getChannelByChannelIndex(-1) is None + assert anode.getChannelByChannelIndex(9) is None + + @pytest.mark.unit def test_deleteChannel_try_to_delete_primary_channel(capsys): """Try to delete primary channel.""" @@ -215,7 +244,6 @@ def test_deleteChannel_try_to_delete_primary_channel(capsys): assert re.search(r'Warning: Only SECONDARY channels can be deleted', out, re.MULTILINE) assert err == '' - @pytest.mark.unit def test_deleteChannel_secondary(): """Try to delete a secondary channel.""" From cbd41efb19617239cb5124c43f21cfc057d0e1b9 Mon Sep 17 00:00:00 2001 From: Mike Kinney Date: Thu, 23 Dec 2021 00:40:21 -0800 Subject: [PATCH 2/2] add unit test for catchAndIgnore() --- meshtastic/tests/test_stream_interface.py | 2 +- meshtastic/tests/test_util.py | 17 +++++++++++++++-- meshtastic/util.py | 2 +- 3 files changed, 17 insertions(+), 4 deletions(-) diff --git a/meshtastic/tests/test_stream_interface.py b/meshtastic/tests/test_stream_interface.py index 57c65d22..a3e400c2 100644 --- a/meshtastic/tests/test_stream_interface.py +++ b/meshtastic/tests/test_stream_interface.py @@ -8,7 +8,7 @@ @pytest.mark.unit def test_StreamInterface(): - """Test that we can instantiate a StreamInterface""" + """Test that we cannot instantiate a StreamInterface""" with pytest.raises(Exception) as pytest_wrapped_e: StreamInterface(noProto=True) assert pytest_wrapped_e.type == Exception diff --git a/meshtastic/tests/test_util.py b/meshtastic/tests/test_util.py index b3c6d397..6b9e94b2 100644 --- a/meshtastic/tests/test_util.py +++ b/meshtastic/tests/test_util.py @@ -1,10 +1,13 @@ """Meshtastic unit tests for util.py""" import re +import logging import pytest -from meshtastic.util import fixme, stripnl, pskToString, our_exit, support_info, genPSK256, fromStr, fromPSK, quoteBooleans +from meshtastic.util import (fixme, stripnl, pskToString, our_exit, + support_info, genPSK256, fromStr, fromPSK, + quoteBooleans, catchAndIgnore) @pytest.mark.unit @@ -120,7 +123,7 @@ def test_our_exit_non_zero_return_value(): @pytest.mark.unit def test_fixme(): - """Test fixme""" + """Test fixme()""" with pytest.raises(Exception) as pytest_wrapped_e: fixme("some exception") assert pytest_wrapped_e.type == Exception @@ -136,3 +139,13 @@ def test_support_info(capsys): assert re.search(r'Machine', out, re.MULTILINE) assert re.search(r'Executable', out, re.MULTILINE) assert err == '' + + +@pytest.mark.unit +def test_catchAndIgnore(caplog): + """Test catchAndIgnore() does not actually throw an exception, but just logs""" + def some_closure(): + raise Exception('foo') + with caplog.at_level(logging.DEBUG): + catchAndIgnore("something", some_closure) + assert re.search(r'Exception thrown in something', caplog.text, re.MULTILINE) diff --git a/meshtastic/util.py b/meshtastic/util.py index b4376f92..9f9cba83 100644 --- a/meshtastic/util.py +++ b/meshtastic/util.py @@ -102,7 +102,7 @@ def fixme(message): def catchAndIgnore(reason, closure): - """Call a closure but if it throws an excpetion print it and continue""" + """Call a closure but if it throws an exception print it and continue""" try: closure() except BaseException as ex: