Files
LorenEteval_Furious/tests/test_models_and_services.py
T
2026-08-29 19:11:20 +08:00

1376 lines
50 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Copyright (C) 2024present Loren Eteval & contributors <loren.eteval@proton.me>
#
# This file is part of Furious.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
"""Exercise models, persistence, logging, metrics, and settings migration."""
from __future__ import annotations
from Furious.Controllers.SettingsController import (
APPLICATION_THEME_SETTING,
SettingsController,
)
from Furious.Frozenlib import (
AppBinarySettings,
AppBuiltinProxyMode,
AppSettings,
ApplicationTheme,
)
from Furious.Models import (
CoreConfiguration,
ProfileMetadata,
ServerProfile,
profileConnectionFingerprint,
)
from Furious.Repository.Servers import UserServer, UserServers
from Furious.Repository.Subscriptions import SubscriptionGroup, UserSubs
from Furious.Service.LogManager import (
APPLICATION_LOG_CATEGORY,
CORE_LOG_CATEGORY,
TUN2SOCKS_LOG_CATEGORY,
LogManager,
)
from Furious.Service.MetricsHistory import (
DOWNLOAD_SPEED_METRIC,
DOWNLOAD_USAGE_METRIC,
MetricsHistory,
)
from PySide6 import QtCore
from collections import OrderedDict
import threading
import unittest
import weakref
from unittest import mock
from tests.support import application, isolatedSettings, processQtEvents
class _ObservedEntryIndex(OrderedDict):
"""Record global-index traversal and removal requested by one operation."""
def __init__(self, entries=()):
"""Copy existing entries before beginning operation instrumentation."""
self.iterationRequests = 0
self.deletedKeys = []
self.oldestRemovals = 0
super().__init__(entries)
def __iter__(self):
"""Count direct traversal of the global chronological index."""
self.iterationRequests += 1
return super().__iter__()
def items(self):
"""Count item traversal of the global chronological index."""
self.iterationRequests += 1
return super().items()
def keys(self):
"""Count key traversal of the global chronological index."""
self.iterationRequests += 1
return super().keys()
def values(self):
"""Count value traversal of the global chronological index."""
self.iterationRequests += 1
return super().values()
def __delitem__(self, key):
"""Record direct sequence-key deletion during category clearing."""
self.deletedKeys.append(key)
return super().__delitem__(key)
def popitem(self, last=True):
"""Record global oldest-entry eviction without counting it as a scan."""
if not last:
self.oldestRemovals += 1
return super().popitem(last=last)
class ProfileModelTest(unittest.TestCase):
"""Verify metadata separation, compatibility, and copy semantics."""
def testCoreConfigurationRejectsNonObjectJSONWithoutRaising(self):
"""Treat valid non-object JSON roots as invalid connection documents."""
for value in ('[]', 'null', '42', '"text"'):
with self.subTest(value=value):
configuration = CoreConfiguration(value)
self.assertEqual(configuration, {})
self.assertTrue(configuration.constructionError())
configuration = CoreConfiguration({1: 'value'})
self.assertEqual(configuration, {})
self.assertEqual(
configuration.constructionError(),
'configuration keys must be strings',
)
def testCoreConfigurationReportsSerializationFailure(self):
"""Keep the empty sentinel while exposing a useful diagnostic."""
configuration = CoreConfiguration({'unsupported': object()})
self.assertEqual(configuration.toJSONString(), '')
self.assertTrue(configuration.serializationError())
configuration['unsupported'] = 'supported'
self.assertTrue(configuration.toJSONString())
self.assertEqual(configuration.serializationError(), '')
def testLegacyMetadataPreservesUnknownFields(self):
"""Promote known legacy fields while retaining forward-only metadata."""
metadata = ProfileMetadata.fromMapping(
{
'remark': 'Legacy name',
'subsId': 'subscription-id',
'delayResult': '42 ms',
'speedResult': '8 MiB/s',
'tags': 'work, ipv6',
'favorite': 'true',
'futureMetadata': {'value': 7},
}
)
self.assertEqual(metadata.displayName, 'Legacy name')
self.assertEqual(metadata.subscriptionSource, 'subscription-id')
self.assertTrue(metadata.subscriptionManaged)
self.assertEqual(metadata.latency, '42 ms')
self.assertEqual(metadata.speed, '8 MiB/s')
self.assertEqual(metadata.tags, ('work', 'ipv6'))
self.assertTrue(metadata.favorite)
self.assertEqual(metadata.extras['futureMetadata'], {'value': 7})
self.assertEqual(
ProfileMetadata.fromMapping(metadata.toMapping()).toMapping(),
metadata.toMapping(),
)
def testMetadataPromotesKnownExtrasWithoutCollisions(self):
"""Migrate newly recognized fields out of nested forward metadata."""
metadata = ProfileMetadata.fromMapping(
{
'remark': 'Explicit legacy name',
'subsId': 'subscription-id',
'extras': {
'displayName': 'Previously unknown name',
'favorite': True,
'subscriptionManaged': True,
'subscriptionProfileKey': 'stale:key',
'futureMetadata': 7,
},
}
)
self.assertEqual(metadata.displayName, 'Explicit legacy name')
self.assertTrue(metadata.favorite)
self.assertEqual(metadata.subscriptionSource, 'subscription-id')
self.assertTrue(metadata.subscriptionManaged)
self.assertEqual(metadata.subscriptionProfileKey, 'stale:key')
self.assertEqual(metadata.extras, {'futureMetadata': 7})
def testIndependentCopyGetsNewIdentityAndNoSubscriptionOwner(self):
"""Keep manual copies independent from subscription synchronization."""
original = ServerProfile.fromConfiguration(
CoreConfiguration({'type': 'fixture', 'address': 'example.com'}),
{
'displayName': 'Managed',
'subscriptionSource': 'source',
'subscriptionManaged': True,
'subscriptionProfileKey': 'upstream:1',
},
)
copied = original.independentCopy()
self.assertNotEqual(copied.metadata.profileId, original.metadata.profileId)
self.assertEqual(copied.connection, original.connection)
self.assertIsNot(copied.connection, original.connection)
self.assertEqual(copied.metadata.subscriptionSource, '')
self.assertFalse(copied.metadata.subscriptionManaged)
self.assertEqual(copied.metadata.subscriptionProfileKey, '')
def testProfileExportAndRemoteAddressRemainCoreNeutral(self):
"""Delegate generic profile behavior without protocol-specific options."""
class Configuration(CoreConfiguration):
@property
def itemProtocol(self):
return 'Shadowsocks'
@property
def itemAddress(self):
return 'display.example'
def remoteAddress(self):
return 'routing.example'
def toURI(self, remark=''):
return f'fixture://{remark}'
profile = ServerProfile.fromConfiguration(
Configuration({'type': 'fixture'}),
{'displayName': 'Profile'},
)
self.assertEqual(profile.toURI(), 'fixture://Profile')
self.assertEqual(profile.itemAddress, 'display.example')
self.assertEqual(profile.remoteAddress(), 'routing.example')
def testConnectionFingerprintIsCanonicalAndRejectsUnsupportedValues(self):
"""Hash only canonical JSON connection semantics."""
first = ServerProfile.fromConfiguration(
CoreConfiguration({'address': 'example.com', 'port': 443}),
{'displayName': 'First'},
)
second = ServerProfile.fromConfiguration(
CoreConfiguration({'port': 443, 'address': 'example.com'}),
{'displayName': 'Second'},
)
self.assertEqual(
profileConnectionFingerprint(first),
profileConnectionFingerprint(second),
)
with self.assertRaisesRegex(TypeError, 'JSON-compatible'):
profileConnectionFingerprint(CoreConfiguration({'value': object()}))
def testUserServerMappingRemainsBackwardCompatible(self):
"""Persist the canonical legacy record shape plus per-profile metadata."""
profile = ServerProfile.fromConfiguration(
CoreConfiguration({'type': 'fixture', 'port': 1080}),
{
'displayName': 'Fixture',
'group': 'Tests',
'annotations': 'local only',
'favorite': True,
'futureField': 'preserved',
},
)
mapping = UserServer.fromProfile(profile).toMapping()
restored = UserServer.metadataFromMapping(mapping)
self.assertEqual(set(('remark', 'config', 'subsId')) - set(mapping), set())
self.assertEqual(restored.displayName, 'Fixture')
self.assertEqual(restored.group, 'Tests')
self.assertEqual(restored.annotations, 'local only')
self.assertTrue(restored.favorite)
self.assertEqual(restored.extras['futureField'], 'preserved')
class IsolatedRepositoryTest(unittest.TestCase):
"""Prove repositories round-trip exclusively through temporary QSettings."""
def testServerRepositoryRoundTripUsesCanonicalModel(self):
"""Restore connection and metadata without touching production state."""
with isolatedSettings() as settings:
repository = UserServers()
repository.data().append(
ServerProfile.fromConfiguration(
CoreConfiguration({'type': 'fixture', 'value': 9}),
{
'displayName': 'Temporary profile',
'tags': ('one', 'two'),
'favorite': True,
'unknown': 'retained',
},
)
)
repository.sync()
self.assertTrue(settings.contains('Configuration'))
restored = UserServers().data()
self.assertEqual(len(restored), 1)
self.assertEqual(restored[0].connection['value'], 9)
self.assertEqual(restored[0].itemRemark, 'Temporary profile')
self.assertEqual(restored[0].metadata.tags, ('one', 'two'))
self.assertTrue(restored[0].metadata.favorite)
self.assertEqual(restored[0].metadata.extras['unknown'], 'retained')
def testSubscriptionRepositoryNormalizesAndOrdersLegacyGroups(self):
"""Keep future fields while migrating URL-era subscription records."""
with isolatedSettings():
repository = UserSubs()
repository.upsertGroup(
SubscriptionGroup.fromMapping(
'second',
{
'remark': 'Zulu',
'webURL': 'https://two.invalid',
'sortOrder': 2,
'futureField': 'two',
},
)
)
repository.upsertGroup(
SubscriptionGroup.fromMapping(
'first',
{
'remark': 'Alpha',
'webURL': 'https://one.invalid',
'sortOrder': 1,
'enabled': 'false',
},
)
)
repository.sync()
restored = UserSubs()
groups = restored.groups()
self.assertEqual(tuple(group.id for group in groups), ('first', 'second'))
self.assertFalse(groups[0].enabled)
self.assertEqual(groups[1].extras['futureField'], 'two')
self.assertEqual(restored.removeGroup('first').remark, 'Alpha')
self.assertIsNone(restored.group('first'))
class SettingsMigrationTest(unittest.TestCase):
"""Verify forward/backward-compatible application-theme persistence."""
def testLegacyDarkModeMigratesWithoutRemovingLegacyValue(self):
"""Create the new preference while retaining the old binary key."""
with isolatedSettings() as settings:
settings.setValue('DarkMode', AppBinarySettings.ON_)
SettingsController()
self.assertEqual(
AppSettings.get(APPLICATION_THEME_SETTING),
ApplicationTheme.Dark.value,
)
self.assertEqual(settings.value('DarkMode'), AppBinarySettings.ON_)
def testNewPreferenceSynchronizesLegacyReaders(self):
"""Keep older releases able to read forced dark and non-dark choices."""
with isolatedSettings() as settings:
SettingsController()
SettingsController.setApplicationTheme(ApplicationTheme.Dark)
self.assertEqual(settings.value('DarkMode'), AppBinarySettings.ON_)
SettingsController.setApplicationTheme(ApplicationTheme.Light)
self.assertEqual(settings.value('DarkMode'), AppBinarySettings.OFF)
self.assertEqual(
settings.value(APPLICATION_THEME_SETTING),
ApplicationTheme.Light.value,
)
def testSharedNetworkPreferencesEmitOnlyRealChanges(self):
"""Synchronize multiple views without publishing duplicate mutations."""
application()
with (
isolatedSettings(),
mock.patch(
'Furious.Controllers.SettingsController.PLATFORM',
'Linux',
),
mock.patch(
'Furious.Controllers.SettingsController.showMBoxNewChangesNextTime'
),
):
AppSettings.turnOFF('VPNMode')
AppSettings.set(
'SystemProxyMode',
AppBuiltinProxyMode.Auto.value,
)
controller = SettingsController()
tunStates = []
proxyModes = []
controller.tunModeChanged.connect(tunStates.append)
controller.systemProxyModeChanged.connect(proxyModes.append)
controller.setTUNMode(True)
controller.setTUNMode(True)
controller.setSystemProxyMode(AppBuiltinProxyMode.NoChanges.value)
controller.setSystemProxyMode(AppBuiltinProxyMode.NoChanges.value)
self.assertEqual(tunStates, [True])
self.assertEqual(
proxyModes,
[AppBuiltinProxyMode.NoChanges.value],
)
self.assertTrue(AppSettings.isStateON_('VPNMode'))
self.assertEqual(
AppSettings.get('SystemProxyMode'),
AppBuiltinProxyMode.NoChanges.value,
)
class LogManagerTest(unittest.TestCase):
"""Verify bounded, categorized, and thread-safe structured logging."""
def _assertIndexesConsistent(self, manager):
"""Verify active/retired indexes and aggregate accounting agree."""
with manager._lock:
liveSequences = []
liveCharacters = 0
for generation in manager._activeGenerationsLocked():
generationSequences = tuple(generation.entries)
categorySequences = []
categoryCharacters = 0
self.assertEqual(
generationSequences,
tuple(sorted(generationSequences)),
)
for categoryId, categoryEntries in generation.entriesByCategory.items():
self.assertEqual(
categoryEntries.characterCount,
sum(
len(entry.message)
for entry in categoryEntries.entries.values()
),
)
for sequence, entry in categoryEntries.entries.items():
self.assertEqual(entry.categoryId, categoryId)
self.assertIs(generation.entries[sequence], entry)
categorySequences.append(sequence)
categoryCharacters += categoryEntries.characterCount
self.assertEqual(
tuple(sorted(categorySequences)),
generationSequences,
)
self.assertEqual(generation.characterCount, categoryCharacters)
liveSequences.extend(generationSequences)
liveCharacters += generation.characterCount
self.assertEqual(
tuple(entry.sequence for entry in manager.entries()),
tuple(sorted(liveSequences)),
)
self.assertEqual(manager.entryCount(), len(liveSequences))
self.assertEqual(manager._retainedEntryCount, len(liveSequences))
self.assertEqual(
manager.retainedCharacters,
liveCharacters,
)
self.assertEqual(manager._retainedCharacters, liveCharacters)
self.assertEqual(
manager._retiredEntryCount,
sum(batch.entryCount for batch in manager._retiredBatches),
)
self.assertEqual(
manager._retiredCharacters,
sum(batch.characterCount for batch in manager._retiredBatches),
)
def testBoundedBufferCategoriesAndRuntimeClear(self):
"""Retain only the newest entries and clear runtime categories alone."""
manager = LogManager(maximumEntries=4)
manager.append('application 1')
manager.append('core 1', CORE_LOG_CATEGORY)
manager.append('application 2')
manager.append('core 2', CORE_LOG_CATEGORY)
manager.append('application 3')
sequence, entries = manager.snapshot()
self.assertEqual(sequence, 5)
self.assertEqual(
tuple(entry.message for entry in entries),
('core 1', 'application 2', 'core 2', 'application 3'),
)
self.assertEqual(
tuple(entry.message for entry in manager.entries(CORE_LOG_CATEGORY)),
('core 1', 'core 2'),
)
manager.clear(runtimeOnly=True)
self.assertEqual(
tuple(entry.categoryId for entry in manager.entries()),
(APPLICATION_LOG_CATEGORY, APPLICATION_LOG_CATEGORY),
)
def testConcurrentProducersReceiveUniqueOrderedSequences(self):
"""Serialize worker-thread appends without losing or duplicating entries."""
manager = LogManager(maximumEntries=500)
def produce(prefix):
"""Append one deterministic worker batch."""
for index in range(100):
manager.append(f'{prefix}-{index}')
workers = tuple(
threading.Thread(target=produce, args=(prefix,))
for prefix in ('a', 'b', 'c')
)
for worker in workers:
worker.start()
for worker in workers:
worker.join(5)
entries = manager.entries()
sequences = tuple(entry.sequence for entry in entries)
self.assertTrue(all(not worker.is_alive() for worker in workers))
self.assertEqual(len(entries), 300)
self.assertEqual(sequences, tuple(range(1, 301)))
self.assertEqual(len({entry.message for entry in entries}), 300)
def testAutoClearRetainsOnlyApplicationHistoryAtCoreThreshold(self):
"""Clear every non-Application category at the Core threshold."""
manager = LogManager(
maximumEntries=20,
autoClearMaximumEntries=3,
)
cleared = []
componentCategory = manager.registerComponent(
'component.fixture',
'Fixture',
)
manager.entriesCleared.connect(cleared.append)
manager.append('application retained', APPLICATION_LOG_CATEGORY)
for index in range(3):
manager.append(f'old core {index}', CORE_LOG_CATEGORY)
manager.append('old tun2socks', TUN2SOCKS_LOG_CATEGORY)
manager.append('old component', componentCategory.id)
manager.append('new core after clear', CORE_LOG_CATEGORY)
self.assertEqual(
tuple(entry.message for entry in manager.entries()),
('application retained', 'new core after clear'),
)
self.assertEqual(manager.entryCount(CORE_LOG_CATEGORY), 1)
self.assertEqual(manager.entryCount(TUN2SOCKS_LOG_CATEGORY), 0)
self.assertEqual(manager.entryCount(componentCategory.id), 0)
self.assertEqual(manager.entryCount(APPLICATION_LOG_CATEGORY), 1)
self.assertEqual(
cleared,
[
frozenset(
{
CORE_LOG_CATEGORY,
TUN2SOCKS_LOG_CATEGORY,
componentCategory.id,
}
)
],
)
def testDisablingAutoClearLeavesOnlyGlobalBoundActive(self):
"""Do not apply the Core threshold while automatic clearing is disabled."""
manager = LogManager(
maximumEntries=10,
autoClearMaximumEntries=3,
autoClearEnabled=False,
)
manager.append('application retained', APPLICATION_LOG_CATEGORY)
for index in range(3):
manager.append(f'core {index}', CORE_LOG_CATEGORY)
manager.append(f'tun2socks {index}', TUN2SOCKS_LOG_CATEGORY)
self.assertEqual(manager.entryCount(CORE_LOG_CATEGORY), 3)
manager.setAutoClearEnabled(True)
self.assertTrue(manager.autoClearEnabled)
self.assertEqual(manager.entryCount(CORE_LOG_CATEGORY), 0)
self.assertEqual(manager.entryCount(TUN2SOCKS_LOG_CATEGORY), 0)
self.assertEqual(manager.entryCount(APPLICATION_LOG_CATEGORY), 1)
def testCharacterBudgetsRemainHardWhenAutoClearIsDisabled(self):
"""Bound both total text and one hostile entry independently of count."""
manager = LogManager(
maximumEntries=100,
maximumCharacters=80,
maximumEntryCharacters=32,
autoClearEnabled=False,
)
manager.append('a' * 100, CORE_LOG_CATEGORY)
self.assertEqual(len(manager.entries()[0].message), 32)
self.assertIn('truncated', manager.entries()[0].message)
for index in range(20):
manager.append(f'{index:02d}-' + ('x' * 17), CORE_LOG_CATEGORY)
self.assertLessEqual(manager.retainedCharacters, 80)
self.assertLessEqual(manager.entryCount(), 4)
self.assertEqual(
manager.retainedCharacters,
sum(len(entry.message) for entry in manager.entries()),
)
self.assertEqual(
manager.entryCount(CORE_LOG_CATEGORY),
manager.entryCount(),
)
self._assertIndexesConsistent(manager)
def testCategoryOperationsDoNotTraverseUnrelatedApplicationHistory(self):
"""Make Core rollover constant regardless of runtime/history skew."""
cases = (
(1_000, 7_000, 2_000),
(9_000, 900, 100),
)
for applicationCount, coreCount, tunCount in cases:
with self.subTest(
application=applicationCount,
core=coreCount,
tun2socks=tunCount,
):
manager = LogManager(
maximumEntries=20_000,
autoClearMaximumEntries=coreCount,
)
for index in range(applicationCount):
manager.append(
f'application {index}',
APPLICATION_LOG_CATEGORY,
)
for index in range(coreCount):
manager.append(f'core {index}', CORE_LOG_CATEGORY)
for index in range(tunCount):
manager.append(f'tun2socks {index}', TUN2SOCKS_LOG_CATEGORY)
applicationGeneration = manager._applicationGeneration
runtimeGeneration = manager._runtimeGeneration
observedIndexes = []
for generation in manager._activeGenerationsLocked():
observedEntries = _ObservedEntryIndex(generation.entries)
generation.entries = observedEntries
observedIndexes.append(observedEntries)
for categoryEntries in generation.entriesByCategory.values():
observedCategory = _ObservedEntryIndex(categoryEntries.entries)
categoryEntries.entries = observedCategory
observedIndexes.append(observedCategory)
sequence, coreEntries = manager.snapshot(CORE_LOG_CATEGORY)
self.assertEqual(
sequence,
applicationCount + coreCount + tunCount,
)
self.assertEqual(len(coreEntries), coreCount)
self.assertEqual(
sum(index.iterationRequests for index in observedIndexes),
1,
)
for index in observedIndexes:
index.iterationRequests = 0
manager.append('new core after clear', CORE_LOG_CATEGORY)
self.assertIs(manager._applicationGeneration, applicationGeneration)
self.assertIsNot(manager._runtimeGeneration, runtimeGeneration)
self.assertEqual(
sum(index.iterationRequests for index in observedIndexes),
0,
)
self.assertEqual(
sum(len(index.deletedKeys) for index in observedIndexes),
0,
)
self.assertEqual(
sum(index.oldestRemovals for index in observedIndexes),
0,
)
self.assertEqual(
manager.retiredEntryCount,
coreCount + tunCount,
)
self.assertEqual(
manager.entryCount(APPLICATION_LOG_CATEGORY),
applicationCount,
)
self.assertEqual(manager.entryCount(CORE_LOG_CATEGORY), 1)
self.assertEqual(manager.entryCount(TUN2SOCKS_LOG_CATEGORY), 0)
self.assertEqual(
tuple(entry.message for entry in manager.entries()),
tuple(f'application {index}' for index in range(applicationCount))
+ ('new core after clear',),
)
self._assertIndexesConsistent(manager)
def testWholeGenerationClearDoesNoPhysicalEntryWorkOnCaller(self):
"""Swap every active stream without traversing or destroying its entries."""
manager = LogManager(maximumEntries=1_000, autoClearEnabled=False)
otherCategory = manager.registerComponent(
'component.persistent',
'Persistent',
runtime=False,
)
for index in range(100):
manager.append(f'application {index}', APPLICATION_LOG_CATEGORY)
manager.append(f'core {index}', CORE_LOG_CATEGORY)
manager.append(f'other {index}', otherCategory.id)
observedIndexes = []
for generation in manager._activeGenerationsLocked():
observedEntries = _ObservedEntryIndex(generation.entries)
generation.entries = observedEntries
observedIndexes.append(observedEntries)
for categoryEntries in generation.entriesByCategory.values():
observedCategory = _ObservedEntryIndex(categoryEntries.entries)
categoryEntries.entries = observedCategory
observedIndexes.append(observedCategory)
self.assertEqual(manager.entryCount(), 300)
self.assertEqual(manager.entryCount(CORE_LOG_CATEGORY), 100)
self.assertGreater(manager.retainedCharacters, 0)
self.assertEqual(
sum(index.iterationRequests for index in observedIndexes),
0,
)
manager.append('application after observation', APPLICATION_LOG_CATEGORY)
self.assertEqual(
sum(index.iterationRequests for index in observedIndexes),
0,
)
manager.clear()
self.assertEqual(manager.entryCount(), 0)
self.assertEqual(manager.retainedCharacters, 0)
self.assertEqual(manager.retiredEntryCount, 301)
self.assertEqual(manager.entries(), tuple())
self.assertEqual(
sum(index.iterationRequests for index in observedIndexes),
0,
)
self.assertEqual(
sum(len(index.deletedKeys) for index in observedIndexes),
0,
)
self.assertEqual(
sum(index.oldestRemovals for index in observedIndexes),
0,
)
self._assertIndexesConsistent(manager)
def testSnapshotsNeverTraverseRetiredGenerations(self):
"""Materialize only live streams after an immediate runtime rollover."""
manager = LogManager(maximumEntries=100, autoClearEnabled=False)
manager.append('application', APPLICATION_LOG_CATEGORY)
manager.append('core', CORE_LOG_CATEGORY)
manager.append('tun2socks', TUN2SOCKS_LOG_CATEGORY)
retiredGeneration = manager._runtimeGeneration
observedGlobal = _ObservedEntryIndex(retiredGeneration.entries)
observedCore = _ObservedEntryIndex(
retiredGeneration.entriesByCategory[CORE_LOG_CATEGORY].entries
)
retiredGeneration.entries = observedGlobal
retiredGeneration.entriesByCategory[CORE_LOG_CATEGORY].entries = observedCore
manager.clear(runtimeOnly=True)
self.assertEqual(manager.entries(CORE_LOG_CATEGORY), tuple())
self.assertEqual(
tuple(entry.message for entry in manager.entries()),
('application',),
)
self.assertEqual(observedGlobal.iterationRequests, 0)
self.assertEqual(observedCore.iterationRequests, 0)
self.assertEqual(observedGlobal.oldestRemovals, 0)
self.assertEqual(manager.retiredEntryCount, 2)
def testSelectiveCategoryClearUsesDocumentedSharedStreamFallback(self):
"""Unlink only k selected entries when another category shares a stream."""
manager = LogManager(maximumEntries=1_000, autoClearEnabled=False)
for index in range(100):
manager.append(f'core {index}', CORE_LOG_CATEGORY)
for index in range(10):
manager.append(f'tun2socks {index}', TUN2SOCKS_LOG_CATEGORY)
runtimeGeneration = manager._runtimeGeneration
observedGlobal = _ObservedEntryIndex(runtimeGeneration.entries)
observedCore = _ObservedEntryIndex(
runtimeGeneration.entriesByCategory[CORE_LOG_CATEGORY].entries
)
runtimeGeneration.entries = observedGlobal
runtimeGeneration.entriesByCategory[CORE_LOG_CATEGORY].entries = observedCore
manager.clear(CORE_LOG_CATEGORY)
self.assertIs(manager._runtimeGeneration, runtimeGeneration)
self.assertEqual(observedCore.iterationRequests, 1)
self.assertEqual(len(observedGlobal.deletedKeys), 100)
self.assertEqual(observedGlobal.oldestRemovals, 0)
self.assertEqual(manager.entryCount(CORE_LOG_CATEGORY), 0)
self.assertEqual(manager.entryCount(TUN2SOCKS_LOG_CATEGORY), 10)
self.assertEqual(manager.retiredEntryCount, 100)
observedGlobal.iterationRequests = 0
observedGlobal.deletedKeys.clear()
manager.clear(TUN2SOCKS_LOG_CATEGORY)
self.assertIsNot(manager._runtimeGeneration, runtimeGeneration)
self.assertEqual(observedGlobal.iterationRequests, 0)
self.assertEqual(observedGlobal.deletedKeys, [])
self.assertEqual(observedGlobal.oldestRemovals, 0)
self.assertEqual(manager.entryCount(), 0)
self.assertEqual(manager.retiredEntryCount, 110)
self._assertIndexesConsistent(manager)
def testRetiredCleanupIsBoundedAndEventuallyReleasesEntries(self):
"""Release at most the fixed budget from a retired generation per turn."""
manager = LogManager(maximumEntries=100, autoClearEnabled=False)
manager.RetiredCleanupBudget = 3
manager.AppendRetiredCleanupBudget = 1
entryReferences = []
for index in range(10):
entry = manager.append(f'core {index}', CORE_LOG_CATEGORY)
entryReferences.append(weakref.ref(entry))
del entry
runtimeGeneration = manager._runtimeGeneration
observedEntries = _ObservedEntryIndex(runtimeGeneration.entries)
runtimeGeneration.entries = observedEntries
manager.clear(runtimeOnly=True)
self.assertEqual(manager.retiredEntryCount, 10)
self.assertEqual(observedEntries.oldestRemovals, 0)
self.assertTrue(all(reference() is not None for reference in entryReferences))
manager.append('application', APPLICATION_LOG_CATEGORY)
self.assertEqual(manager.retiredEntryCount, 9)
self.assertEqual(observedEntries.oldestRemovals, 1)
cleanupTurns = 0
while manager.retiredEntryCount:
before = manager.retiredEntryCount
with manager._lock:
cleaned = manager._cleanupRetiredLocked()
cleanupTurns += 1
self.assertLessEqual(cleaned, manager.RetiredCleanupBudget)
self.assertEqual(manager.retiredEntryCount, before - cleaned)
self.assertEqual(cleanupTurns, 3)
self.assertEqual(observedEntries.oldestRemovals, 10)
self.assertEqual(manager.retiredCharacters, 0)
self.assertEqual(len(manager._retiredBatches), 0)
self.assertTrue(all(reference() is None for reference in entryReferences))
self._assertIndexesConsistent(manager)
def testQueuedCleanupEventuallyDrainsWithoutFurtherLogging(self):
"""Finish physical reclamation through bounded queued manager turns."""
application()
manager = LogManager(maximumEntries=100, autoClearEnabled=False)
manager.RetiredCleanupBudget = 2
entryReferences = []
for index in range(9):
entry = manager.append(f'core {index}', CORE_LOG_CATEGORY)
entryReferences.append(weakref.ref(entry))
del entry
manager.clear(runtimeOnly=True)
self.assertEqual(manager.retiredEntryCount, 9)
processQtEvents()
self.assertEqual(manager.retiredEntryCount, 0)
self.assertEqual(manager.retiredCharacters, 0)
self.assertTrue(all(reference() is None for reference in entryReferences))
def testQueuedCleanupDoesNotOutliveDestroyedManager(self):
"""Discard pending self-delivery at the manager's QObject boundary."""
application()
manager = LogManager(maximumEntries=100, autoClearEnabled=False)
manager.RetiredCleanupBudget = 1
destroyed = []
manager.destroyed.connect(lambda: destroyed.append(True))
for index in range(20):
manager.append(f'core {index}', CORE_LOG_CATEGORY)
manager.clear(runtimeOnly=True)
managerReference = weakref.ref(manager)
manager.deleteLater()
del manager
processQtEvents()
self.assertEqual(destroyed, [True])
self.assertIsNone(managerReference())
def testRepeatedFastClearsKeepRetiredEntryBacklogBounded(self):
"""Prevent retired generations from accumulating beyond live capacity."""
manager = LogManager(maximumEntries=128, autoClearEnabled=False)
manager.RetiredCleanupBudget = 1
for index in range(manager.maximumEntries):
manager.append(f'initial {index}', CORE_LOG_CATEGORY)
manager.clear(runtimeOnly=True)
self.assertEqual(manager.retiredEntryCount, manager.maximumEntries)
for index in range(500):
manager.append(f'replacement {index}', CORE_LOG_CATEGORY)
manager.clear(runtimeOnly=True)
self.assertLessEqual(
manager.retiredEntryCount,
manager.maximumEntries,
)
self.assertLessEqual(
manager.entryCount() + manager.retiredEntryCount,
manager.maximumEntries,
)
self.assertLessEqual(
len(manager._retiredBatches),
manager.retiredEntryCount,
)
while manager.retiredEntryCount:
with manager._lock:
manager._cleanupRetiredLocked()
self.assertEqual(manager.retiredCharacters, 0)
self.assertEqual(len(manager._retiredBatches), 0)
self._assertIndexesConsistent(manager)
def testRepeatedCoreAutoClearKeepsOnlyTheNewestRuntimeEpoch(self):
"""Roll over repeatedly without exposing or accumulating older Core lines."""
manager = LogManager(
maximumEntries=100,
autoClearMaximumEntries=1,
)
manager.RetiredCleanupBudget = 1
manager.append('application', APPLICATION_LOG_CATEGORY)
firstGenerationId = manager._runtimeGeneration.identifier
for index in range(100):
manager.append(f'core {index}', CORE_LOG_CATEGORY)
self.assertEqual(
tuple(entry.message for entry in manager.entries(CORE_LOG_CATEGORY)),
(f'core {index}',),
)
self.assertLessEqual(manager.retiredEntryCount, 1)
self.assertGreater(manager._runtimeGeneration.identifier, firstGenerationId)
self.assertEqual(
tuple(entry.message for entry in manager.entries()),
('application', 'core 99'),
)
self.assertEqual(manager.entryCount(APPLICATION_LOG_CATEGORY), 1)
self.assertEqual(manager.entryCount(CORE_LOG_CATEGORY), 1)
self._assertIndexesConsistent(manager)
def testRetentionEvictsOldestEntriesFromBothIndexes(self):
"""Keep category indexes synchronized through repeated global eviction."""
manager = LogManager(
maximumEntries=4,
maximumCharacters=20,
maximumEntryCharacters=20,
autoClearEnabled=False,
)
categoryIds = (
APPLICATION_LOG_CATEGORY,
CORE_LOG_CATEGORY,
TUN2SOCKS_LOG_CATEGORY,
)
for index in range(3):
manager.append(f'{index:04d}', categoryIds[index])
observedIndexes = []
for generation in manager._activeGenerationsLocked():
observedEntries = _ObservedEntryIndex(generation.entries)
generation.entries = observedEntries
observedIndexes.append(observedEntries)
for index in range(3, 20):
manager.append(f'{index:04d}', categoryIds[index % len(categoryIds)])
entries = manager.entries()
self.assertEqual(
tuple(entry.message for entry in entries),
('0016', '0017', '0018', '0019'),
)
self.assertEqual(
sum(index.oldestRemovals for index in observedIndexes),
16,
)
self.assertEqual(manager.retainedCharacters, 16)
self._assertIndexesConsistent(manager)
def testGlobalRetentionIgnoresRetiredGenerations(self):
"""Evict the oldest live stream head across a runtime rollover."""
manager = LogManager(
maximumEntries=4,
maximumCharacters=100,
maximumEntryCharacters=100,
autoClearEnabled=False,
)
manager.RetiredCleanupBudget = 1
manager.append('application 1', APPLICATION_LOG_CATEGORY)
manager.append('old core', CORE_LOG_CATEGORY)
manager.append('application 2', APPLICATION_LOG_CATEGORY)
manager.append('old tun2socks', TUN2SOCKS_LOG_CATEGORY)
manager.clear(runtimeOnly=True)
self.assertEqual(manager.retiredEntryCount, 2)
self.assertEqual(
tuple(entry.message for entry in manager.entries()),
('application 1', 'application 2'),
)
manager.append('new core', CORE_LOG_CATEGORY)
manager.append('application 3', APPLICATION_LOG_CATEGORY)
manager.append('new tun2socks', TUN2SOCKS_LOG_CATEGORY)
self.assertEqual(
tuple(entry.message for entry in manager.entries()),
(
'application 2',
'new core',
'application 3',
'new tun2socks',
),
)
self.assertEqual(manager.retiredEntryCount, 0)
self.assertEqual(manager.entryCount(APPLICATION_LOG_CATEGORY), 2)
self.assertEqual(manager.entryCount(CORE_LOG_CATEGORY), 1)
self.assertEqual(manager.entryCount(TUN2SOCKS_LOG_CATEGORY), 1)
self._assertIndexesConsistent(manager)
def testRegisteredCategoryClearAndClearAllPreserveSignals(self):
"""Clear one late category and then all entries with compatible signals."""
manager = LogManager(maximumEntries=20, autoClearEnabled=False)
category = manager.registerComponent(
'component.audit',
'Audit',
runtime=False,
)
cleared = []
manager.entriesCleared.connect(cleared.append)
manager.append('application', APPLICATION_LOG_CATEGORY)
manager.append('audit 1', category.id)
manager.append('core', CORE_LOG_CATEGORY)
manager.append('audit 2', category.id)
self.assertEqual(manager.snapshot('unknown.category')[1], tuple())
self.assertEqual(manager.entryCount(category.id), 2)
self.assertEqual(manager.plainText(category.id), 'audit 1\naudit 2')
manager.clear(runtimeOnly=True)
self.assertEqual(
tuple(entry.message for entry in manager.entries()),
('application', 'audit 1', 'audit 2'),
)
self.assertEqual(
cleared,
[frozenset({CORE_LOG_CATEGORY, TUN2SOCKS_LOG_CATEGORY})],
)
manager.clear(category.id)
self.assertEqual(
tuple(entry.message for entry in manager.entries()),
('application',),
)
self.assertEqual(
cleared,
[
frozenset({CORE_LOG_CATEGORY, TUN2SOCKS_LOG_CATEGORY}),
frozenset({category.id}),
],
)
self._assertIndexesConsistent(manager)
manager.clear()
manager.clear()
self.assertEqual(manager.entries(), tuple())
self.assertEqual(
cleared,
[
frozenset({CORE_LOG_CATEGORY, TUN2SOCKS_LOG_CATEGORY}),
frozenset({category.id}),
None,
],
)
self._assertIndexesConsistent(manager)
def testConcurrentAppendClearAndSnapshotsKeepIndexesConsistent(self):
"""Serialize snapshots and category clears racing with log producers."""
manager = LogManager(maximumEntries=5_000, autoClearEnabled=False)
category = manager.registerComponent('component.concurrent', 'Concurrent')
started = threading.Event()
finished = threading.Event()
failures = []
observations = []
def produce():
"""Append interleaved categories while the observer reads and clears."""
try:
started.set()
categoryIds = (
APPLICATION_LOG_CATEGORY,
CORE_LOG_CATEGORY,
category.id,
)
for index in range(3_000):
manager.append(
f'entry {index}',
categoryIds[index % len(categoryIds)],
)
except Exception as error:
failures.append(error)
finally:
finished.set()
def observeAndClear():
"""Take filtered snapshots and clear one category during ingestion."""
try:
started.wait(5)
while True:
manager.snapshot(CORE_LOG_CATEGORY)
manager.snapshot(category.id)
manager.clear(category.id)
observations.append(True)
if finished.wait(0.0001):
break
except Exception as error:
failures.append(error)
observer = threading.Thread(target=observeAndClear)
producer = threading.Thread(target=produce)
observer.start()
producer.start()
producer.join(10)
observer.join(10)
self.assertFalse(producer.is_alive())
self.assertFalse(observer.is_alive())
self.assertEqual(failures, [])
self.assertTrue(observations)
sequences = tuple(entry.sequence for entry in manager.entries())
self.assertEqual(sequences, tuple(sorted(sequences)))
self.assertEqual(len(sequences), len(set(sequences)))
self._assertIndexesConsistent(manager)
def testEntrySignalsAndCrossThreadChangeNotificationsRemainCompatible(self):
"""Emit every entry and coalesce presentation refreshes by producer batch."""
application()
manager = LogManager(maximumEntries=100, autoClearEnabled=False)
added = []
changed = []
manager.entryAdded.connect(added.append)
manager.entriesChanged.connect(changed.append)
def produce():
"""Publish one batch without allowing the Qt queue to drain midway."""
for index in range(50):
manager.append(f'worker {index}', CORE_LOG_CATEGORY)
worker = threading.Thread(target=produce)
worker.start()
worker.join(5)
self.assertFalse(worker.is_alive())
self.assertEqual(added, [])
self.assertEqual(changed, [])
processQtEvents()
self.assertEqual(len(added), 50)
self.assertEqual(
tuple(entry.message for entry in added),
tuple(f'worker {index}' for index in range(50)),
)
self.assertEqual(changed, [50])
manager.append('application 1', APPLICATION_LOG_CATEGORY)
manager.append('application 2', APPLICATION_LOG_CATEGORY)
self.assertEqual(len(added), 52)
processQtEvents()
self.assertEqual(changed, [50, 52])
class MetricsHistoryTest(unittest.TestCase):
"""Verify bounded history and metric-specific aggregation semantics."""
def testPruningNormalizationAndAggregation(self):
"""Prune stale values, average speed, and retain latest usage."""
manager = MetricsHistory(maximumHistorySeconds=10)
changed = []
manager.historyChanged.connect(lambda: changed.append(True))
manager.recordSample(
{
DOWNLOAD_SPEED_METRIC: -5,
DOWNLOAD_USAGE_METRIC: 100,
'unknown': 9,
},
sampledAt=0,
)
manager.recordSample(
{
DOWNLOAD_SPEED_METRIC: 20,
DOWNLOAD_USAGE_METRIC: 150,
},
sampledAt=11,
)
manager.recordSample(
{
DOWNLOAD_SPEED_METRIC: 40,
DOWNLOAD_USAGE_METRIC: 190,
},
sampledAt=19,
)
self.assertEqual(manager.sampleCount(), 2)
self.assertEqual(len(changed), 3)
self.assertEqual(
tuple(
point.value
for point in manager.series(
DOWNLOAD_SPEED_METRIC,
20,
granularitySeconds=20,
now=19,
)
),
(30.0,),
)
self.assertEqual(
tuple(
point.value
for point in manager.series(
DOWNLOAD_USAGE_METRIC,
20,
granularitySeconds=20,
now=21,
)
),
(190.0,),
)
def testInvalidSamplesDoNotPolluteHistory(self):
"""Ignore unsupported and non-finite values without emitting changes."""
manager = MetricsHistory()
changed = []
manager.historyChanged.connect(lambda: changed.append(True))
manager.recordSample({'unknown': 1}, sampledAt=1)
manager.recordSample({DOWNLOAD_SPEED_METRIC: float('nan')}, sampledAt=2)
self.assertEqual(manager.rawSamples(), tuple())
self.assertEqual(changed, [])
def testDefensiveSampleCeilingBoundsBurstCadence(self):
"""Retain only the newest samples even when time does not advance."""
manager = MetricsHistory(
maximumHistorySeconds=24 * 60 * 60,
maximumSampleCount=25,
)
for index in range(1000):
manager.recordSample({DOWNLOAD_SPEED_METRIC: index}, sampledAt=1)
self.assertEqual(manager.sampleCount(), 25)
self.assertEqual(manager.rawSamples()[0].values[DOWNLOAD_SPEED_METRIC], 975)
self.assertEqual(manager.rawSamples()[-1].values[DOWNLOAD_SPEED_METRIC], 999)
class TranslationExtractorTest(unittest.TestCase):
"""Verify static translation extraction and constant interpolation."""
def testApplicationConstantFStringIsExtractedAndResolved(self):
"""Accept constant-only f-strings while rejecting runtime formatting."""
import Translation
content = """
_(f'{APPLICATION_NAME} is ready')
_(f'{runtimeValue} is not static')
_(f'{APPLICATION_NAME!r} is not supported')
"""
keys = tuple(Translation.getTranslationKeys(content))
self.assertEqual(keys, ('{APPLICATION_NAME} is ready',))
self.assertEqual(
Translation.resolveAppConstants(keys[0]),
f'{Translation.APPLICATION_NAME} is ready',
)
if __name__ == '__main__':
unittest.main()