From fa111d8d4c367da403d1f67dfb7c888c68fe6920 Mon Sep 17 00:00:00 2001 From: kdmukai <934746+kdmukai@users.noreply.github.com> Date: Sun, 28 Dec 2025 07:56:53 -0600 Subject: [PATCH 1/2] Improvements for SettingsQR Generator --- src/seedsigner/models/settings_definition.py | 51 +++++++++----------- 1 file changed, 24 insertions(+), 27 deletions(-) diff --git a/src/seedsigner/models/settings_definition.py b/src/seedsigner/models/settings_definition.py index e9310d1d..8bf534e3 100644 --- a/src/seedsigner/models/settings_definition.py +++ b/src/seedsigner/models/settings_definition.py @@ -1,4 +1,5 @@ import os +import pathlib from dataclasses import dataclass from typing import Any, List @@ -182,6 +183,7 @@ class SettingsConstants: LOCALE__VIETNAMESE: "Tiếng Việt (Vietnamese)", } + @classmethod def get_detected_languages(cls) -> list[tuple[str, str]]: """ @@ -189,22 +191,20 @@ class SettingsConstants: Scans the filesystem to autodiscover which language codes are onboard. """ - # Will normally be the launch dir (where main.py is located)... - cwd = os.getcwd() - - # ...except when running the tests which happens one dir higher - if "src" not in cwd: - cwd = os.path.join(cwd, "src") + # Back out from the models/ dir to reach the seedsigner root + models_dir = pathlib.Path(__file__).parent.resolve() + seedsigner_root = models_dir.parent.resolve() # Pre-load English since there's no "en" entry in the translations folder; also # it should always appear first in the list anyway. detected_languages = [(cls.LOCALE__ENGLISH, cls.ALL_LOCALES[cls.LOCALE__ENGLISH])] locales_present = set() - for root, dirs, files in os.walk(os.path.join(cwd, "seedsigner", "resources", "seedsigner-translations", "l10n")): + for root, dirs, files in os.walk(os.path.join(seedsigner_root, "resources", "seedsigner-translations", "l10n")): for file in [f for f in files if f.endswith(".mo")]: # `root` will be [...]seedsigner/resources/seedsigner-translations/l10n/pt_BR/LC_MESSAGES - locales_present.add(root.split(f"l10n{ os.sep }")[1].split(os.sep)[0]) + # Isolate the language code from the path + locales_present.add(root.rsplit(os.sep, 2)[-2]) for locale in cls.ALL_LOCALES.keys(): if locale in locales_present: @@ -451,6 +451,9 @@ class SettingsEntry: elif type(self.default_value) == tuple: self.default_value = self.default_value[0] + if not self.abbreviated_name: + self.abbreviated_name = self.attr_name + @property def selection_options_display_names(self) -> List[str]: @@ -774,15 +777,25 @@ class SettingsDefinition: @classmethod - def get_defaults(cls) -> dict: + def get_defaults(cls, use_abbreviated_name: bool = False, skip_hidden: bool = False) -> dict: + """ + * use_abbreviated_name: Only used by the test suite. + * skip_hidden: Only used by the test suite. + """ as_dict = {} for entry in SettingsDefinition.settings_entries: + if skip_hidden and entry.visibility == SettingsConstants.VISIBILITY__HIDDEN: + continue + if not use_abbreviated_name: + attr_name = entry.attr_name + else: + attr_name = entry.abbreviated_name if type(entry.default_value) == list: # Must copy the default_value list, otherwise we'll inadvertently change # defaults when updating these attrs - as_dict[entry.attr_name] = list(entry.default_value) + as_dict[attr_name] = list(entry.default_value) else: - as_dict[entry.attr_name] = entry.default_value + as_dict[attr_name] = entry.default_value return as_dict @@ -795,19 +808,3 @@ class SettingsDefinition: output["settings_entries"].append(settings_entry.to_dict()) return output - - - -if __name__ == "__main__": - import json - import os - - hostname = os.uname()[1] - - if hostname == "seedsigner-os": - output_file = "/mnt/microsd/settings_definition.json" - else: - output_file = "settings_definition.json" - - with open(output_file, 'w') as json_file: - json.dump(SettingsDefinition.to_dict(), json_file, indent=4) From e9b0809485e81009744033f9555d818df7a64607 Mon Sep 17 00:00:00 2001 From: kdmukai <934746+kdmukai@users.noreply.github.com> Date: Sun, 28 Dec 2025 07:57:25 -0600 Subject: [PATCH 2/2] Improve tests, maintainability --- tests/test_settings.py | 53 ++++++++++++++++++++++++-------- tests/test_settingsqr_decoder.py | 6 ++-- 2 files changed, 44 insertions(+), 15 deletions(-) diff --git a/tests/test_settings.py b/tests/test_settings.py index f29cd7fb..72a7ab5c 100644 --- a/tests/test_settings.py +++ b/tests/test_settings.py @@ -1,6 +1,8 @@ import json import pytest + from base import BaseTest + from seedsigner.models.settings import InvalidSettingsQRData, Settings from seedsigner.models.settings_definition import SettingsConstants, SettingsDefinition @@ -98,14 +100,35 @@ class TestSettings(BaseTest): _verify_defaults_loaded(SettingsConstants.SETTING__SIG_TYPES) + +class SettingsQRBase(BaseTest): + """ + Reusable base test class for testing SettingsQR data. + """ + def setup_method(self): + super().setup_method() + settingsqr_dict = SettingsDefinition.get_defaults(use_abbreviated_name=True, skip_hidden=True) + + # Build baseline SettingsQR config based on defaults + self.settingsqr_prefix = "settings::v1" + self.settingsqr_default_attrs_str = "" + for abbreviated_attr_name, value in settingsqr_dict.items(): + if isinstance(value, list): + value_str = ",".join(value) + else: + value_str = str(value) + self.settingsqr_default_attrs_str += f" {abbreviated_attr_name}={value_str}" + + + +class TestSettingsQRParser(SettingsQRBase): def test_parse_settingsqr_data(self): """ SettingsQR parser should successfully parse a valid settingsqr input string and return the resulting config_name and formatted settings_update_dict. """ settings_name = "Test SettingsQR" - settingsqr_data = f"""settings::v1 name={ settings_name.replace(" ", "_") } persistent=D denom=thr network=M qr_density=M sigs=ss,ms scripts=nat,nes,tr xpub_qr=urca,sta xpub_details=E passphrase=E camera=180 compact_seedqr=E bip85=D priv_warn=E dire_warn=E partners=E""" - + settingsqr_data = f"""{self.settingsqr_prefix} name={ settings_name.replace(" ", "_") } {self.settingsqr_default_attrs_str}""" # First explicitly set settings that differ from the settingsqr_data self.settings.set_value(SettingsConstants.SETTING__COMPACT_SEEDQR, SettingsConstants.OPTION__DISABLED) @@ -129,35 +152,37 @@ class TestSettings(BaseTest): def test_settingsqr_version(self): """ SettingsQR parser should accept SettingsQR v1 and reject any others """ - settingsqr_data = "settings::v1 name=Foo" + settingsqr_data = f"{self.settingsqr_prefix} {self.settingsqr_default_attrs_str}" config_name, settings_update_dict = Settings.parse_settingsqr(settingsqr_data) # Accepts update with no Exceptions self.settings.update(new_settings=settings_update_dict) - settingsqr_data = "settings::v2 name=Foo" + settingsqr_data = f"settings::v2 {self.settingsqr_default_attrs_str}" with pytest.raises(InvalidSettingsQRData) as e: Settings.parse_settingsqr(settingsqr_data) assert "Unsupported SettingsQR version" in str(e.value) # Should also fail if version omitted - settingsqr_data = "settings name=Foo" + settingsqr_data = f"settings {self.settingsqr_default_attrs_str}" with pytest.raises(InvalidSettingsQRData) as e: Settings.parse_settingsqr(settingsqr_data) # And if "settings" is omitted entirely - settingsqr_data = "name=Foo" + settingsqr_data = self.settingsqr_default_attrs_str with pytest.raises(InvalidSettingsQRData) as e: Settings.parse_settingsqr(settingsqr_data) def test_settingsqr_ignores_unrecognized_setting(self): """ SettingsQR parser should ignore unrecognized settings """ - settingsqr_data = "settings::v1 name=Foo favorite_food=bacon passphrase=E" + settings_entry = SettingsDefinition.get_settings_entry(SettingsConstants.SETTING__NETWORK) + unknown_attr = "favorite_food" + settingsqr_data = f"{self.settingsqr_prefix} {unknown_attr}=bacon {settings_entry.abbreviated_name}={settings_entry.default_value}" config_name, settings_update_dict = Settings.parse_settingsqr(settingsqr_data) - assert "favorite_food" not in settings_update_dict - assert "passphrase" in settings_update_dict + assert unknown_attr not in settings_update_dict + assert settings_entry.attr_name in settings_update_dict # Accepts update with no Exceptions self.settings.update(new_settings=settings_update_dict) @@ -165,10 +190,11 @@ class TestSettings(BaseTest): def test_settingsqr_fails_unrecognized_option(self): """ SettingsQR parser should fail if a settings has an unrecognized option """ - settingsqr_data = "settings::v1 name=Foo passphrase=Yep" + settings_entry = SettingsDefinition.get_settings_entry(SettingsConstants.SETTING__NETWORK) + settingsqr_data = f"{self.settingsqr_prefix} {settings_entry.abbreviated_name}=fake_value" with pytest.raises(InvalidSettingsQRData) as e: Settings.parse_settingsqr(settingsqr_data) - assert "passphrase" in str(e.value) + assert settings_entry.attr_name in str(e.value) def test_settingsqr_fails_empty_values(self): @@ -181,10 +207,11 @@ class TestSettings(BaseTest): def test_settingsqr_parses_line_break_separators(self): """ SettingsQR parser should read line breaks as acceptable separators """ - settingsqr_data = "settings::v1\nname=Foo\nsigs=ss,ms\nscripts=nat,nes,tr\npassphrase=E\n" + attrs_with_line_breaks = self.settingsqr_default_attrs_str.replace(' ', '\n') + settingsqr_data = f"{self.settingsqr_prefix}\n{attrs_with_line_breaks}" config_name, settings_update_dict = Settings.parse_settingsqr(settingsqr_data) - assert len(settings_update_dict.keys()) == 3 + assert len(settings_update_dict.keys()) == self.settingsqr_default_attrs_str.count('=') # Accepts update with no Exceptions self.settings.update(new_settings=settings_update_dict) diff --git a/tests/test_settingsqr_decoder.py b/tests/test_settingsqr_decoder.py index 3f7285c1..7b40c927 100644 --- a/tests/test_settingsqr_decoder.py +++ b/tests/test_settingsqr_decoder.py @@ -1,15 +1,17 @@ +from test_settings import SettingsQRBase from seedsigner.models.decode_qr import DecodeQR, DecodeQRStatus -class TestSettingsQRDecoder: + +class TestSettingsQRDecoder(SettingsQRBase): def test_decode_settingsqr(self): """ Assume the QR reader decodes the SettingsQR content correctly and begin this test with parsing the result. """ settings_name = "Test SettingsQR" - settings_qr_str = f"""settings::v1 name={ settings_name.replace(" ", "_") } persistent=D xpub_qr=urca,sta denom=thr network=M qr_density=M sigs=ss,ms scripts=nat,nes,tr xpub_details=E passphrase=E camera=180 compact_seedqr=E bip85=D priv_warn=E dire_warn=E partners=E""" + settings_qr_str = f"""{self.settingsqr_prefix} name={ settings_name.replace(" ", "_") } {self.settingsqr_default_attrs_str}""" # Now parse the settings_qr_str decoder = DecodeQR()