harperreed / serial_proxy

MIT License
0 stars 0 forks source link

sweep: can you refactor, and add some tests #2

Open harperreed opened 1 month ago

harperreed commented 1 month ago

Refactor SerialProxy class and add unit tests

Description:

Refactor the SerialProxy class in src/serial_proxy/serial_proxy.py to improve code organization and testability. Add comprehensive unit tests to ensure the functionality of the class.

Tasks:

Test:

In src/serial_proxy/tests/test_serial_proxy.py, add the following test cases:

Additional tasks:

sweep-ai[bot] commented 1 month ago

🚀 Here's the PR! #3

Sweeping

Search Results

Response

Let's implement the proposed solution to refactor the SerialProxy class and add unit tests. We'll start by refactoring the src/serial_proxy/serial_proxy.py file:

src/serial_proxy/serial_proxy.py

--- 
+++ 
@@ -1,70 +1,60 @@
 import serial
-import json
-from datetime import datetime
+from typing import Union, List, Optional
+from .logger import Logger

 class SerialProxy:
-    def __init__(self, port, baudrate, log_file='serial_log.json'):
+    def __init__(self, port: str, baudrate: int, log_file: str = 'serial_log.json'):
         self.serial = serial.Serial(port, baudrate)
-        self.log_file = log_file
+        self.logger = Logger(log_file)

-    def write(self, data):
-        self.log("WRITE", data)
+    def write(self, data: Union[str, bytes]) -> int:
+        self.logger.log("WRITE", data)
         return self.serial.write(data)

-    def read(self, size=1):
+    def read(self, size: int = 1) -> bytes:
         data = self.serial.read(size)
-        self.log("READ", data)
+        self.logger.log("READ", data)
         return data

-    def readline(self):
+    def readline(self) -> bytes:
         data = self.serial.readline()
-        self.log("READLINE", data)
+        self.logger.log("READLINE", data)
         return data

-    def readlines(self):
+    def readlines(self) -> List[bytes]:
         data = self.serial.readlines()
-        self.log("READLINES", data)
+        self.logger.log("READLINES", data)
         return data

-    def in_waiting(self):
+    def in_waiting(self) -> int:
         return self.serial.in_waiting

-    def flush(self):
-        self.log("FLUSH", "")
+    def flush(self) -> None:
+        self.logger.log("FLUSH", "")
         return self.serial.flush()

-    def flushInput(self):
-        self.log("FLUSH_INPUT", "")
+    def flushInput(self) -> None:
+        self.logger.log("FLUSH_INPUT", "")
         return self.serial.flushInput()

-    def flushOutput(self):
-        self.log("FLUSH_OUTPUT", "")
+    def flushOutput(self) -> None:
+        self.logger.log("FLUSH_OUTPUT", "")
         return self.serial.flushOutput()

-    def close(self):
-        self.log("CLOSE", "")
+    def close(self) -> None:
+        self.logger.log("CLOSE", "")
         return self.serial.close()

-    def open(self):
-        self.log("OPEN", "")
+    def open(self) -> None:
+        self.logger.log("OPEN", "")
         return self.serial.open()

-    def __enter__(self):
+    def __enter__(self) -> 'SerialProxy':
         return self

-    def __exit__(self, exc_type, exc_val, exc_tb):
+    def __exit__(self, exc_type: Optional[type], exc_val: Optional[Exception], exc_tb: Optional[object]) -> None:
         self.close()

     @property
-    def is_open(self):
+    def is_open(self) -> bool:
         return self.serial.is_open
-
-    def log(self, operation, data):
-        log_entry = {
-            "timestamp": datetime.now().isoformat(),
-            "operation": operation,
-            "data": data.hex() if isinstance(data, bytes) else str(data)
-        }
-        with open(self.log_file, 'a') as f:
-            json.dump(log_entry, f)
-            f.write('\n')  # Add a newline for readability and to separate entries

Now, let's create a new file for the Logger class:

src/serial_proxy/logger.py

--- 
+++ 
@@ -1 +1,25 @@
+import json
+from datetime import datetime
+from typing import Union

+class Logger:
+    def __init__(self, log_file: str):
+        self.log_file = log_file
+
+    def log(self, operation: str, data: Union[str, bytes, List[bytes]]) -> None:
+        log_entry = {
+            "timestamp": datetime.now().isoformat(),
+            "operation": operation,
+            "data": self._format_data(data)
+        }
+        with open(self.log_file, 'a') as f:
+            json.dump(log_entry, f)
+            f.write('\n')  # Add a newline for readability and to separate entries
+
+    def _format_data(self, data: Union[str, bytes, List[bytes]]) -> str:
+        if isinstance(data, bytes):
+            return data.hex()
+        elif isinstance(data, list):
+            return [self._format_data(item) for item in data]
+        else:
+            return str(data)

Now, let's create the unit tests file:

src/serial_proxy/tests/test_serial_proxy.py

--- 
+++ 
@@ -1 +1,89 @@
+import pytest
+from unittest.mock import Mock, patch
+from serial_proxy.serial_proxy import SerialProxy

+@pytest.fixture
+def mock_serial():
+    with patch('serial_proxy.serial_proxy.serial.Serial') as mock:
+        yield mock
+
+@pytest.fixture
+def mock_logger():
+    with patch('serial_proxy.serial_proxy.Logger') as mock:
+        yield mock
+
+def test_initialization(mock_serial, mock_logger):
+    proxy = SerialProxy('/dev/ttyUSB0', 9600)
+    mock_serial.assert_called_once_with('/dev/ttyUSB0', 9600)
+    mock_logger.assert_called_once_with('serial_log.json')
+
+def test_write(mock_serial, mock_logger):
+    proxy = SerialProxy('/dev/ttyUSB0', 9600)
+    proxy.write(b'test')
+    mock_serial.return_value.write.assert_called_once_with(b'test')
+    mock_logger.return_value.log.assert_called_once_with("WRITE", b'test')
+
+def test_read(mock_serial, mock_logger):
+    mock_serial.return_value.read.return_value = b'test'
+    proxy = SerialProxy('/dev/ttyUSB0', 9600)
+    result = proxy.read(4)
+    assert result == b'test'
+    mock_serial.return_value.read.assert_called_once_with(4)
+    mock_logger.return_value.log.assert_called_once_with("READ", b'test')
+
+def test_readline(mock_serial, mock_logger):
+    mock_serial.return_value.readline.return_value = b'test\n'
+    proxy = SerialProxy('/dev/ttyUSB0', 9600)
+    result = proxy.readline()
+    assert result == b'test\n'
+    mock_serial.return_value.readline.assert_called_once()
+    mock_logger.return_value.log.assert_called_once_with("READLINE", b'test\n')
+
+def test_readlines(mock_serial, mock_logger):
+    mock_serial.return_value.readlines.return_value = [b'test1\n', b'test2\n']
+    proxy = SerialProxy('/dev/ttyUSB0', 9600)
+    result = proxy.readlines()
+    assert result == [b'test1\n', b'test2\n']
+    mock_serial.return_value.readlines.assert_called_once()
+    mock_logger.return_value.log.assert_called_once_with("READLINES", [b'test1\n', b'test2\n'])
+
+def test_flush_operations(mock_serial, mock_logger):
+    proxy = SerialProxy('/dev/ttyUSB0', 9600)
+    proxy.flush()
+    proxy.flushInput()
+    proxy.flushOutput()
+    mock_serial.return_value.flush.assert_called_once()
+    mock_serial.return_value.flushInput.assert_called_once()
+    mock_serial.return_value.flushOutput.assert_called_once()
+    mock_logger.return_value.log.assert_has_calls([
+        Mock(args=("FLUSH", "")),
+        Mock(args=("FLUSH_INPUT", "")),
+        Mock(args=("FLUSH_OUTPUT", ""))
+    ])
+
+def test_open_close(mock_serial, mock_logger):
+    proxy = SerialProxy('/dev/ttyUSB0', 9600)
+    proxy.open()
+    proxy.close()
+    mock_serial.return_value.open.assert_called_once()
+    mock_serial.return_value.close.assert_called_once()
+    mock_logger.return_value.log.assert_has_calls([
+        Mock(args=("OPEN", "")),
+        Mock(args=("CLOSE", ""))
+    ])
+
+def test_context_manager(mock_serial, mock_logger):
+    with SerialProxy('/dev/ttyUSB0', 9600) as proxy:
+        pass
+    mock_serial.return_value.close.assert_called_once()
+
+def test_is_open_property(mock_serial):
+    mock_serial.return_value.is_open = True
+
+    proxy = SerialProxy('/dev/ttyUSB0', 9600)
+    assert proxy.is_open == True
+
+def test_in_waiting(mock_serial):
+    mock_serial.return_value.in_waiting = 5
+    proxy = SerialProxy('/dev/ttyUSB0', 9600)
+    assert proxy.in_waiting() == 5

Now, let's update the README.md file to include information about the new test suite:

README.md

--- 
+++ 
@@ -34,6 +34,22 @@
 - Automatic logging of all serial operations to a JSON file
 - Context manager support for easy resource management

+## Development
+
+### Running Tests
+
+To run the test suite, first install the development dependencies:
+
+```
+pip install -r requirements-dev.txt
+```
+
+Then, run the tests using pytest:
+
+```
+pytest src/serial_proxy/tests
+```
+
 ## License

 This project is licensed under the MIT License - see the LICENSE file for details.

Finally, let's create a requirements-dev.txt file for development dependencies:

requirements-dev.txt

--- 
+++ 
@@ -1 +1,2 @@
-
+pytest==7.3.1
+pytest-mock==3.10.0

These changes implement the requested refactoring of the SerialProxy class, add comprehensive unit tests, and update the project documentation. The code is now more organized, testable, and follows best practices such as type hinting and separation of concerns.

Continue the conversation with Sweep here: https://app.sweep.dev/c/9274c699-1e23-4ec5-bdbe-5da16f4685f3.

:book: For more information on how to use Sweep, please read our documentation.

Tracking ID: c52ed7b49b