Files
LorenEteval_Furious/tests/test_frozenlib.py
T
2026-09-09 12:18:00 +08:00

882 lines
30 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 Frozenlib state helpers and mocked host-operation boundaries."""
from __future__ import annotations
from Furious.Controllers.SettingsController import SettingsController
from Furious.Frozenlib import AppSettings, Mixins
from Furious.Window.SettingsPage import _ToggleSettingsCard
from PySide6 import QtCore
from PySide6.QtWidgets import QPushButton
from unittest import mock
import importlib
import logging
import socket
import subprocess
import sys
import threading
import types
import unittest
import weakref
from tests.support import application, isolatedSettings, processQtEvents
StartupOnBootModule = importlib.import_module('Furious.Frozenlib.StartupOnBoot')
SystemProxyModule = importlib.import_module('Furious.Frozenlib.SystemProxy')
SystemRoutingTableModule = importlib.import_module(
'Furious.Frozenlib.SystemRoutingTable'
)
TcpingModule = importlib.import_module('Furious.Frozenlib.Tcping')
UtilityModule = importlib.import_module('Furious.Frozenlib.Utility')
Win32SessionModule = importlib.import_module('Furious.Frozenlib.Win32Session')
class FrozenlibQtContextTest(unittest.TestCase):
"""Verify Qt context helpers restore exact prior state when nested."""
@classmethod
def setUpClass(cls):
"""Create the suite-owned QApplication before constructing widgets."""
application()
def testDisabledContextPreservesPriorAndNestedState(self):
"""Restore both initially enabled and initially disabled widgets."""
button = QPushButton()
with Mixins.QSetDisabledContext(button):
self.assertFalse(button.isEnabled())
with Mixins.QSetDisabledContext(button):
self.assertFalse(button.isEnabled())
self.assertFalse(button.isEnabled())
self.assertTrue(button.isEnabled())
button.setDisabled(True)
with Mixins.QSetDisabledContext(button):
self.assertFalse(button.isEnabled())
self.assertFalse(button.isEnabled())
button.deleteLater()
processQtEvents()
def testSignalContextPreservesPriorAndNestedState(self):
"""Restore both initially unblocked and initially blocked QObjects."""
qobject = QtCore.QObject()
with Mixins.QBlockSignalContext(qobject):
self.assertTrue(qobject.signalsBlocked())
with Mixins.QBlockSignalContext(qobject):
self.assertTrue(qobject.signalsBlocked())
self.assertTrue(qobject.signalsBlocked())
self.assertFalse(qobject.signalsBlocked())
qobject.blockSignals(True)
with Mixins.QBlockSignalContext(qobject):
self.assertTrue(qobject.signalsBlocked())
self.assertTrue(qobject.signalsBlocked())
def testFailedToggleRequestRestoresPersistedState(self):
"""Keep a switch synchronized when its host-side callback fails."""
with isolatedSettings():
AppSettings.turnOFF('StartupOnBoot')
callback = mock.Mock(return_value=False)
card = _ToggleSettingsCard(None, 'StartupOnBoot', callback)
card.checkBox.setChecked(True)
callback.assert_called_once_with(True)
self.assertFalse(card.checkBox.isChecked())
self.assertTrue(AppSettings.isStateOFF('StartupOnBoot'))
card.deleteLater()
processQtEvents()
class CleanupOnExitTest(unittest.TestCase):
"""Verify application cleanup isolates independently owned resources."""
def testCleanupFailureDoesNotSkipLaterOwners(self):
"""Continue cleanup after one participant raises and reset registry state."""
calls = []
poolType = type(Mixins.CleanupOnExit.ObjectsPool)
class Participant(Mixins.CleanupOnExit):
def __init__(self, name: str, raises=False):
self.name = name
self.raises = raises
super().__init__(uniqueCleanup=False)
def cleanup(self):
calls.append(self.name)
if self.raises:
raise RuntimeError('cleanup fixture')
with mock.patch.object(
Mixins.CleanupOnExit,
'ObjectsPool',
poolType(),
), mock.patch.object(Mixins.CleanupOnExit, 'VisitedType', {}):
participants = (
Participant('first'),
Participant('failing', raises=True),
Participant('last'),
)
with self.assertLogs('Furious.Frozenlib.Mixins', level='ERROR'):
Mixins.CleanupOnExit.cleanupAll()
self.assertEqual(calls, ['first', 'failing', 'last'])
self.assertEqual(len(Mixins.CleanupOnExit.ObjectsPool), 0)
self.assertEqual(Mixins.CleanupOnExit.VisitedType, {})
self.assertEqual(len(participants), 3)
class FrozenlibUtilityTest(unittest.TestCase):
"""Verify bounded caches, commands, throttling, and dual-stack probes."""
def tearDown(self):
"""Clear shared utility caches so tests remain order-independent."""
UtilityModule.isValidIPAddress.cache_clear()
UtilityModule.parseHostPort.cache_clear()
UtilityModule.absolutePath.cache_clear()
UtilityModule.versionToValue.cache_clear()
def testCallRateLimitedSkipsExcessCallsWithoutSleeping(self):
"""Throttle on the leading edge without blocking the caller thread."""
calls = []
@UtilityModule.callRateLimited(maxCallPerSecond=2)
def callback(value):
calls.append(value)
return value
with (
mock.patch.object(
UtilityModule.time,
'monotonic',
side_effect=(10.0, 10.1, 10.5),
),
mock.patch.object(UtilityModule.time, 'sleep') as sleep,
):
self.assertEqual(callback('first'), 'first')
self.assertIsNone(callback('suppressed'))
self.assertEqual(callback('second'), 'second')
self.assertEqual(calls, ['first', 'second'])
sleep.assert_not_called()
def testCallOnceOnlyCachesFreeFunctionResult(self):
"""Preserve one shared result for ordinary decorated functions."""
calls = []
@UtilityModule.callOnceOnly
def callback(value):
calls.append(value)
return value
self.assertEqual(callback('first'), 'first')
self.assertEqual(callback('second'), 'first')
self.assertEqual(calls, ['first'])
def testCallOnceOnlyCachesEachInstanceWithoutRetainingIt(self):
"""Give methods independent state owned only by their live instance."""
class Fixture:
"""Expose one decorated method and its invocation count."""
def __init__(self):
"""Initialize the fixture invocation count."""
self.calls = 0
@UtilityModule.callOnceOnly
def callback(self, value):
"""Return the first value supplied to this fixture."""
self.calls += 1
return value
first = Fixture()
second = Fixture()
self.assertEqual(first.callback('first'), 'first')
self.assertEqual(first.callback('ignored'), 'first')
self.assertEqual(second.callback('second'), 'second')
self.assertEqual((first.calls, second.calls), (1, 1))
reference = weakref.ref(first)
del first
self.assertIsNone(reference())
def testCallOnceOnlyRetriesAfterFailure(self):
"""Cache only successful invocations so transient failures can retry."""
class Fixture:
"""Fail its first decorated invocation."""
def __init__(self):
"""Initialize the fixture invocation count."""
self.calls = 0
@UtilityModule.callOnceOnly
def callback(self):
"""Fail once and then return the successful call count."""
self.calls += 1
if self.calls == 1:
raise RuntimeError('fixture failure')
return self.calls
fixture = Fixture()
with self.assertRaisesRegex(RuntimeError, 'fixture failure'):
fixture.callback()
self.assertEqual(fixture.callback(), 2)
self.assertEqual(fixture.callback(), 2)
self.assertEqual(fixture.calls, 2)
def testRateLimitRejectsInvalidRates(self):
"""Reject zero and negative rates instead of dividing or sleeping."""
with self.assertRaises(ValueError):
UtilityModule.callRateLimited(0)
with self.assertRaises(ValueError):
UtilityModule.callRateLimited(-1)
def testExternalCommandsPreserveCallerTimeoutPolicy(self):
"""Forward caller options without imposing a shared command timeout."""
completed = subprocess.CompletedProcess(['fixture'], 0)
with (
mock.patch.object(UtilityModule, 'PLATFORM', 'Linux'),
mock.patch.object(
UtilityModule.subprocess,
'run',
return_value=completed,
) as run,
):
self.assertIs(
UtilityModule.runExternalCommand(['fixture']),
completed,
)
self.assertIs(
UtilityModule.runExternalCommand(['fixture'], timeout=1.5),
completed,
)
self.assertEqual(
run.call_args_list,
[
mock.call(['fixture']),
mock.call(['fixture'], timeout=1.5),
],
)
def testHostPortCacheIsBoundedAndRetainsIPv6Parsing(self):
"""Bound externally influenced keys while preserving IPv6 authority parsing."""
for index in range(UtilityModule.parseHostPort.cache_info().maxsize + 25):
UtilityModule.parseHostPort(f'host-{index}.example:443')
info = UtilityModule.parseHostPort.cache_info()
self.assertIsNotNone(info.maxsize)
self.assertLessEqual(info.currsize, info.maxsize)
self.assertEqual(
UtilityModule.parseHostPort('[2001:db8::1]:8443'),
('2001:db8::1', '8443'),
)
def testExternallyKeyedUtilityCachesAreBounded(self):
"""Prevent address, path, and version inputs from growing global caches."""
cachedFunctions = (
UtilityModule.isValidIPAddress,
UtilityModule.absolutePath,
UtilityModule.versionToValue,
)
for function in cachedFunctions:
function.cache_clear()
for index in range(function.cache_info().maxsize + 25):
function(str(index))
info = function.cache_info()
self.assertIsNotNone(info.maxsize)
self.assertLessEqual(info.currsize, info.maxsize)
def testTcpingUsesResolverIPv6AndFallsBackToIPv4(self):
"""Use getaddrinfo candidates rather than IPv4-only gethostbyname."""
candidates = [
(
socket.AF_INET6,
socket.SOCK_STREAM,
socket.IPPROTO_TCP,
'',
('2001:db8::1', 443, 0, 0),
),
(
socket.AF_INET,
socket.SOCK_STREAM,
socket.IPPROTO_TCP,
'',
('192.0.2.1', 443),
),
]
sockets = []
class FakeSocket:
"""Record one fully mocked TCP connection attempt."""
def __init__(self, family, socketType, protocol):
self.family = family
self.socketType = socketType
self.protocol = protocol
self.timeout = None
self.address = None
sockets.append(self)
def __enter__(self):
return self
def __exit__(self, *_args):
return False
def settimeout(self, timeout):
self.timeout = timeout
def connect(self, address):
self.address = address
if self.family == socket.AF_INET6:
raise OSError('IPv6 fixture unavailable')
with (
mock.patch.object(
TcpingModule.socket,
'getaddrinfo',
return_value=candidates,
) as getaddrinfo,
mock.patch.object(TcpingModule.socket, 'socket', side_effect=FakeSocket),
mock.patch.object(
TcpingModule.time,
'perf_counter',
side_effect=(10.0, 10.125),
),
):
sent, rtts = TcpingModule.tcping(
'dual-stack.example',
443,
timeout=0.5,
count=1,
interval=0,
)
self.assertEqual(sent, 1)
self.assertEqual(rtts, [0.125])
self.assertEqual(
[item.family for item in sockets], [socket.AF_INET6, socket.AF_INET]
)
self.assertEqual(sockets[0].address, ('2001:db8::1', 443, 0, 0))
self.assertEqual(sockets[1].address, ('192.0.2.1', 443))
self.assertEqual([item.timeout for item in sockets], [0.5, 0.5])
getaddrinfo.assert_called_once_with(
'dual-stack.example',
443,
family=socket.AF_UNSPEC,
type=socket.SOCK_STREAM,
proto=socket.IPPROTO_TCP,
)
class MockedPlatformHelperTest(unittest.TestCase):
"""Exercise platform helpers without touching host startup or networking."""
def testProxyMutationsReturnHostFailureWithoutDisclosingSecrets(self):
"""Preserve false results from each native boundary for callers to handle."""
proxy = SystemProxyModule._SystemProxy()
native = types.SimpleNamespace(
set=mock.Mock(return_value=False),
off=mock.Mock(return_value=False),
pac=mock.Mock(return_value=False),
)
for platform in ('Windows', 'Linux', 'Darwin'):
with self.subTest(platform=platform), mock.patch.object(
SystemProxyModule, 'PLATFORM', platform
), mock.patch.object(
SystemProxyModule, 'handleAppSystemProxyMode', return_value=True
), mock.patch.dict(
sys.modules, {'sysproxy': native}
), mock.patch.object(
SystemProxyModule,
'linuxProxyConfig',
side_effect=OSError('private-token'),
), mock.patch.object(
SystemProxyModule,
'darwinProxyConfig',
side_effect=OSError('private-token'),
), self.assertLogs(
SystemProxyModule.logger, level='ERROR'
) as captured:
self.assertIs(proxy.set('127.0.0.1:10809', 'localhost'), False)
self.assertIs(proxy.off(), False)
self.assertIs(proxy.pac('https://invalid.test/private-token'), False)
self.assertNotIn('private-token', '\n'.join(captured.output))
def testIgnoredProxyMutationsDoNotTouchHost(self):
"""Do-not-change remains a deliberate no-op rather than a failed mutation."""
with mock.patch.object(
SystemProxyModule, 'handleAppSystemProxyMode', return_value=False
), mock.patch.object(SystemProxyModule, 'runExternalCommand') as command:
self.assertIsNone(SystemProxyModule.SystemProxy.set('127.0.0.1:10809', ''))
self.assertIsNone(SystemProxyModule.SystemProxy.off())
self.assertIsNone(SystemProxyModule.SystemProxy.pac('https://invalid.test'))
command.assert_not_called()
def testDarwinProxyChecksEveryEnabledServiceCommand(self):
"""Check enabled services and propagate even a later service rejection."""
for fail in (False, True):
commands = []
def run(command, **kwargs):
commands.append(command)
self.assertTrue(kwargs.get('check'))
self.assertEqual(kwargs.get('timeout'), 5.0)
if '-listallnetworkservices' in command:
return subprocess.CompletedProcess(
command,
0,
stdout=b'Header\nWi-Fi\n*Disabled service\nEthernet\n',
)
if fail and 'Ethernet' in command:
raise subprocess.CalledProcessError(1, command)
return subprocess.CompletedProcess(command, 0)
with self.subTest(fail=fail), mock.patch.object(
SystemProxyModule, 'runExternalCommand', side_effect=run
):
if fail:
with self.assertRaises(subprocess.CalledProcessError):
SystemProxyModule.darwinProxyConfig(
'setwebproxy', '127.0.0.1', '10809'
)
else:
SystemProxyModule.darwinProxyConfig(
'setwebproxy', '127.0.0.1', '10809'
)
self.assertEqual(
[command[2] for command in commands[1:]], ['Wi-Fi', 'Ethernet']
)
def testLinuxProxyCommandTimeoutBecomesExplicitFailure(self):
"""A hung host settings command has a deadline and cannot claim success."""
with (
mock.patch.object(SystemProxyModule, 'PLATFORM', 'Linux'),
mock.patch.object(
SystemProxyModule, 'handleAppSystemProxyMode', return_value=True
),
mock.patch.object(
SystemProxyModule.SystemRuntime, 'flatpakID', return_value=''
),
mock.patch.object(
SystemProxyModule,
'runExternalCommand',
side_effect=subprocess.TimeoutExpired('gsettings', 5.0),
) as command,
self.assertLogs(SystemProxyModule.logger, level='ERROR'),
):
self.assertIs(SystemProxyModule.SystemProxy.off(), False)
self.assertTrue(command.call_args.kwargs['check'])
self.assertEqual(command.call_args.kwargs['timeout'], 5.0)
def testPacDiagnosticsDoNotExposeConfiguredUrl(self):
"""Avoid logging credential-bearing PAC URLs on any outcome."""
proxy = SystemProxyModule._SystemProxy()
pacUrl = 'https://user:credential-token@invalid.test/proxy.pac'
with (
mock.patch.object(SystemProxyModule, 'PLATFORM', 'Linux'),
mock.patch.object(
SystemProxyModule,
'handleAppSystemProxyMode',
return_value=True,
),
mock.patch.object(SystemProxyModule, 'linuxProxyConfig') as configure,
self.assertLogs(SystemProxyModule.logger, level='INFO') as captured,
):
proxy.pac(pacUrl)
self.assertEqual(
configure.call_args_list,
[
mock.call('proxy', 'autoconfig-url', pacUrl),
mock.call('proxy', 'mode', 'auto'),
],
)
diagnostics = '\n'.join(captured.output)
self.assertIn('set proxy PAC success', diagnostics)
self.assertNotIn(pacUrl, diagnostics)
self.assertNotIn('credential-token', diagnostics)
def testProxyDaemonClearsIndependentExitAndCanRestart(self):
"""Release an exited daemon reference without retaining a stale owner."""
proxy = SystemProxyModule._SystemProxy()
started = [threading.Event(), threading.Event()]
release = [threading.Event(), threading.Event()]
attempts = iter(zip(started, release))
def runDaemon():
"""Block each fully mocked daemon until the test releases it."""
startedEvent, releaseEvent = next(attempts)
startedEvent.set()
releaseEvent.wait(1)
native = types.SimpleNamespace(daemon_on_=mock.Mock(side_effect=runDaemon))
firstThread, secondThread = None, None
with (
mock.patch.object(SystemProxyModule, 'PLATFORM', 'Windows'),
mock.patch.object(
SystemProxyModule,
'handleAppSystemProxyMode',
return_value=True,
),
mock.patch.dict(sys.modules, {'sysproxy': native}),
):
try:
proxy.daemonOn_()
self.assertTrue(started[0].wait(1))
firstThread = proxy._daemonThread
release[0].set()
firstThread.join(1)
self.assertFalse(firstThread.is_alive())
self.assertIsNone(proxy._daemonThread)
proxy.daemonOn_()
self.assertTrue(started[1].wait(1))
secondThread = proxy._daemonThread
self.assertIsNot(secondThread, firstThread)
self.assertEqual(native.daemon_on_.call_count, 2)
finally:
release[0].set()
release[1].set()
for thread in (firstThread, secondThread):
if isinstance(thread, threading.Thread):
thread.join(1)
def testProxyDaemonShutdownWaitIsBounded(self):
"""Retain a still-live proxy daemon after one bounded shutdown wait."""
proxy = SystemProxyModule._SystemProxy()
proxy.DaemonShutdownTimeout = 0.01
release = threading.Event()
thread = threading.Thread(target=release.wait, daemon=True)
thread.start()
proxy._daemonThread = thread
native = types.SimpleNamespace(daemon_off=mock.Mock(return_value=True))
with (
mock.patch.object(SystemProxyModule, 'PLATFORM', 'Windows'),
mock.patch.object(
SystemProxyModule,
'handleAppSystemProxyMode',
return_value=True,
),
mock.patch.dict(sys.modules, {'sysproxy': native}),
):
try:
proxy.daemonOff()
self.assertIs(proxy._daemonThread, thread)
self.assertTrue(thread.is_alive())
native.daemon_off.assert_called_once_with()
finally:
release.set()
thread.join(1)
def testSessionShutdownWaitIsBounded(self):
"""Return from shutdown while retaining a still-live session listener."""
session = Win32SessionModule._Win32Session()
session.ShutdownTimeout = 0.01
release = threading.Event()
thread = threading.Thread(target=release.wait, daemon=True)
thread.start()
session._daemonThread = thread
native = types.SimpleNamespace(off=mock.Mock(return_value=True))
with (
mock.patch.object(Win32SessionModule, 'PLATFORM', 'Windows'),
mock.patch.dict(sys.modules, {'win32session': native}),
):
try:
self.assertFalse(session.off())
self.assertIs(session._daemonThread, thread)
self.assertTrue(thread.is_alive())
native.off.assert_called_once_with()
finally:
release.set()
thread.join(1)
def testSessionStartFailureIsLoggedAndClearsReference(self):
"""Return failure instead of leaking a thread-start exception."""
class FailingThread:
"""Provide one deterministic thread whose start always fails."""
def __init__(self, *args, **kwargs):
self.args = args
self.kwargs = kwargs
def is_alive(self):
return False
def start(self):
raise RuntimeError('fixture start failure')
session = Win32SessionModule._Win32Session()
native = types.SimpleNamespace(run=mock.Mock())
with (
mock.patch.object(Win32SessionModule, 'PLATFORM', 'Windows'),
mock.patch.object(
Win32SessionModule.threading,
'Thread',
FailingThread,
),
mock.patch.dict(sys.modules, {'win32session': native}),
self.assertLogs(Win32SessionModule.logger, logging.ERROR) as logs,
):
self.assertFalse(session.run())
self.assertIsNone(session._daemonThread)
native.run.assert_not_called()
self.assertIn('failed to start Windows session listener', logs.output[0])
def testStartupHelpersReturnMockedHostResults(self):
"""Expose success and failure instead of discarding registration results."""
with (
mock.patch.object(StartupOnBootModule, 'PLATFORM', 'Linux'),
mock.patch.object(
StartupOnBootModule.SystemRuntime,
'isScriptMode',
return_value=False,
),
mock.patch.object(
StartupOnBootModule.os,
'makedirs',
side_effect=PermissionError,
),
):
self.assertFalse(StartupOnBootModule.StartupOnBoot.on_())
with (
mock.patch.object(StartupOnBootModule, 'PLATFORM', 'Linux'),
mock.patch.object(
StartupOnBootModule.os,
'remove',
side_effect=FileNotFoundError,
),
):
self.assertTrue(StartupOnBootModule.StartupOnBoot.off())
def testStartupSettingChangesOnlyAfterHostSuccess(self):
"""Do not persist a state that the mocked host rejected."""
with isolatedSettings():
AppSettings.turnOFF('StartupOnBoot')
with mock.patch(
'Furious.Controllers.SettingsController.StartupOnBoot.on_',
return_value=False,
):
self.assertFalse(SettingsController.setStartupOnBoot(True))
self.assertTrue(AppSettings.isStateOFF('StartupOnBoot'))
with mock.patch(
'Furious.Controllers.SettingsController.StartupOnBoot.on_',
return_value=True,
):
self.assertTrue(SettingsController.setStartupOnBoot(True))
self.assertTrue(AppSettings.isStateON_('StartupOnBoot'))
with mock.patch(
'Furious.Controllers.SettingsController.StartupOnBoot.off',
return_value=False,
):
self.assertFalse(SettingsController.setStartupOnBoot(False))
self.assertTrue(AppSettings.isStateON_('StartupOnBoot'))
def testLinuxSystemProxyUsesOnlyMockedCommands(self):
"""Cover proxy configuration without changing the real desktop proxy."""
with (
mock.patch.object(SystemProxyModule, 'PLATFORM', 'Linux'),
mock.patch.object(
SystemProxyModule,
'handleAppSystemProxyMode',
return_value=True,
),
mock.patch.object(SystemProxyModule, 'linuxProxyConfig') as configure,
):
SystemProxyModule.SystemProxy.set(
'127.0.0.1:10809',
'localhost;127.0.0.1',
)
self.assertEqual(configure.call_count, 6)
self.assertEqual(
configure.call_args_list[-1],
mock.call('proxy', 'mode', 'manual'),
)
def testRoutingTableUsesOnlyMockedCommands(self):
"""Cover route command construction without changing real routes."""
completed = subprocess.CompletedProcess(
['route'],
0,
stdout=b'',
stderr=b'',
)
with (
mock.patch.object(SystemRoutingTableModule, 'PLATFORM', 'Windows'),
mock.patch.object(
SystemRoutingTableModule,
'runExternalCommand',
return_value=completed,
) as run,
):
SystemRoutingTableModule.SystemRoutingTable.add(
'203.0.113.0',
'192.0.2.1',
)
run.assert_called_once_with(
['route', 'add', '203.0.113.0', '192.0.2.1', 'metric', '5'],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
)
def testDnsHelperUsesOnlyMockedCommands(self):
"""Cover DNS command construction without changing host DNS settings."""
completed = subprocess.CompletedProcess(
['netsh'],
0,
stdout=b'',
stderr=b'',
)
with (
mock.patch.object(SystemRoutingTableModule, 'PLATFORM', 'Windows'),
mock.patch.object(
SystemRoutingTableModule,
'runExternalCommand',
return_value=completed,
) as run,
):
SystemRoutingTableModule.SystemRoutingTable.WIN32SetInterfaceDNS(
'Fixture Adapter',
address='192.0.2.53',
dhcp=False,
)
run.assert_called_once_with(
'netsh interface ip set dns name="Fixture Adapter" static 192.0.2.53',
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
shell=True,
check=True,
)
def testSessionHelperUsesOnlyMockedNativeModule(self):
"""Cover session startup and shutdown without host-session monitoring."""
native = types.SimpleNamespace(
set=mock.Mock(),
run=mock.Mock(),
off=mock.Mock(return_value=True),
)
session = Win32SessionModule._Win32Session()
callback = mock.Mock()
with (
mock.patch.object(Win32SessionModule, 'PLATFORM', 'Windows'),
mock.patch.dict(sys.modules, {'win32session': native}),
):
self.assertTrue(session.set(callback))
self.assertTrue(session.run())
self.assertTrue(session.off())
native.set.assert_called_once_with(callback)
native.run.assert_called_once_with()
native.off.assert_called_once_with()
self.assertIsNone(session._daemonThread)
if __name__ == '__main__':
unittest.main()