Fix cross-platform test assumptions

Signed-off-by: Loren Eteval <loren.eteval@proton.me>
This commit is contained in:
Loren Eteval
2026-09-19 15:11:10 +08:00
parent 2056c7005b
commit d669c3aac6
4 changed files with 54 additions and 6 deletions
+6
View File
@@ -246,6 +246,12 @@ Linux, and macOS with Python 3.13 and PySide6 6.8.3. The publication workflow re
that job before PyPI publication. Opt-in tests and packaged/manual smoke checks remain that job before PyPI publication. Opt-in tests and packaged/manual smoke checks remain
separate; local Windows results do not establish the other CI targets. separate; local Windows results do not establish the other CI targets.
The settings harness compares canonical paths on every access so macOS temporary-directory
aliases and Windows short names do not look like sandbox escapes. Its isolation regressions
cover an aliased root and rejection of paths outside that root. The QR responsiveness test
queues unrelated work after generation starts and requires delivery before completion;
it does not assume a platform-specific order between zero-delay Qt timers.
The Home/Log workflow regressions exercise debounced typing, explicit submit/clear, navigation catch-up, focus-scoped 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 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 `test_ui_behavior.py`; no production log source or profile network is started. The two optional pause regressions
+3 -1
View File
@@ -108,7 +108,9 @@ def _initializeSettingsSandbox():
global _settingsDirectory global _settingsDirectory
if _settingsDirectory is not None: if _settingsDirectory is not None:
return Path(_settingsDirectory.name) # TemporaryDirectory may retain a macOS /var alias or Windows short
# path. Match the canonical root used on initialization and by Qt.
return Path(_settingsDirectory.name).resolve()
_settingsDirectory = tempfile.TemporaryDirectory( _settingsDirectory = tempfile.TemporaryDirectory(
prefix='furious-tests-settings-root-' prefix='furious-tests-settings-root-'
+35 -2
View File
@@ -26,6 +26,9 @@ from PySide6.QtTest import QTest
from PySide6.QtWidgets import QPushButton, QVBoxLayout, QWidget from PySide6.QtWidgets import QPushButton, QVBoxLayout, QWidget
from pathlib import Path from pathlib import Path
from unittest import mock
import tests.support as SupportModule
from tests.support import ( from tests.support import (
application, application,
@@ -58,7 +61,9 @@ class TestHarnessIsolationTest(unittest.TestCase):
assertIsolatedSettings(outer) assertIsolatedSettings(outer)
self.assertIn(settingsSandboxPath(), Path(outer.fileName()).parents) self.assertIn(
settingsSandboxPath(), Path(outer.fileName()).resolve().parents
)
with isolatedSettings() as inner: with isolatedSettings() as inner:
innerIdentity = (app.organizationName(), app.applicationName()) innerIdentity = (app.organizationName(), app.applicationName())
@@ -87,7 +92,35 @@ class TestHarnessIsolationTest(unittest.TestCase):
assertIsolatedSettings(settings) assertIsolatedSettings(settings)
self.assertIn(settingsSandboxPath(), Path(settings.fileName()).parents) self.assertIn(
settingsSandboxPath(), Path(settings.fileName()).resolve().parents
)
def testExistingSandboxRootUsesTheSameCanonicalPath(self):
"""Normalize reused temporary-directory aliases before containment checks."""
root = settingsSandboxPath().resolve()
alias = root / '..' / root.name
# TemporaryDirectory.name can retain an alias even though QSettings
# returns the canonical path (macOS /var or Windows short user names).
with mock.patch.object(SupportModule._settingsDirectory, 'name', str(alias)):
self.assertEqual(settingsSandboxPath(), root)
with isolatedSettings() as settings:
assertIsolatedSettings(settings)
settings.setValue('alias-fixture', 'isolated')
settings.sync()
self.assertEqual(settings.value('alias-fixture'), 'isolated')
def testSettingsOutsideSandboxAreStillRejected(self):
"""Reject a sibling path even when its name shares the sandbox prefix."""
root = settingsSandboxPath().resolve()
outside = root.with_name(root.name + '-outside') / 'settings.ini'
settings = QtCore.QSettings(str(outside), QtCore.QSettings.Format.IniFormat)
# Constructing this object does not write; rejection must precede writes.
with self.assertRaisesRegex(AssertionError, 'escaped sandbox'):
assertIsolatedSettings(settings)
class NavigationBehaviorTest(unittest.TestCase): class NavigationBehaviorTest(unittest.TestCase):
+10 -3
View File
@@ -168,7 +168,7 @@ class QRCodeExportScalabilityTest(unittest.TestCase):
self.assertEqual(appendExportItem.call_count, expected) self.assertEqual(appendExportItem.call_count, expected)
def testGenerationYieldsToAnUnrelatedQtEvent(self): def testGenerationYieldsToAnUnrelatedQtEvent(self):
"""Deliver unrelated work after one attempt and before batch completion.""" """Deliver unrelated work during generation, before batch completion."""
profiles = self.profiles(6) profiles = self.profiles(6)
attempts = [] attempts = []
marker = [] marker = []
@@ -178,6 +178,12 @@ class QRCodeExportScalabilityTest(unittest.TestCase):
"""Record one exported snapshot.""" """Record one exported snapshot."""
attempts.append(profile.itemRemark) attempts.append(profile.itemRemark)
if len(attempts) == 1:
# Zero-timer ordering is platform-dependent. Queue the marker
# only once generation has started, then require it to run
# before all profiles finish rather than after exactly one.
QtCore.QTimer.singleShot(0, lambda: marker.append(len(attempts)))
return f'socks://{len(attempts)}.example:1080#Fixture' return f'socks://{len(attempts)}.example:1080#Fixture'
with mock.patch( with mock.patch(
@@ -191,13 +197,14 @@ class QRCodeExportScalabilityTest(unittest.TestCase):
side_effect=self.qrImage, side_effect=self.qrImage,
): ):
result = window.startExportByIndex(range(len(profiles))) result = window.startExportByIndex(range(len(profiles)))
QtCore.QTimer.singleShot(0, lambda: marker.append(len(attempts)))
self.assertIs(result, window) self.assertIs(result, window)
self.assertEqual(attempts, []) self.assertEqual(attempts, [])
self.assertTrue(window.isVisible()) self.assertTrue(window.isVisible())
self.assertTrue(waitFor(lambda: bool(marker))) self.assertTrue(waitFor(lambda: bool(marker)))
self.assertEqual(marker, [1]) self.assertEqual(len(marker), 1)
self.assertGreaterEqual(marker[0], 1)
self.assertLess(marker[0], len(profiles))
self.assertTrue(waitFor(lambda: not window.isExporting())) self.assertTrue(waitFor(lambda: not window.isExporting()))
self.assertEqual(len(attempts), len(profiles)) self.assertEqual(len(attempts), len(profiles))