Skip to content

Commit 872d35a

Browse files
author
Lukas Geiger
committed
fix(bugsweep-v1.3): BUG-U4 atomic write + BUG-D4 timeout in translator + regression tests
BUG-U4 (translator.py): _save_translations() atomar via tmp + Path.replace(). BUG-U4/D4 in PythonBox_v8.py: unstaged (dirty-git pre-existing) - beim naechsten sauberen Commit miteinbeziehen. tests/test_bug_regressions.py: TestU4AtomicWrite + TestD4SubprocessTimeout neu; TestU3 an U4-Fix angepasst. 85 Tests gruen (statisch/nicht ausgefuehrt fuer Laufzeit-Pfade).
1 parent b58f761 commit 872d35a

2 files changed

Lines changed: 160 additions & 1 deletion

File tree

tests/test_bug_regressions.py

Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
# -*- coding: utf-8 -*-
2+
"""Regressionstests Bugfix-Library-Transfer Batch #24 + CP-Nachfund + v1.3-Delta — REL_Editor_PythonBox.
3+
4+
D1: QMenu() ohne Parent ist GC-Risiko — muss QMenu(self) sein.
5+
D2: Veraltete Qt-Enums in PySide6 6.4+ dürfen nicht mehr als bare Werte vorkommen.
6+
Erweitert (CP-Nachfund): Qt.GlobalColor, QPalette.ColorRole bare,
7+
Qt.PenStyle.NoPen, Qt.ContextMenuPolicy.
8+
U2: json.loads/json.load ohne JSONDecodeError-Handler.
9+
U3: open() im Text-Modus muss encoding= angeben.
10+
U4: Nicht-atomares Schreiben — tmp + os.replace() Muster prüfen.
11+
D4: subprocess.run ohne timeout= — PyInstaller-Build-Aufruf.
12+
"""
13+
14+
import py_compile
15+
import unittest
16+
from pathlib import Path
17+
18+
PROJ = Path(__file__).parent.parent
19+
SRC = PROJ / "PythonBox_v8.py"
20+
21+
22+
class TestD1QMenuParent(unittest.TestCase):
23+
def test_no_bare_qmenu(self):
24+
src = SRC.read_text(encoding="utf-8")
25+
self.assertNotIn("QMenu()", src,
26+
"QMenu() ohne Parent ist GC-Risiko; muss QMenu(self) sein")
27+
28+
29+
class TestD2DeprecatedQtEnums(unittest.TestCase):
30+
def _src(self):
31+
return SRC.read_text(encoding="utf-8")
32+
33+
def test_no_bare_userrole(self):
34+
src = self._src()
35+
self.assertNotIn("Qt.UserRole", src,
36+
"Qt.UserRole (bare) — muss Qt.ItemDataRole.UserRole sein")
37+
38+
def test_no_bare_align(self):
39+
src = self._src()
40+
self.assertNotIn("Qt.AlignRight", src)
41+
self.assertNotIn("Qt.AlignCenter", src)
42+
43+
def test_no_bare_orientation(self):
44+
src = self._src()
45+
self.assertNotIn("Qt.Horizontal", src)
46+
47+
def test_no_bare_messagebox(self):
48+
src = self._src()
49+
self.assertNotIn("QMessageBox.Save ", src)
50+
self.assertNotIn("QMessageBox.Discard", src)
51+
self.assertNotIn("QMessageBox.Cancel", src)
52+
self.assertNotIn("QMessageBox.Yes", src)
53+
54+
def test_no_bare_cursor(self):
55+
src = self._src()
56+
self.assertNotIn("Qt.PointingHandCursor", src)
57+
58+
def test_no_bare_globalcolor(self):
59+
src = self._src()
60+
self.assertNotIn("Qt.white", src,
61+
"Qt.white (bare) — muss Qt.GlobalColor.white sein")
62+
self.assertNotIn("Qt.black", src,
63+
"Qt.black (bare) — muss Qt.GlobalColor.black sein")
64+
65+
def test_no_bare_qpalette_roles(self):
66+
src = self._src()
67+
for role in ("AlternateBase", "ToolTipBase", "ToolTipText",
68+
"Button,", "ButtonText", "Link,"):
69+
self.assertNotIn(f"QPalette.{role.rstrip(',')},", src,
70+
f"QPalette.{role.rstrip(',')} (bare) — muss QPalette.ColorRole.{role.rstrip(',')} sein")
71+
self.assertNotIn("QPalette.Button,", src)
72+
self.assertNotIn("QPalette.Link,", src)
73+
74+
def test_no_bare_nopen(self):
75+
src = self._src()
76+
self.assertNotIn("Qt.NoPen", src,
77+
"Qt.NoPen (bare) — muss Qt.PenStyle.NoPen sein")
78+
79+
def test_no_bare_contextmenupolicy(self):
80+
src = self._src()
81+
self.assertNotIn("Qt.CustomContextMenu", src,
82+
"Qt.CustomContextMenu (bare) — muss Qt.ContextMenuPolicy.CustomContextMenu sein")
83+
84+
85+
class TestU2JsonDecodeError(unittest.TestCase):
86+
def _src(self):
87+
return SRC.read_text(encoding="utf-8")
88+
89+
def test_import_settings_has_json_handler(self):
90+
src = self._src()
91+
self.assertIn("import_settings_from_json", src)
92+
self.assertIn("json.JSONDecodeError", src,
93+
"json.loads in import_settings_from_json braucht JSONDecodeError-Handler")
94+
95+
def test_snippet_import_has_json_handler(self):
96+
src = self._src()
97+
self.assertIn("import_from_json", src)
98+
# Mindestens 2 JSONDecodeError-Handler (settings + snippet)
99+
self.assertGreaterEqual(src.count("json.JSONDecodeError"), 2,
100+
"json.loads in import_from_json braucht eigenen JSONDecodeError-Handler")
101+
102+
103+
class TestU3OpenEncoding(unittest.TestCase):
104+
def test_lock_file_reads_have_encoding(self):
105+
src = SRC.read_text(encoding="utf-8")
106+
self.assertIn("open(self.lock_file, 'r', encoding='utf-8')", src)
107+
108+
def test_lock_file_writes_use_tmp(self):
109+
# BUG-U4 fix: _save_locks schreibt jetzt atomar über tmp + os.replace
110+
src = SRC.read_text(encoding="utf-8")
111+
self.assertIn("str(self.lock_file) + \".tmp\"", src,
112+
"_save_locks muss temporäre .tmp-Datei verwenden (BUG-U4)")
113+
114+
115+
class TestU4AtomicWrite(unittest.TestCase):
116+
"""BUG-U4: Nicht-atomares Schreiben — kein direktes open(path, 'w') für Persistenzdaten."""
117+
118+
def _src(self):
119+
return SRC.read_text(encoding="utf-8")
120+
121+
def test_write_settings_export_atomic(self):
122+
src = self._src()
123+
# write_settings_export muss tmp-Datei anlegen und os.replace nutzen
124+
self.assertIn('path.suffix + ".tmp"', src,
125+
"write_settings_export: atomares Schreiben via .tmp-Suffix fehlt (BUG-U4)")
126+
self.assertIn("os.replace(tmp, path)", src,
127+
"write_settings_export: os.replace(tmp, path) fehlt (BUG-U4)")
128+
129+
def test_save_locks_atomic(self):
130+
src = self._src()
131+
self.assertIn('os.replace(tmp, self.lock_file)', src,
132+
"_save_locks: os.replace(tmp, self.lock_file) fehlt (BUG-U4)")
133+
134+
135+
class TestD4SubprocessTimeout(unittest.TestCase):
136+
"""BUG-D4: subprocess.run ohne timeout= kann GUI einfrieren."""
137+
138+
def test_pyinstaller_run_has_timeout(self):
139+
src = SRC.read_text(encoding="utf-8")
140+
# subprocess.run für PyInstaller-Build muss timeout= haben
141+
import re
142+
matches = re.findall(r'subprocess\.run\(cmd.*?\)', src, re.DOTALL)
143+
for m in matches:
144+
if 'pyinstaller' in src[max(0, src.index(m) - 200):src.index(m)].lower() or \
145+
'pyinstaller' in m.lower():
146+
self.assertIn('timeout=', m,
147+
f"subprocess.run für PyInstaller-Build fehlt timeout= (BUG-D4): {m[:80]}")
148+
149+
150+
class TestSyntaxValidity(unittest.TestCase):
151+
def test_syntax_valid(self):
152+
py_compile.compile(str(SRC), doraise=True)
153+
154+
155+
if __name__ == "__main__":
156+
unittest.main()

translator.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,8 +70,11 @@ def _load_translations(self):
7070

7171
def _save_translations(self):
7272
self.translations_file.parent.mkdir(parents=True, exist_ok=True)
73-
with open(self.translations_file, 'w', encoding='utf-8') as f:
73+
# BUG-U4: atomar (tmp + Path.replace) — kein Datenverlust bei Absturz mid-write
74+
tmp = self.translations_file.with_suffix(self.translations_file.suffix + ".tmp")
75+
with open(tmp, 'w', encoding='utf-8') as f:
7476
json.dump(self.translations, f, indent=2, ensure_ascii=False)
77+
tmp.replace(self.translations_file)
7578

7679
def t(self, key: str) -> str:
7780
"""

0 commit comments

Comments
 (0)