mirror of
https://github.com/LorenEteval/Furious.git
synced 2026-10-08 22:59:48 +03:00
Check supported Python versions
Signed-off-by: Loren Eteval <loren.eteval@proton.me>
This commit is contained in:
+6
-3
@@ -57,9 +57,12 @@ Read `.github/workflows/deploy-pypi.yml` with `tests/README.md`; paths are relat
|
||||
- Validate YAML and every affected expression/shell. Trace each changed matrix row through dependency installation,
|
||||
source/native import checks, Nuitka/installer output, packaged architecture/dependency checks, artifact upload, and tag
|
||||
gates. When a target cannot run locally, add a narrow CI assertion that fails before publication with a useful reason.
|
||||
- `workflows/source-tests.yml` runs isolated source unittest discovery on Windows, Linux, and macOS and is a
|
||||
required dependency of PyPI publication through `workflows/deploy-pypi.yml`. It can also run manually. Hourly binary
|
||||
builds retain their separate artifact scope. Source tests do not establish packaged behavior or Python/Qt
|
||||
- `workflows/source-tests.yml` separates full cross-platform source unittest discovery from focused Linux
|
||||
interpreter-version checks. The latter use compatible Qt/native wheel pins, dependency consistency, native-binding
|
||||
imports without starting runtimes, application compilation, cold imports and real compatibility behavior. These
|
||||
checks do not certify the entire suite on every version.
|
||||
The reusable workflow is a required dependency of PyPI publication through `workflows/deploy-pypi.yml`, so its
|
||||
version matrix must also pass. It can run manually. Hourly binary builds retain their separate artifact scope. Source tests do not establish packaged behavior or Python/Qt
|
||||
floors beyond their matrix. Do not call an artifact build a regression-test pass; use `tests/README.md` for
|
||||
source verification. The source suite's offscreen Qt environment exercises widgets and event delivery, not native
|
||||
tray integration, privilege prompts, or an installed application's host effects. Follow actual `needs` and tag gates
|
||||
|
||||
@@ -31,3 +31,57 @@ jobs:
|
||||
run: python -m pip install -r requirements.txt "PySide6-Essentials==6.8.3" "PySide6-Addons==6.8.3"
|
||||
- name: Run isolated behavioral and resource tests
|
||||
run: python -m unittest discover -s tests -v
|
||||
|
||||
python-compatibility:
|
||||
name: Python ${{ matrix.python }} compatibility
|
||||
runs-on: ubuntu-22.04
|
||||
timeout-minutes: 20
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- python: "3.8"
|
||||
pyside6: "6.6.3.1"
|
||||
- python: "3.9"
|
||||
pyside6: "6.8.3"
|
||||
- python: "3.10"
|
||||
pyside6: "6.8.3"
|
||||
- python: "3.11"
|
||||
pyside6: "6.8.3"
|
||||
- python: "3.12"
|
||||
pyside6: "6.8.3"
|
||||
- python: "3.13"
|
||||
pyside6: "6.8.3"
|
||||
- python: "3.14"
|
||||
pyside6: "6.10.3"
|
||||
env:
|
||||
QT_QPA_PLATFORM: offscreen
|
||||
PYTHONUTF8: "1"
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- uses: actions/setup-python@v7
|
||||
with:
|
||||
python-version: ${{ matrix.python }}
|
||||
- name: Install Linux Qt runtime libraries
|
||||
run: sudo apt-get update && sudo apt-get install -y libegl1 libopengl0
|
||||
- name: Install interpreter-compatible dependencies
|
||||
run: >-
|
||||
python -m pip install --only-binary=:all: -r requirements.txt
|
||||
"PySide6-Essentials==${{ matrix.pyside6 }}"
|
||||
"PySide6-Addons==${{ matrix.pyside6 }}"
|
||||
"zxing-cpp==2.2.0; python_version == '3.8'"
|
||||
- name: Check installed dependency consistency
|
||||
run: python -m pip check
|
||||
- name: Check native binding imports without starting runtimes
|
||||
run: |
|
||||
python - <<'PY'
|
||||
from importlib import import_module
|
||||
|
||||
for name in ('xray', 'hysteria', 'hysteria2', 'tun2socks', 'sing_tun'):
|
||||
import_module(name)
|
||||
print('Native binding imports passed; no runtime or TUN started')
|
||||
PY
|
||||
- name: Compile application source on this interpreter
|
||||
run: python -m compileall -q Furious Translation.py
|
||||
- name: Check cold imports and compatibility behavior
|
||||
run: python -m unittest tests.test_public_api tests.test_python_compatibility -v
|
||||
|
||||
+18
-4
@@ -15,9 +15,23 @@ below. To reproduce the dependency selection in
|
||||
python -m pip install -r requirements.txt "PySide6-Essentials==6.8.3" "PySide6-Addons==6.8.3"
|
||||
```
|
||||
|
||||
CI uses Python 3.13. Linux CI also installs `libegl1` and `libopengl0` for Qt.
|
||||
Backend Python packages in `requirements.txt` are import dependencies; tests do
|
||||
not require separately installed proxy-core executables or a working proxy.
|
||||
The full CI source suite uses Python 3.13 on Windows, Linux, and macOS.
|
||||
A separate Linux job checks Python 3.8 through 3.14 with compatible Qt pins:
|
||||
6.6.3.1 for Python 3.8, 6.8.3 for 3.9–3.13, and 6.10.3 for 3.14. Python 3.8
|
||||
also pins zxing-cpp 2.2.0, which has a published wheel for that interpreter.
|
||||
The job checks dependency consistency and harmless native-binding imports,
|
||||
compiles application source, then runs:
|
||||
|
||||
```text
|
||||
python -m unittest tests.test_public_api tests.test_python_compatibility -v
|
||||
```
|
||||
|
||||
These focused checks exercise cold imports, metadata, exact string operations,
|
||||
and real executor cancellation. They do not run the entire behavioral suite
|
||||
on every interpreter. Python 3.8 runs legacy branches; native-3.9-only cases
|
||||
are skipped there and exercised by the newer rows. Linux jobs install
|
||||
`libegl1` and `libopengl0` for Qt. Backend packages are import dependencies;
|
||||
tests require neither separate proxy-core executables nor a working proxy.
|
||||
|
||||
Select the offscreen platform **before starting Python**. Some test modules
|
||||
import Qt-backed Furious modules before `tests.support` can set its defensive
|
||||
@@ -92,7 +106,7 @@ worker. Choose tests by the changed contract rather than by filename alone.
|
||||
| [test_controllers.py](test_controllers.py) | Connection state/error/reconnect transitions, reentrant cancellation/replacement before launch and during completion, startup restoration, shared settings, routing fallback persistence and tray/selector agreement after custom-routing disable/re-enable. |
|
||||
| [test_runtime_lifecycle.py](test_runtime_lifecycle.py) | Qt-thread exit dispatch, commit/exit races, duplicate and late exits, idempotent release and retained failure/retry ownership, failed reap/handle close, spawn failure, queue/timer disposal on preparation failure. |
|
||||
| [test_external_core.py](test_external_core.py) | Harmless real process launch/output/shutdown, partial thread-start rollback, non-finite timeout rejection, failed reap/thread-join retry, readiness/TUN metadata, Windows paths with spaces, subscription rejection of executable profiles, bounded DNS references. |
|
||||
| [test_python_compatibility.py](test_python_compatibility.py) | Once-only implementation selection without import-time metadata enumeration, exact affix removal, both metadata entry-point APIs and plugin discovery, distribution-version failures, real modern/legacy executor cancellation and statistics-owner shutdown, and profile import with an unsubscriptable collections ABC. Legacy API shapes are simulated; this does not replace execution on the minimum interpreter. |
|
||||
| [test_python_compatibility.py](test_python_compatibility.py) | Once-only implementation selection without import-time metadata enumeration, exact affix removal, both metadata entry-point APIs and plugin discovery, distribution-version failures, real modern/legacy executor cancellation and statistics-owner shutdown, and profile import with an unsubscriptable collections ABC. Runs actual interpreter APIs alongside legacy-shape simulations; native-only cases skip on Python 3.8. The version matrix executes this module with the cold-import checks. |
|
||||
| [test_frozenlib.py](test_frozenlib.py) | Nested state guards, cleanup isolation, native peer destruction during connection/theme/translation/cleanup notifications, bounded caches/throttling, dual-stack probe selection, mocked proxy/DNS/routes/startup/session boundaries and failure handling. |
|
||||
| [test_native_tun_semantics.py](test_native_tun_semantics.py) | Xray/Hysteria2 runtime-copy TUN preservation/replacement, managed-TUN failures, download-test stripping, prevention of a second tun2socks owner. |
|
||||
| [test_sing_tun.py](test_sing_tun.py) | tun2socks engine default, saved engine choices and ownership-aware reconnect notices, sing-tun Go stack default, development-stack and congestion-control compatibility, independent backend settings and host defaults, selected-only snapshots, captured TUN log sources in both startup paths, staged settings and malformed storage, SOCKS authentication/UDP, harmless real spawn/status/stop, native readiness, cancellation, synchronous compatibility, mocked DNS/routing ownership including development IPv6 rule ordering and refused cleanup. |
|
||||
|
||||
@@ -27,11 +27,12 @@ from Furious.Service.TrafficStatsManager import TrafficStatsManager
|
||||
from shiboken6 import delete as deleteQObject
|
||||
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
from importlib import metadata
|
||||
from importlib import import_module, metadata
|
||||
from pathlib import Path
|
||||
from types import SimpleNamespace
|
||||
from unittest import mock
|
||||
|
||||
import sys
|
||||
import threading
|
||||
import unittest
|
||||
|
||||
@@ -42,6 +43,9 @@ from tests.support import (
|
||||
runPythonChild,
|
||||
)
|
||||
|
||||
RegistryModule = import_module('Furious.Plugins.Registry')
|
||||
TrafficStatsManagerModule = import_module('Furious.Service.TrafficStatsManager')
|
||||
|
||||
|
||||
class _LegacyExecutor:
|
||||
"""Expose the Python 3.8 shutdown signature over a real worker pool."""
|
||||
@@ -70,7 +74,7 @@ class _MetadataProvider:
|
||||
if not self.selectable:
|
||||
raise AttributeError('legacy metadata has no EntryPoints type')
|
||||
|
||||
return metadata.EntryPoints
|
||||
return object
|
||||
|
||||
|
||||
class PythonCompatibilityTest(unittest.TestCase):
|
||||
@@ -137,6 +141,9 @@ class PythonCompatibilityTest(unittest.TestCase):
|
||||
"""Repeated calls retain their selected APIs without new capability probes."""
|
||||
for modern in (False, True):
|
||||
with self.subTest(modern=modern):
|
||||
if modern and sys.version_info < (3, 9):
|
||||
self.skipTest('native affix methods require Python 3.9')
|
||||
|
||||
version = mock.MagicMock()
|
||||
version.__ge__.return_value = modern
|
||||
fetch = mock.Mock(return_value=() if modern else {})
|
||||
@@ -209,7 +216,7 @@ class PythonCompatibilityTest(unittest.TestCase):
|
||||
return {'furious.plugins': entries, 'unrelated': ()}
|
||||
|
||||
provider = _MetadataProvider(selectable, mock.Mock(side_effect=fetch))
|
||||
compatibility = self._loadCompatibility((3, 13), provider)[
|
||||
compatibility = self._loadCompatibility(sys.version_info, provider)[
|
||||
'PythonCompatibility'
|
||||
]
|
||||
|
||||
@@ -218,9 +225,8 @@ class PythonCompatibilityTest(unittest.TestCase):
|
||||
|
||||
registry = PluginRegistry()
|
||||
try:
|
||||
with mock.patch(
|
||||
'Furious.Plugins.Registry.PythonCompatibility',
|
||||
compatibility,
|
||||
with mock.patch.object(
|
||||
RegistryModule, 'PythonCompatibility', compatibility
|
||||
):
|
||||
registry.discover()
|
||||
|
||||
@@ -238,23 +244,36 @@ class PythonCompatibilityTest(unittest.TestCase):
|
||||
|
||||
self.assertEqual(provider.probes, 1)
|
||||
|
||||
def testInstalledMetadataUsesTheActualInterpreterAPI(self):
|
||||
"""Real metadata lookup works without opening any native TUN engine."""
|
||||
self.assertEqual(
|
||||
PythonCompatibility.entryPoints('furious.tests.nonexistent-entry-points'),
|
||||
(),
|
||||
)
|
||||
self.assertEqual(SingTUN.version(), metadata.version('sing-tun'))
|
||||
|
||||
def testDistributionVersionKeepsMissingMetadataBehavior(self):
|
||||
"""The engine reports unavailable only for missing distribution metadata."""
|
||||
target = 'Furious.Frozenlib.PythonCompatibility.metadata.version'
|
||||
|
||||
with mock.patch(target, return_value='0.9.7.dev0') as version:
|
||||
with mock.patch.object(
|
||||
metadata, 'version', return_value='0.9.7.dev0'
|
||||
) as version:
|
||||
self.assertEqual(SingTUN.version(), '0.9.7.dev0')
|
||||
version.assert_called_once_with('sing-tun')
|
||||
|
||||
with mock.patch(
|
||||
target, side_effect=PythonCompatibility.PackageNotFoundError('sing-tun')
|
||||
with mock.patch.object(
|
||||
metadata,
|
||||
'version',
|
||||
side_effect=PythonCompatibility.PackageNotFoundError('sing-tun'),
|
||||
):
|
||||
self.assertEqual(SingTUN.version(), 'unavailable')
|
||||
|
||||
with mock.patch(target, side_effect=ValueError('invalid metadata')):
|
||||
with mock.patch.object(
|
||||
metadata, 'version', side_effect=ValueError('invalid metadata')
|
||||
):
|
||||
with self.assertRaisesRegex(ValueError, 'invalid metadata'):
|
||||
SingTUN.version()
|
||||
|
||||
@unittest.skipIf(sys.version_info < (3, 9), 'native shutdown requires Python 3.9')
|
||||
def testModernShutdownDoesNotRetryUnrelatedTypeErrors(self):
|
||||
"""Native shutdown errors propagate without an unsafe fallback retry."""
|
||||
executor = mock.Mock()
|
||||
@@ -273,6 +292,9 @@ class PythonCompatibilityTest(unittest.TestCase):
|
||||
"""Both APIs reject new work, cancel queued calls, and allow a real drain."""
|
||||
for version in ((3, 8), (3, 9)):
|
||||
with self.subTest(version=version):
|
||||
if version >= (3, 9) and sys.version_info < (3, 9):
|
||||
self.skipTest('native shutdown requires Python 3.9')
|
||||
|
||||
started, release, queuedRan = (
|
||||
threading.Event(),
|
||||
threading.Event(),
|
||||
@@ -344,9 +366,8 @@ class PythonCompatibilityTest(unittest.TestCase):
|
||||
|
||||
compatibility = self._loadCompatibility((3, 8))['PythonCompatibility']
|
||||
|
||||
with mock.patch(
|
||||
'Furious.Service.TrafficStatsManager.PythonCompatibility',
|
||||
compatibility,
|
||||
with mock.patch.object(
|
||||
TrafficStatsManagerModule, 'PythonCompatibility', compatibility
|
||||
):
|
||||
manager._closeExecutor()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user