Files
LorenEteval_Furious/tests/test_interface.py
T
Loren Eteval b15fa6bfe6 Remove obsolete unsupported-platform exit path
Route startup failures through the normal fatal-error flow now that
desktop startup no longer rejects systems without a tray. Remove the
unused semantic exit code and its generated translations and tests.

Signed-off-by: Loren Eteval <loren.eteval@proton.me>
2026-08-24 18:41:14 +08:00

282 lines
10 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Copyright (C) 2024–present 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/>.
"""Verify the low-level application, runtime, editor, and storage contracts."""
from __future__ import annotations
from Furious.Frozenlib.Constants import PLATFORM
from Furious.Interface import (
ApplicationRunner,
CoreRuntime,
EditorBinding,
EditorWidgetBinding,
StorageBackend,
)
from Furious.Models import CoreConfiguration
from Furious.Models.Encoding import UJSONEncoder
import json
import unittest
from unittest import mock
class RuntimeFixture(CoreRuntime):
"""Provide a side-effect-free runtime implementation for contract tests."""
@staticmethod
def name() -> str:
"""Return the fixture runtime name."""
return 'Test runtime'
@staticmethod
def version() -> str:
"""Return the fixture runtime version."""
return '1.0'
def start(self, *args, **kwargs) -> bool:
"""Report a successful fixture start."""
return True
def stop(self):
"""Stop the fixture runtime."""
class EditorFixture(EditorWidgetBinding):
"""Provide a minimal editor binding with stable widget ownership."""
def __init__(self):
"""Initialize the fixture editor."""
super().__init__()
self.value = None
self._widgets = (object(), object())
def inputToFactory(self):
"""Return the current fixture input."""
return self.value
def factoryToInput(self, value):
"""Load a fixture value into the editor."""
self.value = value
def widgets(self):
"""Return the stable fixture widgets."""
return self._widgets
class StorageFixture(StorageBackend):
"""Provide a mutable in-memory repository for contract tests."""
def __init__(self):
"""Initialize the fixture repository."""
self.collection = []
self.persisted = None
def sync(self):
"""Record a snapshot of the live collection."""
self.persisted = list(self.collection)
def data(self):
"""Return the intentionally live mutable collection."""
return self.collection
class InterfaceContractTest(unittest.TestCase):
"""Exercise stable behavior at the low-level interface boundary."""
def testApplicationAndRuntimeExitCodesRemainStable(self):
"""Protect process exit values consumed by callers and host runtimes."""
self.assertEqual(ApplicationRunner.ExitCode.ExitSuccess.value, 0)
self.assertEqual(ApplicationRunner.ExitCode.UnknownException.value, 61)
self.assertEqual(ApplicationRunner.ExitCode.AssertionError.value, 63)
self.assertEqual(CoreRuntime.ExitCode.ConfigurationError.value, 23)
self.assertEqual(
CoreRuntime.ExitCode.ServerStartFailure.value,
4294967295 if PLATFORM == 'Windows' else 255,
)
def testCoreRuntimeExitCallback(self):
"""Keep the runtime callback argument order stable."""
observed = []
runtime = RuntimeFixture(
exitCallback=lambda runtime, exitcode: observed.append((runtime, exitcode))
)
runtime.callExitCallback(7)
self.assertEqual(observed, [(runtime, 7)])
def testPlainDictionarySerializationUsesSharedUJSONEncoder(self):
"""Reuse the shared high-performance encoder for plain dictionaries."""
runtime = RuntimeFixture()
document = {'name': '节点', 'url': 'https://example.com/path'}
with mock.patch(
'Furious.Interface.Runtime.UJSONEncoder.encode',
wraps=UJSONEncoder.encode,
) as encode:
encoded = runtime.toJSONString(document)
encode.assert_called_once_with(document)
self.assertEqual(json.loads(encoded), document)
self.assertEqual(runtime.startError(), '')
def testForwardSlashEscapingRemainsCompatible(self):
"""Preserve the existing optional forward-slash escaping convention."""
runtime = RuntimeFixture()
encoded = runtime.toJSONString(
{'url': 'https://example.com'}, escape_forward_slashes=True
)
self.assertIn(r'https:\/\/example.com', encoded)
self.assertEqual(json.loads(encoded), {'url': 'https://example.com'})
def testDictionarySubclassSerializerRemainsAuthoritative(self):
"""Allow CoreConfiguration-style dictionary subclasses to serialize themselves."""
class Configuration(dict):
def __init__(self):
super().__init__()
self.kwargs = None
def toJSONString(self, **kwargs):
self.kwargs = kwargs
return '{"custom": true}'
runtime = RuntimeFixture()
configuration = Configuration()
encoded = runtime.toJSONString(configuration, indent=2)
self.assertEqual(encoded, '{"custom": true}')
self.assertEqual(configuration.kwargs, {'indent': 2})
self.assertEqual(runtime.startError(), '')
def testSerializationFailuresPopulateStartErrorAndLogDetails(self):
"""Keep the legacy empty return while making failures diagnosable."""
runtime = RuntimeFixture()
with self.assertLogs('Furious.Interface.Runtime', level='ERROR'):
encoded = runtime.toJSONString({'value': object()})
self.assertEqual(encoded, '')
self.assertEqual(runtime.startError(), 'Invalid server configuration')
with self.assertLogs('Furious.Interface.Runtime', level='ERROR'):
encoded = runtime.toJSONString(object())
self.assertEqual(encoded, '')
self.assertEqual(runtime.startError(), 'Invalid server configuration')
def testCoreConfigurationSerializationDiagnosticReachesRuntime(self):
"""Forward the model serializer's concrete failure to startup callers."""
runtime = RuntimeFixture()
configuration = CoreConfiguration({'value': object()})
with self.assertLogs('Furious.Interface.Runtime', level='ERROR'):
encoded = runtime.toJSONString(configuration)
self.assertEqual(encoded, '')
self.assertEqual(runtime.startError(), configuration.serializationError())
def testSuccessfulSerializationClearsPreviousStartError(self):
"""Expose only the failure from the current launch preparation attempt."""
runtime = RuntimeFixture()
runtime.setStartError('old failure')
self.assertEqual(runtime.toJSONString('{}'), '{}')
self.assertEqual(runtime.startError(), '')
def testEmbeddedRuntimeLaunchSpecsExposeSerializationFailures(self):
"""Give the connection layer a useful error for every embedded runtime."""
from Furious.Backends.Hysteria1.Process import Hysteria1
from Furious.Backends.Hysteria2.Process import Hysteria2
from Furious.Backends.Xray.Process import XrayCore
launchAttempts = (
(XrayCore, lambda runtime: runtime.launchSpec(object())),
(Hysteria1, lambda runtime: runtime.launchSpec(object(), '', '')),
(Hysteria2, lambda runtime: runtime.launchSpec(object())),
)
for runtimeType, launch in launchAttempts:
with self.subTest(runtime=runtimeType.__name__):
runtime = object.__new__(runtimeType)
CoreRuntime.__init__(runtime)
with self.assertLogs('Furious.Interface.Runtime', level='ERROR'):
launchSpec = launch(runtime)
self.assertIsNone(launchSpec)
self.assertEqual(runtime.startError(), 'Invalid server configuration')
def testRepresentativeImplementationsShareCoreRuntimeContract(self):
"""Cover subprocess and multiprocessing-backed runtime implementations."""
from Furious.Backends.ExternalCore import ExternalCoreProcess
from Furious.Backends.Hysteria1.Process import Hysteria1
from Furious.Backends.Hysteria2.Process import Hysteria2
from Furious.Backends.Xray.Process import XrayCore
from Furious.Core import CoreProcessWorker, Tun2socks
for runtimeType in (
ExternalCoreProcess,
Hysteria1,
Hysteria2,
XrayCore,
Tun2socks,
):
with self.subTest(runtime=runtimeType.__name__):
self.assertTrue(issubclass(runtimeType, CoreRuntime))
for runtimeType in (Hysteria1, Hysteria2, XrayCore, Tun2socks):
with self.subTest(processBacked=runtimeType.__name__):
self.assertTrue(issubclass(runtimeType, CoreProcessWorker))
self.assertFalse(issubclass(ExternalCoreProcess, CoreProcessWorker))
def testEditorBindingDirectionAndWidgetIdentity(self):
"""Keep editor data flow explicit and widget references stable."""
editor = EditorFixture()
widgets = editor.widgets()
editor.factoryToInput('value')
self.assertIsInstance(editor, EditorBinding)
self.assertEqual(editor.inputToFactory(), 'value')
self.assertIs(editor.widgets(), widgets)
def testStorageDataIsTheLiveManagedCollection(self):
"""Document intentional mutation-before-sync repository behavior."""
storage = StorageFixture()
data = storage.data()
data.append('value')
storage.sync()
self.assertIs(data, storage.data())
self.assertEqual(storage.persisted, ['value'])
if __name__ == '__main__':
unittest.main()