|
| 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() |
0 commit comments