diff --git a/Furious/Application/DesktopApplication.py b/Furious/Application/DesktopApplication.py index df24c164..4789e223 100644 --- a/Furious/Application/DesktopApplication.py +++ b/Furious/Application/DesktopApplication.py @@ -1034,6 +1034,7 @@ class DesktopApplication(ApplicationRunner, SingletonApplication): self.addCustomFont() # self.configureApplicationFont() + self.configureLogging() self._logRuntimeInformation() diff --git a/Furious/Controllers/ConnectionController.py b/Furious/Controllers/ConnectionController.py index 2987af40..087c299d 100644 --- a/Furious/Controllers/ConnectionController.py +++ b/Furious/Controllers/ConnectionController.py @@ -192,6 +192,7 @@ class ConnectionController(QtCore.QObject): """Restore disconnected state after all runtime resources stop.""" self._startOperation = None self._pendingHttpProxy = '' + self.progressFinished.emit(True) self._setActiveProfile(None) @@ -275,6 +276,7 @@ class ConnectionController(QtCore.QObject): self._lastError = None self._setActiveProfile(configuration) self._pendingHttpProxy = httpProxy + self._startConnecting() logManager = AppLogManager() @@ -405,6 +407,7 @@ class ConnectionController(QtCore.QObject): interval *= 2 self._actionTimer.start(interval) + self._runPostConnectTasksOnce() return True @@ -459,6 +462,7 @@ class ConnectionController(QtCore.QObject): self._startOperation = None self._emitRuntimesChanged() + self._reset() def startDisconnection(self, notification: str = '') -> bool: diff --git a/Furious/Service/ConnectionManager.py b/Furious/Service/ConnectionManager.py index 01425578..9e9fadfb 100644 --- a/Furious/Service/ConnectionManager.py +++ b/Furious/Service/ConnectionManager.py @@ -249,6 +249,7 @@ class _RuntimeReadinessProbe(QtCore.QObject): self._terminal = True self._timer.stop() self._socket.abort() + self.ready.emit() def _finishFailed(self, message): @@ -259,6 +260,7 @@ class _RuntimeReadinessProbe(QtCore.QObject): self._terminal = True self._timer.stop() self._socket.abort() + self.failed.emit(str(message)) def cancel(self): @@ -330,6 +332,7 @@ class _ConditionProbe(QtCore.QObject): self._terminal = True self._timer.stop() + self.finished.emit(bool(success)) def cancel(self): @@ -377,6 +380,7 @@ class ConnectionStartOperation(QtCore.QObject): self.proxyModeOnly = proxyModeOnly self.log = log self.options = dict(options or {}) + self.attempt = _ConnectionStartAttempt( manager, manager._runtimeConfiguration(config, deepcopy) ) @@ -558,6 +562,7 @@ class ConnectionStartOperation(QtCore.QObject): self._readinessProbe = None self._conditionContinuation = '' + self._fail('Failed to start core', message) def _afterPrimaryReady(self): @@ -605,6 +610,7 @@ class ConnectionStartOperation(QtCore.QObject): f'\'DefaultPrimaryGatewayIP\': {userGateway}. ' f'\'PrimaryAdapterInterfaceIP\': {userInterfaceIP}' ) + self._gateway, self._interface = userGateway, userInterfaceIP else: logger.info( @@ -612,6 +618,7 @@ class ConnectionStartOperation(QtCore.QObject): '\'DefaultPrimaryGatewayIP\' and ' '\'PrimaryAdapterInterfaceIP\'' ) + defaultGateway = SystemRoutingTable.getDefaultGateway() if PLATFORM == 'Darwin': @@ -944,6 +951,7 @@ class ConnectionStartOperation(QtCore.QObject): for sourceIP, destinationIP in SystemRoutingTable.managedRoutes if iproute.find(route(sourceIP, destinationIP)) == -1 ) + tempdir = os.environ.get('TMPDIR') if SystemRuntime.flatpakID() else None with tempfile.NamedTemporaryFile( @@ -1013,8 +1021,10 @@ class ConnectionStartOperation(QtCore.QObject): self.attempt.commit(self.exitCallback) self._terminal = True + self._setStage(ConnectionStartStage.Succeeded) self.succeeded.emit(self) + self.manager._finishStartOperation(self) self.deleteLater() @@ -1048,8 +1058,10 @@ class ConnectionStartOperation(QtCore.QObject): self.manager._lastStartError = concise self.attempt.rollback(f'connection startup failed: {details or concise}') + self._setStage(ConnectionStartStage.Failed) self.failed.emit(self, concise, str(details or '')) + self.manager._finishStartOperation(self) self.deleteLater() @@ -1060,9 +1072,12 @@ class ConnectionStartOperation(QtCore.QObject): self._terminal = True self._cancelObservers() + self.attempt.rollback('connection startup cancelled') + self._setStage(ConnectionStartStage.Cancelled) self.cancelled.emit(self) + self.manager._finishStartOperation(self) self.deleteLater() diff --git a/Furious/Service/DnsResolver.py b/Furious/Service/DnsResolver.py index ec5f7967..a6323e84 100644 --- a/Furious/Service/DnsResolver.py +++ b/Furious/Service/DnsResolver.py @@ -70,6 +70,7 @@ class DnsResolutionOperation(QtCore.QObject): logger.error(f'failed to start DNS resolution for {self._domain!r}: {ex}') self._resultMap['error'] = True + self._finish() return @@ -114,6 +115,7 @@ class DnsResolutionOperation(QtCore.QObject): self._terminal = True self._timer.stop() + self.finished.emit( bool(self._resultMap['error']), list(self._resultMap['result'].keys()), diff --git a/Furious/Service/LogManager.py b/Furious/Service/LogManager.py index 6673e1d2..c778fbf9 100644 --- a/Furious/Service/LogManager.py +++ b/Furious/Service/LogManager.py @@ -331,6 +331,7 @@ class LogManager(QtCore.QObject): self._retainedEntryCount = 0 self._retainedCharacters = 0 self._generationSequence = 0 + # Three fixed active streams preserve exact global ordering with an O(n) # three-way merge and make oldest-live selection constant time. Runtime # categories are isolated so disconnect/runtime clear is one reference @@ -341,6 +342,7 @@ class LogManager(QtCore.QObject): self._otherGeneration = self._newGenerationLocked('other') self._runtimeCategoryIds = frozenset() self._nonApplicationCategoryIds = frozenset() + # Retired batches remain strongly owned until bounded cleanup removes # their entries. Merely dropping a large OrderedDict here would make the # clear caller synchronously execute thousands of CPython decrefs. diff --git a/Furious/Service/ProfileTesting.py b/Furious/Service/ProfileTesting.py index c8948960..92d1284d 100644 --- a/Furious/Service/ProfileTesting.py +++ b/Furious/Service/ProfileTesting.py @@ -523,6 +523,7 @@ class _LatencyScheduler(QtCore.QObject): worker = self.pingWorkerFactory(job, self) self.activeJobs[id(job)] = (job, worker) + self.threadPool.start(worker) @QtCore.Slot(object, object) @@ -534,6 +535,7 @@ class _LatencyScheduler(QtCore.QObject): return self.completeJob(job, result) + self.scheduleDrain() def reconcileProfiles(self): @@ -551,12 +553,14 @@ class _LatencyScheduler(QtCore.QObject): for job, worker in list(self.activeJobs.values()): if self._resolveTarget(job.target) is None: job.state = ProfileTestJobState.Cancelled + cancel = getattr(worker, 'cancel', None) if callable(cancel): cancel() self.discardTcpingJobs(lambda job: self._resolveTarget(job.target) is None) + self.scheduleDrain() def invalidateSubscriptions(self, subscriptionIds): @@ -575,6 +579,7 @@ class _LatencyScheduler(QtCore.QObject): for job, worker in list(self.activeJobs.values()): if job.target.subscriptionSource in subscriptionIds: job.state = ProfileTestJobState.Cancelled + cancel = getattr(worker, 'cancel', None) if callable(cancel): @@ -624,6 +629,7 @@ class _LatencyScheduler(QtCore.QObject): for job, worker in list(self.activeJobs.values()): job.state = ProfileTestJobState.Cancelled + cancel = getattr(worker, 'cancel', None) if callable(cancel): @@ -815,6 +821,7 @@ class _DownloadSpeedWorker(HttpGetManager): return self.cancelled = True + self.coreStartupTimer.stop() self.timeoutTimer.stop() @@ -985,6 +992,7 @@ class _DownloadSpeedWorker(HttpGetManager): self.setResult('Core start failed', publish=False) self._releaseRuntime() + self.publishProgress() def hasDataCallback(self, networkReply, **_kwargs): @@ -1033,6 +1041,7 @@ class _DownloadSpeedWorker(HttpGetManager): self.setResult(value, publish=False) self._releaseRuntime() + self.publishProgress() @@ -1081,6 +1090,7 @@ class _DownloadSpeedScheduler(QtCore.QObject): _DownloadSpeedTestJob(ProfileTestTarget.capture(profile), options) for profile in profiles ) + self.scheduleDrain() def cancelAll(self): @@ -1208,6 +1218,7 @@ class _DownloadSpeedScheduler(QtCore.QObject): self.activePorts.discard(port) worker.deleteLater() + self.scheduleDrain() def reconcileProfiles(self): @@ -1285,6 +1296,7 @@ class ProfileTestManager(QtCore.QObject): self._profilesProvider = profilesProvider or Storage.UserServers self._targets = _currentTargets(self._profilesProvider()) + self._latencyScheduler = _LatencyScheduler( self.resolveTarget, self.applyResult, @@ -1376,6 +1388,7 @@ class ProfileTestManager(QtCore.QObject): if concurrent else self._serialDownloadScheduler ) + scheduler.enqueue(profiles, options) def clearResults(self, profiles): @@ -1411,6 +1424,7 @@ class ProfileTestManager(QtCore.QObject): for profile in self._profilesProvider() if profile.metadata.subscriptionSource in subscriptionIds ] + self.clearResults(profiles) self.reconcileProfiles() diff --git a/Furious/Service/RuntimeLease.py b/Furious/Service/RuntimeLease.py index 5168ac70..c138fa12 100644 --- a/Furious/Service/RuntimeLease.py +++ b/Furious/Service/RuntimeLease.py @@ -71,6 +71,7 @@ class RuntimeEventRouter(QtCore.QObject): self._attemptMethod = '' self._committedCallback = None self._terminalDelivered = False + self._publishedConnection = connectWeakly( self._published, self, diff --git a/Furious/Service/SubscriptionManager.py b/Furious/Service/SubscriptionManager.py index 3c648377..f4995935 100644 --- a/Furious/Service/SubscriptionManager.py +++ b/Furious/Service/SubscriptionManager.py @@ -660,6 +660,7 @@ class SubscriptionManager(HttpGetManager): logger.exception( f'failed to commit subscription {context.get("unique", "")!r}' ) + self._failOperation(context, error) return @@ -667,8 +668,10 @@ class SubscriptionManager(HttpGetManager): committed = {**context, 'syncResult': result} self.subscriptionCommitted.emit(context['unique']) + self._recordGroupSuccess(committed, result) self.subscriptionStateChanged.emit((context['unique'],)) + self._finishOperation(committed, successful=committed, structural=True) logger.info( @@ -815,7 +818,9 @@ class SubscriptionManager(HttpGetManager): controller = AppConnectionController() wasConnected = controller is not None and controller.isConnected() + result = self.synchronizer.commit(servers, plan) + newActivatedIndex = next( ( index diff --git a/Furious/Service/SubscriptionSync.py b/Furious/Service/SubscriptionSync.py index 0170ef0a..3c5d0954 100644 --- a/Furious/Service/SubscriptionSync.py +++ b/Furious/Service/SubscriptionSync.py @@ -140,6 +140,7 @@ class SubscriptionSynchronizer: profiles[index].metadata.profileId: profiles[index] for index in managedIndexes } + synchronized = [] for prepared in plan.profiles: @@ -246,6 +247,7 @@ class SubscriptionSynchronizer: ): profiles[index] for index in managedIndexes } + synchronized = [] incomingMetadata = [] existingUpdates = [] diff --git a/Furious/Service/TcpingService.py b/Furious/Service/TcpingService.py index d8b87d11..980d3128 100644 --- a/Furious/Service/TcpingService.py +++ b/Furious/Service/TcpingService.py @@ -128,6 +128,7 @@ class TcpingProbe(QtCore.QObject): """Start one non-blocking TCP connection attempt.""" self.elapsedTimer.start() self.timeoutTimer.start(self.request.timeoutMilliseconds) + self.socket.connectToHost(self.request.address, self.request.port) @QtCore.Slot() @@ -153,6 +154,7 @@ class TcpingProbe(QtCore.QObject): return self.completionHasRun = True + self.timeoutTimer.stop() if self.socket.state() != QAbstractSocket.SocketState.UnconnectedState: @@ -215,6 +217,7 @@ class TcpingEngine(QtCore.QObject): return self.pendingRequests.extend(requests) + self.drain() def drain(self): @@ -320,6 +323,7 @@ class TcpingEngine(QtCore.QObject): return self.stopping = True + self.pendingRequests.clear() for probe in list(self.activeProbes.values()): diff --git a/Furious/Service/TrafficStatsManager.py b/Furious/Service/TrafficStatsManager.py index 3bd3fa03..9b10a53f 100644 --- a/Furious/Service/TrafficStatsManager.py +++ b/Furious/Service/TrafficStatsManager.py @@ -332,6 +332,7 @@ class TrafficStatsManager( self._sampleTimer.stop() self._generation += 1 self._cancelCurrentQuery() + self._monitor = monitor self._resetSamples() self._beginConnectionUsage() @@ -375,6 +376,7 @@ class TrafficStatsManager( monitor = getPluginRegistry().trafficStatsMonitorForRuntimes( self._activeRuntimes() ) + self._activateMonitor(monitor) @QtCore.Slot() @@ -488,6 +490,7 @@ class TrafficStatsManager( if counters is None: self._previousCounters = None self._previousSampleTime = None + self.statisticsUnavailable.emit() return @@ -510,6 +513,7 @@ class TrafficStatsManager( monitor = getPluginRegistry().trafficStatsMonitorForRuntimes( self._activeRuntimes() ) + self._activateMonitor(monitor) def disconnectedCallback(self): diff --git a/Furious/Widget/ServerTableView.py b/Furious/Widget/ServerTableView.py index 12d54c4a..cffd2e55 100644 --- a/Furious/Widget/ServerTableView.py +++ b/Furious/Widget/ServerTableView.py @@ -268,6 +268,7 @@ class DeleteServersProgressDialog(AppQTransientDialog): Storage.UserServers().pop(deleteIndex) self.table.sourceModel.endRemoveRows() + self.table.reconcileProfileTestJobs() if not self.deletedActivated and deleteIndex < Storage.UserActivatedItemIndex(): @@ -276,6 +277,7 @@ class DeleteServersProgressDialog(AppQTransientDialog): ) self.deletedCount += 1 + self.updateStatus() singleShotWeakly(0, self, 'deleteNext') @@ -1716,6 +1718,7 @@ class ServerTableView( showTrayMessage=showTrayMessage, parent=self.window(), ) + dialog.open() return 0 diff --git a/Furious/Window/LogPage.py b/Furious/Window/LogPage.py index 0961c56c..9d529065 100644 --- a/Furious/Window/LogPage.py +++ b/Furious/Window/LogPage.py @@ -197,6 +197,7 @@ class LogPage(Mixins.QTranslatable, QMainWindow): self.pageTitleLabel.setObjectName('LogPageTitle') self.filterLabel = AppQLabel(_('Log Type')) + # Category names can be either translated built-ins or literal plugin # metadata, so LogPage rebuilds this application-styled selector with # the correct per-category translation policy. @@ -403,6 +404,7 @@ class LogPage(Mixins.QTranslatable, QMainWindow): self.autoScrollSwitch.toggled.connect(self._autoScrollChanged) self.autoClearSwitch.toggled.connect(self._autoClearChanged) self.manager.categoryRegistered.connect(self._categoryRegistered) + # Do not drive the document directly from entryAdded: cross-thread Qt # delivery can occur after that entry was evicted or its generation was # cleared, and one queued signal per line defeats batching under a burst. @@ -665,6 +667,7 @@ class LogPage(Mixins.QTranslatable, QMainWindow): self._documentMutation = False self.textBrowser.viewport().update() + self._renderedCategoryId = categoryId self._representationInvalid = False @@ -715,6 +718,7 @@ class LogPage(Mixins.QTranslatable, QMainWindow): ) self._entryCursor = None + self._requestRefresh(invalidate=True, immediate=True) return @@ -748,6 +752,7 @@ class LogPage(Mixins.QTranslatable, QMainWindow): document = self.textBrowser.document() start = self._highlightNextBlock maximumEnd = min(document.blockCount(), start + self.HighlightBatchSize) + elapsed = QtCore.QElapsedTimer() elapsed.start() end = start diff --git a/Furious/Window/QRCodeWindow.py b/Furious/Window/QRCodeWindow.py index b01b1106..f02c2b5a 100644 --- a/Furious/Window/QRCodeWindow.py +++ b/Furious/Window/QRCodeWindow.py @@ -360,6 +360,7 @@ class QRCodeWindow(AppQMainWindow): self.cancelExport() self._clearTabs() + self._exportProcessedCount = 0 self._exportGeneratedCount = 0 @@ -372,15 +373,18 @@ class QRCodeWindow(AppQMainWindow): if self.appendExportItem(items[0]): self._exportProcessedCount = 1 self._exportGeneratedCount = 1 + self.show() else: self._exportProcessedCount = 1 + self.deleteLater() return self self._exportItems = items self._exporting = True + self.show() self._exportTimer.start(0) diff --git a/tests/test_connection_startup_async.py b/tests/test_connection_startup_async.py index cd2908c5..ec557ffc 100644 --- a/tests/test_connection_startup_async.py +++ b/tests/test_connection_startup_async.py @@ -222,14 +222,19 @@ class ConnectionStartupAsyncTest(TestCase): readiness=CoreRuntimeStartup(endpoint=endpoint, timeout=1500), ) manager = self._manager() + operation = self._operation(manager, launch) + succeeded = [] timerEvents = [] + operation.succeeded.connect(succeeded.append) QtCore.QTimer.singleShot(0, lambda: timerEvents.append(True)) self.assertEqual(operation.stage, ConnectionStartStage.Pending) + self.assertTrue(waitFor(lambda: bool(succeeded), timeout=0.5)) + self.assertEqual(timerEvents, [True]) self.assertEqual(manager.runtimes, [runtime]) self.assertEqual(runtime.startOptions[0], {}) diff --git a/tests/test_endpoint_info.py b/tests/test_endpoint_info.py index a6c3e96e..e67f01d2 100644 --- a/tests/test_endpoint_info.py +++ b/tests/test_endpoint_info.py @@ -738,9 +738,11 @@ class EndpointInfoServiceTest(unittest.TestCase): ) self.assertFalse(hasattr(widget.countryRow, 'flagLabel')) self.assertFalse(hasattr(widget, 'statusLabel')) + self.assertEqual(widget.bodyLayout.stretch(0), 1) self.assertEqual(widget.bodyLayout.stretch(1), 2) self.assertGreaterEqual(widget.minimumHeight(), 360) + self.assertIsInstance(widget.mapWidget.webView, QWebEngineView) self.assertEqual(widget.mapWidget.webProfile.httpUserAgent(), 'Mozilla/5.0') self.assertNotIn('Furious', widget.mapWidget.webProfile.httpUserAgent()) @@ -753,6 +755,7 @@ class EndpointInfoServiceTest(unittest.TestCase): widget.mapWidget._lastWebState['fontPointSize'], QtGui.QFontInfo(widget.mapWidget.font()).pointSizeF(), ) + themePalette = AppStyleSheet.paletteForTheme(widget.mapWidget._theme) self.assertEqual( widget.mapWidget._lastWebState['surfaceColor'], @@ -770,11 +773,13 @@ class EndpointInfoServiceTest(unittest.TestCase): widget.mapWidget._lastWebState['mutedTextColor'], themePalette['muted'], ) + self.assertAlmostEqual(widget.mapWidget._lastWebState['markerLatitude'], 34.05) self.assertAlmostEqual( widget.mapWidget._lastWebState['markerLongitude'], -118.24 ) self.assertTrue(scripts) + self.assertEqual( widget.ipv4Row.valueContainer.objectName(), 'EndpointFieldValueContainer', @@ -796,6 +801,7 @@ class EndpointInfoServiceTest(unittest.TestCase): QtCore.Qt.ScrollPhase.ScrollUpdate, False, ) + QtCore.QCoreApplication.sendEvent(widget.mapWidget.webView, wheelEvent) self.assertTrue(wheelEvent.isAccepted()) @@ -810,12 +816,14 @@ class EndpointInfoServiceTest(unittest.TestCase): self.assertIn("document.documentElement.dataset.theme", mapScript) self.assertIn('--endpoint-attribution-background', html) self.assertIn('--endpoint-text-muted', html) + self.assertIn('endpoint-loading-overlay', html) self.assertIn('endpoint-loading-spinner', html) self.assertIn('width: 16px', html) self.assertIn('height: 16px', html) self.assertIn('0.8s linear infinite', html) self.assertIn('border-right-color: transparent', html) + self.assertIn('--endpoint-font-family', html) self.assertIn("JSON.stringify(state.fontFamily", mapScript) self.assertIn('applyLoadingState()', mapScript) @@ -823,11 +831,13 @@ class EndpointInfoServiceTest(unittest.TestCase): self.assertIn('.maplibregl-canvas:focus', html) self.assertIn('outline: none', html) self.assertNotIn('color-mix(', html) + self.assertIn('const MAP_PALETTES = {', mapScript) self.assertIn('const createPalette = (themeState)', mapScript) self.assertIn('const createStyle = (themeState)', mapScript) self.assertIn('style: createStyle(state)', mapScript) self.assertIn('const styleKey = ()', mapScript) + self.assertIn("url: 'https://tiles.openfreemap.org/planet'", mapScript) self.assertIn("glyphs: 'https://tiles.openfreemap.org/fonts/", mapScript) self.assertIn("'source-layer': 'water'", mapScript) @@ -843,6 +853,7 @@ class EndpointInfoServiceTest(unittest.TestCase): self.assertNotIn('tile.openstreetmap.org', html) self.assertNotIn('opacity: 0.60', html) self.assertNotIn('opacity: 0.60', mapScript) + self.assertTrue(mapScriptPath.is_file()) self.assertTrue((widget.mapWidget.HtmlPath.parent / 'maplibre-gl.js').is_file()) self.assertTrue( @@ -897,6 +908,7 @@ class EndpointInfoServiceTest(unittest.TestCase): ) previousRevision = widget.mapWidget._viewRevision + service.completeRefresh() processQtEvents() diff --git a/tests/test_log_manager_generation.py b/tests/test_log_manager_generation.py index bedd30b4..55aea090 100644 --- a/tests/test_log_manager_generation.py +++ b/tests/test_log_manager_generation.py @@ -643,11 +643,14 @@ class GenerationLogManagerContractTest(unittest.TestCase): globalCursor = globalBatch.cursor secondCursor = manager.entriesSince(None, 'other.second').cursor + manager.clear('other.extra') manager.append('second new', 'other.second') self.assertTrue(manager.entriesSince(globalCursor).resetRequired) + secondBatch = manager.entriesSince(secondCursor, 'other.second') + self.assertTrue(secondBatch.resetRequired) self.assertEqual( tuple(entry.message for entry in secondBatch.entries), @@ -666,10 +669,12 @@ class GenerationLogManagerContractTest(unittest.TestCase): batched = self.makeManager(**options) repeated = self.makeManager(**options) messages = tuple(f'core-{index}-payload' for index in range(11)) + added = [] cleared = [] changed = [] observedSnapshots = [] + batched.entryAdded.connect(added.append) batched.entryAdded.connect( lambda _entry: observedSnapshots.append(batched.entries()) @@ -698,16 +703,20 @@ class GenerationLogManagerContractTest(unittest.TestCase): self.assertEqual(batchEntries, repeatedEntries) self.assertEqual(batched.entries(), repeated.entries()) self.assertEqual(batched.retainedCharacters, repeated.retainedCharacters) + self.assertEqual(added, list(batchEntries)) self.assertTrue(observedSnapshots) self.assertTrue( all(snapshot == batched.entries() for snapshot in observedSnapshots) ) self.assertEqual(len(cleared), 3) + processQtEvents() + self.assertEqual(changed, [batchEntries[-1].sequence]) self.assertLessEqual(batched.entryCount(), batched.maximumEntries) self.assertLessEqual(batched.retainedCharacters, batched.maximumCharacters) + _assertManagerInvariants(self, batched) _assertManagerInvariants(self, repeated) diff --git a/tests/test_profile_test_jobs.py b/tests/test_profile_test_jobs.py index fc552ae7..d6225b6b 100644 --- a/tests/test_profile_test_jobs.py +++ b/tests/test_profile_test_jobs.py @@ -395,16 +395,20 @@ class ProfileTestServiceTest(unittest.TestCase): processQtEvents() first = _ControlledDownloadWorker.instances[0] + active.deleted = True queued.deleted = True self._setProfiles((valid,)) + manager.reconcileProfiles() self.assertEqual(first.cancelCount, 1) self.assertEqual(len(scheduler.queue), 1) first.publish('stale result') + self.assertEqual(valid.metadata.speed, '') + processQtEvents() self.assertFalse(isValid(first)) @@ -518,11 +522,13 @@ class ProfileTestServiceTest(unittest.TestCase): manager.testPing((running, queued, valid)) processQtEvents() + first = pool.started[0] running.deleted = True queued.deleted = True self._setProfiles((valid,)) + manager.reconcileProfiles() self.assertIs(first.job.state, ProfileTestJobState.Cancelled) @@ -801,6 +807,7 @@ class ProfileTestServiceTest(unittest.TestCase): ) self._setProfiles((retained, replacement, other, manual)) + manager.invalidateSubscriptions({'group-a'}, clearResults=True) self.assertIs(activeLatency.job.state, ProfileTestJobState.Cancelled) @@ -824,6 +831,7 @@ class ProfileTestServiceTest(unittest.TestCase): self.assertEqual(retained.metadata.speed, '') self.assertEqual(replacement.metadata.latency, '') self.assertEqual(replacement.metadata.speed, '') + self.assertEqual(other.metadata.latency, 'other latency') self.assertEqual(other.metadata.speed, 'other speed') self.assertEqual(manual.metadata.latency, 'manual latency') diff --git a/tests/test_qt_interactions.py b/tests/test_qt_interactions.py index 81497ae0..19d31d6f 100644 --- a/tests/test_qt_interactions.py +++ b/tests/test_qt_interactions.py @@ -258,6 +258,7 @@ class ServerTableQtInteractionTest(unittest.TestCase): """Assert one semantic color invalidation and resulting paint.""" connectionController.state = state isAdmin.return_value = admin + sourceSpy = QSignalSpy(table.sourceModel.dataChanged) proxySpy = QSignalSpy(table.proxyModel.dataChanged) paintCounter.count = 0 @@ -300,12 +301,14 @@ class ServerTableQtInteractionTest(unittest.TestCase): tuple(int(role) for role in sourceArguments[2]), expectedRoles, ) + self.assertEqual(proxyArguments[0].row(), proxyIndex.row()) self.assertEqual(proxyArguments[1].row(), proxyIndex.row()) self.assertEqual( tuple(int(role) for role in proxyArguments[2]), expectedRoles, ) + self.assertEqual( table.sourceModel.data( sourceIndex, diff --git a/tests/test_subscription_manager.py b/tests/test_subscription_manager.py index ccd2c603..f4166bcd 100644 --- a/tests/test_subscription_manager.py +++ b/tests/test_subscription_manager.py @@ -476,16 +476,19 @@ class SubscriptionManagerTest(TestCase): manager._synchronizeProfiles = mock.Mock( side_effect=(RuntimeError('injected failure'), committed) ) + completed = [] committedSubscriptions = [] stateChanges = [] structuralChanges = [] + manager.updateCompleted.connect(completed.append) manager.subscriptionCommitted.connect(committedSubscriptions.append) manager.subscriptionStateChanged.connect( lambda uniques: stateChanges.append(tuple(uniques)) ) manager.subscriptionsChanged.connect(lambda: structuralChanges.append(True)) + failed = {'unique': 'group-a', 'profiles': ()} successful = {'unique': 'group-b', 'profiles': ()} @@ -1058,6 +1061,7 @@ class SubscriptionManagerTest(TestCase): groupATimer = manager._autoUpdateTimers['group-a'] groupBTimer = manager._autoUpdateTimers['group-b'] groupBTimerId = groupBTimer.timerId() + groupAReply = _AbortableReply() groupBReply = _AbortableReply() destroyed = [] @@ -1068,6 +1072,7 @@ class SubscriptionManagerTest(TestCase): manager._replySubscriptions.update( {groupAReply: 'group-a', groupBReply: 'group-b'} ) + subscriptions.pop('group-a') with mock.patch( diff --git a/tests/test_subscription_sync.py b/tests/test_subscription_sync.py index 92f26509..a353d0ba 100644 --- a/tests/test_subscription_sync.py +++ b/tests/test_subscription_sync.py @@ -93,15 +93,18 @@ class SubscriptionSynchronizerTest(unittest.TestCase): self.assertIs(profiles[0], manual) self.assertIs(profiles[1], retained) self.assertIs(profiles[-1], other) + self.assertEqual(retained.metadata.profileId, originalId) self.assertEqual(retained.itemRemark, 'Remote label') self.assertTrue(retained.metadata.favorite) self.assertEqual(retained.connection['address'], 'new.example') + self.assertTrue(removed.deleted) self.assertEqual( tuple(item.index for item in profiles), tuple(range(len(profiles))), ) + self.assertEqual(manual.itemSubscription, '') self.assertFalse(manual.itemSubscriptionManaged) self.assertEqual(other.connection['address'], 'other.example') diff --git a/tests/test_ui_behavior.py b/tests/test_ui_behavior.py index fa449953..5ff973df 100644 --- a/tests/test_ui_behavior.py +++ b/tests/test_ui_behavior.py @@ -892,6 +892,7 @@ class QRCodeWindowBehaviorTest(unittest.TestCase): '', 'socks://three.example:1080#Three', ) + window.show() processQtEvents() @@ -901,6 +902,7 @@ class QRCodeWindowBehaviorTest(unittest.TestCase): self.assertEqual(window.tabWidget.tabToolTip(0), '1 - Profile 1') self.assertEqual(window.tabWidget.tabToolTip(1), '3 - Profile 3') + firstPage = window.tabWidget.widget(0) firstPageReference = weakref.ref(firstPage) firstPageDestroyed = []