Test backend editor compatibility

Signed-off-by: Loren Eteval <loren.eteval@proton.me>
This commit is contained in:
Loren Eteval
2026-08-21 12:21:30 +08:00
parent cc2cc02b98
commit c47397a0ce
4 changed files with 285 additions and 5 deletions
+3 -2
View File
@@ -20,6 +20,7 @@ clients, or real proxy cores.
| 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` |
| Backend structured-editor observational load and unknown-value preservation | `test_backend_editor_contract.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` |
| Proxy-only endpoint discovery, caching, and presentation | `test_endpoint_info.py` |
@@ -75,7 +76,7 @@ Then run the desired test tier.
python -m unittest discover -s tests -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
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_backend_editor_contract 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
@@ -84,7 +85,7 @@ python -m unittest tests.test_external_core tests.test_qt_lifetime -v
python -m unittest tests.test_qt_stress tests.test_process_stress -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 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_backend_editor_contract 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
```
+166
View File
@@ -0,0 +1,166 @@
# Copyright (C) 2024present Loren Eteval & contributors <loren.eteval@proton.me>
#
# This file is part of Furious.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
"""Protect observational loading and lossless backend editor round trips."""
from __future__ import annotations
from Furious.Backends.Configuration import ConfigXray
from Furious.Backends.Hysteria1.Editor import GuiHy1ItemBasicProtocol
from Furious.Backends.Xray.TlsEditor import GuiVTLSQGroupBox
from Furious.Backends.Xray.TransportEditor import GuiVTransportQGroupBox
from tests.support import application, collectAtBoundary
import copy
import unittest
class BackendEditorContractTest(unittest.TestCase):
"""Verify mature editors preserve values they do not yet understand."""
@classmethod
def setUpClass(cls):
"""Create the process-wide Qt application used by real editor widgets."""
application()
def tearDown(self):
"""Process deferred Qt deletion between real-widget tests."""
collectAtBoundary()
@staticmethod
def xrayConfiguration(streamSettings: dict) -> ConfigXray:
"""Return a minimal Xray document with one tagged proxy outbound."""
return ConfigXray(
{
'outbounds': [
{
'tag': 'proxy',
'protocol': 'vless',
'streamSettings': copy.deepcopy(streamSettings),
}
]
}
)
def testHysteria1UnknownProtocolIsVisibleAndPreserved(self):
"""Round-trip a future Hysteria 1 protocol through the shared combo."""
binding = GuiHy1ItemBasicProtocol(title='Protocol', translatable=False)
configuration = {'protocol': 'future-protocol'}
original = copy.deepcopy(configuration)
optionCount = binding._input.count()
binding.factoryToInput(configuration)
self.assertEqual(binding.text(), 'future-protocol')
self.assertEqual(binding._input.count(), optionCount)
self.assertEqual(binding._input.findText('future-protocol'), -1)
self.assertEqual(configuration, original)
self.assertFalse(binding.inputToFactory(configuration))
self.assertEqual(configuration, original)
for widget in binding.widgets():
widget.deleteLater()
def testXrayUnknownTransportIsVisibleAndPreserved(self):
"""Keep an unsupported transport and its settings untouched."""
configuration = self.xrayConfiguration(
{
'network': 'future-transport',
'futureTransportSettings': {
'token': 'preserve',
'futureField': 7,
},
}
)
original = copy.deepcopy(configuration)
group = GuiVTransportQGroupBox()
group.factoryToInput(configuration)
self.assertEqual(configuration, original)
self.assertEqual(
group.page(group.currentIndex()).networkText(), 'future-transport'
)
self.assertFalse(group.inputToFactory(configuration))
self.assertEqual(configuration, original)
group.deleteLater()
def testXrayTransportAliasIsNormalizedWhileLoading(self):
"""Normalize legacy upstream aliases while preserving their settings."""
configuration = self.xrayConfiguration(
{
'network': 'http',
'httpSettings': {
'host': ['example.com'],
'path': '/future-safe',
'futureField': {'preserve': True},
},
}
)
group = GuiVTransportQGroupBox()
group.factoryToInput(configuration)
self.assertEqual(
configuration.proxyStreamSettingsObject['network'],
'h2',
)
self.assertEqual(
configuration.proxyStreamSettingsObject['httpSettings'],
{
'host': ['example.com'],
'path': '/future-safe',
'futureField': {'preserve': True},
},
)
self.assertEqual(group.page(group.currentIndex()).networkText(), 'h2')
self.assertFalse(group.inputToFactory(configuration))
group.deleteLater()
def testXrayUnknownSecurityIsVisibleAndPreserved(self):
"""Keep unsupported TLS modes and sibling settings untouched."""
configuration = self.xrayConfiguration(
{
'network': 'tcp',
'security': 'future-security',
'future-securitySettings': {
'certificate': 'preserve',
'futureField': True,
},
}
)
original = copy.deepcopy(configuration)
group = GuiVTLSQGroupBox()
group.factoryToInput(configuration)
self.assertEqual(configuration, original)
self.assertEqual(
group.page(group.currentIndex())._containers[0].text(),
'future-security',
)
self.assertFalse(group.inputToFactory(configuration))
self.assertEqual(configuration, original)
group.deleteLater()
if __name__ == '__main__':
unittest.main()
+106 -3
View File
@@ -478,10 +478,113 @@ class Hysteria2CompatibilityTest(unittest.TestCase):
self.assertTrue(editor.inputToFactory(profile))
self.assertEqual(profile.connection['realm']['ipMode'], 'v6')
self.assertEqual(profile.connection['obfs']['type'], 'salamander')
self.assertEqual(
profile.connection['obfs']['future-obfs'],
{'token': 'discard'},
self.assertNotIn('future-obfs', profile.connection['obfs'])
editor.close()
def testUnknownCompactEditorValuesRoundTripUntouched(self):
"""Display and preserve future enum and tagged-union values verbatim."""
profile = self.profile(
{
'server': 'example.com:443',
'auth': 'secret',
'realm': {
'ipMode': 'future-mode',
'futureRealmField': {'preserve': True},
},
'congestion': {
'type': 'future-congestion',
'bbrProfile': 'future-profile',
'futureCongestionField': 7,
},
'obfs': {
'type': 'future-obfs',
'future-obfs': {
'token': 'preserve',
'futureOption': True,
},
},
}
)
original = copy.deepcopy(profile.connection)
editor = Hysteria2Editor()
editor.factoryToInput(profile)
self.assertEqual(profile.connection, original)
self.assertEqual(
self.binding(editor, ('realm', 'ipMode')).text(),
'future-mode',
)
self.assertEqual(
editor.basicGroup._containers[3].bindings[0].text(),
'future-congestion',
)
self.assertEqual(
editor.basicGroup._containers[3].bindings[1].text(),
'future-profile',
)
self.assertEqual(
editor.advancedGroup.obfsItem.page(
editor.advancedGroup.obfsItem.currentIndex()
).obfsTypeText(),
'future-obfs',
)
self.assertFalse(editor.inputToFactory(profile))
self.assertEqual(profile.connection, original)
editor.close()
def testClearingCongestionFieldPreservesUnknownSiblings(self):
"""Remove only the represented congestion leaf when clearing it."""
profile = self.profile(
{
'server': 'example.com:443',
'auth': 'secret',
'congestion': {
'type': 'bbr',
'bbrProfile': 'fast',
'futureCongestionField': {'preserve': True},
},
}
)
editor = Hysteria2Editor()
editor.factoryToInput(profile)
editor.basicGroup._containers[3].bindings[0].setText('')
self.assertTrue(editor.inputToFactory(profile))
self.assertNotIn('type', profile.connection['congestion'])
self.assertEqual(profile.connection['congestion']['bbrProfile'], 'fast')
self.assertEqual(
profile.connection['congestion']['futureCongestionField'],
{'preserve': True},
)
editor.close()
def testImplicitGeckoPacketDefaultsRemainAbsent(self):
"""Do not materialize effective Gecko defaults during an untouched save."""
profile = self.profile(
{
'server': 'example.com:443',
'auth': 'secret',
'obfs': {
'type': 'gecko',
'gecko': {
'password': 'secret',
'futureGeckoField': {'preserve': True},
},
},
}
)
original = copy.deepcopy(profile.connection)
editor = Hysteria2Editor()
editor.factoryToInput(profile)
self.assertFalse(editor.inputToFactory(profile))
self.assertEqual(profile.connection, original)
editor.close()
+10
View File
@@ -98,6 +98,9 @@ class EditorMappingTest(unittest.TestCase):
'environment': {'TOKEN': 'one=two', 'UNICODE': '测试'},
'useApplicationTun2socks': True,
'tunRemoteAddress': '2001:db8::42',
'futureExternalCoreField': {
'nested': ['preserve', 7],
},
}
)
profile = ServerProfile.fromConfiguration(
@@ -107,8 +110,11 @@ class EditorMappingTest(unittest.TestCase):
with isolatedSettings():
editor = ExternalCoreEditor()
original = copy.deepcopy(profile.connection)
editor.factoryToInput(profile)
self.assertEqual(profile.connection, original)
self.assertEqual(
editor._argumentsInput.values(),
['--config', 'name with spaces.json'],
@@ -148,6 +154,10 @@ class EditorMappingTest(unittest.TestCase):
profile.connection.tunRemoteAddress(),
'server.example.com',
)
self.assertEqual(
profile.connection['futureExternalCoreField'],
{'nested': ['preserve', 7]},
)
editor.close()