Skip to content

Commit 72ac42a

Browse files
committed
Added FPGA-ID support to RetroAccount.
Improved launcher and its log to be a bit more resilient.
1 parent 2564d71 commit 72ac42a

27 files changed

Lines changed: 2958 additions & 76 deletions

.github/build_new_db.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,7 @@ def fetch_file(path: str, url: str, auth_token: str = None) -> None:
130130

131131
fetch_file('update_all_latest_log.sh', 'https://raw.githubusercontent.com/theypsilon/Update_All_MiSTer/refs/heads/master/src/update_all_latest_log.sh')
132132
fetch_file('update_all_timeline.sh', 'https://raw.githubusercontent.com/theypsilon/Update_All_MiSTer/refs/heads/master/src/update_all_timeline.sh')
133+
fetch_file('Linker.rbf', 'https://raw.githubusercontent.com/theypsilon/Update_All_MiSTer/refs/heads/bin/Linker.rbf')
133134

134135
save_json(generate_pocket_firmware_details(), 'pocket_firmware_details.json')
135136

@@ -142,7 +143,7 @@ def fetch_file(path: str, url: str, auth_token: str = None) -> None:
142143

143144
subprocess.run(['zip', 'update_all.zip', 'update_all.sh'], check=True)
144145

145-
subprocess.run(['git', 'add', 'update_all.pyz', 'update_all.pyz.sha256', 'update_all.sh', 'update_all_latest_log.sh', 'update_all_timeline.sh', 'update_all.zip', 'mad_db.json.zip', 'pocket_firmware_details.json', 'timeline_plus.enc', 'timeline.json'], check=True)
146+
subprocess.run(['git', 'add', 'update_all.pyz', 'update_all.pyz.sha256', 'update_all.sh', 'update_all_latest_log.sh', 'update_all_timeline.sh', 'Linker.rbf', 'update_all.zip', 'mad_db.json.zip', 'pocket_firmware_details.json', 'timeline_plus.enc', 'timeline.json'], check=True)
146147
subprocess.run(['git', 'commit', '-m', '-'], check=True)
147148

148149
new_db['files'] = {
@@ -171,6 +172,11 @@ def fetch_file(path: str, url: str, auth_token: str = None) -> None:
171172
'hash': hash_file('timeline.json'),
172173
'tags': [2, 1]
173174
},
175+
'Scripts/.config/update_all/Linker.rbf': {
176+
'size': os.path.getsize('Linker.rbf'),
177+
'hash': hash_file('Linker.rbf'),
178+
'tags': [2],
179+
},
174180
'Scripts/update_all.sh': {
175181
'size': os.path.getsize('update_all.sh'),
176182
'hash': hash_file('update_all.sh'),

src/debug.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,9 @@ def run_launcher(**kwargs): send_build(**kwargs), exec_ssh(f'/media/fat/Scripts/
2828
def store_push(**kwargs): scp_file('update_all.json', '/media/fat/Scripts/.config/update_all/update_all.json', **kwargs)
2929
def store_pull(**kwargs): scp_file('/media/fat/Scripts/.config/update_all/update_all.json', 'update_all.json', **kwargs)
3030
def log_pull(**kwargs): scp_file('/media/fat/Scripts/.config/update_all/update_all.log', 'update_all.log', **kwargs)
31+
def send_linker(**kwargs):
32+
exec_ssh('mkdir -p /media/fat/Scripts/.config/update_all', **kwargs)
33+
scp_file('Linker.rbf', '/media/fat/Scripts/.config/update_all/Linker.rbf', **kwargs)
3134

3235
def send_build(env=None, build_target=None, launcher_target=None, debug=None, **kwargs):
3336
env = {'DEBUG': debug or 'true', **os.environ.copy(), **(env or {}), 'MISTER': 'true'}
@@ -48,7 +51,9 @@ def operations_dict(env=None, retries=False):
4851
'build': lambda: [send_build(env=env, retries=retries), print('OK')],
4952
'run': lambda: run_build(env=env, retries=retries),
5053
'launcher': lambda: run_launcher(env=env, retries=retries),
54+
'send_linker': lambda: [send_linker(retries=retries), print('OK')],
5155
'copy': lambda: scp_file(sys.argv[2], f'/media/fat/{sys.argv[2]}'),
56+
'rcopy': lambda: [scp_file(f'/media/fat/{sys.argv[2]}', Path(sys.argv[2]).name), print('OK')],
5257
'local_run': lambda: local_run(env=env),
5358
'local_run_tiny': lambda: local_run_tiny(),
5459
'local_run_small': lambda: local_run_small(),

src/test/integration/test_environment_setup.py

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,8 @@
2020
from test.ini_assertions import testableIni
2121
from test.testing_objects import downloader_ini, update_all_ini, update_jtcores_ini, update_names_txt_ini
2222
from update_all.config import Config
23-
from update_all.constants import KENV_DEBUG, KENV_LOCATION_STR, FILE_update_all_storage, KENV_TRANSITION_SERVICE_ONLY
23+
from update_all.constants import KENV_DEBUG, KENV_LOCATION_STR, FILE_update_all_storage, KENV_TRANSITION_SERVICE_ONLY, \
24+
KENV_UPDATE_ALL_CHIP_ID_RESULT
2425
from update_all.databases import DB_ID_NAMES_TXT, AllDBs, DB_ID_ARCADE_NAMES_TXT, all_dbs
2526
from update_all.environment_setup import EnvironmentSetupResult
2627
from update_all.local_store import LocalStore
@@ -87,6 +88,17 @@ def test_setup___with_empty_downloader_ini_and_debug_env___returns_config_with_v
8788
expected_config=Config(verbose=True, databases={all_dbs('').UPDATE_ALL_MISTER.db_id})
8889
)
8990

91+
def test_setup___with_chip_id_result_env___stores_chip_id_result_in_config(self):
92+
self.assertSetup(
93+
files={downloader_ini: ''},
94+
expected_files={downloader_ini: Path('test/fixtures/downloader_ini/downloader_ini_empty_but_update_all.ini').read_text()},
95+
env={KENV_UPDATE_ALL_CHIP_ID_RESULT: '0123456789abcdef'},
96+
expected_config=Config(
97+
databases={all_dbs('').UPDATE_ALL_MISTER.db_id},
98+
chip_id_result='0123456789abcdef'
99+
)
100+
)
101+
90102
def test_setup___with_default_downloader_ini_and_update_all_ini_with_disabled_arcade_organizer___returns_config_with_default_databases_and_disabled_ao(self):
91103
self.assertSetup(files={
92104
downloader_ini: Path('test/fixtures/downloader_ini/default_downloader.ini').read_text(),

src/test/integration/test_update_all_service.py

Lines changed: 46 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,11 @@
1515

1616
# You can download the latest version of this tool from:
1717
# https://github.com/theypsilon/Update_All_MiSTer
18+
import unittest
19+
from unittest.mock import MagicMock
20+
1821
from update_all.config import Config
19-
from update_all.constants import FILE_mister_downloader_needs_reboot, EXIT_CODE_REQUIRES_EARLY_EXIT
22+
from update_all.constants import FILE_mister_downloader_needs_reboot, EXIT_CODE_REQUIRES_EARLY_EXIT, COMMAND_SHOW_CHIP_ID_RESULT
2023
from update_all.environment_setup import EnvironmentSetupResult
2124
from update_all.local_store import LocalStore
2225
from update_all.other import GenericProvider
@@ -25,10 +28,10 @@
2528
from test.file_system_tester_state import FileSystemState
2629
from test.update_all_service_tester import UpdateAllServiceFactoryTester, UpdateAllServiceTester, \
2730
default_env, EnvironmentSetupStub, default_databases, local_store
28-
import unittest
2931

3032

31-
def tester(files=None, folders=None, config: Config = None, store: LocalStore = None, env_stub: EnvironmentSetupStub = None):
33+
def tester(files=None, folders=None, config: Config = None, store: LocalStore = None, env_stub: EnvironmentSetupStub = None,
34+
settings_screen=None):
3235
state = FileSystemState(files=files, folders=folders)
3336
config_provider = GenericProvider[Config]()
3437
config_provider.initialize(config or Config(databases=default_databases()))
@@ -39,7 +42,8 @@ def tester(files=None, folders=None, config: Config = None, store: LocalStore =
3942
environment_setup=env_stub or EnvironmentSetupStub(),
4043
file_system=FileSystemFactory(state=state, config_provider=config_provider).create_for_system_scope(),
4144
config_provider=config_provider,
42-
store_provider=store_provider
45+
store_provider=store_provider,
46+
settings_screen=settings_screen,
4347
), state
4448

4549

@@ -89,3 +93,41 @@ def test_full_run___when_env_setup_requires_early_exit___returns_exit_code_requi
8993
stub = EnvironmentSetupStub(EnvironmentSetupResult(requires_early_exit=True))
9094
sut, _ = tester(config=Config(databases=default_databases(), transition_service_only=True), env_stub=stub)
9195
self.assertEqual(EXIT_CODE_REQUIRES_EARLY_EXIT, sut.full_run(UpdateAllServicePass.NewRun))
96+
97+
def test_full_run___with_show_chip_id_result_command___opens_chip_id_result_menu_and_returns_without_update_flow(self):
98+
events = []
99+
settings_screen = MagicMock()
100+
settings_screen.load_chip_id_result_menu.side_effect = lambda: events.append('menu')
101+
sut, _ = tester(
102+
config=Config(databases=default_databases(), command=COMMAND_SHOW_CHIP_ID_RESULT),
103+
settings_screen=settings_screen,
104+
)
105+
sut._start_background_jobs = MagicMock(side_effect=lambda: events.append('start_background_jobs'))
106+
sut._hard_wait_background_jobs = MagicMock(side_effect=lambda: events.append('hard_wait_background_jobs'))
107+
108+
result = sut.full_run(UpdateAllServicePass.NewRun)
109+
110+
self.assertEqual(0, result)
111+
self.assertEqual(['menu'], events)
112+
sut._start_background_jobs.assert_not_called()
113+
settings_screen.load_chip_id_result_menu.assert_called_once_with()
114+
sut._hard_wait_background_jobs.assert_not_called()
115+
116+
def test_full_run___with_show_chip_id_result_command_and_menu_failure___returns_without_update_flow(self):
117+
events = []
118+
settings_screen = MagicMock()
119+
settings_screen.load_chip_id_result_menu.side_effect = RuntimeError('boom')
120+
sut, _ = tester(
121+
config=Config(databases=default_databases(), command=COMMAND_SHOW_CHIP_ID_RESULT),
122+
settings_screen=settings_screen,
123+
)
124+
sut._start_background_jobs = MagicMock(side_effect=lambda: events.append('start_background_jobs'))
125+
sut._hard_wait_background_jobs = MagicMock(side_effect=lambda: events.append('hard_wait_background_jobs'))
126+
127+
result = sut.full_run(UpdateAllServicePass.NewRun)
128+
129+
self.assertEqual(0, result)
130+
self.assertEqual([], events)
131+
sut._start_background_jobs.assert_not_called()
132+
settings_screen.load_chip_id_result_menu.assert_called_once_with()
133+
sut._hard_wait_background_jobs.assert_not_called()

src/test/unit/test_countdown.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,6 @@
2222

2323
from update_all.countdown import _countdown_left_padding, _countdown_status_line
2424
from update_all.logger import FileLoggerDecorator, PrintLogger
25-
from update_all.other import GenericProvider
26-
2725

2826
class TestCountdown(unittest.TestCase):
2927
def test_countdown_status_line___adds_clear_carriage_return_and_left_padding(self):
@@ -32,6 +30,6 @@ def test_countdown_status_line___adds_clear_carriage_return_and_left_padding(sel
3230
def test_countdown_left_padding___unwraps_decorated_print_logger(self):
3331
inner_logger = PrintLogger()
3432
inner_logger._overscan = 4
35-
logger = FileLoggerDecorator(inner_logger, GenericProvider())
33+
logger = FileLoggerDecorator(inner_logger, '/tmp/update_all_test.log')
3634

3735
self.assertEqual(4, _countdown_left_padding(logger))

src/test/unit/test_other.py

Lines changed: 67 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,14 @@
1616
# You can download the latest version of this tool from:
1717
# https://github.com/theypsilon/Update_All_MiSTer
1818

19+
import os
20+
import tempfile
1921
import unittest
22+
import zipfile
23+
from unittest.mock import patch
2024

21-
from update_all.other import any_to_bool, any_to_nonfalsy_str, calculate_overscan, TerminalSize, OverscanDim, calculate_outer_box
25+
from update_all.other import any_to_bool, any_to_nonfalsy_str, calculate_overscan, TerminalSize, OverscanDim, calculate_outer_box, \
26+
current_update_all_archive_path, is_mister_scripts_menu_fb_launch
2227

2328

2429
class TestOther(unittest.TestCase):
@@ -47,6 +52,28 @@ def test_any_to_nonfalsy_str(self):
4752
with self.subTest(value=value, expected=expected):
4853
self.assertEqual(expected, any_to_nonfalsy_str(value))
4954

55+
def test_current_update_all_archive_path___when_argv0_is_zipapp_with_any_extension___returns_path(self):
56+
path = _temp_zipapp_with_suffix('.sh')
57+
try:
58+
with patch('update_all.other.sys.argv', [path]):
59+
self.assertEqual(path, current_update_all_archive_path())
60+
finally:
61+
_remove(path)
62+
63+
def test_current_update_all_archive_path___when_argv0_is_not_zipapp___returns_none(self):
64+
with tempfile.NamedTemporaryFile(delete=False) as file:
65+
path = file.name
66+
file.write(b'#!/bin/bash\n')
67+
try:
68+
with patch('update_all.other.sys.argv', [path]):
69+
self.assertIsNone(current_update_all_archive_path())
70+
finally:
71+
_remove(path)
72+
73+
def test_current_update_all_archive_path___when_argv0_is_missing___returns_none(self):
74+
with patch('update_all.other.sys.argv', ['/tmp/missing_update_all']):
75+
self.assertIsNone(current_update_all_archive_path())
76+
5077
def test_calculate_overscan(self):
5178
def _size(columns=100, lines=50):
5279
return TerminalSize(columns=columns, lines=lines)
@@ -85,8 +112,47 @@ def test_calculate_outer_box___keeps_zero_line_overscan_outside_screen(self):
85112
screen_dims = _ScreenDims(TerminalSize(columns=80, lines=40), OverscanDim(cols=1, lines=0))
86113
self.assertEqual((-1, 40, 0, 79), calculate_outer_box(screen_dims))
87114

115+
def test_is_mister_scripts_menu_fb_launch___when_tty2_with_script_wrapper_and_mister_ancestor___returns_true(self):
116+
with patch('update_all.other._current_tty', return_value='/dev/tty2'), \
117+
patch('update_all.other._ancestor_process_descriptions', return_value=[
118+
('bash', '/bin/bash /tmp/script'),
119+
('agetty', '/sbin/agetty -l /tmp/script tty2'),
120+
('MiSTer', '/media/fat/MiSTer'),
121+
]):
122+
self.assertTrue(is_mister_scripts_menu_fb_launch())
123+
124+
def test_is_mister_scripts_menu_fb_launch___when_tty2_without_mister_ancestor___returns_false(self):
125+
with patch('update_all.other._current_tty', return_value='/dev/tty2'), \
126+
patch('update_all.other._ancestor_process_descriptions', return_value=[
127+
('bash', '/bin/bash /tmp/script'),
128+
('agetty', '/sbin/agetty -l /tmp/script tty2'),
129+
]):
130+
self.assertFalse(is_mister_scripts_menu_fb_launch())
131+
132+
def test_is_mister_scripts_menu_fb_launch___when_not_tty2___returns_false(self):
133+
with patch('update_all.other._current_tty', return_value='/dev/pts/0'), \
134+
patch('update_all.other._ancestor_process_descriptions') as ancestors:
135+
self.assertFalse(is_mister_scripts_menu_fb_launch())
136+
ancestors.assert_not_called()
137+
88138

89139
class _ScreenDims:
90140
def __init__(self, term_size: TerminalSize, overscan_dim: OverscanDim):
91141
self.term_size = term_size
92142
self.overscan_dim = overscan_dim
143+
144+
145+
def _temp_zipapp_with_suffix(suffix: str) -> str:
146+
with tempfile.NamedTemporaryFile(delete=False, suffix=suffix) as file:
147+
path = file.name
148+
with zipfile.ZipFile(path, 'w') as archive:
149+
archive.writestr('__main__.py', '')
150+
return path
151+
152+
153+
def _remove(*paths: str) -> None:
154+
for path in paths:
155+
try:
156+
os.remove(path)
157+
except FileNotFoundError:
158+
pass

0 commit comments

Comments
 (0)