Fix platform-dependent regression test fixtures

Signed-off-by: Loren Eteval <loren.eteval@proton.me>
This commit is contained in:
Loren Eteval
2026-09-19 15:40:02 +08:00
parent d669c3aac6
commit cc7e7f56d5
9 changed files with 140 additions and 39 deletions
+16
View File
@@ -252,6 +252,22 @@ cover an aliased root and rejection of paths outside that root. The QR responsiv
queues unrelated work after generation starts and requires delivery before completion;
it does not assume a platform-specific order between zero-delay Qt timers.
On the CI Qt 6.8.3 runtime, the shared offscreen application uses
`fixtures/offscreen.json` to provide a 1920×1080 virtual desktop. Geometry fixtures
account for translated controls' Qt minimum sizes instead of assuming one host's
font metrics. Menu tests explicitly reactivate the owning test window after popup
dismissal: macOS offscreen does not supply the desktop's reactivation event. They
still assert the retained child focus and focus-scoped shortcuts. Selection-color
checks sample an empty cell in logical coordinates, avoiding text glyphs and
high-DPI image-coordinate assumptions. Log catch-up checks wait for the separate
queued scroll update as well as document rendering.
The singleton race uses a short unique socket name that fits under macOS's long
temporary paths and retains child stderr in failure diagnostics. The Windows
executable-path test copies a Python interpreter and its DLLs into a disposable
path with spaces, so it also works when Python and the checkout are on different
volumes; it waits for child output before asserting shutdown.
The Home/Log workflow regressions exercise debounced typing, explicit submit/clear, navigation catch-up, focus-scoped
Find/edit shortcuts, and live log filtering. These are real Qt tests in `test_qt_interactions.py` and
`test_ui_behavior.py`; no production log source or profile network is started. The two optional pause regressions
+12
View File
@@ -0,0 +1,12 @@
{
"screens": [
{
"name": "Furious test screen",
"width": 1920,
"height": 1080,
"logicalDpi": 96,
"logicalBaseDpi": 96,
"dpr": 1
}
]
}
+17 -1
View File
@@ -63,7 +63,23 @@ class TestApplication(QApplication):
def __init__(self):
"""Initialize one side-effect-free application for this test process."""
super().__init__([])
arguments = []
if os.environ.get('QT_QPA_PLATFORM') == 'offscreen':
# The plugin's default 800x800 desktop cannot contain the translated
# MainWindow minimum on all hosts. Give geometry tests a real-sized
# virtual desktop without altering any physical display.
configuration = Path(__file__).with_name('fixtures') / 'offscreen.json'
# Platform arguments are colon-separated, including on Windows.
configuration = os.path.relpath(configuration)
arguments = [
'furious-tests',
'-platform',
f'offscreen:configfile={configuration}',
]
super().__init__(arguments)
self.setApplicationName('Furious Tests')
self.setOrganizationName('Furious Tests')
+5 -2
View File
@@ -675,7 +675,9 @@ class ApplicationLifecycleTransactionTest(TestCase):
def testConcurrentProcessesElectExactlyOnePrimary(self):
"""Exercise the real Qt local-server race with isolated processes."""
serverName = f'furious-singleton-test-{uuid.uuid4()}'
# Qt prefixes relative socket names with the temporary directory. macOS
# paths are long enough that a descriptive prefix exceeds sun_path.
serverName = uuid.uuid4().hex
script = textwrap.dedent(r"""
import os
import sys
@@ -799,7 +801,8 @@ class ApplicationLifecycleTransactionTest(TestCase):
for process in processes:
stdout, stderr = process.communicate(timeout=15)
outputs.append(stdout)
outputs.append(stdout + stderr)
self.assertEqual(
process.returncode,
0,
+25 -9
View File
@@ -48,6 +48,7 @@ import sys
import json
import time
import tempfile
import shutil
import threading
import subprocess
import unittest
@@ -593,27 +594,42 @@ class ExternalCoreProcessTest(unittest.TestCase):
lease.release()
@unittest.skipUnless(os.name == 'nt', 'Windows executable hard-link coverage')
@unittest.skipUnless(os.name == 'nt', 'Windows executable path coverage')
def testExecutablePathContainingSpaces(self):
"""Launch an executable hard link whose local path contains spaces."""
"""Launch a copied interpreter whose local path contains spaces."""
with tempfile.TemporaryDirectory(
prefix='furious executable path ', dir=Path.cwd()
) as directory:
executable = Path(directory) / 'python executable.exe'
# The interpreter and checkout can be on different Windows volumes.
# Copy the base interpreter (not a venv redirector) and its DLLs;
# PYTHONHOME supplies its existing standard library without installing
# an environment or writing into the interpreter's directory.
shutil.copy2(sys._base_executable, executable)
os.link(sys.executable, executable)
for library in Path(sys.base_prefix).glob('python*.dll'):
shutil.copy2(library, directory)
config = self.configuration(
['-c', 'import time; time.sleep(60)'],
['-c', 'import time; print("ready", flush=True); time.sleep(60)'],
directory,
environment={'PYTHONHOME': sys.base_prefix},
)
config['executable'] = str(executable)
messages = []
runtime = ExternalCoreProcess(config, msgCallback=messages.append)
runtime = ExternalCoreProcess(config)
runtime.start()
runtime.stop()
try:
runtime.start()
self.assertTrue(
self.waitFor(
lambda: any('ready' in message for message in messages)
),
messages,
)
self.assertTrue(runtime.isRunning())
finally:
runtime.stop()
self.assertFalse(runtime.isRunning())
+5 -3
View File
@@ -33,7 +33,7 @@ from Furious.Backends.Hysteria2.Protocols import Hysteria2ProtocolHandler
from Furious.Backends.Hysteria2.TunSettingsDialog import (
GuiHysteria2TUNSettingsGroupBoxInterface,
)
from Furious.Frozenlib import AppSettings, Mixins
from Furious.Frozenlib import AppSettings, Mixins, PLATFORM
from Furious.Models.Profile import ServerProfile
from Furious.Plugins.Runtime import serializeRuntimeConfiguration
from Furious.Qt import AppStyleSheet
@@ -226,7 +226,7 @@ class Hysteria2CompatibilityTest(unittest.TestCase):
)
self.assertEqual(
proxyBandwidth.bandwidthFields._widget.layout().contentsMargins().top(),
20,
2 if PLATFORM == 'Darwin' else 20,
)
editor.show()
@@ -346,7 +346,9 @@ class Hysteria2CompatibilityTest(unittest.TestCase):
self.assertEqual(rowLayout.contentsMargins().left(), 0)
self.assertEqual(rowLayout.contentsMargins().right(), 0)
self.assertEqual(rowLayout.spacing(), 8)
self.assertEqual(
rowLayout.spacing(), 10 if PLATFORM == 'Darwin' else 8
)
self.assertIsNotNone(
rowLayout.itemAt(rowLayout.count() - 1).spacerItem()
)
+25 -6
View File
@@ -243,6 +243,8 @@ class AppQMainWindowLifecycleTest(unittest.TestCase):
)
with isolatedSettings():
AppSettings.set('Language', 'EN')
for windowType, expectedSize in cases:
with self.subTest(windowType=windowType.__name__):
window = windowType()
@@ -250,7 +252,13 @@ class AppQMainWindowLifecycleTest(unittest.TestCase):
with patch('Furious.Qt.QtWidgets.moveToCenter') as moveToCenter:
window.show()
self.assertEqual(window.size(), expectedSize)
self.assertEqual(
window.size(),
# Use the enforced layout constraint: a fixed-size
# window may deliberately override its size hint.
expectedSize.expandedTo(window.minimumSize()),
)
moveToCenter.assert_called_once_with(window)
window.move(61, 73)
@@ -343,12 +351,18 @@ class MainWindowNavigationSessionTest(unittest.TestCase):
def testNewWindowResetsSessionStateButRestoresGeometry(self):
"""Reset page and expansion while retaining persistent geometry."""
expectedSize = QtCore.QSize(760, 600)
with isolatedSettings() as settings:
AppSettings.set('Language', 'EN')
with _realMainWindow() as firstWindow:
firstWindow.show()
expectedSize = (
QtCore.QSize(760, 600)
.expandedTo(firstWindow.minimumSizeHint())
.expandedTo(firstWindow.minimumSize())
)
firstWindow.resize(expectedSize)
self.assertEqual(firstWindow.size(), expectedSize)
firstWindow.navigationView.setExpanded(True, animated=False)
firstWindow.showPage('settings')
firstWindow.cleanup()
@@ -723,7 +737,14 @@ class MainWindowGeometryTest(unittest.TestCase):
def testRoutingWindowKeepsValidSmallGeometryAndIgnoresInvalidState(self):
"""Preserve intentional compact geometry independently from layout state."""
with isolatedSettings():
expected = QtCore.QRect(35, 45, 420, 260)
AppSettings.set('Language', 'EN')
window = XrayRoutingWindow()
compactSize = (
QtCore.QSize(420, 260)
.expandedTo(window.minimumSizeHint())
.expandedTo(window.minimumSize())
)
expected = QtCore.QRect(QtCore.QPoint(35, 45), compactSize)
AppSettings.set(
'UserRoutingWindowGeometry',
self._saveGeometry(expected),
@@ -733,8 +754,6 @@ class MainWindowGeometryTest(unittest.TestCase):
QtCore.QByteArray(b'broken'),
)
window = XrayRoutingWindow()
with patch('Furious.Qt.QtWidgets.moveToCenter') as moveToCenter:
window.show()
+31 -16
View File
@@ -1316,6 +1316,16 @@ class SharedSettingsQtWorkflowTest(unittest.TestCase):
p.metadata.profileId for p in (profiles[0], profiles[2])
}
def selectionColor():
# Grab in logical widget coordinates; raw image pixels
# otherwise need conversion for the device pixel ratio.
return (
table.viewport()
.grab(QtCore.QRect(sample, QtCore.QSize(1, 1)))
.toImage()
.pixelColor(0, 0)
)
for theme in (AppStyleSheet.Light, AppStyleSheet.Dark):
with self.subTest(theme=theme), mock.patch.object(
application(), 'theme', return_value=theme
@@ -1338,11 +1348,12 @@ class SharedSettingsQtWorkflowTest(unittest.TestCase):
QTest.keyRelease(table, QtCore.Qt.Key_Control)
processQtEvents()
rect = table.visualRect(table.proxyModel.index(0, 0))
sample = QtCore.QPoint(rect.right() - 12, rect.center().y())
before = (
table.viewport().grab().toImage().pixelColor(sample)
)
# The address is empty in this fixture. Sample its
# background rather than a font-dependent remark glyph.
index = table.proxyModel.index(0, 2)
self.assertEqual(index.data(), '')
sample = table.visualRect(index).center()
before = selectionColor()
self.assertEqual(
before,
@@ -1357,7 +1368,7 @@ class SharedSettingsQtWorkflowTest(unittest.TestCase):
self.assertFalse(home.testMenu.isVisible())
self.assertEqual(
table.viewport().grab().toImage().pixelColor(sample),
selectionColor(),
before,
)
@@ -1373,7 +1384,7 @@ class SharedSettingsQtWorkflowTest(unittest.TestCase):
expectedIds,
)
self.assertEqual(
table.viewport().grab().toImage().pixelColor(sample),
selectionColor(),
before,
)
@@ -1393,9 +1404,13 @@ class SharedSettingsQtWorkflowTest(unittest.TestCase):
expectedIds,
)
self.assertTrue(home.testButton.hasFocus())
# macOS offscreen activates popups but does not
# reactivate their owner on dismissal. Supply only
# window activation; Qt must retain the focused child.
home.activateWindow()
self.assertTrue(waitFor(home.testButton.hasFocus))
self.assertEqual(
table.viewport().grab().toImage().pixelColor(sample),
selectionColor(),
before,
)
@@ -1416,7 +1431,7 @@ class SharedSettingsQtWorkflowTest(unittest.TestCase):
self.assertFalse(table.property('keepSelectionHighlighted'))
self.assertEqual(
table.viewport().grab().toImage().pixelColor(sample),
selectionColor(),
QtGui.QColor(
AppStyleSheet.paletteForTheme(theme)['raised']
),
@@ -1426,14 +1441,12 @@ class SharedSettingsQtWorkflowTest(unittest.TestCase):
home.testButton.setFocus(QtCore.Qt.TabFocusReason)
processQtEvents()
before = table.viewport().grab().toImage().pixelColor(sample)
before = selectionColor()
QTest.keyPress(home.testButton, QtCore.Qt.Key_Space)
processQtEvents()
self.assertEqual(
table.viewport().grab().toImage().pixelColor(sample), before
)
self.assertEqual(selectionColor(), before)
QTest.keyRelease(home.testButton, QtCore.Qt.Key_Space)
processQtEvents()
@@ -1443,7 +1456,8 @@ class SharedSettingsQtWorkflowTest(unittest.TestCase):
QTest.keyClick(home.testMenu, QtCore.Qt.Key_Escape)
processQtEvents()
self.assertTrue(home.testButton.hasFocus())
home.activateWindow()
self.assertTrue(waitFor(home.testButton.hasFocus))
home.searchLineEdit.setFocus()
@@ -1510,8 +1524,9 @@ class SharedSettingsQtWorkflowTest(unittest.TestCase):
self.assertEqual(imported, [True])
table = home.userServersQTableWidget
home.activateWindow()
table.setFocus()
processQtEvents()
self.assertTrue(waitFor(table.hasFocus))
QTest.keyClick(table, QtCore.Qt.Key_V, QtCore.Qt.ControlModifier)
processQtEvents()
+4 -2
View File
@@ -1999,7 +1999,8 @@ class UnifiedLogPageTest(unittest.TestCase):
scrollbar = page.textBrowser.verticalScrollBar()
self.assertGreater(scrollbar.maximum(), 0)
self.assertEqual(scrollbar.value(), scrollbar.maximum())
# Rendering completion schedules a separate zero-delay scroll update.
self.assertTrue(waitFor(lambda: scrollbar.value() == scrollbar.maximum()))
self.assertTrue(page._followTail)
page.hide()
@@ -3130,7 +3131,8 @@ class DialogBehaviorTest(unittest.TestCase):
[rule['ruleTag'] for rule in view.rules()], ['0', '1', '2', '3']
)
self.assertEqual(view.selectedIndex, [2])
self.assertTrue(view.hasFocus())
dialog.activateWindow()
self.assertTrue(waitFor(view.hasFocus))
def testHomeAndRoutingMoveMenusRetranslateWithoutChangingActions(self):
with isolatedSettings():