gp2040ce-binary-tools/tests/test_storage.py
Brian S. Stephan b3f6f86950
ship precompiled protobuf files for convenience
current libraries seem to have problems on Windows with thinking files
are duplicated when they are not, making it impossible to compile .proto
files at runtime in this tool on that platform. this adds a fallback of
using shipped, precompiled files in the package. I was already intending
on providing this as an option anyway, so might as well start doing it
now.

Signed-off-by: Brian S. Stephan <bss@incorporeal.org>
2024-01-09 09:40:07 -06:00

211 lines
8.1 KiB
Python

"""Unit tests for the storage module.
SPDX-FileCopyrightText: © 2023 Brian S. Stephan <bss@incorporeal.org>
SPDX-License-Identifier: MIT
"""
import os
import sys
import unittest.mock as mock
import pytest
from decorator import decorator
import gp2040ce_bintools.storage as storage
HERE = os.path.dirname(os.path.abspath(__file__))
@decorator
def with_pb2s(test, *args, **kwargs):
"""Wrap a test with precompiled pb2 files on the path."""
proto_path = os.path.join(HERE, 'test-files', 'pb2-files')
sys.path.append(proto_path)
test(*args, **kwargs)
sys.path.pop()
del sys.modules['config_pb2']
def test_config_footer(storage_dump):
"""Test that a config footer is identified as expected."""
size, crc, magic = storage.get_config_footer(storage_dump)
assert size == 3309
assert crc == 2661279683
assert magic == '0x65e3f1d2'
def test_config_footer_way_too_small(storage_dump):
"""Test that a config footer isn't detected if the size is way too small."""
with pytest.raises(storage.ConfigLengthError):
_, _, _ = storage.get_config_footer(storage_dump[-11:])
def test_config_footer_too_small(storage_dump):
"""Test that a config footer isn't detected if the size is smaller than that found in the header."""
with pytest.raises(storage.ConfigLengthError):
_, _, _ = storage.get_config_footer(storage_dump[-1000:])
def test_whole_board_too_small(whole_board_dump):
"""Test that a storage section isn't detected if the size is too small to contain where it should be."""
with pytest.raises(storage.ConfigLengthError):
_, _, _ = storage.get_user_storage_section(whole_board_dump[-100000:])
def test_config_footer_bad_magic(storage_dump):
"""Test that a config footer isn't detected if the magic is incorrect."""
unmagical = bytearray(storage_dump)
unmagical[-1] = 0
with pytest.raises(storage.ConfigMagicError):
_, _, _ = storage.get_config_footer(unmagical)
def test_config_footer_bad_crc(storage_dump):
"""Test that a config footer isn't detected if the CRC checksums don't match."""
corrupt = bytearray(storage_dump)
corrupt[-50:-40] = bytearray(0 * 10)
with pytest.raises(storage.ConfigCrcError):
_, _, _ = storage.get_config_footer(corrupt)
@with_pb2s
def test_get_config_from_file_storage_dump():
"""Test that we can open a storage dump file and find its config."""
filename = os.path.join(HERE, 'test-files', 'test-storage-area.bin')
config = storage.get_config_from_file(filename)
assert config.boardVersion == 'v0.7.5'
assert config.addonOptions.bootselButtonOptions.enabled is False
assert config.addonOptions.ps4Options.enabled is False
@with_pb2s
def test_get_config_from_file_whole_board_dump():
"""Test that we can open a storage dump file and find its config."""
filename = os.path.join(HERE, 'test-files', 'test-whole-board.bin')
config = storage.get_config_from_file(filename, whole_board=True)
assert config.boardVersion == 'v0.7.5'
assert config.addonOptions.bootselButtonOptions.enabled is False
@with_pb2s
def test_get_board_config_from_file_whole_board_dump():
"""Test that we can open a storage dump file and find its config."""
filename = os.path.join(HERE, 'test-files', 'test-whole-board-with-board-config.bin')
config = storage.get_config_from_file(filename, whole_board=True, board_config=True)
assert config.boardVersion == 'v0.7.6-15-g71f4512'
assert config.addonOptions.bootselButtonOptions.enabled is False
@with_pb2s
def test_get_config_from_file_file_not_fonud_ok():
"""If we allow opening a file that doesn't exist (e.g. for the editor), check we get an empty config."""
filename = os.path.join(HERE, 'test-files', 'nope.bin')
config = storage.get_config_from_file(filename, allow_no_file=True)
assert config.boardVersion == ''
def test_get_config_from_file_file_not_fonud_raise():
"""If we don't allow opening a file that doesn't exist (e.g. for the editor), check we get an error."""
filename = os.path.join(HERE, 'test-files', 'nope.bin')
with pytest.raises(FileNotFoundError):
_ = storage.get_config_from_file(filename)
@with_pb2s
def test_config_parses(storage_dump):
"""Test that we need the config_pb2 to exist/be compiled for reading the config to work."""
config = storage.get_config(storage_dump)
assert config.boardVersion == 'v0.7.5'
assert config.hotkeyOptions.hotkey01.dpadMask == 0
assert config.hotkeyOptions.hotkey02.dpadMask == 1
@with_pb2s
def test_config_from_whole_board_parses(whole_board_dump):
"""Test that we can read in a whole board and still find the config section."""
config = storage.get_config(storage.get_user_storage_section(whole_board_dump))
assert config.boardVersion == 'v0.7.5'
assert config.hotkeyOptions.hotkey01.dpadMask == 0
assert config.hotkeyOptions.hotkey02.dpadMask == 1
@with_pb2s
def test_serialize_config_with_footer(storage_dump, config_binary):
"""Test that reserializing a read in config matches the original.
Note that this isn't going to produce an *identical* result, because new message fields
may have default values that get saved in the reserialized binary, so we can still only test
some particular parts. But it should work.
"""
config = storage.get_config(storage_dump)
assert config.boardVersion == 'v0.7.5'
reserialized = storage.serialize_config_with_footer(config)
assert storage_dump[-4:] == reserialized[-4:]
@with_pb2s
def test_serialize_modified_config_with_footer(storage_dump):
"""Test that we can serialize a modified config."""
config = storage.get_config(storage_dump)
config.boardVersion = 'v0.7.5-cool'
serialized = storage.serialize_config_with_footer(config)
config_size, _, _ = storage.get_config_footer(serialized)
assert config_size == config.ByteSize()
assert len(serialized) == config_size + 12
def test_pad_config_to_storage(config_binary):
"""Test that we can properly pad a config section to the correct storage section size."""
storage_section = storage.pad_config_to_storage_size(config_binary)
assert len(storage_section) == 16384
def test_pad_config_to_storage_raises(config_binary):
"""Test that we raise an exception if the config is bigger than the storage section."""
with pytest.raises(storage.ConfigLengthError):
_ = storage.pad_config_to_storage_size(config_binary * 5)
@with_pb2s
def test_get_board_config_from_usb(config_binary):
"""Test we attempt to read from the proper location over USB."""
mock_out = mock.MagicMock()
mock_out.device.idVendor = 0xbeef
mock_out.device.idProduct = 0xcafe
mock_out.device.bus = 1
mock_out.device.address = 2
mock_in = mock.MagicMock()
with mock.patch('gp2040ce_bintools.storage.get_bootsel_endpoints', return_value=(mock_out, mock_in)) as mock_get:
with mock.patch('gp2040ce_bintools.storage.read', return_value=config_binary) as mock_read:
config, _, _ = storage.get_board_config_from_usb()
mock_get.assert_called_once()
mock_read.assert_called_with(mock_out, mock_in, 0x101F8000, 16384)
assert config == storage.get_config(config_binary)
@with_pb2s
def test_get_user_config_from_usb(config_binary):
"""Test we attempt to read from the proper location over USB."""
mock_out = mock.MagicMock()
mock_out.device.idVendor = 0xbeef
mock_out.device.idProduct = 0xcafe
mock_out.device.bus = 1
mock_out.device.address = 2
mock_in = mock.MagicMock()
with mock.patch('gp2040ce_bintools.storage.get_bootsel_endpoints', return_value=(mock_out, mock_in)) as mock_get:
with mock.patch('gp2040ce_bintools.storage.read', return_value=config_binary) as mock_read:
config, _, _ = storage.get_user_config_from_usb()
mock_get.assert_called_once()
mock_read.assert_called_with(mock_out, mock_in, 0x101FC000, 16384)
assert config == storage.get_config(config_binary)
@with_pb2s
def test_json_config_parses(config_json):
"""Test that we can import a JSON config into a message."""
config = storage.get_config_from_json(config_json)
assert config.boardVersion == 'v0.7.6-15-g71f4512'