allow edit-config to fallback to new config if old doesn't exist

This commit is contained in:
Brian S. Stephan 2023-07-12 00:08:55 -05:00
parent 6a147aa1d8
commit 8c5bd4397f
Signed by: bss
GPG Key ID: 3DE06D3180895FCB
3 changed files with 114 additions and 22 deletions

View File

@ -17,7 +17,9 @@ from textual.widgets.tree import TreeNode
from gp2040ce_bintools import core_parser, handler
from gp2040ce_bintools.builder import write_new_config_to_filename, write_new_config_to_usb
from gp2040ce_bintools.storage import get_config_from_file, get_config_from_usb
from gp2040ce_bintools.pico import get_bootsel_endpoints, read
from gp2040ce_bintools.storage import (STORAGE_MEMORY_ADDRESS, STORAGE_SIZE, ConfigReadError, get_config,
get_config_from_file, get_new_config)
logger = logging.getLogger(__name__)
@ -130,27 +132,28 @@ class ConfigEditor(App):
def __init__(self, *args, **kwargs):
"""Initialize config."""
self.config_filename = kwargs.pop('config_filename', None)
self.whole_board = kwargs.pop('whole_board', False)
self.usb = kwargs.pop('usb', False)
# load the config
if self.usb:
self.config, self.endpoint_out, self.endpoint_in = get_config_from_usb()
self.source_name = (f"DEVICE ID {hex(self.endpoint_out.device.idVendor)}:"
f"{hex(self.endpoint_out.device.idProduct)} "
f"on bus {self.endpoint_out.device.bus} address {self.endpoint_out.device.address}")
else:
self.config = get_config_from_file(self.config_filename, whole_board=self.whole_board)
self.source_name = self.config_filename
super().__init__(*args, **kwargs)
# disable normal logging and enable console logging if we're not headless
# disable normal logging and enable console logging
logger.debug("reconfiguring logging...")
root = logging.getLogger()
root.setLevel(logging.DEBUG)
root.removeHandler(handler)
root.addHandler(TextualHandler())
self.config_filename = kwargs.pop('config_filename', None)
self.usb = kwargs.pop('usb', False)
self.whole_board = kwargs.pop('whole_board', False)
self.create_new = kwargs.pop('create_new', False)
super().__init__(*args, **kwargs)
self._load_config()
if self.usb:
self.source_name = (f"DEVICE ID {hex(self.endpoint_out.device.idVendor)}:"
f"{hex(self.endpoint_out.device.idProduct)} "
f"on bus {self.endpoint_out.device.bus} address {self.endpoint_out.device.address}")
else:
self.source_name = self.config_filename
def compose(self) -> ComposeResult:
"""Compose the UI."""
yield Header()
@ -312,6 +315,35 @@ class ConfigEditor(App):
logger.debug("opening edit screen for %s", field_descriptor.name)
self.push_screen(EditScreen(node, field_value))
def _load_config(self):
"""Based on how this was initialized, get the config in a variety of ways."""
if self.usb:
try:
self.endpoint_out, self.endpoint_in = get_bootsel_endpoints()
config_binary = read(self.endpoint_out, self.endpoint_in, STORAGE_MEMORY_ADDRESS, STORAGE_SIZE)
self.config = get_config(bytes(config_binary))
except ConfigReadError:
if self.create_new:
logger.warning("creating new config as the read one was invalid!")
self.config = get_new_config()
else:
raise
else:
try:
self.config = get_config_from_file(self.config_filename, whole_board=self.whole_board)
except FileNotFoundError:
if self.create_new:
logger.warning("creating new config as the read one was invalid!")
self.config = get_new_config()
else:
raise
except ConfigReadError:
if self.create_new:
logger.warning("creating new config as the read one was invalid!")
self.config = get_new_config()
else:
raise
def pb_field_to_node_label(field_descriptor, field_value):
"""Provide the pretty label for a tree node.
@ -360,10 +392,13 @@ def edit_config():
group.add_argument('--filename', help=".bin file of a GP2040-CE board's config + footer or entire storage section, "
"or of a GP2040-CE's whole board dump if --whole-board is specified")
parser.add_argument('--whole-board', action='store_true', help="indicate the binary file is a whole board dump")
parser.add_argument('--new-if-not-found', action='store_true', default=True,
help="if the file/USB device doesn't have a config section, start a new one (default: enabled)")
args, _ = parser.parse_known_args()
if args.usb:
app = ConfigEditor(usb=True)
app = ConfigEditor(usb=True, create_new=args.new_if_not_found)
else:
app = ConfigEditor(config_filename=args.filename, whole_board=args.whole_board)
app = ConfigEditor(config_filename=args.filename, whole_board=args.whole_board,
create_new=args.new_if_not_found)
app.run()

View File

@ -24,15 +24,19 @@ FOOTER_MAGIC = b'\x65\xe3\xf1\xd2'
#################
class ConfigCrcError(ValueError):
class ConfigReadError(ValueError):
"""General exception for failing to read/verify the GP2040-CE config for some reason."""
class ConfigCrcError(ConfigReadError):
"""Exception raised when the CRC checksum in the footer doesn't match the actual content's."""
class ConfigLengthError(ValueError):
class ConfigLengthError(ConfigReadError):
"""Exception raised when a length sanity check fails."""
class ConfigMagicError(ValueError):
class ConfigMagicError(ConfigReadError):
"""Exception raised when the config section does not have the magic value in its footer."""
@ -158,6 +162,16 @@ def get_storage_section(content: bytes) -> bytes:
return content[STORAGE_BINARY_LOCATION:(STORAGE_BINARY_LOCATION + STORAGE_SIZE)]
def get_new_config() -> Message:
"""Wrap the creation of a new Config message.
Returns:
the initialized Config
"""
config_pb2 = get_config_pb2()
return config_pb2.Config()
def pad_config_to_storage_size(config: bytes) -> bytearray:
"""Provide a copy of the config (with footer) padded with zero bytes to be the proper storage section size.

View File

@ -1,13 +1,15 @@
"""Test the Textual GUI."""
import os
import sys
import unittest.mock as mock
import pytest
from decorator import decorator
from textual.widgets import Tree
from gp2040ce_bintools import get_config_pb2
from gp2040ce_bintools.gui import ConfigEditor
from gp2040ce_bintools.storage import get_config_from_file
from gp2040ce_bintools.storage import ConfigReadError, get_config, get_config_from_file
HERE = os.path.dirname(os.path.abspath(__file__))
@ -24,6 +26,47 @@ async def with_pb2s(test, *args, **kwargs):
del sys.modules['config_pb2']
@pytest.mark.asyncio
@with_pb2s
async def test_load_configs():
"""Test a variety of ways the editor may get initialized."""
test_config_filename = os.path.join(HERE, 'test-files/test-config.bin')
empty_config = get_config_pb2().Config()
with open(test_config_filename, 'rb') as file_:
test_config_binary = file_.read()
test_config = get_config(test_config_binary)
app = ConfigEditor(config_filename=os.path.join(HERE, 'test-files/test-config.bin'))
assert app.config == test_config
app = ConfigEditor(config_filename=os.path.join(HERE, 'test-files/test-config.binooooooo'), create_new=True)
assert app.config == empty_config
with pytest.raises(FileNotFoundError):
app = ConfigEditor(config_filename=os.path.join(HERE, 'test-files/test-config.binooooooo'))
app = ConfigEditor(config_filename=os.path.join(HERE, 'test-files/test-firmware.bin'), create_new=True)
assert app.config == empty_config
with pytest.raises(ConfigReadError):
app = ConfigEditor(config_filename=os.path.join(HERE, 'test-files/test-firmware.bin'))
with mock.patch('gp2040ce_bintools.gui.get_bootsel_endpoints', return_value=(mock.MagicMock(), mock.MagicMock())):
with mock.patch('gp2040ce_bintools.gui.read', return_value=b'\x00'):
with pytest.raises(ConfigReadError):
app = ConfigEditor(usb=True)
with mock.patch('gp2040ce_bintools.gui.get_bootsel_endpoints', return_value=(mock.MagicMock(), mock.MagicMock())):
with mock.patch('gp2040ce_bintools.gui.read', return_value=b'\x00'):
app = ConfigEditor(usb=True, create_new=True)
assert app.config == empty_config
with mock.patch('gp2040ce_bintools.gui.get_bootsel_endpoints', return_value=(mock.MagicMock(), mock.MagicMock())):
with mock.patch('gp2040ce_bintools.gui.read', return_value=test_config_binary):
app = ConfigEditor(usb=True)
assert app.config == test_config
@pytest.mark.asyncio
@with_pb2s
async def test_simple_tree_building():