From f06c405fb7b6357ce8febbd9daaa797b97a4965b Mon Sep 17 00:00:00 2001 From: Loren Eteval Date: Sat, 19 Sep 2026 13:45:52 +0800 Subject: [PATCH] Improve logical block spacing Signed-off-by: Loren Eteval --- Furious/Backends/Xray/RoutingWindow.py | 6 +++++ Furious/Service/ConnectionManager.py | 4 +++ Furious/Service/LogManager.py | 3 +++ Furious/Window/SubscriptionPage.py | 3 +++ tests/test_controllers.py | 37 ++++++++++++++++++++++++++ tests/test_profile_test_jobs.py | 1 + tests/test_ui_behavior.py | 33 +++++++++++++++++++++++ 7 files changed, 87 insertions(+) diff --git a/Furious/Backends/Xray/RoutingWindow.py b/Furious/Backends/Xray/RoutingWindow.py index 43c79ca..96aa315 100644 --- a/Furious/Backends/Xray/RoutingWindow.py +++ b/Furious/Backends/Xray/RoutingWindow.py @@ -960,10 +960,12 @@ class RoutingRulesDialog(AppQTransientDialog): super().__init__(parent) self.routing = routing + self.setWindowTitle(_('Routing Rules')) self.setWindowModality(QtCore.Qt.WindowModality.WindowModal) self.listView = RoutingRulesListView(self.routing, parent=self) + self.routingUnique = routingUnique self.originalRules = copy.deepcopy(self.listView.rules()) @@ -1248,9 +1250,11 @@ class UserRoutingTableView(Mixins.QTranslatable, AppQTableView): return unique = self.routingUniqueByRow(row) + routing['domainStrategy'] = text self.sourceModel.emitAllChanged() + self._routingChanged(unique) def setEnabled(self, row: int, state: str): @@ -1265,9 +1269,11 @@ class UserRoutingTableView(Mixins.QTranslatable, AppQTableView): return unique = self.routingUniqueByRow(row) + routing['enabled'] = enabled self.sourceModel.emitAllChanged() + self._routingChanged(unique, disabled=not enabled) def _routingChanged(self, unique, *, disabled=False): diff --git a/Furious/Service/ConnectionManager.py b/Furious/Service/ConnectionManager.py index 9e9fadf..72957b9 100644 --- a/Furious/Service/ConnectionManager.py +++ b/Furious/Service/ConnectionManager.py @@ -441,6 +441,7 @@ class ConnectionStartOperation(QtCore.QObject): ) = self.manager._prepareTUNPolicy(configcopy, self.proxyModeOnly) except TUNPreparationError as ex: self.manager._lastStartError = str(ex) + self._fail(str(ex), 'native TUN preparation failed') return @@ -665,6 +666,7 @@ class ConnectionStartOperation(QtCore.QObject): exitCallback=router.publish, msgCallback=self.msgCallbackTUN_, ) + router.attach(tun, self) self._tun = tun @@ -1050,6 +1052,7 @@ class ConnectionStartOperation(QtCore.QObject): return self._terminal = True + self._cancelObservers() concise = str(message or '') @@ -1071,6 +1074,7 @@ class ConnectionStartOperation(QtCore.QObject): return False self._terminal = True + self._cancelObservers() self.attempt.rollback('connection startup cancelled') diff --git a/Furious/Service/LogManager.py b/Furious/Service/LogManager.py index ee23994..6d52766 100644 --- a/Furious/Service/LogManager.py +++ b/Furious/Service/LogManager.py @@ -937,10 +937,12 @@ class LogManager(QtCore.QObject): characterCount = len(entry.message) generation = self._generationForCategoryLocked(category) + generation.append(entry, characterCount) self._retainedEntryCount += 1 self._retainedCharacters += characterCount + self._enforceRetentionLimitsLocked() events.append((clearedCategoryIds, entry)) @@ -1089,6 +1091,7 @@ class LogManager(QtCore.QObject): self._retainedEntryCount -= retiredCategory.entryCount self._retainedCharacters -= retiredCategory.characterCount + self._retireBatchLocked(retiredCategory) if changed: diff --git a/Furious/Window/SubscriptionPage.py b/Furious/Window/SubscriptionPage.py index 7b83d45..b9644ba 100644 --- a/Furious/Window/SubscriptionPage.py +++ b/Furious/Window/SubscriptionPage.py @@ -176,12 +176,15 @@ class _SubscriptionEditorDialog(AppQTransientDialog): form.addWidget(self.remarkLabel, 0, 0) form.addWidget(self.remarkEdit, 0, 1) form.addLayout(enabledLayout, 0, 2, 1, 2) + form.addWidget(self.urlLabel, 1, 0) form.addWidget(self.urlEdit, 1, 1, 1, 3) + form.addWidget(self.autoUpdateLabel, 2, 0) form.addWidget(self.autoUpdateComboBox, 2, 1) form.addWidget(self.proxyLabel, 2, 2) form.addWidget(self.proxyComboBox, 2, 3) + form.addWidget(self.userAgentLabel, 3, 0) form.addWidget(self.userAgentEdit, 3, 1) form.addWidget(self.filterLabel, 3, 2) diff --git a/tests/test_controllers.py b/tests/test_controllers.py index a31f5b5..62d5dc2 100644 --- a/tests/test_controllers.py +++ b/tests/test_controllers.py @@ -712,6 +712,7 @@ class RoutingControllerTest(unittest.TestCase): from Furious.Widget.RoutingSelector import RoutingSelector app = application() + with isolatedSettings(): profile = ServerProfile.fromConfiguration( ConfigXray( @@ -723,11 +724,14 @@ class RoutingControllerTest(unittest.TestCase): } ) ) + routings = { 'active': {'remark': 'My routing', 'enabled': True, 'rules': []} } + registry = PluginRegistry() registry.register(XrayPlugin()) + core = FixtureCoreManager() launchedRoutes = [] @@ -735,13 +739,16 @@ class RoutingControllerTest(unittest.TestCase): launchedRoutes.append( registry.normalizeRouting(configuration, kwargs['routing']) ) + return True core.start = start + connection = ConnectionController( coreManager=core, updatesManager=FixtureUpdatesManager() ) logManager = LogManager() + with ( mock.patch.object(app, 'connectionController', connection), mock.patch.object(app, 'logManager', logManager), @@ -759,36 +766,50 @@ class RoutingControllerTest(unittest.TestCase): ): AppSettings.set('Routing', 'Custom:active') controller = RoutingController() + with mock.patch.object(app, 'routingController', controller): view = UserRoutingTableView() tray = RoutingAction(parent=view) selector = RoutingSelector(parent=view) + try: self.assertTrue(connection.startConnection(profile)) + self.assertEqual(launchedRoutes, ['Custom:active']) + view.indexWidget(view.sourceModel.index(0, 2)).setCurrentIndex( 1 ) + prompt = view.findChild(MBoxNewChangesNextTime) self.assertIsNotNone(prompt) + prompt.button(AppQMessageBox.StandardButton.Yes).click() processQtEvents() + fallback = AppBuiltinRouting.BypassMainlandChina.value + self.assertEqual(launchedRoutes, ['Custom:active', fallback]) + tray.rebuildMenu() + self.assertEqual(controller.routing, fallback) + view.indexWidget(view.sourceModel.index(0, 2)).setCurrentIndex( 0 ) tray.rebuildMenu() + self.assertEqual(controller.routing, fallback) self.assertEqual(AppSettings.get('Routing'), fallback) self.assertEqual(selector.currentData(), fallback) + checked = [ action.routingValue for action in tray._menu.actions() if action.isChecked() ] + self.assertEqual(checked, [fallback]) self.assertEqual(len(launchedRoutes), 2) self.assertEqual(view.findChildren(MBoxNewChangesNextTime), []) @@ -798,22 +819,30 @@ class RoutingControllerTest(unittest.TestCase): for action in tray._menu.actions() if getattr(action, 'routingValue', None) == 'Custom:active' ) + customAction.trigger() + self.assertEqual( launchedRoutes, ['Custom:active', fallback, 'Custom:active'] ) self.assertEqual(selector.currentData(), 'Custom:active') + view.indexWidget(view.sourceModel.index(0, 2)).setCurrentIndex( 1 ) + prompt = view.findChild(MBoxNewChangesNextTime) + prompt.button(AppQMessageBox.StandardButton.No).click() processQtEvents() + self.assertEqual(len(launchedRoutes), 3) + view.indexWidget(view.sourceModel.index(0, 2)).setCurrentIndex( 0 ) tray.rebuildMenu() + self.assertEqual(controller.routing, fallback) self.assertEqual(AppSettings.get('Routing'), fallback) self.assertEqual(len(launchedRoutes), 3) @@ -823,6 +852,7 @@ class RoutingControllerTest(unittest.TestCase): view.deleteLater() controller.deleteLater() processQtEvents() + connection.deleteLater() logManager.deleteLater() registry.shutdown() @@ -831,9 +861,11 @@ class RoutingControllerTest(unittest.TestCase): def testOnlyExplicitInvalidationPersistsFallbackWithoutReconnecting(self): """Capability refresh stays observational until a selected choice is revoked.""" connection = mock.Mock(activeProfile=self.connectedProfile) + registry = mock.Mock() registry.routingOptions.return_value = (SimpleNamespace(id='fallback'),) registry.normalizeRouting.return_value = 'fallback' + with ( isolatedSettings(), mock.patch( @@ -847,14 +879,19 @@ class RoutingControllerTest(unittest.TestCase): ): AppSettings.set('Routing', 'unavailable') controller = RoutingController() + try: controller.refresh(force=True) + self.assertEqual(controller.routing, 'fallback') self.assertEqual(AppSettings.get('Routing'), 'unavailable') + self.assertFalse(controller.invalidateRouting('other')) self.assertEqual(AppSettings.get('Routing'), 'unavailable') + self.assertTrue(controller.invalidateRouting('unavailable')) self.assertEqual(AppSettings.get('Routing'), 'fallback') + self.assertFalse(controller.invalidateRouting('fallback')) connection.startReconnection.assert_not_called() finally: diff --git a/tests/test_profile_test_jobs.py b/tests/test_profile_test_jobs.py index 43865be..764cb47 100644 --- a/tests/test_profile_test_jobs.py +++ b/tests/test_profile_test_jobs.py @@ -1036,6 +1036,7 @@ class ProfileTestServiceTest(unittest.TestCase): for worker in _ControlledDownloadWorker.instances if worker.profile.metadata.profileId == other.metadata.profileId ) + otherDownload.finish('3.00 MiB/s') processQtEvents() diff --git a/tests/test_ui_behavior.py b/tests/test_ui_behavior.py index 419e89c..df378e0 100644 --- a/tests/test_ui_behavior.py +++ b/tests/test_ui_behavior.py @@ -2479,6 +2479,7 @@ class RoutingChangeNoticeTest(unittest.TestCase): app = application() self.addCleanup(collectAtBoundary) self.enterContext(isolatedSettings()) + self.routings = { unique: { 'remark': 'Same display name', @@ -2491,9 +2492,11 @@ class RoutingChangeNoticeTest(unittest.TestCase): } for unique in ('active', 'other') } + self.connection = mock.Mock() self.connection.isConnected.return_value = True self.routing = mock.Mock(routing='Custom:active') + self.enterContext( mock.patch.object(app, 'connectionController', self.connection) ) @@ -2504,6 +2507,7 @@ class RoutingChangeNoticeTest(unittest.TestCase): self.notice = self.enterContext( mock.patch('Furious.Backends.Xray.RoutingWindow.showMBoxNewChangesNextTime') ) + self.view = UserRoutingTableView() self.addCleanup(self.disposeView) @@ -2524,30 +2528,40 @@ class RoutingChangeNoticeTest(unittest.TestCase): self.view.flushAll() self.view.setDomainStrategy(0, 'AsIs') self.view.setEnabled(0, 'Enabled') + self.notice.assert_not_called() strategy = self.view.indexWidget(self.view.sourceModel.index(0, 1)) strategy.setCurrentText('IPOnDemand') + self.assertEqual(self.routings['active']['domainStrategy'], 'IPOnDemand') self.notice.assert_called_once_with(parent=self.view) + self.notice.reset_mock() enabled = self.view.indexWidget(self.view.sourceModel.index(0, 2)) enabled.setCurrentIndex(1) + self.assertFalse(self.routings['active']['enabled']) self.notice.assert_called_once_with(parent=self.view) + self.notice.reset_mock() self.view.setDomainStrategy(1, 'IPIfNonMatch') self.view.setEnabled(1, 'Disabled') + self.connection.isConnected.return_value = False self.view.setDomainStrategy(0, 'AsIs') self.view.setEnabled(0, 'Enabled') + self.connection.isConnected.return_value = True self.routing.routing = 'Global' self.view.setDomainStrategy(0, 'IPIfNonMatch') + self.notice.assert_not_called() + processQtEvents() + exceptionHook.assert_not_called() def testRuleChangesNotifyOnCloseWithoutRetargetingSelection(self): @@ -2558,6 +2572,7 @@ class RoutingChangeNoticeTest(unittest.TestCase): ) as exceptionHook: dialog = self.openRules() view = dialog.listView + if change == 'add': view.appendRule({'domain': ['new.test'], 'outboundTag': 'proxy'}) elif change == 'edit': @@ -2567,12 +2582,16 @@ class RoutingChangeNoticeTest(unittest.TestCase): else: view.setCurrentIndex(view.rulesModel.index(0, 0)) view.moveSelectedRules('down') + self.view.selectRow(1) self.notice.assert_not_called() + dialog.closeWindowButton.click() processQtEvents() + self.notice.assert_called_once_with(parent=self.view) exceptionHook.assert_not_called() + self.notice.reset_mock() def testRuleNoticeSkipsUnchangedRevertedStaleAndInactiveEdits(self): @@ -2587,10 +2606,12 @@ class RoutingChangeNoticeTest(unittest.TestCase): ): with self.subTest(case=case), mock.patch('sys.excepthook') as exceptionHook: dialog = self.openRules(1 if case == 'other' else 0) + if case != 'unchanged': dialog.listView.appendRule( {'domain': ['new.test'], 'outboundTag': 'proxy'} ) + if case == 'reverted': dialog.listView.deleteRules([len(dialog.listView.rules()) - 1]) elif case == 'disconnected': @@ -2599,10 +2620,13 @@ class RoutingChangeNoticeTest(unittest.TestCase): self.routing.routing = 'Custom:other' elif case == 'replaced': self.routings['active'] = copy.deepcopy(self.routings['active']) + dialog.reject() processQtEvents() + self.notice.assert_not_called() exceptionHook.assert_not_called() + self.connection.isConnected.return_value = True self.routing.routing = 'Custom:active' @@ -2615,23 +2639,30 @@ class RoutingChangeNoticeTest(unittest.TestCase): from shiboken6 import delete as deleteQObject, isValid self.notice.side_effect = showMBoxNewChangesNextTime + for button in ( AppQMessageBox.StandardButton.No, AppQMessageBox.StandardButton.Yes, ): dialog = self.openRules() dialog.listView.appendRule({'domain': ['new.test'], 'outboundTag': 'proxy'}) + dialog.closeWindowButton.click() processQtEvents() + prompt = self.view.findChild(MBoxNewChangesNextTime) + self.assertIsNotNone(prompt) self.assertTrue(prompt.isVisible()) self.assertFalse(isValid(dialog)) self.assertIn(prompt, AppQDialog._openDialogs.values()) + prompt.button(button).click() processQtEvents() + self.assertFalse(isValid(prompt)) self.assertNotIn(prompt, AppQDialog._openDialogs.values()) + if button == AppQMessageBox.StandardButton.No: self.connection.startReconnection.assert_not_called() else: @@ -2639,8 +2670,10 @@ class RoutingChangeNoticeTest(unittest.TestCase): self.view.setDomainStrategy(0, 'IPOnDemand') prompt = self.view.findChild(MBoxNewChangesNextTime) + deleteQObject(self.view) processQtEvents() + self.assertFalse(isValid(prompt)) self.assertNotIn(prompt, AppQDialog._openDialogs.values())