diff --git a/tests/README.md b/tests/README.md index bbc5db5..2b0082b 100644 --- a/tests/README.md +++ b/tests/README.md @@ -1,10 +1,12 @@ # Furious test suite -The suite uses Python's built-in `unittest` runner. Qt tests select the -`offscreen` platform before importing PySide6, construct one deliberately small -test `QApplication`, and route `QSettings` to a unique temporary directory. -They do not initialize Furious's singleton IPC server, production repositories, -system proxy, TUN, routing, update network clients, or real proxy cores. +The suite uses Python's built-in `unittest` runner. In the main test process, +Qt tests use one deliberately small `QApplication` and route `QSettings` to a +unique temporary directory. Focused lifecycle regressions may start a hermetic +child Python process, and external-runtime tests use that child interpreter as +a harmless stand-in for a core. Tests do not initialize Furious's singleton IPC +server, production repositories, system proxy, TUN, routing, update network +clients, or real proxy cores. ## Coverage map @@ -12,10 +14,11 @@ system proxy, TUN, routing, update network clients, or real proxy cores. | --- | --- | | Configuration, profiles, migration, repositories | `test_models_and_services.py` | | Low-level application, runtime, editor, and storage contracts | `test_interface.py` | +| Application composition, startup rollback, and connection ownership | `test_architecture_refactors.py` | | Plugin registration, capability dispatch, factories, rollback | `test_plugin_architecture.py` | | Controller state and error transitions with injected runtimes | `test_controllers.py` | -| SOCKS/SIP002 Shadowsocks codecs and generated round trips | `test_socks_uri.py`, `test_shadowsocks_uri.py` | -| Subscription-group reconciliation and ownership isolation | `test_subscription_sync.py` | +| SOCKS and SIP002 Shadowsocks codecs and generated round trips | `test_socks_uri.py`, `test_shadowsocks_uri.py` | +| Subscription workflow, timers, stale requests, and reconciliation | `test_subscription_manager.py`, `test_subscription_sync.py` | | External process launch, output, shutdown, threads, TUN metadata | `test_external_core.py` | | Xray/Hysteria2 native-TUN ownership and proxy-only stripping | `test_native_tun_semantics.py` | | Rolling metrics, stable buckets, lazy rendering, and hover | `test_metrics_behavior.py` | @@ -38,7 +41,14 @@ than by starting the production application runtime. ## Commands -From the repository root, select the offscreen Qt platform for your shell. +Run commands from the repository root with the project and its runtime +dependencies installed in the active environment. If the root contains a +`.venv*` or `venv*` environment, activate it first; the examples intentionally +use its `python` command rather than a machine-specific interpreter path. + +Select the offscreen Qt platform **before Python starts**. Some test modules +import Qt-backed Furious modules before `tests.support` can apply its defensive +default, so setting it only after test discovery begins is too late. Windows PowerShell: @@ -58,15 +68,14 @@ Linux, macOS, and other Unix-compatible shells: export QT_QPA_PLATFORM=offscreen ``` -Then run the desired test tier. These commands use the active Python -environment, so activate the project's virtual environment first when needed. +Then run the desired test tier. ```text -# Everything +# Complete suite, including repeated lifetime/process stress tests python -m unittest discover -s tests -v -# Fast logic, persistence, plugin, controller, codec, and UI behavior -python -m unittest tests.test_interface tests.test_models_and_services tests.test_plugin_architecture tests.test_controllers tests.test_subscription_sync tests.test_socks_uri tests.test_shadowsocks_uri tests.test_native_tun_semantics tests.test_metrics_behavior tests.test_endpoint_info tests.test_service_runtime tests.test_frozenlib tests.test_isolation_and_navigation tests.test_ui_behavior -v +# Regular logic, persistence, plugin, controller, codec, and UI regressions +python -m unittest tests.test_interface tests.test_models_and_services tests.test_architecture_refactors tests.test_plugin_architecture tests.test_controllers tests.test_subscription_manager tests.test_subscription_sync tests.test_socks_uri tests.test_shadowsocks_uri tests.test_native_tun_semantics tests.test_metrics_behavior tests.test_endpoint_info tests.test_service_runtime tests.test_frozenlib tests.test_isolation_and_navigation tests.test_ui_behavior -v # Direct Qt/process integration and destruction/lifetime checks python -m unittest tests.test_external_core tests.test_qt_lifetime -v @@ -74,21 +83,38 @@ python -m unittest tests.test_external_core tests.test_qt_lifetime -v # Explicit slow stress tier python -m unittest tests.test_qt_stress tests.test_process_stress -v -# Order-independence spot check (reverse the module order, then run all tests) -python -m unittest tests.test_ui_behavior tests.test_isolation_and_navigation tests.test_metrics_behavior tests.test_subscription_sync tests.test_controllers tests.test_plugin_architecture tests.test_models_and_services -v +# Shared-state order-independence spot check +python -m unittest tests.test_ui_behavior tests.test_isolation_and_navigation tests.test_frozenlib tests.test_service_runtime tests.test_endpoint_info tests.test_metrics_behavior tests.test_native_tun_semantics tests.test_shadowsocks_uri tests.test_socks_uri tests.test_subscription_sync tests.test_subscription_manager tests.test_controllers tests.test_plugin_architecture tests.test_architecture_refactors tests.test_models_and_services tests.test_interface -v python -m unittest discover -s tests -v ``` -No external network access or installed Xray/Hysteria executable is required. +To run one module, class, or method while developing, pass its dotted test name +to the same runner, for example: + +```text +python -m unittest tests.test_endpoint_info -v +python -m unittest tests.test_endpoint_info.EndpointInfoServiceTest -v +``` + +No external network access or separately installed Xray/Hysteria executable is +required. Process-boundary tests create only temporary scripts and launch the +active Python interpreter. Two regressions cover Windows-specific behavior and +are skipped on other platforms; native handle/RSS trend checks use the safe +platform counters available on the current host. + +Several negative-path tests intentionally emit warning or error log messages, +and the stress tier prints resource summaries. Treat the runner's final status +and exit code as authoritative; expected diagnostic output still ends in +`OK`. ## Packaged-build smoke procedure -Packaged/Nuitka builds cannot be safely driven by these in-process `unittest` -fixtures. For an optional release smoke check, use an otherwise disposable test -OS account or VM, redirect all Furious application-data/settings locations to a -temporary directory, and keep system proxy and TUN disabled. Open and close each -transient editor family 50 times, verify one reusable `TextEditorWindow` does -not duplicate actions, and compare live-object diagnostics from an instrumented +Packaged/Nuitka behavior is outside the source-level `unittest` fixtures. For an +optional release smoke check, use an otherwise disposable test OS account or +VM, redirect all Furious application-data/settings locations to a temporary +directory, and keep system proxy and TUN disabled. Open and close each transient +editor family 50 times, verify one reusable `TextEditorWindow` does not +duplicate actions, and compare live-object diagnostics from an instrumented build before/after the loop. Do not run this procedure against a production profile or rely on process-name cleanup; close only the exact packaged process started for the smoke test. @@ -96,6 +122,8 @@ started for the smoke test. ## Isolation rules - Tests clean up only exact subprocess handles/PIDs and threads they create. +- Child-process lifecycle tests disable single-instance discovery and host + integration before running a real Qt event loop. - Tests never search for, signal, or terminate another Furious/core process. - Persistence tests use temporary INI-backed `QSettings` namespaces. - Controller tests inject fake runtime managers and patch host-mutation APIs. diff --git a/tests/test_plugin_architecture.py b/tests/test_plugin_architecture.py index 238bdd6..8651e09 100644 --- a/tests/test_plugin_architecture.py +++ b/tests/test_plugin_architecture.py @@ -72,6 +72,7 @@ class FixtureProtocolHandler(ProtocolHandler): 'FIXTURE', 'Fixture', 'Add Fixture...', + 'Add Fixture', subscriptionImportable=True, ) schemes = ('fixture',) @@ -244,6 +245,14 @@ class PluginRegistryTest(unittest.TestCase): self.registry.capability(CapabilityKind.Protocol, 'FIXTURE'), self.plugin.capabilities[0], ) + self.assertEqual( + self.plugin.capabilities[0].descriptor.addActionText, + 'Add Fixture...', + ) + self.assertEqual( + self.plugin.capabilities[0].descriptor.editorWindowTitle, + 'Add Fixture', + ) self.assertEqual( self.registry.pluginsWithCapability(CapabilityKind.CoreRuntimeFactory), (self.plugin,), @@ -352,7 +361,12 @@ class PluginRollbackTest(unittest.TestCase): class Handler(FixtureProtocolHandler): """Use a distinct protocol ID with the same scheme.""" - descriptor = ProtocolDescriptor('OTHER', 'Other', 'Add Other') + descriptor = ProtocolDescriptor( + 'OTHER', + 'Other', + 'Add Other...', + 'Add Other', + ) capabilities = (Handler(),) diff --git a/tests/test_ui_behavior.py b/tests/test_ui_behavior.py index f9eafbc..7682cfe 100644 --- a/tests/test_ui_behavior.py +++ b/tests/test_ui_behavior.py @@ -24,8 +24,15 @@ from Furious.Backends.ExternalCore.Configuration import ( ConfigExternalCore, ) from Furious.Backends.ExternalCore.Editor import ExternalCoreEditor +from Furious.Backends.Hysteria1.Editor import Hysteria1Editor +from Furious.Backends.Hysteria2.Editor import Hysteria2Editor from Furious.Backends.Xray.AssetListWidget import XrayAssetListWidget from Furious.Backends.Xray.RoutingWindow import RoutingRulesDialog +from Furious.Backends.Xray.ShadowsocksEditor import ShadowsocksEditor +from Furious.Backends.Xray.SocksEditor import SocksEditor +from Furious.Backends.Xray.TrojanEditor import TrojanEditor +from Furious.Backends.Xray.VlessEditor import VlessEditor +from Furious.Backends.Xray.VmessEditor import VmessEditor from Furious.Actions.Connection import ConnectAction from Furious.Controllers.ConnectionController import ( ConnectionController, @@ -35,9 +42,9 @@ from Furious.Controllers.SettingsController import ( LOG_AUTO_CLEAR_SETTING, LOG_AUTO_SCROLL_DOWN_SETTING, ) -from Furious.Frozenlib import AppSettings +from Furious.Frozenlib import AppSettings, Mixins from Furious.Models import ProfileMetadata, ServerProfile -from Furious.Qt import AppHue, AppQMessageBox, AppQSwitch +from Furious.Qt import AppHue, AppQMessageBox, AppQSwitch, gettext as _ from Furious.Service import ( APPLICATION_LOG_CATEGORY, CORE_LOG_CATEGORY, @@ -144,6 +151,38 @@ class EditorMappingTest(unittest.TestCase): editor.close() + def testEveryProtocolEditorRetranslatesItsDedicatedWindowTitle(self): + """Retain title source text when switching from Chinese to English.""" + editorTypes = ( + (ExternalCoreEditor, 'Add External Core'), + (Hysteria1Editor, 'Add Hysteria1 Server'), + (Hysteria2Editor, 'Add Hysteria2 Server'), + (ShadowsocksEditor, 'Add Shadowsocks Server'), + (SocksEditor, 'Add SOCKS Server'), + (TrojanEditor, 'Add Trojan Server'), + (VlessEditor, 'Add VLESS Server'), + (VmessEditor, 'Add VMess Server'), + ) + + with isolatedSettings(): + AppSettings.set('Language', 'ZH') + editors = [] + + for editorType, sourceTitle in editorTypes: + editor = editorType(windowTitle=_(sourceTitle)) + editors.append((editor, sourceTitle)) + + self.assertEqual(editor.windowTitle(), _(sourceTitle)) + + AppSettings.set('Language', 'EN') + + Mixins.QTranslatable.retranslateAll() + + for editor, sourceTitle in editors: + self.assertEqual(editor.windowTitle(), sourceTitle) + + editor.close() + def testSubscriptionEditorNormalizesPresentationValues(self): """Return one complete subscription record from its visual controls.""" with isolatedSettings():