Skip to content
Open
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
159 changes: 159 additions & 0 deletions oocana/tests/test_mainframe_callbacks.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
import unittest
from unittest.mock import Mock, MagicMock, patch
Copy link

Copilot AI Jan 31, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

patch is imported but never used. If the project runs any linting/static checks, this will fail; even without linting it adds noise. Remove the unused import (or use it in the tests if intended).

Suggested change
from unittest.mock import Mock, MagicMock, patch
from unittest.mock import Mock, MagicMock

Copilot uses AI. Check for mistakes.


class TestMainframeCallbacks(unittest.TestCase):
"""Test Mainframe callback management methods."""

def setUp(self):
"""Create a Mainframe instance with mocked MQTT client."""
from oocana import Mainframe

self.mainframe = Mainframe("mqtt://localhost:1883")

# Manually set up mock client without calling connect()
self.mock_mqtt_client = MagicMock()
self.mock_mqtt_client.is_connected.return_value = True
self.mainframe.client = self.mock_mqtt_client

def test_add_report_callback_adds_callable(self):
"""Test add_report_callback adds a callable function."""
callback = Mock()
self.mainframe.add_report_callback(callback)

# Internal check - callback should be in the set
self.assertIn(callback, self.mainframe._Mainframe__report_callbacks)

Comment on lines +24 to +26
Copy link

Copilot AI Jan 31, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These tests assert internal state via name-mangled private attributes (e.g. _Mainframe__session_callbacks). This couples the tests to implementation details and will break on refactors (rename, type change, etc.). Prefer asserting externally observable behavior (e.g. message_callback_add/subscribe/unsubscribe calls and callback invocation), or add a small public/intended-for-testing accessor on Mainframe if state inspection is required.

Copilot uses AI. Check for mistakes.
def test_add_report_callback_rejects_non_callable(self):
"""Test add_report_callback raises ValueError for non-callable."""
with self.assertRaises(ValueError):
self.mainframe.add_report_callback("not a function")

def test_remove_report_callback_removes_existing(self):
"""Test remove_report_callback removes existing callback."""
callback = Mock()
self.mainframe.add_report_callback(callback)
self.mainframe.remove_report_callback(callback)

self.assertNotIn(callback, self.mainframe._Mainframe__report_callbacks)

def test_remove_report_callback_logs_warning_for_missing(self):
"""Test remove_report_callback logs warning for non-existent callback."""
callback = Mock()
# Should not raise, just log warning
self.mainframe.remove_report_callback(callback)

Comment on lines +40 to +45
Copy link

Copilot AI Jan 31, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

test_remove_report_callback_logs_warning_for_missing currently has no assertion that a warning was logged, so it can pass even if logging is removed or the code logs at a different level. If the intent is to verify the warning behavior, assert it explicitly (e.g., via assertLogs on the module logger or by patching self.mainframe._logger.warning).

Copilot uses AI. Check for mistakes.
def test_add_session_callback_adds_callable(self):
"""Test add_session_callback adds a callable function."""
callback = Mock()
session_id = "test_session"
self.mainframe.add_session_callback(session_id, callback)

self.assertIn(session_id, self.mainframe._Mainframe__session_callbacks)
self.assertIn(callback, self.mainframe._Mainframe__session_callbacks[session_id])

def test_add_session_callback_rejects_non_callable(self):
"""Test add_session_callback raises ValueError for non-callable."""
with self.assertRaises(ValueError):
self.mainframe.add_session_callback("session", "not a function")

def test_add_session_callback_subscribes_once(self):
"""Test add_session_callback only subscribes once for same session."""
callback1 = Mock()
callback2 = Mock()
session_id = "test_session"

self.mainframe.add_session_callback(session_id, callback1)
subscribe_count = self.mock_mqtt_client.message_callback_add.call_count

self.mainframe.add_session_callback(session_id, callback2)
# Should not subscribe again
self.assertEqual(self.mock_mqtt_client.message_callback_add.call_count, subscribe_count)

def test_remove_session_callback_removes_existing(self):
"""Test remove_session_callback removes existing callback."""
callback = Mock()
session_id = "test_session"
self.mainframe.add_session_callback(session_id, callback)
self.mainframe.remove_session_callback(session_id, callback)

# After removing last callback, session_id should be removed from dict
self.assertNotIn(session_id, self.mainframe._Mainframe__session_callbacks)

def test_remove_session_callback_unsubscribes_on_last(self):
"""Test remove_session_callback unsubscribes when last callback removed."""
callback = Mock()
session_id = "test_session"
self.mainframe.add_session_callback(session_id, callback)

# Reset mock to track unsubscribe call
self.mock_mqtt_client.reset_mock()

self.mainframe.remove_session_callback(session_id, callback)
self.mock_mqtt_client.unsubscribe.assert_called_once()

Comment on lines +92 to +94
Copy link

Copilot AI Jan 31, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These unsubscribe assertions only check the call count (assert_called_once()), not that the correct topic was unsubscribed. Since Mainframe.unsubscribe() passes a topic argument, this test should assert the expected argument(s) (and ideally also assert message_callback_remove was called with the same topic) to avoid false positives.

Suggested change
self.mainframe.remove_session_callback(session_id, callback)
self.mock_mqtt_client.unsubscribe.assert_called_once()
self.mainframe.remove_session_callback(session_id, callback)
# Ensure unsubscribe and message_callback_remove are each called once
self.mock_mqtt_client.unsubscribe.assert_called_once()
self.mock_mqtt_client.message_callback_remove.assert_called_once()
# Ensure both operations use the same topic argument
unsubscribe_args, _ = self.mock_mqtt_client.unsubscribe.call_args
remove_args, _ = self.mock_mqtt_client.message_callback_remove.call_args
self.assertGreaterEqual(len(unsubscribe_args), 1)
self.assertGreaterEqual(len(remove_args), 1)
self.assertEqual(unsubscribe_args[0], remove_args[0])

Copilot uses AI. Check for mistakes.
def test_add_request_response_callback_adds_callable(self):
"""Test add_request_response_callback adds a callable function."""
callback = Mock()
session_id = "test_session"
request_id = "test_request"
self.mainframe.add_request_response_callback(session_id, request_id, callback)

self.assertIn(request_id, self.mainframe._Mainframe__request_response_callbacks)
self.assertIn(callback, self.mainframe._Mainframe__request_response_callbacks[request_id])

def test_add_request_response_callback_rejects_non_callable(self):
"""Test add_request_response_callback raises ValueError for non-callable."""
with self.assertRaises(ValueError):
self.mainframe.add_request_response_callback("session", "request", "not a function")

def test_remove_request_response_callback_removes_existing(self):
"""Test remove_request_response_callback removes existing callback."""
callback = Mock()
session_id = "test_session"
request_id = "test_request"
self.mainframe.add_request_response_callback(session_id, request_id, callback)
self.mainframe.remove_request_response_callback(session_id, request_id, callback)

# After removing last callback, request_id should be removed
self.assertNotIn(request_id, self.mainframe._Mainframe__request_response_callbacks)

def test_remove_request_response_callback_unsubscribes_on_last(self):
"""Test remove_request_response_callback unsubscribes when last callback removed."""
callback = Mock()
session_id = "test_session"
request_id = "test_request"
self.mainframe.add_request_response_callback(session_id, request_id, callback)

self.mock_mqtt_client.reset_mock()

self.mainframe.remove_request_response_callback(session_id, request_id, callback)
self.mock_mqtt_client.unsubscribe.assert_called_once()
Copy link

Copilot AI Jan 31, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same issue here: assert_called_once() doesn’t verify which topic was unsubscribed. Consider asserting unsubscribe/message_callback_remove were called with f"session/{session_id}/request/{request_id}/response" so the test actually validates the request/response lifecycle behavior.

Suggested change
self.mock_mqtt_client.unsubscribe.assert_called_once()
self.mock_mqtt_client.unsubscribe.assert_called_once_with(
f"session/{session_id}/request/{request_id}/response"
)

Copilot uses AI. Check for mistakes.

def test_multiple_callbacks_same_session(self):
"""Test multiple callbacks can be added for same session."""
callback1 = Mock()
callback2 = Mock()
session_id = "test_session"

self.mainframe.add_session_callback(session_id, callback1)
self.mainframe.add_session_callback(session_id, callback2)

self.assertEqual(len(self.mainframe._Mainframe__session_callbacks[session_id]), 2)

def test_remove_one_callback_keeps_others(self):
"""Test removing one callback keeps other callbacks intact."""
callback1 = Mock()
callback2 = Mock()
session_id = "test_session"

self.mainframe.add_session_callback(session_id, callback1)
self.mainframe.add_session_callback(session_id, callback2)
self.mainframe.remove_session_callback(session_id, callback1)

self.assertIn(callback2, self.mainframe._Mainframe__session_callbacks[session_id])
self.assertNotIn(callback1, self.mainframe._Mainframe__session_callbacks[session_id])


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