From c2995bc038590c0d7f8d6a0c8fa559c905d17f82 Mon Sep 17 00:00:00 2001 From: Anna Bocharova Date: Wed, 10 Jul 2024 12:36:01 +0000 Subject: [PATCH 1/6] Renaming Relay::relays to cache. --- octoprint_octorelay/driver.py | 6 +++--- tests/test_driver.py | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/octoprint_octorelay/driver.py b/octoprint_octorelay/driver.py index cfaef838..c3ac4f75 100755 --- a/octoprint_octorelay/driver.py +++ b/octoprint_octorelay/driver.py @@ -6,7 +6,7 @@ def xor(left: bool, right: bool) -> bool: return left is not right class Relay(): - relays: List["Relay"] = [] + cache: List["Relay"] = [] def __init__(self, pin: int, inverted: bool, pin_factory=None): self.pin = pin # GPIO pin @@ -41,11 +41,11 @@ def toggle(self, desired_state: Optional[bool] = None) -> bool: @classmethod def get_or_create_relay(cls, pin: int, inverted: bool, pin_factory=None): - for relay in cls.relays: + for relay in cls.cache: if relay.pin == pin: if xor(relay.inverted, inverted): relay.inverted = inverted return relay relay = cls(pin, inverted, pin_factory) - cls.relays.append(relay) + cls.cache.append(relay) return relay diff --git a/tests/test_driver.py b/tests/test_driver.py index 06804f18..e9202397 100644 --- a/tests/test_driver.py +++ b/tests/test_driver.py @@ -70,18 +70,18 @@ def test_toggle__no_argument(self): def test_get_or_create_relay(self): # Test creating a new relay relay1 = Relay.get_or_create_relay(17, False, MockFactory()) - self.assertEqual(len(Relay.relays), 1) + self.assertEqual(len(Relay.cache), 1) self.assertEqual(relay1.pin, 17) self.assertFalse(relay1.inverted) # Test retrieving the existing relay with the same pin and inversion relay2 = Relay.get_or_create_relay(17, True, MockFactory()) self.assertIs(relay1, relay2) - self.assertEqual(len(Relay.relays), 1) # Should still be 1 + self.assertEqual(len(Relay.cache), 1) # Should still be 1 # Test retrieving the existing relay with the same pin but different inversion relay3 = Relay.get_or_create_relay(17, True, MockFactory()) - self.assertEqual(len(Relay.relays), 1) # Should still be 1 + self.assertEqual(len(Relay.cache), 1) # Should still be 1 self.assertIs(relay1, relay3) self.assertTrue(relay1.inverted) # Inversion should be updated From f9c9b69bff4a2ed6ba59930bb3b581d7df26426a Mon Sep 17 00:00:00 2001 From: Anna Bocharova Date: Wed, 10 Jul 2024 12:40:22 +0000 Subject: [PATCH 2/6] Renaming Relay::get_or_create_relay() to ensure(). --- octoprint_octorelay/__init__.py | 10 +++++----- octoprint_octorelay/driver.py | 2 +- tests/test_driver.py | 8 ++++---- tests/test_init.py | 12 ++++++------ 4 files changed, 16 insertions(+), 16 deletions(-) diff --git a/octoprint_octorelay/__init__.py b/octoprint_octorelay/__init__.py index e9b35336..4f0615c2 100755 --- a/octoprint_octorelay/__init__.py +++ b/octoprint_octorelay/__init__.py @@ -108,7 +108,7 @@ def handle_list_all_command(self) -> Listing: settings = self._settings.get([], merged=True) # expensive for index in RELAY_INDEXES: if bool(settings[index]["active"]): - relay = Relay.get_or_create_relay( + relay = Relay.ensure( int(settings[index]["relay_pin"] or 0), bool(settings[index]["inverted_output"]) ) @@ -124,7 +124,7 @@ def handle_get_status_command(self, index: str) -> bool: settings = self._settings.get([index], merged=True) # expensive if not bool(settings["active"]): raise HandlingException(400) - return Relay.get_or_create_relay( + return Relay.ensure( int(settings["relay_pin"] or 0), bool(settings["inverted_output"]) ).is_closed() @@ -241,7 +241,7 @@ def toggle_relay(self, index, target: Optional[bool] = None) -> bool: self._printer.disconnect() pin = int(settings["relay_pin"] or 0) inverted = bool(settings["inverted_output"]) - relay = Relay.get_or_create_relay(pin, inverted) + relay = Relay.ensure(pin, inverted) self._logger.debug( f"Toggling the relay {index} on pin {pin}" if target is None else f"Turning the relay {index} {'ON' if target else 'OFF'} (pin {pin})" @@ -313,7 +313,7 @@ def update_ui(self): )) for index in RELAY_INDEXES: active = bool(settings[index]["active"]) - relay = Relay.get_or_create_relay( + relay = Relay.ensure( int(settings[index]["relay_pin"] or 0), bool(settings[index]["inverted_output"]) ) @@ -362,7 +362,7 @@ def input_polling(self): for index in RELAY_INDEXES: active = self.model[index]["active"] model_state = self.model[index]["relay_state"] # bool since v3.1 - actual_state = Relay.get_or_create_relay( + actual_state = Relay.ensure( self.model[index]["relay_pin"], self.model[index]["inverted_output"] ).is_closed() if active else False diff --git a/octoprint_octorelay/driver.py b/octoprint_octorelay/driver.py index c3ac4f75..6fe9c016 100755 --- a/octoprint_octorelay/driver.py +++ b/octoprint_octorelay/driver.py @@ -40,7 +40,7 @@ def toggle(self, desired_state: Optional[bool] = None) -> bool: return desired_state @classmethod - def get_or_create_relay(cls, pin: int, inverted: bool, pin_factory=None): + def ensure(cls, pin: int, inverted: bool, pin_factory=None): for relay in cls.cache: if relay.pin == pin: if xor(relay.inverted, inverted): diff --git a/tests/test_driver.py b/tests/test_driver.py index e9202397..9a6c1701 100644 --- a/tests/test_driver.py +++ b/tests/test_driver.py @@ -67,20 +67,20 @@ def test_toggle__no_argument(self): self.assertEqual(relay.toggle(), case["expected_relay_state"]) self.assertEqual(relay.relay.is_lit, case["expected_pin_state"]) - def test_get_or_create_relay(self): + def test_ensure(self): # Test creating a new relay - relay1 = Relay.get_or_create_relay(17, False, MockFactory()) + relay1 = Relay.ensure(17, False, MockFactory()) self.assertEqual(len(Relay.cache), 1) self.assertEqual(relay1.pin, 17) self.assertFalse(relay1.inverted) # Test retrieving the existing relay with the same pin and inversion - relay2 = Relay.get_or_create_relay(17, True, MockFactory()) + relay2 = Relay.ensure(17, True, MockFactory()) self.assertIs(relay1, relay2) self.assertEqual(len(Relay.cache), 1) # Should still be 1 # Test retrieving the existing relay with the same pin but different inversion - relay3 = Relay.get_or_create_relay(17, True, MockFactory()) + relay3 = Relay.ensure(17, True, MockFactory()) self.assertEqual(len(Relay.cache), 1) # Should still be 1 self.assertIs(relay1, relay3) self.assertTrue(relay1.inverted) # Inversion should be updated diff --git a/tests/test_init.py b/tests/test_init.py index 2e2a52fc..b91aae56 100644 --- a/tests/test_init.py +++ b/tests/test_init.py @@ -30,7 +30,7 @@ relayMock = Mock() relayConstructorMock = Mock(return_value=relayMock) -relayConstructorMock.get_or_create_relay = Mock(return_value=relayMock) +relayConstructorMock.ensure = Mock(return_value=relayMock) sys.modules["octoprint_octorelay.driver"] = Mock( Relay=relayConstructorMock @@ -469,9 +469,9 @@ def test_input_polling(self): } relayMock.is_closed = Mock(return_value=True) self.plugin_instance.input_polling() - self.assertEqual(relayConstructorMock.get_or_create_relay.call_count, 2) - relayConstructorMock.get_or_create_relay.assert_any_call(17, False) - relayConstructorMock.get_or_create_relay.assert_any_call(18, False) + self.assertEqual(relayConstructorMock.ensure.call_count, 2) + relayConstructorMock.ensure.assert_any_call(17, False) + relayConstructorMock.ensure.assert_any_call(18, False) self.plugin_instance.update_ui.assert_called_with() self.plugin_instance._logger.debug.assert_called_with("relay: r3 has changed its pin state") @@ -510,7 +510,7 @@ def test_update_ui(self): "upcoming": None } self.plugin_instance.update_ui() - relayConstructorMock.get_or_create_relay.assert_called_with(17, False) + relayConstructorMock.ensure.assert_called_with(17, False) for index in RELAY_INDEXES: self.plugin_instance._settings.get.assert_any_call([], merged=True) self.plugin_instance._plugin_manager.send_plugin_message.assert_called_with( @@ -555,7 +555,7 @@ def test_toggle_relay(self, system_mock): "cmd_off": "CommandOFF" }) self.plugin_instance.toggle_relay("r4", case["target"]) - relayConstructorMock.get_or_create_relay.assert_called_with(17, case["inverted"]) + relayConstructorMock.ensure.assert_called_with(17, case["inverted"]) relayMock.toggle.assert_called_with(case["target"]) system_mock.assert_called_with(case["expectedCommand"]) if case["expectedCommand"] == "CommandON": From 7c47d6892d12ef6db1a23d4958911dcb9704f3a9 Mon Sep 17 00:00:00 2001 From: Anna Bocharova Date: Wed, 10 Jul 2024 12:47:28 +0000 Subject: [PATCH 3/6] Renaming class Relay to Driver. --- octoprint_octorelay/__init__.py | 12 +++++----- octoprint_octorelay/driver.py | 4 ++-- tests/test_driver.py | 40 ++++++++++++++++----------------- tests/test_init.py | 4 ++-- 4 files changed, 30 insertions(+), 30 deletions(-) diff --git a/octoprint_octorelay/__init__.py b/octoprint_octorelay/__init__.py index 4f0615c2..4981785f 100755 --- a/octoprint_octorelay/__init__.py +++ b/octoprint_octorelay/__init__.py @@ -17,7 +17,7 @@ STARTUP, PRINTING_STOPPED, PRINTING_STARTED, PRIORITIES, FALLBACK_PRIORITY, PREEMPTIVE_CANCELLATION_CUTOFF, CANCEL_TASK_COMMAND, USER_ACTION, TURNED_ON ) -from .driver import Relay +from .driver import Driver from .task import Task from .listing import Listing from .migrations import migrate @@ -108,7 +108,7 @@ def handle_list_all_command(self) -> Listing: settings = self._settings.get([], merged=True) # expensive for index in RELAY_INDEXES: if bool(settings[index]["active"]): - relay = Relay.ensure( + relay = Driver.ensure( int(settings[index]["relay_pin"] or 0), bool(settings[index]["inverted_output"]) ) @@ -124,7 +124,7 @@ def handle_get_status_command(self, index: str) -> bool: settings = self._settings.get([index], merged=True) # expensive if not bool(settings["active"]): raise HandlingException(400) - return Relay.ensure( + return Driver.ensure( int(settings["relay_pin"] or 0), bool(settings["inverted_output"]) ).is_closed() @@ -241,7 +241,7 @@ def toggle_relay(self, index, target: Optional[bool] = None) -> bool: self._printer.disconnect() pin = int(settings["relay_pin"] or 0) inverted = bool(settings["inverted_output"]) - relay = Relay.ensure(pin, inverted) + relay = Driver.ensure(pin, inverted) self._logger.debug( f"Toggling the relay {index} on pin {pin}" if target is None else f"Turning the relay {index} {'ON' if target else 'OFF'} (pin {pin})" @@ -313,7 +313,7 @@ def update_ui(self): )) for index in RELAY_INDEXES: active = bool(settings[index]["active"]) - relay = Relay.ensure( + relay = Driver.ensure( int(settings[index]["relay_pin"] or 0), bool(settings[index]["inverted_output"]) ) @@ -362,7 +362,7 @@ def input_polling(self): for index in RELAY_INDEXES: active = self.model[index]["active"] model_state = self.model[index]["relay_state"] # bool since v3.1 - actual_state = Relay.ensure( + actual_state = Driver.ensure( self.model[index]["relay_pin"], self.model[index]["inverted_output"] ).is_closed() if active else False diff --git a/octoprint_octorelay/driver.py b/octoprint_octorelay/driver.py index 6fe9c016..bf0ee657 100755 --- a/octoprint_octorelay/driver.py +++ b/octoprint_octorelay/driver.py @@ -5,8 +5,8 @@ def xor(left: bool, right: bool) -> bool: return left is not right -class Relay(): - cache: List["Relay"] = [] +class Driver(): + cache: List["Driver"] = [] def __init__(self, pin: int, inverted: bool, pin_factory=None): self.pin = pin # GPIO pin diff --git a/tests/test_driver.py b/tests/test_driver.py index 9a6c1701..c00f161c 100644 --- a/tests/test_driver.py +++ b/tests/test_driver.py @@ -5,29 +5,29 @@ from gpiozero.pins.mock import MockFactory # pylint: disable=wrong-import-position -from octoprint_octorelay.driver import Relay +from octoprint_octorelay.driver import Driver # avoid keeping other modules automatically imported by this test del sys.modules["octoprint_octorelay"] del sys.modules["octoprint_octorelay.migrations"] del sys.modules["octoprint_octorelay.task"] -class TestRelayDriver(unittest.TestCase): +class TestDriverDriver(unittest.TestCase): def test_constructor(self): - relay = Relay(18, True, MockFactory()) - self.assertIsInstance(relay, Relay) + relay = Driver(18, True, MockFactory()) + self.assertIsInstance(relay, Driver) self.assertEqual(relay.pin, 18) self.assertTrue(relay.inverted) def test_serialization(self): - relay = Relay(18, True, MockFactory()) + relay = Driver(18, True, MockFactory()) serialization = f"{relay}" - self.assertEqual(serialization, "Relay(pin=18,inverted=True,closed=True)") + self.assertEqual(serialization, "Driver(pin=18,inverted=True,closed=True)") def test_close(self): cases = [ - { "relay": Relay(18, False, MockFactory()), "expected_pin_state": True }, - { "relay": Relay(18, True, MockFactory()), "expected_pin_state": False } + { "relay": Driver(18, False, MockFactory()), "expected_pin_state": True }, + { "relay": Driver(18, True, MockFactory()), "expected_pin_state": False } ] for case in cases: case["relay"].close() @@ -35,8 +35,8 @@ def test_close(self): def test_open(self): cases = [ - { "relay": Relay(18, False, MockFactory()), "expected_pin_state": False }, - { "relay": Relay(18, True, MockFactory()), "expected_pin_state": True } + { "relay": Driver(18, False, MockFactory()), "expected_pin_state": False }, + { "relay": Driver(18, True, MockFactory()), "expected_pin_state": True } ] for case in cases: case["relay"].open() @@ -50,7 +50,7 @@ def test_is_closed(self): { "mocked_state": 0, "inverted": True, "expected_relay_state": True }, ] for case in cases: - relay = Relay(18, case["inverted"], MockFactory()) + relay = Driver(18, case["inverted"], MockFactory()) relay.relay.value = case["mocked_state"] self.assertEqual(relay.is_closed(), case["expected_relay_state"]) @@ -62,32 +62,32 @@ def test_toggle__no_argument(self): { "mocked_state": 0, "inverted": True, "expected_pin_state": True, "expected_relay_state": False }, ] for case in cases: - relay = Relay(18, case["inverted"], MockFactory()) + relay = Driver(18, case["inverted"], MockFactory()) relay.relay.value = case["mocked_state"] self.assertEqual(relay.toggle(), case["expected_relay_state"]) self.assertEqual(relay.relay.is_lit, case["expected_pin_state"]) def test_ensure(self): # Test creating a new relay - relay1 = Relay.ensure(17, False, MockFactory()) - self.assertEqual(len(Relay.cache), 1) + relay1 = Driver.ensure(17, False, MockFactory()) + self.assertEqual(len(Driver.cache), 1) self.assertEqual(relay1.pin, 17) self.assertFalse(relay1.inverted) # Test retrieving the existing relay with the same pin and inversion - relay2 = Relay.ensure(17, True, MockFactory()) + relay2 = Driver.ensure(17, True, MockFactory()) self.assertIs(relay1, relay2) - self.assertEqual(len(Relay.cache), 1) # Should still be 1 + self.assertEqual(len(Driver.cache), 1) # Should still be 1 # Test retrieving the existing relay with the same pin but different inversion - relay3 = Relay.ensure(17, True, MockFactory()) - self.assertEqual(len(Relay.cache), 1) # Should still be 1 + relay3 = Driver.ensure(17, True, MockFactory()) + self.assertEqual(len(Driver.cache), 1) # Should still be 1 self.assertIs(relay1, relay3) self.assertTrue(relay1.inverted) # Inversion should be updated def tearDown(self): - # Clear relays after each test - Relay.relays = [] + # Clear cache after each test + Driver.cache = [] if __name__ == "__main__": unittest.main() diff --git a/tests/test_init.py b/tests/test_init.py index b91aae56..2b171504 100644 --- a/tests/test_init.py +++ b/tests/test_init.py @@ -33,7 +33,7 @@ relayConstructorMock.ensure = Mock(return_value=relayMock) sys.modules["octoprint_octorelay.driver"] = Mock( - Relay=relayConstructorMock + Driver=relayConstructorMock ) # pylint: disable=wrong-import-position @@ -915,7 +915,7 @@ def test_handle_update_command(self, system_mock): "target": False, "closed": True, "expectedError": False, - "expectedResult": False, # from the !closed returned by mocked Relay::toggle() below + "expectedResult": False, # from the !closed returned by mocked Driver::toggle() below "expectedToggle": True, "expectedCommand": "CommandOffMock" }, From 18f433303b1bc22bd372eb2393c351d3f9070187 Mon Sep 17 00:00:00 2001 From: Anna Bocharova Date: Wed, 10 Jul 2024 12:51:21 +0000 Subject: [PATCH 4/6] Renaming Driver::relay to handle. --- octoprint_octorelay/driver.py | 6 +++--- tests/test_driver.py | 10 +++++----- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/octoprint_octorelay/driver.py b/octoprint_octorelay/driver.py index bf0ee657..a6db059a 100755 --- a/octoprint_octorelay/driver.py +++ b/octoprint_octorelay/driver.py @@ -11,7 +11,7 @@ class Driver(): def __init__(self, pin: int, inverted: bool, pin_factory=None): self.pin = pin # GPIO pin self.inverted = inverted # marks the relay as normally closed - self.relay = LED(pin, pin_factory=pin_factory) + self.handle = LED(pin, pin_factory=pin_factory) def __repr__(self) -> str: return f"{type(self).__name__}(pin={self.pin},inverted={self.inverted},closed={self.is_closed()})" @@ -26,7 +26,7 @@ def open(self): def is_closed(self) -> bool: """Returns the logical state of the relay.""" - return xor(self.inverted, self.relay.is_lit) + return xor(self.inverted, self.handle.is_lit) def toggle(self, desired_state: Optional[bool] = None) -> bool: """ @@ -36,7 +36,7 @@ def toggle(self, desired_state: Optional[bool] = None) -> bool: """ if desired_state is None: desired_state = not self.is_closed() - (self.relay.on if xor(self.inverted, desired_state) else self.relay.off)() + (self.handle.on if xor(self.inverted, desired_state) else self.handle.off)() return desired_state @classmethod diff --git a/tests/test_driver.py b/tests/test_driver.py index c00f161c..d6a529b8 100644 --- a/tests/test_driver.py +++ b/tests/test_driver.py @@ -31,7 +31,7 @@ def test_close(self): ] for case in cases: case["relay"].close() - self.assertEqual(case["relay"].relay.is_lit, case["expected_pin_state"]) + self.assertEqual(case["relay"].handle.is_lit, case["expected_pin_state"]) def test_open(self): cases = [ @@ -40,7 +40,7 @@ def test_open(self): ] for case in cases: case["relay"].open() - self.assertEqual(case["relay"].relay.is_lit, case["expected_pin_state"]) + self.assertEqual(case["relay"].handle.is_lit, case["expected_pin_state"]) def test_is_closed(self): cases = [ @@ -51,7 +51,7 @@ def test_is_closed(self): ] for case in cases: relay = Driver(18, case["inverted"], MockFactory()) - relay.relay.value = case["mocked_state"] + relay.handle.value = case["mocked_state"] self.assertEqual(relay.is_closed(), case["expected_relay_state"]) def test_toggle__no_argument(self): @@ -63,9 +63,9 @@ def test_toggle__no_argument(self): ] for case in cases: relay = Driver(18, case["inverted"], MockFactory()) - relay.relay.value = case["mocked_state"] + relay.handle.value = case["mocked_state"] self.assertEqual(relay.toggle(), case["expected_relay_state"]) - self.assertEqual(relay.relay.is_lit, case["expected_pin_state"]) + self.assertEqual(relay.handle.is_lit, case["expected_pin_state"]) def test_ensure(self): # Test creating a new relay From a2873e39f37302472f508c79bf4a1a2652128934 Mon Sep 17 00:00:00 2001 From: Anna Bocharova Date: Wed, 10 Jul 2024 12:55:54 +0000 Subject: [PATCH 5/6] Fix test suite name. --- tests/test_driver.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_driver.py b/tests/test_driver.py index d6a529b8..e7b67167 100644 --- a/tests/test_driver.py +++ b/tests/test_driver.py @@ -12,7 +12,7 @@ del sys.modules["octoprint_octorelay.migrations"] del sys.modules["octoprint_octorelay.task"] -class TestDriverDriver(unittest.TestCase): +class TestDriver(unittest.TestCase): def test_constructor(self): relay = Driver(18, True, MockFactory()) self.assertIsInstance(relay, Driver) From 6169d70a8bd69e08353fa0f2d34486936cb420c6 Mon Sep 17 00:00:00 2001 From: Anna Bocharova Date: Wed, 10 Jul 2024 13:47:28 +0000 Subject: [PATCH 6/6] Renaming to DriverMock in main test. --- tests/test_init.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/tests/test_init.py b/tests/test_init.py index 2b171504..6ab77ff9 100644 --- a/tests/test_init.py +++ b/tests/test_init.py @@ -29,11 +29,11 @@ sys.modules["octoprint_octorelay.migrations"] = migrationsMock relayMock = Mock() -relayConstructorMock = Mock(return_value=relayMock) -relayConstructorMock.ensure = Mock(return_value=relayMock) +DriverMock = Mock(return_value=relayMock) +DriverMock.ensure = Mock(return_value=relayMock) sys.modules["octoprint_octorelay.driver"] = Mock( - Driver=relayConstructorMock + Driver=DriverMock ) # pylint: disable=wrong-import-position @@ -461,7 +461,7 @@ def test_on_shutdown(self): def test_input_polling(self): # First active relay having state not equal to the one stored in model should trigger UI update self.plugin_instance.update_ui = Mock() - relayConstructorMock.reset_mock() + DriverMock.reset_mock() self.plugin_instance.model = { "r1": { "active": False, "relay_pin": 4, "inverted_output": False, "relay_state": True }, "r2": { "active": True, "relay_pin": 17, "inverted_output": False, "relay_state": True }, @@ -469,9 +469,9 @@ def test_input_polling(self): } relayMock.is_closed = Mock(return_value=True) self.plugin_instance.input_polling() - self.assertEqual(relayConstructorMock.ensure.call_count, 2) - relayConstructorMock.ensure.assert_any_call(17, False) - relayConstructorMock.ensure.assert_any_call(18, False) + self.assertEqual(DriverMock.ensure.call_count, 2) + DriverMock.ensure.assert_any_call(17, False) + DriverMock.ensure.assert_any_call(18, False) self.plugin_instance.update_ui.assert_called_with() self.plugin_instance._logger.debug.assert_called_with("relay: r3 has changed its pin state") @@ -510,7 +510,7 @@ def test_update_ui(self): "upcoming": None } self.plugin_instance.update_ui() - relayConstructorMock.ensure.assert_called_with(17, False) + DriverMock.ensure.assert_called_with(17, False) for index in RELAY_INDEXES: self.plugin_instance._settings.get.assert_any_call([], merged=True) self.plugin_instance._plugin_manager.send_plugin_message.assert_called_with( @@ -555,7 +555,7 @@ def test_toggle_relay(self, system_mock): "cmd_off": "CommandOFF" }) self.plugin_instance.toggle_relay("r4", case["target"]) - relayConstructorMock.ensure.assert_called_with(17, case["inverted"]) + DriverMock.ensure.assert_called_with(17, case["inverted"]) relayMock.toggle.assert_called_with(case["target"]) system_mock.assert_called_with(case["expectedCommand"]) if case["expectedCommand"] == "CommandON":