Files
contact/tests/test_ham_settings.py
2026-09-09 22:50:02 -07:00

147 lines
8.4 KiB
Python

from contextlib import ExitStack
from types import SimpleNamespace
import unittest
from unittest.mock import Mock, patch
from meshtastic.protobuf import admin_pb2, config_pb2, module_config_pb2
from contact.ui import control_ui
from contact.ui.menus import generate_menu_from_protobuf, update_ham_fields
from contact.utilities.save_to_radio import save_changes
from tests.test_support import reset_singletons
class HamSettingsTests(unittest.TestCase):
def build_interface(self, licensed=False):
node = Mock(nodeNum=2, localConfig=config_pb2.Config(), moduleConfig=module_config_pb2.ModuleConfig())
node.localConfig.lora.tx_power = 20
node.localConfig.lora.override_frequency = 915.5
node.getChannelByChannelIndex.return_value = None
info = {'user': {'longName': 'KD2ABC', 'shortName': 'HAM', 'isLicensed': licensed}}
interface = SimpleNamespace(localNode=node, getMyNodeInfo=lambda: info, nodesByNum={2: info})
node.iface = interface
return interface, node
def test_fields_follow_license_selection_and_installed_descriptors(self):
interface, node = self.build_interface()
user = generate_menu_from_protobuf(interface)['Main Menu']['User Settings']
descriptors = admin_pb2.HamParameters.DESCRIPTOR.fields
self.assertTrue(all(field.name not in user for field in descriptors))
user['isLicensed'] = (None, True)
update_ham_fields(user, node)
for field in descriptors:
self.assertIs(user[field.name][0], field)
self.assertEqual(user['call_sign'][1], 'KD2ABC')
self.assertEqual(user['tx_power'][1], 20)
self.assertEqual(user['frequency'][1], 915.5)
self.assertNotIn('longName', user)
user['frequency'] = (user['frequency'][0], 916.0)
update_ham_fields(user, node)
self.assertEqual(user['frequency'][1], 916.0)
user['isLicensed'] = (None, False)
update_ham_fields(user, node)
self.assertTrue(all(field.name not in user for field in descriptors))
self.assertEqual(user['longName'][1], 'KD2ABC')
self.assertEqual(user['shortName'][1], 'HAM')
def test_remote_menu_uses_remote_license_and_radio_values(self):
interface, node = self.build_interface(True)
interface.localNode = Mock()
user = generate_menu_from_protobuf(interface, node=node)['Main Menu']['User Settings']
self.assertEqual(user['frequency'][1], 915.5)
self.assertEqual(user['call_sign'][1], 'KD2ABC')
def test_save_full_ham_message_on_selected_node(self):
for remote in (False, True):
interface, node = self.build_interface(True)
if remote:
interface.localNode = Mock()
user = generate_menu_from_protobuf(interface, node=node)['Main Menu']['User Settings']
changes = {name: value for name, (_, value) in user.items()}
changes['frequency'] = 916.0
state = SimpleNamespace(menu_path=['Main Menu', 'User Settings'])
self.assertTrue(save_changes(interface, changes, state, node=node))
message = node._sendAdmin.call_args.args[0]
self.assertEqual(message.WhichOneof('payload_variant'), 'set_ham_mode')
self.assertEqual(message.set_ham_mode.call_sign, 'KD2ABC')
self.assertEqual(message.set_ham_mode.short_name, 'HAM')
self.assertEqual(message.set_ham_mode.tx_power, 20)
self.assertEqual(message.set_ham_mode.frequency, 916.0)
self.assertEqual(node._sendAdmin.call_args.kwargs['onResponse'], node.onAckNak if remote else None)
self.assertEqual([c[0] for c in node.method_calls], [
*['getChannelByChannelIndex'] * 8, 'ensureSessionKey', 'beginSettingsTransaction',
'setOwner', '_sendAdmin', 'commitSettingsTransaction'])
def test_disabling_license_does_not_send_ham_command(self):
interface, node = self.build_interface(True)
user = generate_menu_from_protobuf(interface)['Main Menu']['User Settings']
user['isLicensed'] = (None, False)
update_ham_fields(user, node)
state = SimpleNamespace(menu_path=['Main Menu', 'User Settings'])
self.assertTrue(save_changes(interface, {k: v[1] for k, v in user.items()}, state, node=node))
node._sendAdmin.assert_not_called()
node.setOwner.assert_called_once_with('KD2ABC', 'HAM', False, False)
def test_edit_through_normal_menu_after_selecting_licensed(self):
reset_singletons()
interface, node = self.build_interface()
win = Mock()
# Open User Settings, select licensed, edit the now-visible callsign, save.
win.getch.side_effect = [10, control_ui.curses.KEY_DOWN, control_ui.curses.KEY_DOWN,
10, control_ui.curses.KEY_DOWN, control_ui.curses.KEY_DOWN, 10, 9, 10, 27]
saved = {}
def capture(_interface, values, _state, **kwargs):
saved.update(values)
return False
try:
with ExitStack() as stack:
stack.enter_context(patch.object(control_ui.curses, 'update_lines_cols'))
stack.enter_context(patch.object(control_ui, 'display_menu', return_value=(win, Mock())))
stack.enter_context(patch.object(control_ui, 'move_highlight'))
stack.enter_context(patch.object(control_ui, 'get_list_input', return_value='True'))
text = stack.enter_context(patch.object(control_ui, 'get_text_input', return_value='KD2XYZ'))
stack.enter_context(patch.object(control_ui, 'save_changes', side_effect=capture))
control_ui.settings_menu(Mock(), interface)
self.assertEqual(text.call_args.args[1], 'call_sign')
self.assertEqual(saved['call_sign'], 'KD2XYZ')
self.assertTrue(saved['isLicensed'])
self.assertEqual(saved['tx_power'], 20)
finally:
reset_singletons()
def test_new_library_fields_are_discovered_edited_and_saved(self):
from google.protobuf import descriptor_pb2, descriptor_pool, message_factory
schema = descriptor_pb2.FileDescriptorProto(name='future_ham.proto', syntax='proto3')
params = schema.message_type.add(name='HamParameters')
for field in admin_pb2.HamParameters.DESCRIPTOR.fields:
params.field.add(name=field.name, number=field.number, type=field.type)
if 'long_name' not in admin_pb2.HamParameters.DESCRIPTOR.fields_by_name:
params.field.add(name='long_name', number=5, type=9)
params.field.add(name='future_option', number=100, type=8)
message = schema.message_type.add(name='AdminMessage')
message.field.add(name='set_ham_mode', number=18, type=11, type_name='.HamParameters')
pool = descriptor_pool.DescriptorPool()
pool.Add(schema)
future_params = message_factory.GetMessageClass(pool.FindMessageTypeByName('HamParameters'))
future_message = message_factory.GetMessageClass(pool.FindMessageTypeByName('AdminMessage'))
interface, node = self.build_interface(True)
with patch.object(admin_pb2, 'HamParameters', future_params), patch.object(admin_pb2, 'AdminMessage', future_message):
user = generate_menu_from_protobuf(interface)['Main Menu']['User Settings']
self.assertIn('long_name', user)
self.assertEqual(user['future_option'][1], False)
changes = {name: setting[1] for name, setting in user.items()}
changes.update(long_name='Alice', future_option=True)
state = SimpleNamespace(menu_path=['Main Menu', 'User Settings'])
self.assertTrue(save_changes(interface, changes, state, node=node))
sent = node._sendAdmin.call_args.args[0]
parsed = future_message.FromString(sent.SerializeToString())
self.assertEqual(parsed.set_ham_mode.long_name, 'Alice')
self.assertTrue(parsed.set_ham_mode.future_option)
def test_remote_user_settings_fetch_lora_before_showing_ham_defaults(self):
_, node = self.build_interface(True)
with patch.object(control_ui, '_request_remote_with_timeout') as request:
self.assertTrue(control_ui._request_remote_section(node, ['Main Menu'], 'User Settings'))
request.assert_called_once_with(node.requestConfig,
node.localConfig.DESCRIPTOR.fields_by_name['lora'],
cancel_callback=None)