diff --git a/tests/fixtures/editor_lifetime_probe.py b/tests/fixtures/editor_lifetime_probe.py index e1cf2099..db153390 100644 --- a/tests/fixtures/editor_lifetime_probe.py +++ b/tests/fixtures/editor_lifetime_probe.py @@ -1846,10 +1846,12 @@ def runConfirmationProbe(iterations=100): def runToolbarOwnershipProbe(iterations=100): """Toolbar teardown releases owned actions while preserving borrowed owners.""" application() + protected = getattr( sys.modules.get('PySide6-postLoad', PySide6), '_protected', None ) protectedBefore = len(protected) if protected is not None else None + references = [] destroyed = [] @@ -1868,6 +1870,7 @@ def runToolbarOwnershipProbe(iterations=100): toolbar = AppQToolBar(action, separator, parent=owner) references.append(weakref.ref(toolbar)) toolbar.destroyed.connect(lambda *_args: destroyed.append(True)) + toolbar.actionTriggered.emit(action) deleteQObject(toolbar) @@ -1902,8 +1905,10 @@ def runToolbarOwnershipProbe(iterations=100): def runModalPickerProbe(iterations=100): """Exercise modal owner-first teardown with actual native widget deletion.""" application() + references = [] destroyed = [] + textModule = importlib.import_module('Furious.Window.TextEditorWindow') assetWindowModule = importlib.import_module('Furious.Backends.Xray.AssetWindow') pathModule = importlib.import_module('Furious.Backends.ExternalCore.Editor') @@ -1916,6 +1921,7 @@ def runModalPickerProbe(iterations=100): loop.quit() QtCore.QTimer.singleShot(0, finish) + loop.exec() def record(object_): @@ -1924,8 +1930,10 @@ def runModalPickerProbe(iterations=100): with isolatedSettings(), tempfile.TemporaryDirectory() as directory: filename = Path(directory) / 'fixture.json' + for _ in range(iterations): filename.write_text('keep', encoding='utf-8') + parent = QWidget() editor = TextEditorWindow(parent) editor.jsonEditor.setPlainText('{"fixture": true}') @@ -1942,6 +1950,7 @@ def runModalPickerProbe(iterations=100): assert not isValid(editor) assert filename.read_text(encoding='utf-8') == 'keep' + del editor, parent for directoryMode in (False, True): @@ -1956,12 +1965,14 @@ def runModalPickerProbe(iterations=100): return str(filename) if directoryMode else (str(filename), '') method = 'getExistingDirectory' if directoryMode else 'getOpenFileName' + with mock.patch.object( pathModule.QFileDialog, method, side_effect=pathSelection ): binding.browse() assert not isValid(binding._input) + deleteQObject(binding._title) del binding, container @@ -1982,8 +1993,11 @@ def runModalPickerProbe(iterations=100): window.xrayAssetListView, 'appendNewItem' ) as append: window.appendNewItem() + append.assert_not_called() + assert not isValid(window) + del window, parent action = importModule.ImportFromFileAction() @@ -1998,14 +2012,19 @@ def runModalPickerProbe(iterations=100): ): with mock.patch.object(importModule, 'profileFromAny') as parse: action.triggeredCallback(False) + parse.assert_not_called() + assert not isValid(action) + del action processQtEvents() assert len(destroyed) == iterations * 5 + collectAtBoundary() + assert all(reference() is None for reference in references) assert not AppQDialog._openDialogs diff --git a/tests/test_frozenlib.py b/tests/test_frozenlib.py index c552866a..73cac21a 100644 --- a/tests/test_frozenlib.py +++ b/tests/test_frozenlib.py @@ -65,6 +65,7 @@ class FrozenlibQtContextTest(unittest.TestCase): first = QtCore.QObject() second = QtCore.QObject() destroyed = QtCore.QObject() + deleteQObject(destroyed) try: diff --git a/tests/test_qt_lifetime.py b/tests/test_qt_lifetime.py index 14db3a82..ca1dc4d6 100644 --- a/tests/test_qt_lifetime.py +++ b/tests/test_qt_lifetime.py @@ -121,6 +121,7 @@ class ToolbarLifetimeTest(unittest.TestCase): if borrowed: separator.setParent(owner) + toolbar = AppQToolBar(action, separator, parent=owner) try: @@ -128,6 +129,7 @@ class ToolbarLifetimeTest(unittest.TestCase): self.assertIs( separator.parent(), owner if borrowed else toolbar ) + toolbar.actionTriggered.emit(action) deleteQObject(toolbar) @@ -155,6 +157,7 @@ class ModalPickerLifetimeTest(unittest.TestCase): loop.quit() QtCore.QTimer.singleShot(0, finish) + loop.exec() def testTextSaveDoesNotTruncateFileAfterWindowDestruction(self): @@ -165,6 +168,7 @@ class ModalPickerLifetimeTest(unittest.TestCase): with isolatedSettings(), tempfile.TemporaryDirectory() as directory: filename = Path(directory) / 'existing.json' filename.write_text('keep original bytes', encoding='utf-8') + parent = QWidget() editor = TextEditorWindow(parent) editor.jsonEditor.setPlainText('{"updated": true}') @@ -238,6 +242,7 @@ with isolatedSettings(): editor = TextEditorWindow(parent) editor.currentIndex = 0 editor.jsonEditor.setPlainText('{"server": "after"}') + rows = [ ServerProfile.fromConfiguration( CoreConfiguration({'server': 'before'}) @@ -309,6 +314,7 @@ with isolatedSettings(): method = ( 'getExistingDirectory' if directoryMode else 'getOpenFileName' ) + try: with mock.patch.object( module.QFileDialog, method, side_effect=select @@ -342,6 +348,7 @@ with isolatedSettings(): window.xrayAssetListView, 'appendNewItem' ) as append: window.appendNewItem() + append.assert_not_called() self.assertFalse(isValid(window)) @@ -354,6 +361,7 @@ with isolatedSettings(): with isolatedSettings(), tempfile.TemporaryDirectory() as directory: filename = Path(directory) / 'input.json' filename.write_text('{}', encoding='utf-8') + action = ImportFromFileAction() mainWindow = mock.Mock() @@ -370,6 +378,7 @@ with isolatedSettings(): with mock.patch.object(module, 'profileFromAny') as parse: with mock.patch.object(module, 'MBoxImportSuccess'): action.triggeredCallback(False) + parse.assert_not_called() mainWindow.appendNewItemByFactory.assert_not_called()