mirror of
https://github.com/SeedSigner/seedsigner.git
synced 2026-10-05 15:08:25 +00:00
Merge pull request #861 from kdmukai/2025-12_settings_cleanup_v2
[Cleanup] `SettingsDefinition` minor improvements, better test maintainability
This commit is contained in:
@@ -1,4 +1,5 @@
|
|||||||
import os
|
import os
|
||||||
|
import pathlib
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from typing import Any, List
|
from typing import Any, List
|
||||||
|
|
||||||
@@ -182,6 +183,7 @@ class SettingsConstants:
|
|||||||
LOCALE__VIETNAMESE: "Tiếng Việt (Vietnamese)",
|
LOCALE__VIETNAMESE: "Tiếng Việt (Vietnamese)",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_detected_languages(cls) -> list[tuple[str, str]]:
|
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.
|
Scans the filesystem to autodiscover which language codes are onboard.
|
||||||
"""
|
"""
|
||||||
# Will normally be the launch dir (where main.py is located)...
|
# Back out from the models/ dir to reach the seedsigner root
|
||||||
cwd = os.getcwd()
|
models_dir = pathlib.Path(__file__).parent.resolve()
|
||||||
|
seedsigner_root = models_dir.parent.resolve()
|
||||||
# ...except when running the tests which happens one dir higher
|
|
||||||
if "src" not in cwd:
|
|
||||||
cwd = os.path.join(cwd, "src")
|
|
||||||
|
|
||||||
# Pre-load English since there's no "en" entry in the translations folder; also
|
# Pre-load English since there's no "en" entry in the translations folder; also
|
||||||
# it should always appear first in the list anyway.
|
# it should always appear first in the list anyway.
|
||||||
detected_languages = [(cls.LOCALE__ENGLISH, cls.ALL_LOCALES[cls.LOCALE__ENGLISH])]
|
detected_languages = [(cls.LOCALE__ENGLISH, cls.ALL_LOCALES[cls.LOCALE__ENGLISH])]
|
||||||
|
|
||||||
locales_present = set()
|
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")]:
|
for file in [f for f in files if f.endswith(".mo")]:
|
||||||
# `root` will be [...]seedsigner/resources/seedsigner-translations/l10n/pt_BR/LC_MESSAGES
|
# `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():
|
for locale in cls.ALL_LOCALES.keys():
|
||||||
if locale in locales_present:
|
if locale in locales_present:
|
||||||
@@ -453,6 +453,9 @@ class SettingsEntry:
|
|||||||
elif type(self.default_value) == tuple:
|
elif type(self.default_value) == tuple:
|
||||||
self.default_value = self.default_value[0]
|
self.default_value = self.default_value[0]
|
||||||
|
|
||||||
|
if not self.abbreviated_name:
|
||||||
|
self.abbreviated_name = self.attr_name
|
||||||
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def selection_options_display_names(self) -> List[str]:
|
def selection_options_display_names(self) -> List[str]:
|
||||||
@@ -776,15 +779,25 @@ class SettingsDefinition:
|
|||||||
|
|
||||||
|
|
||||||
@classmethod
|
@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 = {}
|
as_dict = {}
|
||||||
for entry in SettingsDefinition.settings_entries:
|
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:
|
if type(entry.default_value) == list:
|
||||||
# Must copy the default_value list, otherwise we'll inadvertently change
|
# Must copy the default_value list, otherwise we'll inadvertently change
|
||||||
# defaults when updating these attrs
|
# defaults when updating these attrs
|
||||||
as_dict[entry.attr_name] = list(entry.default_value)
|
as_dict[attr_name] = list(entry.default_value)
|
||||||
else:
|
else:
|
||||||
as_dict[entry.attr_name] = entry.default_value
|
as_dict[attr_name] = entry.default_value
|
||||||
return as_dict
|
return as_dict
|
||||||
|
|
||||||
|
|
||||||
@@ -797,19 +810,3 @@ class SettingsDefinition:
|
|||||||
output["settings_entries"].append(settings_entry.to_dict())
|
output["settings_entries"].append(settings_entry.to_dict())
|
||||||
|
|
||||||
return output
|
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)
|
|
||||||
|
|||||||
+40
-13
@@ -1,6 +1,8 @@
|
|||||||
import json
|
import json
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from base import BaseTest
|
from base import BaseTest
|
||||||
|
|
||||||
from seedsigner.models.settings import InvalidSettingsQRData, Settings
|
from seedsigner.models.settings import InvalidSettingsQRData, Settings
|
||||||
from seedsigner.models.settings_definition import SettingsConstants, SettingsDefinition
|
from seedsigner.models.settings_definition import SettingsConstants, SettingsDefinition
|
||||||
|
|
||||||
@@ -98,14 +100,35 @@ class TestSettings(BaseTest):
|
|||||||
_verify_defaults_loaded(SettingsConstants.SETTING__SIG_TYPES)
|
_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):
|
def test_parse_settingsqr_data(self):
|
||||||
"""
|
"""
|
||||||
SettingsQR parser should successfully parse a valid settingsqr input string and
|
SettingsQR parser should successfully parse a valid settingsqr input string and
|
||||||
return the resulting config_name and formatted settings_update_dict.
|
return the resulting config_name and formatted settings_update_dict.
|
||||||
"""
|
"""
|
||||||
settings_name = "Test SettingsQR"
|
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
|
# First explicitly set settings that differ from the settingsqr_data
|
||||||
self.settings.set_value(SettingsConstants.SETTING__COMPACT_SEEDQR, SettingsConstants.OPTION__DISABLED)
|
self.settings.set_value(SettingsConstants.SETTING__COMPACT_SEEDQR, SettingsConstants.OPTION__DISABLED)
|
||||||
@@ -129,35 +152,37 @@ class TestSettings(BaseTest):
|
|||||||
|
|
||||||
def test_settingsqr_version(self):
|
def test_settingsqr_version(self):
|
||||||
""" SettingsQR parser should accept SettingsQR v1 and reject any others """
|
""" 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)
|
config_name, settings_update_dict = Settings.parse_settingsqr(settingsqr_data)
|
||||||
|
|
||||||
# Accepts update with no Exceptions
|
# Accepts update with no Exceptions
|
||||||
self.settings.update(new_settings=settings_update_dict)
|
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:
|
with pytest.raises(InvalidSettingsQRData) as e:
|
||||||
Settings.parse_settingsqr(settingsqr_data)
|
Settings.parse_settingsqr(settingsqr_data)
|
||||||
assert "Unsupported SettingsQR version" in str(e.value)
|
assert "Unsupported SettingsQR version" in str(e.value)
|
||||||
|
|
||||||
# Should also fail if version omitted
|
# 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:
|
with pytest.raises(InvalidSettingsQRData) as e:
|
||||||
Settings.parse_settingsqr(settingsqr_data)
|
Settings.parse_settingsqr(settingsqr_data)
|
||||||
|
|
||||||
# And if "settings" is omitted entirely
|
# And if "settings" is omitted entirely
|
||||||
settingsqr_data = "name=Foo"
|
settingsqr_data = self.settingsqr_default_attrs_str
|
||||||
with pytest.raises(InvalidSettingsQRData) as e:
|
with pytest.raises(InvalidSettingsQRData) as e:
|
||||||
Settings.parse_settingsqr(settingsqr_data)
|
Settings.parse_settingsqr(settingsqr_data)
|
||||||
|
|
||||||
|
|
||||||
def test_settingsqr_ignores_unrecognized_setting(self):
|
def test_settingsqr_ignores_unrecognized_setting(self):
|
||||||
""" SettingsQR parser should ignore unrecognized settings """
|
""" 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)
|
config_name, settings_update_dict = Settings.parse_settingsqr(settingsqr_data)
|
||||||
|
|
||||||
assert "favorite_food" not in settings_update_dict
|
assert unknown_attr not in settings_update_dict
|
||||||
assert "passphrase" in settings_update_dict
|
assert settings_entry.attr_name in settings_update_dict
|
||||||
|
|
||||||
# Accepts update with no Exceptions
|
# Accepts update with no Exceptions
|
||||||
self.settings.update(new_settings=settings_update_dict)
|
self.settings.update(new_settings=settings_update_dict)
|
||||||
@@ -165,10 +190,11 @@ class TestSettings(BaseTest):
|
|||||||
|
|
||||||
def test_settingsqr_fails_unrecognized_option(self):
|
def test_settingsqr_fails_unrecognized_option(self):
|
||||||
""" SettingsQR parser should fail if a settings has an unrecognized option """
|
""" 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:
|
with pytest.raises(InvalidSettingsQRData) as e:
|
||||||
Settings.parse_settingsqr(settingsqr_data)
|
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):
|
def test_settingsqr_fails_empty_values(self):
|
||||||
@@ -181,10 +207,11 @@ class TestSettings(BaseTest):
|
|||||||
|
|
||||||
def test_settingsqr_parses_line_break_separators(self):
|
def test_settingsqr_parses_line_break_separators(self):
|
||||||
""" SettingsQR parser should read line breaks as acceptable separators """
|
""" 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)
|
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
|
# Accepts update with no Exceptions
|
||||||
self.settings.update(new_settings=settings_update_dict)
|
self.settings.update(new_settings=settings_update_dict)
|
||||||
|
|||||||
@@ -1,15 +1,17 @@
|
|||||||
|
from test_settings import SettingsQRBase
|
||||||
from seedsigner.models.decode_qr import DecodeQR, DecodeQRStatus
|
from seedsigner.models.decode_qr import DecodeQR, DecodeQRStatus
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class TestSettingsQRDecoder:
|
|
||||||
|
class TestSettingsQRDecoder(SettingsQRBase):
|
||||||
def test_decode_settingsqr(self):
|
def test_decode_settingsqr(self):
|
||||||
"""
|
"""
|
||||||
Assume the QR reader decodes the SettingsQR content correctly and begin this test
|
Assume the QR reader decodes the SettingsQR content correctly and begin this test
|
||||||
with parsing the result.
|
with parsing the result.
|
||||||
"""
|
"""
|
||||||
settings_name = "Test SettingsQR"
|
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
|
# Now parse the settings_qr_str
|
||||||
decoder = DecodeQR()
|
decoder = DecodeQR()
|
||||||
|
|||||||
Reference in New Issue
Block a user