From 2e991fa568128ac46babda135e67280d99b46ce2 Mon Sep 17 00:00:00 2001 From: "sam.wedge" Date: Fri, 12 Feb 2021 23:32:32 +0000 Subject: [PATCH] Ensure sketch is compatible with version of Rapiduino --- rapiduino/boards/arduino.py | 17 ++++++ rapiduino/exceptions.py | 23 +++++++- tests/test_boards/test_arduino.py | 92 ++++++++++++++++++++++++++---- tests/test_components/test_base.py | 17 +++++- 4 files changed, 135 insertions(+), 14 deletions(-) diff --git a/rapiduino/boards/arduino.py b/rapiduino/boards/arduino.py index 7f68c2e..1d67cfe 100644 --- a/rapiduino/boards/arduino.py +++ b/rapiduino/boards/arduino.py @@ -13,6 +13,7 @@ ) from rapiduino.communication.serial import SerialConnection from rapiduino.exceptions import ( + ArduinoSketchVersionIncompatibleError, ComponentAlreadyRegisteredError, NotAnalogPinError, NotPwmPinError, @@ -33,6 +34,9 @@ class Arduino: + + min_version = (0, 1, 0) + def __init__( self, pins: Tuple[Pin, ...], @@ -45,6 +49,7 @@ def __init__( self.connection = conn_class.build(port) self.pin_register: Dict[int, str] = {} self.reserved_pin_nums = (rx_pin, tx_pin) + self._assert_compatible_sketch_version() @classmethod def uno( @@ -138,6 +143,18 @@ def deregister_component(self, component_token: str) -> None: for key in keys_to_delete: del self.pin_register[key] + def _assert_compatible_sketch_version(self) -> None: + version = self.version() + if any( + ( + version[0] > self.min_version[0], + version[0] < self.min_version[0], + version[1] < self.min_version[1], + version[2] < self.min_version[2], + ) + ): + raise ArduinoSketchVersionIncompatibleError(version, self.min_version) + def _assert_requested_pins_are_valid( self, component_token: str, pins: Tuple[Pin, ...] ) -> None: diff --git a/rapiduino/exceptions.py b/rapiduino/exceptions.py index dbcb047..2c5aa8f 100644 --- a/rapiduino/exceptions.py +++ b/rapiduino/exceptions.py @@ -1,4 +1,6 @@ -from typing import Optional +from typing import Optional, Tuple + +import rapiduino class SerialConnectionSendDataError(Exception): @@ -81,3 +83,22 @@ class ComponentAlreadyRegisteredWithArduinoError(Exception): def __init__(self) -> None: message = "Device is already registered to an Arduino" super().__init__(message) + + +class ArduinoSketchVersionIncompatibleError(Exception): + def __init__( + self, sketch_version: Tuple[int, ...], min_version: Tuple[int, int, int] + ) -> None: + sketch_version_str = ( + f"{sketch_version[0]}.{sketch_version[1]}.{sketch_version[2]}" + ) + min_version_str = f"{min_version[0]}.{min_version[1]}.{min_version[2]}" + max_version_str = f"{min_version[0] + 1}.0.0" + + message = ( + f"Arduino sketch version {sketch_version_str} is incompatible with" + f" Rapiduino version {rapiduino.__version__}.\n" + "Please upload a compatible sketch version:" + f" Greater or equal to {min_version_str}, less than {max_version_str}" + ) + super().__init__(message) diff --git a/tests/test_boards/test_arduino.py b/tests/test_boards/test_arduino.py index 3244eb4..8a43cba 100644 --- a/tests/test_boards/test_arduino.py +++ b/tests/test_boards/test_arduino.py @@ -21,6 +21,7 @@ ) from rapiduino.communication.serial import SerialConnection from rapiduino.exceptions import ( + ArduinoSketchVersionIncompatibleError, ComponentAlreadyRegisteredError, NotAnalogPinError, NotPwmPinError, @@ -32,8 +33,7 @@ from rapiduino.globals.common import HIGH, INPUT, LOW, OUTPUT -@pytest.fixture -def test_arduino() -> Arduino: +def get_mock_conn_class() -> Mock: def dummy_process_command(command: CommandSpec, *args: int) -> Tuple[int, ...]: data: Tuple[int, ...] if command == CMD_POLL: @@ -41,7 +41,7 @@ def dummy_process_command(command: CommandSpec, *args: int) -> Tuple[int, ...]: elif command == CMD_PARROT: data = (args[0],) elif command == CMD_VERSION: - data = (1, 2, 3) + data = Arduino.min_version elif command == CMD_PINMODE: data = () elif command == CMD_DIGITALREAD: @@ -56,6 +56,16 @@ def dummy_process_command(command: CommandSpec, *args: int) -> Tuple[int, ...]: raise ValueError(f"Mock Arduino does not know how to process {CommandSpec}") return data + mock_conn_class = Mock(spec=SerialConnection) + mock_conn_class.build.return_value.process_command.side_effect = ( + dummy_process_command + ) + + return mock_conn_class + + +@pytest.fixture +def test_arduino() -> Arduino: pins = ( Pin(0), Pin(1, is_analog=True), @@ -64,9 +74,61 @@ def dummy_process_command(command: CommandSpec, *args: int) -> Tuple[int, ...]: Pin(4), Pin(5), ) + return Arduino( + pins=pins, port="", conn_class=get_mock_conn_class(), rx_pin=4, tx_pin=5 + ) + + +def test_if_sketch_version_is_major_change_below_minimum_required_version() -> None: + conn_class = Mock(spec=SerialConnection) + v = Arduino.min_version + sketch_version = (v[0] - 1, v[1], v[2]) + conn_class.build.return_value.process_command.return_value = sketch_version + with pytest.raises(ArduinoSketchVersionIncompatibleError): + Arduino(pins=(), port="", conn_class=conn_class) + + +def test_if_sketch_version_is_minor_change_below_minimum_required_version() -> None: + conn_class = Mock(spec=SerialConnection) + v = Arduino.min_version + sketch_version = (v[0], v[1] - 1, v[2]) + conn_class.build.return_value.process_command.return_value = sketch_version + with pytest.raises(ArduinoSketchVersionIncompatibleError): + Arduino(pins=(), port="", conn_class=conn_class) + + +def test_if_sketch_version_is_micro_change_below_minimum_required_version() -> None: + conn_class = Mock(spec=SerialConnection) + v = Arduino.min_version + sketch_version = (v[0], v[1], v[2] - 1) + conn_class.build.return_value.process_command.return_value = sketch_version + with pytest.raises(ArduinoSketchVersionIncompatibleError): + Arduino(pins=(), port="", conn_class=conn_class) + + +def test_if_sketch_version_is_a_major_change_above_required_version() -> None: + conn_class = Mock(spec=SerialConnection) + v = Arduino.min_version + sketch_version = (v[0] + 1, v[1], v[2]) + conn_class.build.return_value.process_command.return_value = sketch_version + with pytest.raises(ArduinoSketchVersionIncompatibleError): + Arduino(pins=(), port="", conn_class=conn_class) + + +def test_if_sketch_version_is_a_minor_change_above_required_version() -> None: + conn_class = Mock(spec=SerialConnection) + v = Arduino.min_version + sketch_version = (v[0], v[1] + 1, v[2]) + conn_class.build.return_value.process_command.return_value = sketch_version + Arduino(pins=(), port="", conn_class=conn_class) + + +def test_if_sketch_version_is_a_micro_change_above_required_version() -> None: conn_class = Mock(spec=SerialConnection) - conn_class.build.return_value.process_command.side_effect = dummy_process_command - return Arduino(pins=pins, port="", conn_class=conn_class, rx_pin=4, tx_pin=5) + v = Arduino.min_version + sketch_version = (v[0], v[1], v[2] + 1) + conn_class.build.return_value.process_command.return_value = sketch_version + Arduino(pins=(), port="", conn_class=conn_class) def test_poll(test_arduino: Arduino) -> None: @@ -79,7 +141,7 @@ def test_parrot(test_arduino: Arduino) -> None: def test_version(test_arduino: Arduino) -> None: - assert test_arduino.version() == (1, 2, 3) + assert test_arduino.version() == Arduino.min_version def test_pin_mode_with_valid_args(test_arduino: Arduino) -> None: @@ -207,9 +269,15 @@ def test_mega_pin_ids_are_sequential() -> None: @pytest.mark.parametrize( "arduino,expected_pins", [ - pytest.param(Arduino.uno(port="", conn_class=Mock()), get_uno_pins()), - pytest.param(Arduino.nano(port="", conn_class=Mock()), get_nano_pins()), - pytest.param(Arduino.mega(port="", conn_class=Mock()), get_mega_pins()), + pytest.param( + Arduino.uno(port="", conn_class=get_mock_conn_class()), get_uno_pins() + ), + pytest.param( + Arduino.nano(port="", conn_class=get_mock_conn_class()), get_nano_pins() + ), + pytest.param( + Arduino.mega(port="", conn_class=get_mock_conn_class()), get_mega_pins() + ), ], ) def test_classmethods_set_correct_pins( @@ -253,9 +321,9 @@ def test_all_analog_pins_have_an_alias( @pytest.mark.parametrize( "arduino", [ - pytest.param(Arduino.uno(port="", conn_class=Mock())), - pytest.param(Arduino.nano(port="", conn_class=Mock())), - pytest.param(Arduino.mega(port="", conn_class=Mock())), + pytest.param(Arduino.uno(port="", conn_class=get_mock_conn_class())), + pytest.param(Arduino.nano(port="", conn_class=get_mock_conn_class())), + pytest.param(Arduino.mega(port="", conn_class=get_mock_conn_class())), ], ) def test_serial_comms_pins_cannot_be_used(arduino: Arduino) -> None: diff --git a/tests/test_components/test_base.py b/tests/test_components/test_base.py index 77da1ce..5976018 100644 --- a/tests/test_components/test_base.py +++ b/tests/test_components/test_base.py @@ -1,3 +1,4 @@ +from typing import Tuple from unittest.mock import Mock, call import pytest @@ -10,6 +11,8 @@ CMD_DIGITALREAD, CMD_DIGITALWRITE, CMD_PINMODE, + CMD_VERSION, + CommandSpec, ) from rapiduino.communication.serial import SerialConnection from rapiduino.components.base_component import BaseComponent @@ -27,8 +30,14 @@ @pytest.fixture def serial() -> Mock: + def mock_process_command(command: CommandSpec, *args: int) -> Tuple[int, ...]: + if command == CMD_VERSION: + return Arduino.min_version + else: + return tuple([1]) + mock = Mock(spec=SerialConnection) - mock.build.return_value.process_command.return_value = (1,) + mock.build.return_value.process_command.side_effect = mock_process_command return mock @@ -84,6 +93,7 @@ def test_component_connect_runs_setup( calls = serial.build.return_value.process_command.call_args_list expected_calls = [ + call(CMD_VERSION), call(CMD_PINMODE, DIGITAL_PIN_NUM, INPUT.value), call(CMD_DIGITALREAD, DIGITAL_PIN_NUM), call(CMD_DIGITALWRITE, DIGITAL_PIN_NUM, HIGH.value), @@ -133,6 +143,7 @@ def test_pin_mode_if_connected( calls = serial.build.return_value.process_command.call_args_list expected_calls = [ + call(CMD_VERSION), call(CMD_PINMODE, DIGITAL_PIN_NUM, INPUT.value), call(CMD_DIGITALREAD, DIGITAL_PIN_NUM), call(CMD_DIGITALWRITE, DIGITAL_PIN_NUM, HIGH.value), @@ -151,6 +162,7 @@ def test_digital_read_if_connected( calls = serial.build.return_value.process_command.call_args_list expected_calls = [ + call(CMD_VERSION), call(CMD_PINMODE, DIGITAL_PIN_NUM, INPUT.value), call(CMD_DIGITALREAD, DIGITAL_PIN_NUM), call(CMD_DIGITALWRITE, DIGITAL_PIN_NUM, HIGH.value), @@ -169,6 +181,7 @@ def test_digital_write_if_connected( calls = serial.build.return_value.process_command.call_args_list expected_calls = [ + call(CMD_VERSION), call(CMD_PINMODE, DIGITAL_PIN_NUM, INPUT.value), call(CMD_DIGITALREAD, DIGITAL_PIN_NUM), call(CMD_DIGITALWRITE, DIGITAL_PIN_NUM, HIGH.value), @@ -187,6 +200,7 @@ def test_analog_read_if_connected( calls = serial.build.return_value.process_command.call_args_list expected_calls = [ + call(CMD_VERSION), call(CMD_PINMODE, DIGITAL_PIN_NUM, INPUT.value), call(CMD_DIGITALREAD, DIGITAL_PIN_NUM), call(CMD_DIGITALWRITE, DIGITAL_PIN_NUM, HIGH.value), @@ -205,6 +219,7 @@ def test_analog_write_if_connected( calls = serial.build.return_value.process_command.call_args_list expected_calls = [ + call(CMD_VERSION), call(CMD_PINMODE, DIGITAL_PIN_NUM, INPUT.value), call(CMD_DIGITALREAD, DIGITAL_PIN_NUM), call(CMD_DIGITALWRITE, DIGITAL_PIN_NUM, HIGH.value),