diff --git a/Furious/Service/DnsResolver.py b/Furious/Service/DnsResolver.py index 58616c6..1c455e3 100644 --- a/Furious/Service/DnsResolver.py +++ b/Furious/Service/DnsResolver.py @@ -204,6 +204,7 @@ class DnsResolver(HttpGetManager): f'DNS resolution for \'{domain}\' returned an invalid ' f'answer record' ) + resultMap['error'] = True continue @@ -224,6 +225,7 @@ class DnsResolver(HttpGetManager): f'DNS resolution for \'{domain}\' returned an unsupported ' f'non-address answer record' ) + resultMap['error'] = True continue @@ -241,6 +243,7 @@ class DnsResolver(HttpGetManager): f'DNS resolution for \'{domain}\' returned a cyclic ' f'reference to \'{reference}\'' ) + resultMap['error'] = True continue @@ -250,6 +253,7 @@ class DnsResolver(HttpGetManager): f'DNS resolution for \'{domain}\' exceeded the maximum ' f'reference depth {self.MAX_REFERENCE_DEPTH}' ) + resultMap['error'] = True continue @@ -370,6 +374,7 @@ class DnsResolver(HttpGetManager): logger.error( f'DNS resolution for \'{domain}\' reached timeout {timeout // 1000}s' ) + resultMap['error'] = True for networkReply in resultMap['reference']: diff --git a/Furious/Service/ProfileTesting.py b/Furious/Service/ProfileTesting.py index 860e986..c9ec6c4 100644 --- a/Furious/Service/ProfileTesting.py +++ b/Furious/Service/ProfileTesting.py @@ -305,6 +305,7 @@ class _LatencyScheduler(QtCore.QObject): self._publishResult = publishResult self.maxConcurrency = max(int(pingConcurrency), 1) self.tcpingMaxConcurrency = max(int(tcpingConcurrency), 1) + self.threadPool = threadPool self._ownsThreadPool = self.threadPool is None @@ -716,6 +717,7 @@ class _DownloadSpeedWorker(HttpGetManager): self._startInProgress = False self._completionInProgress = False self._pendingCompletionKwargs = None + self._runtimeLease = None self.networkReply = None @@ -1092,6 +1094,7 @@ class _DownloadSpeedScheduler(QtCore.QObject): self.maxConcurrency = max(int(maxConcurrency), 1) self.portRange = portRange self.workerFactory = workerFactory + self.queue = collections.deque() self.activeJobs = {} self.activePorts = set() diff --git a/Furious/Widget/MetricsGraph.py b/Furious/Widget/MetricsGraph.py index a3ba6bf..4110d1f 100644 --- a/Furious/Widget/MetricsGraph.py +++ b/Furious/Widget/MetricsGraph.py @@ -472,6 +472,7 @@ class MetricsGraphWidget(QWidget): transparentFill = QtGui.QColor(fillColor) transparentFill.setAlpha(0) + gradient = QtGui.QLinearGradient( chartRect.topLeft(), chartRect.bottomLeft(), diff --git a/Furious/Widget/SubscriptionTableView.py b/Furious/Widget/SubscriptionTableView.py index 1a518e6..3d2f3cc 100644 --- a/Furious/Widget/SubscriptionTableView.py +++ b/Furious/Widget/SubscriptionTableView.py @@ -684,6 +684,7 @@ class SubscriptionTableView(Mixins.QTranslatable, AppQTableView): def _restoreGroupSelection(self, selectedUniques, currentUnique): """Restore logical selection, current item, and keyboard focus by ID.""" rows = {unique: row for row, unique in enumerate(Storage.UserSubs())} + selection = self.selectionModel() selection.clearSelection() flags = ( @@ -749,6 +750,7 @@ class SubscriptionTableView(Mixins.QTranslatable, AppQTableView): value['sortOrder'] = order self.flushAll() + self.groupsChanged.emit() else: # Do not delete @@ -789,6 +791,7 @@ class SubscriptionTableView(Mixins.QTranslatable, AppQTableView): changed = Storage.moveSubscriptionGroups(selectedUniques, position) self.sourceModel.layoutChanged.emit() + self._restoreGroupSelection(selectedUniques, currentUnique) if changed: diff --git a/Furious/Window/HomePage.py b/Furious/Window/HomePage.py index 7e100bf..51ae0fc 100644 --- a/Furious/Window/HomePage.py +++ b/Furious/Window/HomePage.py @@ -729,6 +729,7 @@ class HomePage(Mixins.QTranslatable, QMainWindow): self.connectionStatus = ConnectionStatusWidget(parent=self) self.networkState = self.connectionStatus.networkState self.trafficStats = self.connectionStatus.trafficStats + self.trafficStatsManager = TrafficStatsManager(parent=self) self.trafficStatsManager.speedChanged.connect(self.trafficStats.setSpeeds) self.trafficStatsManager.usageChanged.connect(self.trafficStats.setUsage) @@ -740,6 +741,7 @@ class HomePage(Mixins.QTranslatable, QMainWindow): self._widget = QWidget() self._widget.setObjectName('HomePageContent') + self._layout = QVBoxLayout(self._widget) self._layout.setContentsMargins(20, 18, 20, 20) self._layout.setSpacing(12) @@ -861,6 +863,7 @@ class HomePage(Mixins.QTranslatable, QMainWindow): self.subscriptionFilterComboBox.currentIndexChanged.connect( self.handleSubscriptionFilterChanged ) + self.systemProxyComboBox.currentIndexChanged.connect( self.handleSystemProxyModeChanged ) diff --git a/Furious/Window/MainWindow.py b/Furious/Window/MainWindow.py index 4d7ba16..a89513c 100644 --- a/Furious/Window/MainWindow.py +++ b/Furious/Window/MainWindow.py @@ -78,6 +78,7 @@ class MainWindow(AppQMainWindow): self.homePage.userServersQTableWidget, parent=self.navigationView, ) + self.metricsHistory = MetricsHistory(parent=self) self.homePage.trafficStatsManager.sampleChanged.connect( self.metricsHistory.recordTrafficSample @@ -89,6 +90,7 @@ class MainWindow(AppQMainWindow): self.metricsHistory, parent=self.navigationView, ) + self.logPage = AppLogPage() self.settingsPage = SettingsPage( tunSettingsDialogFactory=self.homePage.getGuiTUNSettings, @@ -132,8 +134,10 @@ class MainWindow(AppQMainWindow): 'Subscription', 'star.svg', ) + self.pluginNavigationManager = PluginNavigationManager() self.pluginNavigationManager.registerPages(self.navigationView) + self.navigationView.addPage( 'metrics', self.metricsPage, @@ -147,6 +151,7 @@ class MainWindow(AppQMainWindow): 'gear-wide-connected.svg', placement='bottom', ) + self.setCentralWidget(self.navigationView) # Preserve the established application-facing server-management API. diff --git a/Furious/Window/TextEditorWindow.py b/Furious/Window/TextEditorWindow.py index a294b06..dbd80fd 100644 --- a/Furious/Window/TextEditorWindow.py +++ b/Furious/Window/TextEditorWindow.py @@ -242,6 +242,7 @@ class TextEditorWindow(AppQMainWindow): icon=bootstrapIcon('eye.svg'), popupMenu=self.viewMenu, ) + self.closeWindowButton = AppQPushButton( _('Close Window'), icon=bootstrapIcon('window-x.svg'), diff --git a/tests/test_connection_startup_async.py b/tests/test_connection_startup_async.py index 0e5b8f7..2310a79 100644 --- a/tests/test_connection_startup_async.py +++ b/tests/test_connection_startup_async.py @@ -244,8 +244,10 @@ class ConnectionStartupAsyncTest(TestCase): server = self._server() port = server.serverPort() server.close() + runtime = _Runtime() manager = self._manager() + operation = self._operation( manager, PreparedRuntime( @@ -257,10 +259,12 @@ class ConnectionStartupAsyncTest(TestCase): ), ), ) + failures = [] operation.failed.connect(lambda *_args: failures.append(_args)) self.assertTrue(waitFor(lambda: bool(failures))) + self.assertEqual(failures[0][1], 'Failed to start core') self.assertEqual(failures[0][2], 'core readiness check timed out') self.assertEqual(manager.lastStartError, 'Failed to start core') @@ -272,6 +276,7 @@ class ConnectionStartupAsyncTest(TestCase): """Let the runtime's typed exit remain the sole terminal authority.""" runtime = _Runtime() manager = self._manager() + operation = self._operation( manager, PreparedRuntime( @@ -283,6 +288,7 @@ class ConnectionStartupAsyncTest(TestCase): ), ), ) + failures = [] operation.failed.connect(lambda *_args: failures.append(_args)) @@ -291,6 +297,7 @@ class ConnectionStartupAsyncTest(TestCase): self.assertTrue(waitFor(lambda: bool(failures))) processQtEvents(5) + self.assertEqual(len(failures), 1) self.assertIs(failures[0][0], operation) self.assertEqual(failures[0][1], 'Invalid server configuration') @@ -307,6 +314,7 @@ class ConnectionStartupAsyncTest(TestCase): """Preserve code 23 when timeout delivery races a queued runtime event.""" runtime = _Runtime() manager = self._manager() + operation = self._operation( manager, PreparedRuntime( @@ -318,6 +326,7 @@ class ConnectionStartupAsyncTest(TestCase): ), ), ) + failures = [] operation.failed.connect(lambda *_args: failures.append(_args)) @@ -327,6 +336,7 @@ class ConnectionStartupAsyncTest(TestCase): self.assertTrue(waitFor(lambda: bool(failures))) processQtEvents(5) + self.assertEqual(len(failures), 1) self.assertEqual(failures[0][1], 'Invalid server configuration') self.assertEqual( @@ -342,6 +352,7 @@ class ConnectionStartupAsyncTest(TestCase): """Let process termination own the terminal result before timeout.""" runtime = _Runtime() manager = self._manager() + operation = self._operation( manager, PreparedRuntime( @@ -352,6 +363,7 @@ class ConnectionStartupAsyncTest(TestCase): ), ), ) + failures = [] operation.failed.connect(lambda *_args: failures.append(_args)) @@ -360,6 +372,7 @@ class ConnectionStartupAsyncTest(TestCase): self.assertTrue(waitFor(lambda: bool(failures))) processQtEvents(5) + self.assertEqual(len(failures), 1) self.assertEqual(failures[0][1], 'Core terminated unexpectedly') self.assertEqual( @@ -404,8 +417,10 @@ class ConnectionStartupAsyncTest(TestCase): 'Global', deepcopy=False, ) + cancelled = [] firstOperation.cancelled.connect(cancelled.append) + processQtEvents() secondOperation = manager.startAsync( @@ -413,6 +428,7 @@ class ConnectionStartupAsyncTest(TestCase): 'Global', deepcopy=False, ) + succeeded = [] secondOperation.succeeded.connect(succeeded.append) @@ -488,13 +504,16 @@ class ConnectionStartupAsyncTest(TestCase): 'example.test', timeout=200, ) + results = [] operation.finished.connect( lambda error, addresses: results.append((error, addresses)) ) + operation.start() self.assertTrue(waitFor(lambda: bool(results))) + self.assertEqual(results, [(False, ['192.0.2.10'])]) cancelled = DnsResolutionOperation( @@ -502,10 +521,12 @@ class ConnectionStartupAsyncTest(TestCase): 'cancelled.test', timeout=200, ) + staleResults = [] cancelled.finished.connect( lambda error, addresses: staleResults.append((error, addresses)) ) + cancelled.start() cancelled.cancel() processQtEvents(5) diff --git a/tests/test_controllers.py b/tests/test_controllers.py index b6b2e29..cf76e57 100644 --- a/tests/test_controllers.py +++ b/tests/test_controllers.py @@ -185,6 +185,7 @@ class ConnectionControllerTest(unittest.TestCase): coreManager=core, updatesManager=FixtureUpdatesManager(), ) + states = [] interactions = [] controller.stateChanged.connect(states.append) @@ -200,6 +201,7 @@ class ConnectionControllerTest(unittest.TestCase): mock.patch.object(controller, '_runPostConnectTasksOnce'), ): self.assertTrue(controller.startConnection(self.profile)) + self.assertEqual(controller.state, ConnectionState.Connected) self.assertIs(controller.activeProfile, self.profile) self.assertEqual( @@ -216,6 +218,7 @@ class ConnectionControllerTest(unittest.TestCase): self.assertEqual(controller.state, ConnectionState.Disconnected) self.assertIsNone(controller.activeProfile) self.assertEqual(core.stopCalls, 1) + self.assertEqual( states, [ @@ -240,6 +243,7 @@ class ConnectionControllerTest(unittest.TestCase): coreManager=core, updatesManager=FixtureUpdatesManager(), ) + errors = [] notifications = [] controller.errorOccurred.connect(errors.append) @@ -258,6 +262,7 @@ class ConnectionControllerTest(unittest.TestCase): AppSettings.get('Connect'), AppBinarySettings.OFF, ) + self.assertEqual(len(errors), 1) self.assertIn('fixture launch failure', errors[0].message) self.assertEqual(notifications, []) @@ -272,9 +277,11 @@ class ConnectionControllerTest(unittest.TestCase): coreManager=core, updatesManager=FixtureUpdatesManager(), ) + events = [] controller.stateChanged.connect(lambda state: events.append(state.value)) controller.errorOccurred.connect(lambda error: events.append(error.message)) + process = mock.Mock() process.name.return_value = 'Fixture Core' @@ -384,6 +391,7 @@ class ConnectionControllerTest(unittest.TestCase): mock.patch.object(controller, '_runPostConnectTasksOnce'), ): self.assertTrue(controller.startConnection(self.profile)) + self.assertTrue(controller.isConnecting()) proxySet.assert_not_called() @@ -393,6 +401,7 @@ class ConnectionControllerTest(unittest.TestCase): self.assertTrue(controller.isConnected()) proxySet.assert_called_once() + self.assertTrue(controller.startDisconnection()) controller.deleteLater() @@ -405,6 +414,7 @@ class ConnectionControllerTest(unittest.TestCase): coreManager=core, updatesManager=FixtureUpdatesManager(), ) + errors = [] controller.errorOccurred.connect(errors.append) @@ -416,6 +426,7 @@ class ConnectionControllerTest(unittest.TestCase): ), ): self.assertTrue(controller.startConnection(self.profile)) + operation = core.operations[0][0] operation.fail( 'Invalid server configuration', @@ -482,17 +493,21 @@ class ConnectionControllerTest(unittest.TestCase): ): self.assertTrue(controller.startConnection(self.profile)) first = core.operations[0][0] + self.assertTrue(controller.startReconnection()) second = core.operations[1][0] first.succeed() + self.assertTrue(controller.isConnecting()) proxySet.assert_not_called() core.runtimes.append(object()) second.succeed() + self.assertTrue(controller.isConnected()) proxySet.assert_called_once() + controller.startDisconnection() self.assertEqual(core.cancelCalls, [first]) diff --git a/tests/test_log_manager_generation.py b/tests/test_log_manager_generation.py index 3984bab..2f64609 100644 --- a/tests/test_log_manager_generation.py +++ b/tests/test_log_manager_generation.py @@ -456,17 +456,20 @@ class GenerationLogManagerContractTest(unittest.TestCase): maximumCharacters=size * 30 + 100, autoClearMaximumEntries=size, ) + for index in range(size): manager.append(f'core {index}', CORE_LOG_CATEGORY) manager.append(f'other {index}', 'other.extra') observed = [] + for generation in manager._activeGenerationsLocked(): index = _ObservedEntries(generation.entries) generation.entries = index observed.append(index) manager.append('trigger', CORE_LOG_CATEGORY) + self.assertEqual(sum(item.iterations for item in observed), 0) self.assertEqual(sum(item.deletions for item in observed), 0) self.assertEqual(sum(item.oldestRemovals for item in observed), 0) @@ -475,20 +478,26 @@ class GenerationLogManagerContractTest(unittest.TestCase): for index in range(size): manager.append(f'runtime {index}', 'runtime.extra') + runtime = manager._runtimeGeneration watchedRuntime = _ObservedEntries(runtime.entries) runtime.entries = watchedRuntime + manager.clear(runtimeOnly=True) + self.assertEqual(watchedRuntime.iterations, 0) self.assertEqual(watchedRuntime.oldestRemovals, 0) manager.append('application', APPLICATION_LOG_CATEGORY) watched = [] + for generation in manager._activeGenerationsLocked(): index = _ObservedEntries(generation.entries) generation.entries = index watched.append(index) + manager.clear() + self.assertEqual(sum(item.iterations for item in watched), 0) self.assertEqual(sum(item.oldestRemovals for item in watched), 0) _assertManagerInvariants(self, manager) @@ -955,18 +964,23 @@ class GenerationLogManagerContractTest(unittest.TestCase): with self.subTest(backlog=backlog, budget=budget): manager = self.makeManager(maximumEntries=200) manager.RetiredCleanupBudget = budget + for index in range(backlog): manager.append(str(index), CORE_LOG_CATEGORY) if index in (1, 4): manager.clear(runtimeOnly=True) + manager.clear(runtimeOnly=True) + before = manager.retiredEntryCount identifiers = tuple( getattr(batch, 'identifier', None) for batch in manager._retiredBatches ) + with manager._lock: released = manager._cleanupRetiredLocked() + self.assertEqual(released, min(budget, before)) self.assertEqual(manager.retiredEntryCount, before - released) if manager._retiredBatches and identifiers: @@ -974,13 +988,16 @@ class GenerationLogManagerContractTest(unittest.TestCase): getattr(manager._retiredBatches[0], 'identifier', None), identifiers, ) + _assertManagerInvariants(self, manager) manager = self.makeManager() manager.append('retired', CORE_LOG_CATEGORY) manager.clear(runtimeOnly=True) + with manager._lock: self.assertEqual(manager._cleanupRetiredLocked(0), 0) + manager.RetiredCleanupBudget = 0 with manager._lock: self.assertEqual(manager._cleanupRetiredLocked(), 1) @@ -989,25 +1006,32 @@ class GenerationLogManagerContractTest(unittest.TestCase): """Finish each older clear-all stream before touching the next one.""" manager = self.makeManager(autoClearEnabled=False) manager.RetiredCleanupBudget = 1 + manager.append('application 1', APPLICATION_LOG_CATEGORY) manager.append('application 2', APPLICATION_LOG_CATEGORY) manager.append('runtime 1', CORE_LOG_CATEGORY) manager.append('runtime 2', CORE_LOG_CATEGORY) manager.append('other 1', 'other.extra') manager.append('other 2', 'other.extra') + expectedIdentifiers = tuple( generation.identifier for generation in manager._activeGenerationsLocked() ) + manager.clear() + self.assertEqual( tuple(batch.identifier for batch in manager._retiredBatches), expectedIdentifiers, ) + observedHeads = [] + while manager.retiredEntryCount: observedHeads.append(manager._retiredBatches[0].identifier) with manager._lock: self.assertEqual(manager._cleanupRetiredLocked(), 1) + self.assertEqual( observedHeads, [ @@ -1108,6 +1132,7 @@ class GenerationLogManagerContractTest(unittest.TestCase): autoClearEnabled=False, ) categoryIds = [] + for index in range(100): category = manager.registerCategory( LogCategory( @@ -1118,17 +1143,23 @@ class GenerationLogManagerContractTest(unittest.TestCase): ) ) categoryIds.append(category.id) + for index in range(2_000): manager.append(str(index), categoryIds[index % len(categoryIds)]) + for categoryId in (categoryIds[0], categoryIds[-1], categoryIds[51]): expected = tuple( entry for entry in manager.entries() if entry.categoryId != categoryId ) + manager.clear(categoryId) + self.assertEqual(manager.entries(), expected) _assertManagerInvariants(self, manager) + duplicate = manager.category(categoryIds[1]) self.assertIs(manager.registerCategory(duplicate), duplicate) + with self.assertRaises(ValueError): manager.registerCategory(LogCategory(categoryIds[1], 'Different')) @@ -1136,8 +1167,10 @@ class GenerationLogManagerContractTest(unittest.TestCase): """Account normalized storage and preserve state after bad input.""" manager = self.makeManager(maximumEntryCharacters=32) messages = ('', 'x', 'line\r\n', 'šŸ˜€é', 'a\0b', 'n\n' * 20, 'z' * 33) + for message in messages: entry = manager.append(message) + self.assertLessEqual(len(entry.message), 32) _assertManagerInvariants(self, manager) @@ -1146,16 +1179,22 @@ class GenerationLogManagerContractTest(unittest.TestCase): raise RuntimeError('conversion failed') before = manager.entries() + with self.assertRaises(RuntimeError): manager.append(RaisingString()) + with self.assertRaises(RuntimeError): manager.appendMany(('must not commit', RaisingString(), 'unreached')) + with self.assertRaises(TypeError): manager.append('bad timestamp', timestamp='not a datetime') + with self.assertRaises(KeyError): manager.append('unknown', 'missing') + with self.assertRaises(ValueError): manager.clear(CORE_LOG_CATEGORY, runtimeOnly=True) + self.assertEqual(manager.entries(), before) _assertManagerInvariants(self, manager) @@ -1259,6 +1298,7 @@ class GenerationLogManagerContractTest(unittest.TestCase): errors = [] returnedSequences = [] sequenceLock = threading.Lock() + # Producers plus the reader and mutator are the complete # participant set. Keeping the exact cardinality here makes a # failed synchronization a real deadlock signal rather than a @@ -1267,8 +1307,10 @@ class GenerationLogManagerContractTest(unittest.TestCase): def producer(workerIndex): randomizer = random.Random(10_000 + workerIndex) + try: start.wait(5) + for index in range(300): categoryId = randomizer.choice( ( @@ -1280,6 +1322,7 @@ class GenerationLogManagerContractTest(unittest.TestCase): ) ) entry = manager.append(f'{workerIndex}:{index}', categoryId) + with sequenceLock: returnedSequences.append(entry.sequence) except Exception as error: @@ -1289,13 +1332,17 @@ class GenerationLogManagerContractTest(unittest.TestCase): try: start.wait(5) cursor = None + for _index in range(500): entries = manager.entries() + self.assertEqual( tuple(item.sequence for item in entries), tuple(sorted(item.sequence for item in entries)), ) + batch = manager.entriesSince(cursor, CORE_LOG_CATEGORY) + self.assertEqual( tuple(item.sequence for item in batch.entries), tuple(sorted(item.sequence for item in batch.entries)), @@ -1316,10 +1363,13 @@ class GenerationLogManagerContractTest(unittest.TestCase): def mutator(): randomizer = random.Random(77) + try: start.wait(5) + for index in range(180): choice = randomizer.randrange(4) + if choice == 0: manager.clear(runtimeOnly=True) elif choice == 1: @@ -1338,11 +1388,14 @@ class GenerationLogManagerContractTest(unittest.TestCase): threads.extend( (threading.Thread(target=reader), threading.Thread(target=mutator)) ) + for thread in threads: thread.start() + for thread in threads: thread.join(20) self.assertFalse(thread.is_alive(), 'possible LogManager deadlock') + self.assertEqual(errors, []) self.assertEqual(len(returnedSequences), producerCount * 300) self.assertEqual(len(set(returnedSequences)), len(returnedSequences)) diff --git a/tests/test_profile_test_jobs.py b/tests/test_profile_test_jobs.py index a2c9402..9290675 100644 --- a/tests/test_profile_test_jobs.py +++ b/tests/test_profile_test_jobs.py @@ -430,6 +430,7 @@ class ProfileTestServiceTest(unittest.TestCase): profiles = [ self._profile(f'profile-{index}', f'{index}.example') for index in range(4) ] + manager = self._manager(profiles, controlledDownloads=False) scheduler = manager._concurrentDownloadScheduler scheduler.maxConcurrency = len(profiles) @@ -444,6 +445,7 @@ class ProfileTestServiceTest(unittest.TestCase): return worker scheduler.workerFactory = workerFactory + registry = mock.Mock() registry.prepareDownloadTest.side_effect = lambda profile, _port: profile registry.createCoreRuntime.side_effect = ( @@ -485,6 +487,7 @@ class ProfileTestServiceTest(unittest.TestCase): active = self._profile('active', 'active.example') queued = self._profile('queued', 'queued.example') valid = self._profile('valid', 'valid.example') + manager = self._manager((active, queued, valid)) scheduler = manager._serialDownloadScheduler @@ -512,6 +515,7 @@ class ProfileTestServiceTest(unittest.TestCase): processQtEvents() self.assertFalse(isValid(first)) + second = _ControlledDownloadWorker.instances[1] self.assertEqual( second.profile.metadata.profileId, @@ -913,6 +917,7 @@ class ProfileTestServiceTest(unittest.TestCase): self.assertIs(activeLatency.job.state, ProfileTestJobState.Cancelled) self.assertEqual(serialWorker.cancelCount, 1) self.assertEqual(concurrentWorker.cancelCount, 1) + self.assertTrue( all( job.target.subscriptionSource == 'group-b' diff --git a/tests/test_subscription_manager.py b/tests/test_subscription_manager.py index 05c7150..06fb7d0 100644 --- a/tests/test_subscription_manager.py +++ b/tests/test_subscription_manager.py @@ -504,6 +504,7 @@ class SubscriptionManagerTest(TestCase): self.assertEqual(completed[0].successful[0]['unique'], 'group-b') self.assertEqual(completed[0].failed[0]['unique'], 'group-a') self.assertIn('injected failure', completed[0].failed[0]['error']) + self.assertEqual(committedSubscriptions, ['group-b']) self.assertEqual(stateChanges, [('group-b', 'group-a')]) self.assertEqual(structuralChanges, [True]) @@ -704,39 +705,47 @@ class SubscriptionManagerTest(TestCase): ) as lifecycleLog, ): manager.configureAutoUpdate('group-a') + lifecycleLog.assert_not_called() subscriptions['group-a']['autoupdate'] = 'Every 5 mins' manager.configureAutoUpdate('group-a') + self.assertTrue(timer.isActive()) self.assertEqual(timer.interval(), 5 * 60 * 1000) self.assertIn('start auto update job', lifecycleLog.call_args.args[0]) activeTimerId = timer.timerId() manager.configureAutoUpdate('group-a') + self.assertEqual(timer.timerId(), activeTimerId) self.assertEqual(lifecycleLog.call_count, 1) subscriptions['group-a']['autoupdate'] = 'Every 10 mins' manager.configureAutoUpdate('group-a') + self.assertIs(manager._autoUpdateTimers['group-a'], timer) self.assertEqual(timer.interval(), 10 * 60 * 1000) self.assertIn('reschedule auto update job', lifecycleLog.call_args.args[0]) subscriptions['group-a']['enabled'] = False manager.configureAutoUpdate('group-a') + self.assertFalse(timer.isActive()) self.assertIn('stop auto update job', lifecycleLog.call_args.args[0]) manager.configureAutoUpdate('group-a') + self.assertEqual(lifecycleLog.call_count, 3) subscriptions['group-a']['enabled'] = True manager.configureAutoUpdate('group-a') + self.assertTrue(timer.isActive()) self.assertIn('start auto update job', lifecycleLog.call_args.args[0]) self.assertEqual(lifecycleLog.call_count, 4) + manager.deleteLater() def testUnrelatedSubscriptionEditDoesNotRestartItsTimer(self): @@ -1064,6 +1073,7 @@ class SubscriptionManagerTest(TestCase): groupBReply = _AbortableReply() destroyed = [] groupATimer.destroyed.connect(lambda *_args: destroyed.append(True)) + manager._activeReplies.update( {groupAReply: groupAReply, groupBReply: groupBReply} ) @@ -1334,6 +1344,7 @@ class SubscriptionManagerTest(TestCase): [], [], ) + completed = [] structural = [] manager.updateCompleted.connect(completed.append)