Skip to content

Commit 4599b29

Browse files
authored
Merge pull request #679 from boriel/bugfix/crash_on_new_config_file
fix: prevents crash on config load/save errors
2 parents 15025d8 + 247afa0 commit 4599b29

File tree

3 files changed

+72
-15
lines changed

3 files changed

+72
-15
lines changed

src/api/config.py

Lines changed: 21 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -93,11 +93,16 @@ class OPTION(str, Enum):
9393
}
9494

9595

96-
def load_config_from_file(filename: str, section: str, options_: options.Options = None, stop_on_error=True) -> bool:
96+
def load_config_from_file(
97+
filename: str, section: ConfigSections, options_: options.Options | None = None, *, stop_on_error: bool = True
98+
) -> bool:
9799
"""Opens file and read options from the given section. If stop_on_error is set,
98-
the program stop if any error is found. Otherwise the result of the operation will be
100+
the program stop if any error is found. Otherwise, the result of the operation will be
99101
returned (True on success, False on failure)
100102
"""
103+
assert isinstance(section, ConfigSections)
104+
section_ = section.value
105+
101106
if options_ is None:
102107
options_ = OPTIONS
103108

@@ -115,25 +120,30 @@ def load_config_from_file(filename: str, section: str, options_: options.Options
115120
sys.exit(1)
116121
return False
117122

118-
if section not in cfg.sections():
119-
errmsg.msg_output(f"Section '{section}' not found in config file '{filename}'")
123+
if section_ not in cfg.sections():
124+
errmsg.msg_output(f"Section '{section_}' not found in config file '{filename}'")
120125
if stop_on_error:
121126
sys.exit(1)
122127
return False
123128

124129
parsing: Dict[type, Callable] = {int: cfg.getint, float: cfg.getfloat, bool: cfg.getboolean}
125130

126-
for opt in cfg.options(section):
131+
for opt in cfg.options(section_):
127132
options_[opt].value = parsing.get(options_[opt].type, cfg.get)(section=section, option=opt)
128133

129134
return True
130135

131136

132-
def save_config_into_file(filename: str, section: str, options_: options.Options = None, stop_on_error=True) -> bool:
137+
def save_config_into_file(
138+
filename: str, section: ConfigSections, options_: options.Options | None = None, *, stop_on_error: bool = True
139+
) -> bool:
133140
"""Save config into config ini file into the given section. If stop_on_error is set,
134-
the program stop. Otherwise the result of the operation will be
141+
the program stop. Otherwise, the result of the operation will be
135142
returned (True on success, False on failure)
136143
"""
144+
assert isinstance(section, ConfigSections)
145+
section_ = section.value
146+
137147
if options_ is None:
138148
options_ = OPTIONS
139149

@@ -147,16 +157,16 @@ def save_config_into_file(filename: str, section: str, options_: options.Options
147157
sys.exit(1)
148158
return False
149159

150-
cfg[section] = {}
160+
cfg[section_] = {}
151161
for opt_name, opt in options_().items():
152162
if opt_name.startswith("__") or opt.value is None or opt_name in OPTIONS_NOT_SAVED:
153163
continue
154164

155165
if opt.type == bool:
156-
cfg[section][opt_name] = str(opt.value).lower()
166+
cfg[section_][opt_name] = str(opt.value).lower()
157167
continue
158168

159-
cfg[section][opt_name] = str(opt.value)
169+
cfg[section_][opt_name] = str(opt.value)
160170

161171
try:
162172
with open(filename, "wt", encoding="utf-8") as f:
@@ -170,7 +180,7 @@ def save_config_into_file(filename: str, section: str, options_: options.Options
170180
return True
171181

172182

173-
def init():
183+
def init() -> None:
174184
"""Default Options and Compilation Flags"""
175185
OPTIONS(Action.CLEAR)
176186

src/zxbc/args_config.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,15 +7,15 @@
77
import src.api.config
88
import src.api.global_ as gl
99
from src import arch
10-
from src.api import debug, errmsg
10+
from src.api import errmsg
1111
from src.api.config import OPTIONS
1212
from src.api.utils import open_file
1313
from src.zxbc import args_parser
1414

1515
if TYPE_CHECKING:
1616
from argparse import Namespace
1717

18-
__all__ = ["FileType", "parse_options", "set_option_defines"]
18+
__all__ = "FileType", "parse_options", "set_option_defines"
1919

2020

2121
class FileType:
@@ -101,7 +101,6 @@ def parse_options(args: list[str] | None = None) -> Namespace:
101101
OPTIONS.case_insensitive = True
102102

103103
OPTIONS.case_insensitive = options.ignore_case
104-
debug.ENABLED = OPTIONS.debug_level > 0
105104

106105
if options.basic and not options.tzx and not options.tap:
107106
parser.error("Option --BASIC and --autorun requires --tzx or tap format")

tests/api/test_config.py

Lines changed: 49 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
#!/usr/bin/env python
22
# -*- coding: utf-8 -*-
3-
43
import sys
54
import unittest
5+
from unittest import mock
6+
7+
import pytest
68

79
from src.api import config, global_
810

@@ -100,3 +102,49 @@ def test_autorun_ignore_none(self):
100102
config.OPTIONS.autorun = True
101103
config.OPTIONS.autorun = None
102104
self.assertEqual(config.OPTIONS.autorun, True)
105+
106+
@mock.patch("os.path.exists", lambda x: False)
107+
def test_save_config_for_the_1st_time(self):
108+
m = mock.mock_open()
109+
with mock.patch("builtins.open", m):
110+
config.save_config_into_file("dummy_filename", config.ConfigSections.ZXBC)
111+
112+
m().write.assert_has_calls([mock.call("[zxbc]\n")], any_order=True)
113+
114+
def test_save_config_does_not_accept_invalid_section(self):
115+
with pytest.raises(AssertionError):
116+
config.save_config_into_file("dummy_filename", "invalid_section")
117+
118+
def test_load_config_from_file(self):
119+
m = mock.mock_open(read_data="[zxbc]\nheap_size=1234\n")
120+
config.OPTIONS(config.Action.ADD_IF_NOT_DEFINED, name="heap_size", type=int, default=4768, ignore_none=True)
121+
122+
with mock.patch("builtins.open", m):
123+
config.load_config_from_file("dummy_filename", config.ConfigSections.ZXBC)
124+
125+
self.assertEqual(config.OPTIONS.heap_size, 1234)
126+
127+
def test_load_config_from_file_fails_if_no_section(self):
128+
m = mock.mock_open(read_data="[zxbasm]\norg=1234\n")
129+
130+
with mock.patch("builtins.open", m):
131+
result = config.load_config_from_file("dummy_filename", config.ConfigSections.ZXBC, stop_on_error=False)
132+
133+
self.assertFalse(result)
134+
135+
@mock.patch("os.path.exists", lambda x: False)
136+
def test_load_config_from_file_fails_if_no_file(self):
137+
result = config.load_config_from_file("dummy_filename", config.ConfigSections.ZXBC, stop_on_error=False)
138+
self.assertFalse(result)
139+
140+
def test_load_config_from_file_fails_if_duplicated_fields(self):
141+
m = mock.mock_open(read_data="[zxbc]\nheap_size=1234\nheap_size=5678\n")
142+
143+
with mock.patch("builtins.open", m):
144+
result = config.load_config_from_file("dummy_filename", config.ConfigSections.ZXBC, stop_on_error=False)
145+
146+
self.assertFalse(result)
147+
148+
def test_load_config_does_not_accept_invalid_section(self):
149+
with pytest.raises(AssertionError):
150+
config.load_config_from_file("dummy_filename", "invalid_section", stop_on_error=False)

0 commit comments

Comments
 (0)