Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions AddonManagerTest/app/test_network_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ def __init__(self):

def await_response(self, index: int, quiet: bool) -> None:
"""Set up the state that blocking_get() creates while it waits for a response."""
self.synchronous_complete[index] = False
self.synchronous_complete[index] = threading.Event()
if quiet:
self.synchronous_quiet.add(index)

Expand Down Expand Up @@ -87,7 +87,7 @@ def test_quiet_request_still_completes(self):

self.requests.complete_request(1, 404, None)

self.assertTrue(self.requests.synchronous_complete[1])
self.assertTrue(self.requests.synchronous_complete[1].is_set())

def test_quiet_does_not_affect_other_requests(self):
"""Marking one request quiet does not suppress the reporting of any other request."""
Expand Down
38 changes: 38 additions & 0 deletions AddonManagerTest/gui/gui_mocks.py
Original file line number Diff line number Diff line change
Expand Up @@ -201,3 +201,41 @@ def abort_all(self):

def abort(self, index: int):
pass


class FakeNetworkManager(QtCore.QObject):
"""A stand-in for the NetworkManager singleton that never touches the network.

Requests are recorded and answered by whichever thread calls answer_pending_requests(), so a
test decides when a worker sees its response. The completed signal is a real Qt signal, and is
therefore delivered across threads exactly as the real one is."""

completed = QtCore.Signal(int, int, QtCore.QByteArray)

def __init__(self, status: int = 200, response: bytes = b"OK"):
super().__init__()
self.status = status
self.response = response
self.requested_urls = []
self.pending_requests = []
self.aborted_requests = []
self.next_index = 0

def submit_unmonitored_get(
self, url: str, timeout_ms: int = 30000, disable_cache: bool = False
) -> int:
index = self.next_index
self.next_index += 1
self.requested_urls.append(url)
self.pending_requests.append(index)
return index

def answer_pending_requests(self) -> None:
"""Complete every request that has been submitted and not yet answered or aborted."""
while self.pending_requests:
index = self.pending_requests.pop(0)
self.completed.emit(index, self.status, QtCore.QByteArray(self.response))

def abort(self, index: int) -> None:
self.aborted_requests.append(index)
self.pending_requests = [pending for pending in self.pending_requests if pending != index]
125 changes: 125 additions & 0 deletions AddonManagerTest/gui/test_connection_checker.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
# SPDX-License-Identifier: LGPL-2.1-or-later
# SPDX-FileCopyrightText: 2026 FreeCAD Project Association
# SPDX-FileNotice: Part of the AddonManager.

################################################################################
# #
# This addon is free software: you can redistribute it and/or modify #
# it under the terms of the GNU Lesser General Public License as #
# published by the Free Software Foundation, either version 2.1 #
# of the License, or (at your option) any later version. #
# #
# This addon 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 Lesser General Public License for more details. #
# #
# You should have received a copy of the GNU Lesser General Public #
# License along with this addon. If not, see https://www.gnu.org/licenses #
# #
################################################################################

"""Tests for the ConnectionCheckerGUI class."""

import unittest
from unittest.mock import patch

from PySideWrapper import QtCore

from addonmanager_connection_checker import ConnectionCheckerGUI


class FakeConnectionChecker(QtCore.QObject):
"""Stands in for the ConnectionChecker worker, without a thread or a network connection."""

success = QtCore.Signal()
failure = QtCore.Signal(str)

def __init__(self):
super().__init__()
self.running = False
self.interruption_requested = False

def start(self):
self.running = True

def isRunning(self) -> bool:
return self.running

def isFinished(self) -> bool:
return not self.running

def requestInterruption(self):
self.interruption_requested = True


class TestConnectionCheckerGUI(unittest.TestCase):
"""A QThread cannot be restarted, so every check has to get a worker of its own."""

def setUp(self):
checker_patch = patch(
"addonmanager_connection_checker.ConnectionChecker", FakeConnectionChecker
)
checker_patch.start()
self.addCleanup(checker_patch.stop)
self.checker_gui = ConnectionCheckerGUI()
self.addCleanup(self._mark_all_workers_finished)

def _mark_all_workers_finished(self):
"""Stop the delayed message that start() arms from finding a worker still running."""
for checker in [self.checker_gui.connection_checker, *self.checker_gui.retired_checkers]:
if checker is not None:
checker.running = False

def test_each_check_gets_a_new_worker(self):
self.checker_gui.start()
first_checker = self.checker_gui.connection_checker
first_checker.running = False

self.checker_gui.start()

self.assertIsNot(first_checker, self.checker_gui.connection_checker)

def test_new_worker_is_started(self):
self.checker_gui.start()

self.assertTrue(self.checker_gui.connection_checker.isRunning())

def test_unfinished_worker_is_asked_to_stop(self):
self.checker_gui.start()
first_checker = self.checker_gui.connection_checker

self.checker_gui.start()

self.assertTrue(first_checker.interruption_requested)

def test_unfinished_worker_is_kept_alive(self):
"""The worker still owns a network request, so it must outlive the check that started it."""
self.checker_gui.start()
first_checker = self.checker_gui.connection_checker

self.checker_gui.start()

self.assertIn(first_checker, self.checker_gui.retired_checkers)

def test_finished_worker_is_not_kept(self):
self.checker_gui.start()
self.checker_gui.connection_checker.running = False

self.checker_gui.start()

self.assertEqual([], self.checker_gui.retired_checkers)

def test_retired_workers_are_released_once_they_finish(self):
self.checker_gui.start()
first_checker = self.checker_gui.connection_checker
self.checker_gui.start()
first_checker.running = False

self.checker_gui.start()

self.assertNotIn(first_checker, self.checker_gui.retired_checkers)


if __name__ == "__main__":
unittest.main()
Loading
Loading