mirror of
https://github.com/LorenEteval/Furious.git
synced 2026-09-22 23:08:08 +03:00
Extend compiled lifetime probes
Signed-off-by: Loren Eteval <loren.eteval@proton.me>
This commit is contained in:
@@ -223,6 +223,15 @@ A null protected-list count means Nuitka does not expose that diagnostic; inspec
|
||||
installed package configuration and require zero live wrappers and registry entries
|
||||
instead.
|
||||
|
||||
The same standalone fixture also checks both HTTP clients across normal completion, early reply deletion, and
|
||||
manager-first deletion, retaining invalid wrappers deliberately to prove request context is released. It also
|
||||
checks action-owned versus explicitly widget-owned menus and representative asset delete/overwrite confirmations
|
||||
whose views are destroyed while their containing widget remains valid. Keep this harness self-contained so its
|
||||
compiled import graph does not pull in the full unittest modules.
|
||||
`test_qt_lifetime.py` covers 20 cycles for each of five confirmation workflows under each mocked Windows/Linux/macOS
|
||||
branch, plus 30 action/menu cycles for each ownership model. `test_service_runtime.py` additionally rejects duplicate
|
||||
HTTP completion and checks 30 early-deletion cycles per client without per-cycle garbage collection.
|
||||
|
||||
### Evolution regressions and CI
|
||||
|
||||
`test_repository_contracts.py` verifies all-or-nothing hydration and preservation of
|
||||
|
||||
+158
-1
@@ -21,13 +21,25 @@ from __future__ import annotations
|
||||
|
||||
from Furious.Backends import OFFICIAL_PLUGIN_TYPES
|
||||
from Furious.Backends.Xray.RoutingWindow import RoutingRulesDialog
|
||||
from Furious.Backends.Xray.AssetListView import XrayAssetListView
|
||||
import Furious.Backends.Xray.AssetListView as assetModule
|
||||
from Furious.Plugins import blankProfile, initializePluginRegistry
|
||||
from Furious.Qt import AppQDialog, AppQMessageBox, ThemeTransition, connectWeakly
|
||||
from Furious.Qt import (
|
||||
AppQAction,
|
||||
AppQMenu,
|
||||
AppQDialog,
|
||||
AppQMessageBox,
|
||||
ThemeTransition,
|
||||
connectWeakly,
|
||||
)
|
||||
from Furious.Qt.HttpGetManager import HttpGetManager
|
||||
from Furious.Service.EndpointInfoService import ProxyEndpointHttpClient
|
||||
from Furious.Widget.ServerTableView import ServerTableView
|
||||
|
||||
import PySide6
|
||||
|
||||
from PySide6 import QtCore
|
||||
from PySide6.QtNetwork import QNetworkReply
|
||||
from PySide6.QtWidgets import QWidget
|
||||
|
||||
from shiboken6 import isValid
|
||||
@@ -44,6 +56,8 @@ from collections import Counter
|
||||
import argparse
|
||||
import json
|
||||
import weakref
|
||||
from pathlib import Path
|
||||
import tempfile
|
||||
|
||||
PROTOCOL_PATTERNS = {
|
||||
'alternating': ('hysteria2', 'vless'),
|
||||
@@ -236,6 +250,78 @@ class _SignalEndpoint(QtCore.QObject):
|
||||
self.calls += 1
|
||||
|
||||
|
||||
class _PendingReply(QNetworkReply):
|
||||
"""Exercise request destruction without external network traffic."""
|
||||
|
||||
def __init__(self, parent):
|
||||
super().__init__(parent)
|
||||
self.open(QtCore.QIODevice.OpenModeFlag.ReadOnly)
|
||||
|
||||
def abort(self):
|
||||
self.setFinished(True)
|
||||
self.finished.emit()
|
||||
|
||||
def readData(self, maximumLength):
|
||||
return b''
|
||||
|
||||
|
||||
class _RequestPayload:
|
||||
"""Expose whether pending request context still owns plain operation data."""
|
||||
|
||||
|
||||
def runNetworkProbe(iterations=100):
|
||||
"""Verify native teardown releases both reply registries under compilation."""
|
||||
application()
|
||||
result = {}
|
||||
|
||||
for managerType, contextAttribute in (
|
||||
(HttpGetManager, '_replyContexts'),
|
||||
(ProxyEndpointHttpClient, '_pendingRequests'),
|
||||
):
|
||||
for terminal in ('finished', 'replyDestroyed', 'managerDestroyed'):
|
||||
references = []
|
||||
destroyed = []
|
||||
|
||||
for _ in range(iterations):
|
||||
manager = managerType()
|
||||
payload = _RequestPayload()
|
||||
reply = _PendingReply(manager)
|
||||
references.extend((weakref.ref(payload), weakref.ref(reply)))
|
||||
reply.destroyed.connect(lambda *_args: destroyed.append(True))
|
||||
manager.get = lambda _request: reply
|
||||
|
||||
if isinstance(manager, HttpGetManager):
|
||||
manager.webGET('https://invalid.test', payload=payload)
|
||||
else:
|
||||
manager.request('https://invalid.test', payload)
|
||||
|
||||
del manager.get
|
||||
del payload
|
||||
|
||||
if terminal == 'finished':
|
||||
reply.finished.emit()
|
||||
elif terminal == 'replyDestroyed':
|
||||
reply.deleteLater()
|
||||
else:
|
||||
manager.deleteLater()
|
||||
|
||||
processQtEvents()
|
||||
assert not isValid(reply)
|
||||
assert not getattr(manager, contextAttribute)
|
||||
|
||||
if isValid(manager):
|
||||
manager.deleteLater()
|
||||
processQtEvents()
|
||||
|
||||
del reply, manager
|
||||
|
||||
assert len(destroyed) == iterations
|
||||
assert all(reference() is None for reference in references)
|
||||
result[managerType.__name__ + ':' + terminal] = iterations
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def runInfrastructureProbe(iterations=100):
|
||||
"""Check signal, mask, and animation ownership under real Qt destruction."""
|
||||
application()
|
||||
@@ -385,6 +471,75 @@ def runInfrastructureProbe(iterations=100):
|
||||
return result
|
||||
|
||||
|
||||
def runConfirmationProbe(iterations=100):
|
||||
"""Check native menu ownership and representative view-owned prompts."""
|
||||
application()
|
||||
result = {}
|
||||
|
||||
for explicitOwner in (False, True):
|
||||
references = []
|
||||
destroyed = []
|
||||
for _ in range(iterations):
|
||||
owner = QWidget()
|
||||
menu = AppQMenu(parent=owner if explicitOwner else None)
|
||||
action = AppQAction('Menu fixture', menu=menu, parent=owner)
|
||||
references.append(weakref.ref(menu))
|
||||
menu.destroyed.connect(lambda *_args: destroyed.append(True))
|
||||
action.deleteLater()
|
||||
processQtEvents()
|
||||
assert not isValid(action)
|
||||
assert isValid(menu) == explicitOwner
|
||||
owner.deleteLater()
|
||||
processQtEvents()
|
||||
del action, menu, owner
|
||||
|
||||
assert len(destroyed) == iterations
|
||||
assert all(reference() is None for reference in references)
|
||||
result['widgetOwnedMenu' if explicitOwner else 'actionOwnedMenu'] = iterations
|
||||
|
||||
originalAssetDirectory = assetModule.XRAY_ASSET_DIR
|
||||
try:
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
assetModule.XRAY_ASSET_DIR = Path(directory)
|
||||
asset = Path(directory) / 'fixture.dat'
|
||||
asset.write_bytes(b'keep')
|
||||
for overwrite in (False, True):
|
||||
references = []
|
||||
destroyed = []
|
||||
for _ in range(iterations):
|
||||
owner = QWidget()
|
||||
view = XrayAssetListView(parent=owner)
|
||||
view.setCurrentIndex(view.model().index(0, 0))
|
||||
if overwrite:
|
||||
view.appendNewItem(str(asset))
|
||||
else:
|
||||
view.deleteSelectedItem()
|
||||
confirmation = next(iter(AppQDialog._openDialogs.values()))
|
||||
references.append(weakref.ref(confirmation))
|
||||
confirmation.destroyed.connect(
|
||||
lambda *_args: destroyed.append(True)
|
||||
)
|
||||
view.deleteLater()
|
||||
processQtEvents()
|
||||
assert not isValid(view) and not isValid(confirmation)
|
||||
assert isValid(owner)
|
||||
assert asset.read_bytes() == b'keep'
|
||||
owner.deleteLater()
|
||||
processQtEvents()
|
||||
del confirmation, view, owner
|
||||
|
||||
assert len(destroyed) == iterations
|
||||
assert all(reference() is None for reference in references)
|
||||
assert not AppQDialog._openDialogs
|
||||
result[
|
||||
'assetOverwriteOwnerFirst' if overwrite else 'assetDeleteOwnerFirst'
|
||||
] = iterations
|
||||
finally:
|
||||
assetModule.XRAY_ASSET_DIR = originalAssetDirectory
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def main():
|
||||
"""Run the probe as a standalone source or Nuitka executable."""
|
||||
parser = argparse.ArgumentParser()
|
||||
@@ -395,6 +550,8 @@ def main():
|
||||
parser.add_argument('--close-method', choices=CLOSE_METHODS, default='reject')
|
||||
arguments = parser.parse_args()
|
||||
|
||||
print(json.dumps(runConfirmationProbe(arguments.iterations), sort_keys=True))
|
||||
print(json.dumps(runNetworkProbe(arguments.iterations), sort_keys=True))
|
||||
print(json.dumps(runInfrastructureProbe(arguments.iterations), sort_keys=True))
|
||||
|
||||
print(
|
||||
|
||||
Reference in New Issue
Block a user