diff --git a/.gitignore b/.gitignore index 87049d3c03..15c361832c 100644 --- a/.gitignore +++ b/.gitignore @@ -28,3 +28,5 @@ __pycache__ start-rabbitmq stop-rabbitmq rabbitmq.log +.coverage +htmlcov/ diff --git a/pytest.ini b/pytest.ini index 571e122e4f..472578a0f6 100644 --- a/pytest.ini +++ b/pytest.ini @@ -10,6 +10,7 @@ norecursedirs = markers = actuator: Tests for actuator agent actuator_pubsub: Test for actuator agent. + actuator_unit: Unit tests for actuator agent agent: Testing for core agent operations. alert: Testing alerts from the health subsystem. auth: Testing for auth based code. diff --git a/services/core/ActuatorAgent/tests/actuator_fixtures.py b/services/core/ActuatorAgent/tests/actuator_fixtures.py new file mode 100644 index 0000000000..36f121f8a8 --- /dev/null +++ b/services/core/ActuatorAgent/tests/actuator_fixtures.py @@ -0,0 +1,92 @@ +# -*- coding: utf-8 -*- {{{ +# vim: set fenc=utf-8 ft=python sw=4 ts=4 sts=4 et: +# +# Copyright 2019, Battelle Memorial Institute. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# This material was prepared as an account of work sponsored by an agency of +# the United States Government. Neither the United States Government nor the +# United States Department of Energy, nor Battelle, nor any of their +# employees, nor any jurisdiction or organization that has cooperated in the +# development of these materials, makes any warranty, express or +# implied, or assumes any legal liability or responsibility for the accuracy, +# completeness, or usefulness or any information, apparatus, product, +# software, or process disclosed, or represents that its use would not infringe +# privately owned rights. Reference herein to any specific commercial product, +# process, or service by trade name, trademark, manufacturer, or otherwise +# does not necessarily constitute or imply its endorsement, recommendation, or +# favoring by the United States Government or any agency thereof, or +# Battelle Memorial Institute. The views and opinions of authors expressed +# herein do not necessarily state or reflect those of the +# United States Government or any agency thereof. +# +# PACIFIC NORTHWEST NATIONAL LABORATORY operated by +# BATTELLE for the UNITED STATES DEPARTMENT OF ENERGY +# under Contract DE-AC05-76RL01830 +# }}} + +import contextlib + +from mock import create_autospec + +from services.core.ActuatorAgent.actuator.agent import ActuatorAgent, ScheduleManager +from services.core.ActuatorAgent.actuator.scheduler import RequestResult + + +class MockedAsyncResult: + """ + This class is used to help mock Responses from the vip subsystem + """ + def __init__(self, result): + self.result = result + + def get(self): + return self.result + + +@contextlib.contextmanager +def get_actuator_agent(vip_identity: str = "fake_vip_identity", + vip_rpc_call_res: MockedAsyncResult = MockedAsyncResult("fake_result"), + vip_message_peer: str = None, + device_state: dict = {}, + slot_requests_res: RequestResult = RequestResult(True, {("agent-id-1", "task-id-1")}, ""), + cancel_schedule_result: RequestResult = None): + """ + Creates an Actuator agent and mocks all required dependencies for unit testing + :param vip_identity: the identity of the Agent's Subsystem + :param vip_rpc_call_res: the response returned when calling a method of the Agent's Subsystem + :param vip_message_peer: the identity of the Agent's VIP, which is used internally + :param device_state: a mapping between a path and a DeviceState; this is an protected field of the Agent + :param slot_requests_res: the response returned when calling request_slots method of Agent's Schedule Manager + :param cancel_schedule_result: the response retunred when callin cancel_task method of Agent's Schedule Manaager + :return: + """ + ActuatorAgent.core.identity = "fake_core_identity" + actuator_agent = ActuatorAgent() + if vip_identity is not None: + actuator_agent.driver_vip_identity = vip_identity + actuator_agent.vip.rpc.call.return_value = vip_rpc_call_res + actuator_agent.vip.rpc.context.vip_message.peer = vip_message_peer + actuator_agent._device_states = device_state + actuator_agent._schedule_manager = create_autospec(ScheduleManager) + actuator_agent._schedule_manager.request_slots.return_value = slot_requests_res + actuator_agent._schedule_manager.get_next_event_time.return_value = None + actuator_agent._schedule_manager.cancel_task.return_value = cancel_schedule_result + actuator_agent._schedule_manager.get_schedule_state.return_value = {} + actuator_agent.core.schedule.return_value = None + + try: + yield actuator_agent + finally: + actuator_agent.vip.reset_mock() diff --git a/services/core/ActuatorAgent/tests/test_actuator_pubsub_unit.py b/services/core/ActuatorAgent/tests/test_actuator_pubsub_unit.py new file mode 100644 index 0000000000..88f5dbfceb --- /dev/null +++ b/services/core/ActuatorAgent/tests/test_actuator_pubsub_unit.py @@ -0,0 +1,284 @@ +# -*- coding: utf-8 -*- {{{ +# vim: set fenc=utf-8 ft=python sw=4 ts=4 sts=4 et: +# +# Copyright 2019, Battelle Memorial Institute. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# This material was prepared as an account of work sponsored by an agency of +# the United States Government. Neither the United States Government nor the +# United States Department of Energy, nor Battelle, nor any of their +# employees, nor any jurisdiction or organization that has cooperated in the +# development of these materials, makes any warranty, express or +# implied, or assumes any legal liability or responsibility for the accuracy, +# completeness, or usefulness or any information, apparatus, product, +# software, or process disclosed, or represents that its use would not infringe +# privately owned rights. Reference herein to any specific commercial product, +# process, or service by trade name, trademark, manufacturer, or otherwise +# does not necessarily constitute or imply its endorsement, recommendation, or +# favoring by the United States Government or any agency thereof, or +# Battelle Memorial Institute. The views and opinions of authors expressed +# herein do not necessarily state or reflect those of the +# United States Government or any agency thereof. +# +# PACIFIC NORTHWEST NATIONAL LABORATORY operated by +# BATTELLE for the UNITED STATES DEPARTMENT OF ENERGY +# under Contract DE-AC05-76RL01830 +# }}} + +import logging +from datetime import datetime, timedelta +from dateutil.tz import tzutc + +import pytest + +from services.core.ActuatorAgent.actuator import agent +from services.core.ActuatorAgent.actuator.agent import ActuatorAgent +from services.core.ActuatorAgent.actuator.scheduler import RequestResult, DeviceState +from services.core.ActuatorAgent.tests.actuator_fixtures import MockedAsyncResult, get_actuator_agent +from volttrontesting.utils.utils import AgentMock +from volttron.platform.vip.agent import Agent + + +PEER = "peer-1" +SENDER = "sender-1" +HEADERS = {"requesterID": "id-12345"} +MESSAGE = "message-1" +BUS = "bus-1" +GET_TOPIC = "devices/actuators/get/somepath/actuationpoint" +SET_TOPIC = "devices/actuators/set/somepath/actuationpoint" +REQUEST_TOPIC = "devices/actuators/schedule/request" +REVERT_DEVICE_TOPIC = "devices/actuators/revert/device/somedevicepath" +REVERT_POINT_TOPIC = "actuators/revert/point/somedevicepath/someactuationpoint" + +agent._log = logging.getLogger("test_logger") +ActuatorAgent.__bases__ = (AgentMock.imitate(Agent, Agent()),) + + +@pytest.mark.actuator_unit +def test_handle_get_should_succeed(): + with get_actuator_agent() as actuator_agent: + actuator_agent.handle_get(PEER, SENDER, BUS, GET_TOPIC, HEADERS, MESSAGE) + + actuator_agent.vip.rpc.call.assert_called_once() + actuator_agent.vip.pubsub.publish.assert_called_once() + + +@pytest.mark.actuator_unit +def test_handle_get_should_handle_standard_error(caplog): + with get_actuator_agent(vip_identity=None) as actuator_agent: + actuator_agent.handle_get(PEER, SENDER, BUS, GET_TOPIC, HEADERS, MESSAGE) + + actuator_agent.vip.rpc.call.assert_not_called() + actuator_agent.vip.pubsub.publish.assert_called_once() + assert ( + caplog.records[-1].message + == "Actuator Agent Error: {'type': 'AttributeError', " + "'value': \"'ActuatorAgent' object has no attribute 'driver_vip_identity'\"}" + ) + + +@pytest.mark.actuator_unit +@pytest.mark.parametrize( + "sender, device_state", + [ + ( + SENDER, + {"somepath": DeviceState("sender-1", "task-id-1", "anytime")}, + ), + ( + "pubsub.compat", + {"somepath": DeviceState("pubsub.compat", "task-id-1", "anytime")}, + ), + ], +) +def test_handle_set_should_succeed(sender, device_state): + with get_actuator_agent(vip_rpc_call_res=MockedAsyncResult({"foo": "bar"}), device_state=device_state) as actuator_agent: + + actuator_agent.handle_set(PEER, sender, BUS, SET_TOPIC, HEADERS, MESSAGE) + + actuator_agent.vip.rpc.call.assert_called_once() + actuator_agent.vip.pubsub.publish.assert_called() + + +@pytest.mark.actuator_unit +def test_handle_set_should_return_none_on_none_message(caplog): + with get_actuator_agent(vip_identity=None) as actuator_agent: + result = actuator_agent.handle_set(PEER, SENDER, BUS, SET_TOPIC, HEADERS, None) + + assert result is None + actuator_agent.vip.pubsub.publish.assert_called_once() + actuator_agent.vip.rpc.call.assert_not_called() + assert ( + caplog.records[-1].message + == "ValueError: {'type': 'ValueError', 'value': 'missing argument'}" + ) + + +@pytest.mark.actuator_unit +def test_handle_set_should_handle_type_error_on_invalid_sender(caplog): + with get_actuator_agent(vip_identity=None) as actuator_agent: + actuator_agent.handle_set(PEER, None, BUS, SET_TOPIC, HEADERS, MESSAGE) + + actuator_agent.vip.rpc.call.assert_not_called() + actuator_agent.vip.pubsub.publish.assert_called_once() + assert ( + caplog.records[-1].message == "Actuator Agent Error: {'type': 'TypeError', " + "'value': 'Agent id must be a nonempty string'}" + ) + + +@pytest.mark.actuator_unit +def test_handle_set_should_handle_lock_error(caplog): + with get_actuator_agent(vip_identity=None) as actuator_agent: + actuator_agent.handle_set(PEER, SENDER, BUS, SET_TOPIC, HEADERS, MESSAGE) + + actuator_agent.vip.rpc.call.assert_not_called() + actuator_agent.vip.pubsub.publish.assert_called_once() + assert ( + caplog.records[-1].message == "Actuator Agent Error: {'type': 'LockError', " + "'value': 'caller (sender-1) does not have this lock'}" + ) + + +@pytest.mark.actuator_unit +def test_handle_revert_point_should_succeed(): + device_state = { + "actuators/revert/point/somedevicepath": DeviceState( + "sender-1", "task-id-1", "anytime" + ) + } + + with get_actuator_agent(device_state=device_state, vip_rpc_call_res=MockedAsyncResult({"foo": "bar"})) as actuator_agent: + actuator_agent.handle_revert_point( + PEER, SENDER, BUS, REVERT_POINT_TOPIC, HEADERS, MESSAGE + ) + + actuator_agent.vip.rpc.call.assert_called_once() + actuator_agent.vip.pubsub.publish.assert_called_once() + + +@pytest.mark.actuator_unit +def test_handle_revert_point_should_handle_lock_error(caplog): + with get_actuator_agent(vip_identity=None) as actuator_agent: + actuator_agent.handle_revert_point( + PEER, SENDER, BUS, REVERT_POINT_TOPIC, HEADERS, MESSAGE + ) + + actuator_agent.vip.rpc.call.assert_not_called() + actuator_agent.vip.pubsub.publish.assert_called_once() + assert ( + caplog.records[-1].message == "Actuator Agent Error: {'type': 'LockError', " + "'value': 'caller does not have this lock'}" + ) + + +@pytest.mark.actuator_unit +def test_handle_revert_device_should_succeed(): + device_state = { + "somedevicepath": DeviceState("sender-1", "task-id-1", "anytime") + } + + with get_actuator_agent(device_state=device_state, + vip_rpc_call_res=MockedAsyncResult({"foo": "bar"})) as actuator_agent: + actuator_agent.handle_revert_device( + PEER, SENDER, BUS, REVERT_DEVICE_TOPIC, HEADERS, MESSAGE + ) + + actuator_agent.vip.rpc.call.assert_called_once() + actuator_agent.vip.pubsub.publish.assert_called_once() + + +@pytest.mark.actuator_unit +def test_handle_revert_device_should_handle_lock_error(caplog): + with get_actuator_agent(vip_identity=None) as actuator_agent: + actuator_agent.handle_revert_device( + PEER, SENDER, BUS, REVERT_DEVICE_TOPIC, HEADERS, MESSAGE + ) + + actuator_agent.vip.rpc.call.assert_not_called() + actuator_agent.vip.pubsub.publish.assert_called_once() + assert ( + caplog.records[-1].message == "Actuator Agent Error: {'type': 'LockError', " + "'value': 'caller does not have this lock'}" + ) + + +@pytest.mark.actuator_unit +@pytest.mark.parametrize( + "priority, success", + [ + ("HIGH", True), + ("LOW", True), + ("LOW_PREEMPT", True), + ("HIGH", False), + ("LOW", False), + ("LOW_PREEMPT", False), + ], +) +def test_handle_schedule_request_should_succeed_on_new_schedule_request_type( + priority, success +): + headers = { + "type": "NEW_SCHEDULE", + "requesterID": "id-123", + "taskID": "12345", + "priority": priority, + } + + with get_actuator_agent(slot_requests_res=RequestResult(success, {}, "")) as actuator_agent: + actuator_agent.handle_schedule_request( + PEER, SENDER, BUS, REQUEST_TOPIC, headers, create_message() + ) + + actuator_agent.vip.pubsub.publish.assert_called() + + +@pytest.mark.actuator_unit +@pytest.mark.parametrize("success", [True, False]) +def test_handle_schedule_request_should_succeed_on_cancel_schedule_request_type(success): + headers = {"type": "CANCEL_SCHEDULE", "requesterID": "id-123", "taskID": "12345"} + + with get_actuator_agent(slot_requests_res=RequestResult(success, {}, "")) as actuator_agent: + actuator_agent.handle_schedule_request( + PEER, SENDER, BUS, REQUEST_TOPIC, headers, create_message() + ) + + actuator_agent.vip.pubsub.publish.assert_called() + + +@pytest.mark.actuator_unit +@pytest.mark.parametrize("invalid_request_type", ["bad request type", None]) +def test_handle_schedule_request_should_log_invalid_request_type( + invalid_request_type, caplog +): + headers = { + "type": invalid_request_type, + "requesterID": "id-123", + "taskID": "12345", + "priority": "HIGH", + } + + with get_actuator_agent(vip_identity=None) as actuator_agent: + actuator_agent.handle_schedule_request( + PEER, SENDER, BUS, REQUEST_TOPIC, headers, create_message() + ) + + actuator_agent.vip.pubsub.publish.assert_called() + assert caplog.records[-1].message == "handle-schedule_request, invalid request type" + + +def create_message(): + start = str(datetime.now(tz=tzutc()) + timedelta(seconds=10)) + end = str(datetime.now(tz=tzutc()) + timedelta(seconds=20)) + return ["campus/building/device1", start, end] diff --git a/services/core/ActuatorAgent/tests/test_actuator_rpc_unit.py b/services/core/ActuatorAgent/tests/test_actuator_rpc_unit.py new file mode 100644 index 0000000000..3a33ee4654 --- /dev/null +++ b/services/core/ActuatorAgent/tests/test_actuator_rpc_unit.py @@ -0,0 +1,398 @@ +# -*- coding: utf-8 -*- {{{ +# vim: set fenc=utf-8 ft=python sw=4 ts=4 sts=4 et: +# +# Copyright 2019, Battelle Memorial Institute. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# This material was prepared as an account of work sponsored by an agency of +# the United States Government. Neither the United States Government nor the +# United States Department of Energy, nor Battelle, nor any of their +# employees, nor any jurisdiction or organization that has cooperated in the +# development of these materials, makes any warranty, express or +# implied, or assumes any legal liability or responsibility for the accuracy, +# completeness, or usefulness or any information, apparatus, product, +# software, or process disclosed, or represents that its use would not infringe +# privately owned rights. Reference herein to any specific commercial product, +# process, or service by trade name, trademark, manufacturer, or otherwise +# does not necessarily constitute or imply its endorsement, recommendation, or +# favoring by the United States Government or any agency thereof, or +# Battelle Memorial Institute. The views and opinions of authors expressed +# herein do not necessarily state or reflect those of the +# United States Government or any agency thereof. +# +# PACIFIC NORTHWEST NATIONAL LABORATORY operated by +# BATTELLE for the UNITED STATES DEPARTMENT OF ENERGY +# under Contract DE-AC05-76RL01830 +# }}} + +""" +Unit test cases for testing actuator agent using rpc calls. +""" +import logging +from datetime import datetime, timedelta + +import pytest + +from services.core.ActuatorAgent.actuator import agent +from services.core.ActuatorAgent.actuator.agent import ActuatorAgent, LockError +from services.core.ActuatorAgent.actuator.scheduler import RequestResult, DeviceState +from services.core.ActuatorAgent.tests.actuator_fixtures import MockedAsyncResult, \ + get_actuator_agent +from volttrontesting.utils.utils import AgentMock +from volttron.platform.vip.agent import Agent + + +PRIORITY_LOW = "LOW" +SUCCESS = "SUCCESS" +FAILURE = "FAILURE" +REQUESTER_ID = "foo" +TASK_ID = "task-id" +TIME_SLOT_REQUESTS = [ + ["fakedriver0", str(datetime.now()), str(datetime.now() + timedelta(seconds=1))] +] + +agent._log = logging.getLogger("test_logger") +ActuatorAgent.__bases__ = (AgentMock.imitate(Agent, Agent()),) + + +@pytest.mark.actuator_unit +@pytest.mark.parametrize("topic, point", [("path/topic", None), ("another/path/to/topic", 42)]) +def test_get_point_should_succeed(topic, point): + with get_actuator_agent(vip_rpc_call_res=MockedAsyncResult(10.0)) as actuator_agent: + result = actuator_agent.get_point(topic, point=point) + + actuator_agent.vip.rpc.call.assert_called_once() + assert result is not None + + +@pytest.mark.actuator_unit +@pytest.mark.parametrize( + "point, device_state", + [ + ( + 42, + {"foo/bar": DeviceState("requester-id-1", "task-id-1", "anytime")}, + ), + ( + None, + {"foo": DeviceState("requester-id-1", "task-id-1", "anytime")}), + ], +) +def test_set_point_should_succeed(point, device_state): + requester_id = "requester-id-1" + topic = "foo/bar" + value = "some value" + + with get_actuator_agent(vip_message_peer=requester_id, device_state=device_state) as \ + actuator_agent: + result = actuator_agent.set_point(requester_id, topic, value, point=point) + + assert result is not None + + +@pytest.mark.actuator_unit +@pytest.mark.parametrize("rpc_peer", [None, 42, []]) +def test_set_point_should_raise_type_error(rpc_peer): + with pytest.raises(TypeError, match="Agent id must be a nonempty string"): + requester_id = "requester-id-1" + topic = "foo/bar" + value = "some value" + point = None + + with get_actuator_agent(vip_message_peer=rpc_peer) as actuator_agent: + actuator_agent.set_point(requester_id, topic, value, point=point) + + +@pytest.mark.actuator_unit +def test_set_point_should_raise_lock_error_on_non_matching_device(): + with pytest.raises(LockError): + requester_id = "requester-id-1" + topic = "foo/bar" + value = "some value" + + with get_actuator_agent(vip_message_peer="some rpc_peer") as actuator_agent: + actuator_agent.set_point(requester_id, topic, value) + + +@pytest.mark.actuator_unit +def test_scrape_all_should_succeed(): + with get_actuator_agent(vip_rpc_call_res=MockedAsyncResult({})) as actuator_agent: + topic = "whan/that/aprille" + + result = actuator_agent.scrape_all(topic) + + assert isinstance(result, dict) + + + +@pytest.mark.actuator_unit +@pytest.mark.parametrize( + "topics", + [ + ["foo/bar"], + ["foo/bar", "sna/foo"], + [["dev1", "point1"]], + [["dev1", "point1"], ["dev2", "point2"]], + ], +) +def test_get_multiple_points_should_succeed(topics): + mocked_rpc_call_res = MockedAsyncResult(({"result": "value"}, {})) + with get_actuator_agent(vip_rpc_call_res=mocked_rpc_call_res) as actuator_agent: + results, errors = actuator_agent.get_multiple_points(topics) + + assert isinstance(results, dict) + assert isinstance(errors, dict) + assert len(errors) == 0 + + +@pytest.mark.actuator_unit +@pytest.mark.parametrize("invalid_topics", [[(123,)], [(None)], [[123]], [[None]]]) +def test_get_multiple_points_should_return_errors(invalid_topics): + with get_actuator_agent() as actuator_agent: + + results, errors = actuator_agent.get_multiple_points(invalid_topics) + + assert isinstance(results, dict) + assert isinstance(errors, dict) + assert len(errors) == 1 + + +@pytest.mark.actuator_unit +@pytest.mark.parametrize( + "topic_values, device_state", + [ + ([], {}), + ( + [("foo/bar", "roma_value")], + {"foo": DeviceState("requester-id-1", "task-id-1", "anytime")}, + ), + ( + [("foo/bar", "roma_value"), ("sna/fu", "amor_value")], + { + "foo": DeviceState("requester-id-1", "task-id-1", "anytime"), + "sna": DeviceState("requester-id-1", "task-id-1", "anytime"), + }, + ), + ], +) +@pytest.mark.actuator_unit +def test_set_multiple_points_should_succeed(topic_values, device_state): + requester_id = "requester-id-1" + mocked_rpc_call_res = MockedAsyncResult(({})) + with get_actuator_agent(vip_message_peer=requester_id, device_state=device_state, + vip_rpc_call_res=mocked_rpc_call_res) as actuator_agent: + result = actuator_agent.set_multiple_points("request-id-1", topic_values) + + assert result == {} + + +@pytest.mark.actuator_unit +@pytest.mark.parametrize("invalid_topic_values", [[(None,)], [(1234,)]]) +def test_set_multiple_points_should_raise_value_error(invalid_topic_values): + with pytest.raises(ValueError): + requester_id = "requester-id-1" + + with get_actuator_agent(vip_message_peer=requester_id) as actuator_agent: + actuator_agent.set_multiple_points("request-id-1", invalid_topic_values) + + +@pytest.mark.actuator_unit +def test_set_multiple_points_should_raise_lock_error_on_empty_devices(): + with pytest.raises(LockError): + requester_id = "requester-id-1" + topic_values = [("foo/bar", "roma_value")] + + with get_actuator_agent(vip_message_peer=requester_id) as actuator_agent: + actuator_agent.set_multiple_points("request-id-1", topic_values) + + +@pytest.mark.actuator_unit +def test_set_multiple_points_should_raise_lock_error_on_non_matching_requester(): + with pytest.raises(LockError): + requester_id = "wrong-requester" + topic_values = [("foo/bar", "roma_value")] + device_state = { + "foo": DeviceState("requester-id-1", "task-id-1", "anytime") + } + + with get_actuator_agent(vip_message_peer=requester_id, device_state=device_state) \ + as actuator_agent: + actuator_agent.set_multiple_points("request-id-1", topic_values) + + +@pytest.mark.actuator_unit +@pytest.mark.parametrize("point", [None, "foobarpoint"]) +def test_revert_point_should_raise_lock_error_on_empty_devices(point): + with pytest.raises(LockError): + requester_id = "request-id-1" + topic = "foo/bar" + + with get_actuator_agent(vip_message_peer="requester-id-1") as actuator_agent: + actuator_agent.revert_point(requester_id, topic, point=point) + + +@pytest.mark.actuator_unit +@pytest.mark.parametrize("point", [None, "foobarpoint"]) +def test_revert_point_should_raise_lock_error_on_non_matching_requester(point): + with pytest.raises(LockError): + device_state = { + "foo": DeviceState("requester-id-1", "task-id-1", "anytime") + } + requester_id = "request-id-1" + topic = "foo/bar" + + with get_actuator_agent(vip_message_peer="wrong-requester", device_state=device_state) \ + as actuator_agent: + actuator_agent.revert_point(requester_id, topic, point=point) + + +@pytest.mark.actuator_unit +def test_revert_device_should_raise_lock_error_on_empty_devices(): + with pytest.raises(LockError): + requester_id = "request-id-1" + topic = "foo/bar" + + with get_actuator_agent(vip_message_peer="requester-id-1") as actuator_agent: + actuator_agent.revert_device(requester_id, topic) + + +@pytest.mark.actuator_unit +def test_revert_device_should_raise_lock_error_on_non_matching_requester(): + with pytest.raises(LockError): + device_state = { + "foo/bar": DeviceState("requester-id-1", "task-id-1", "anytime") + } + requester_id = "request-id-1" + topic = "foo/bar" + + with get_actuator_agent(vip_message_peer="wrong-requester", device_state=device_state) \ + as actuator_agent: + actuator_agent.revert_device(requester_id, topic) + + +@pytest.mark.actuator_unit +def test_request_new_schedule_should_succeed(): + with get_actuator_agent() as actuator_agent: + result = actuator_agent.request_new_schedule(REQUESTER_ID, TASK_ID, + PRIORITY_LOW, TIME_SLOT_REQUESTS) + + assert result["result"] == SUCCESS + + +@pytest.mark.actuator_unit +def test_request_new_schedule_should_succeed_when_stop_start_times_overlap(): + start = str(datetime.now()) + end = str(datetime.now() + timedelta(seconds=1)) + end2 = str(datetime.now() + timedelta(seconds=2)) + time_slot_requests = [["fakedriver0", start, end], ["fakedriver0", end, end2]] + + with get_actuator_agent() as actuator_agent: + result = actuator_agent.request_new_schedule(REQUESTER_ID, TASK_ID, + PRIORITY_LOW, time_slot_requests) + + assert result["result"] == SUCCESS + + +@pytest.mark.actuator_unit +@pytest.mark.parametrize( + "task_id, expected_info", + [ + (1234, "MALFORMED_REQUEST: TypeError: taskid must be a nonempty string"), + ("", "MALFORMED_REQUEST: TypeError: taskid must be a nonempty string"), + (None, "MISSING_TASK_ID"), + ("task-id-duplicate", "TASK_ID_ALREADY_EXISTS"), + ], +) +def test_request_new_schedule_should_fail_on_invalid_taskid(task_id, expected_info): + false_request_result = RequestResult(False, {}, expected_info) + + with get_actuator_agent(slot_requests_res=false_request_result) as actuator_agent: + result = actuator_agent.request_new_schedule(REQUESTER_ID, task_id, + PRIORITY_LOW, TIME_SLOT_REQUESTS) + + assert result["result"] == FAILURE + assert result["info"] == expected_info + + +@pytest.mark.actuator_unit +@pytest.mark.parametrize( + "invalid_priority, expected_info", + [("LOW2", "INVALID_PRIORITY"), (None, "MISSING_PRIORITY")], +) +def test_request_new_schedule_should_fail_on_invalid_priority(invalid_priority, expected_info): + false_request_result = RequestResult(False, {}, expected_info) + + with get_actuator_agent(slot_requests_res=false_request_result) as actuator_agent: + result = actuator_agent.request_new_schedule(REQUESTER_ID, TASK_ID, + invalid_priority, TIME_SLOT_REQUESTS) + + assert result["result"] == FAILURE + assert result["info"] == expected_info + + +@pytest.mark.actuator_unit +@pytest.mark.parametrize( + "time_slot_request, expected_info", + [ + ( + [], + "MALFORMED_REQUEST_EMPTY"), + ( + [["fakedriver0", str(datetime.now()), ""]], + "MALFORMED_REQUEST: ParserError: String does not contain a date: ", + ), + ( + [["fakedriver0", str(datetime.now())]], + "MALFORMED_REQUEST: ValueError: " + "not enough values to unpack (expected 3, got 2)", + ), + ], +) +def test_request_new_schedule_should_fail_invalid_time_slot_requests(time_slot_request, + expected_info): + false_request_result = RequestResult(False, {}, expected_info) + + with get_actuator_agent(slot_requests_res=false_request_result) as actuator_agent: + result = actuator_agent.request_new_schedule( + REQUESTER_ID, TASK_ID, PRIORITY_LOW, time_slot_request + ) + + assert result["result"] == FAILURE + assert result["info"] == expected_info + + +@pytest.mark.actuator_unit +def test_request_cancel_schedule_should_succeed_happy_path(): + true_request_result = RequestResult( + True, {}, "" + ) + + with get_actuator_agent(cancel_schedule_result=true_request_result) as actuator_agent: + result = actuator_agent.request_cancel_schedule(REQUESTER_ID, TASK_ID) + + assert result["result"] == SUCCESS + + +@pytest.mark.actuator_unit +def test_request_cancel_schedule_should_fail_on_invalid_task_id(): + false_request_result = RequestResult( + False, {}, "TASK_ID_DOES_NOT_EXIST" + ) + invalid_task_id = "invalid-task-id" + + with get_actuator_agent(cancel_schedule_result=false_request_result) as actuator_agent: + result = actuator_agent.request_cancel_schedule(REQUESTER_ID, invalid_task_id) + + assert result["result"] == FAILURE + assert result["info"] == "TASK_ID_DOES_NOT_EXIST"