Skip to content

Commit 62f3e21

Browse files
authored
Set up CI workflow, improve testing and documentation (#13)
- Add and configure CI workflow - Update .gitignore and enhance CONTRIBUTING.md - Add mock robot implementation with unit tests (#5) - Add unit tests for Logger, covering send/receive and log levels - Rename CI workflow to 'build'; remove coverage reporting - Specify test directory in CI workflow
1 parent a58b858 commit 62f3e21

8 files changed

Lines changed: 277 additions & 4 deletions

File tree

.github/workflows/ci.yml

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
name: build
2+
3+
on:
4+
push:
5+
branches: [ main ]
6+
paths: [ "**/*.py", "pyproject.toml", ".github/workflows/**"]
7+
pull_request:
8+
branches: [ main ]
9+
paths: [ "**/*.py", "pyproject.toml", ".github/workflows/**"]
10+
11+
jobs:
12+
build-and-test:
13+
runs-on: ubuntu-latest
14+
steps:
15+
- name: Checkout repository
16+
uses: actions/checkout@v4
17+
18+
- name: Set up Python
19+
uses: actions/setup-python@v5
20+
with:
21+
python-version: '3.11'
22+
23+
- name: Install Poetry
24+
run: |
25+
python -m pip install --upgrade pip
26+
pip install poetry
27+
28+
- name: Install dependencies
29+
run: |
30+
poetry install --no-interaction --with dev
31+
32+
- name: Run tests
33+
run: |
34+
poetry run pytest tests --maxfail=1 --disable-warnings --tb=short -v

.gitignore

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,18 @@
1+
# Caching
12
__pycache__/
2-
*notes*
3+
.*_cache/
34

5+
# Python Package Management
46
*egg-info/
57
*venv/
68

9+
# Internal Usage
710
local/
11+
*notes*
12+
13+
# IDE and Editor Configurations
14+
.idea/
15+
.vscode/
816

917
# Builds
1018
dist/

CONTRIBUTING.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,3 +147,13 @@ poetry run mypy armctl
147147
```
148148

149149
> All code must be properly formatted and pass type checks. Please resolve any issues reported by these tools prior to opening a pull request.
150+
151+
**(Optional) Running Tests Locally:**
152+
153+
While CI/CD automation will run tests on your pull request, you can speed up debugging by running the test suite locally:
154+
155+
```bash
156+
poetry run pytest tests
157+
```
158+
159+
This is optional, but helps catch issues before submitting your pull request.

poetry.lock

Lines changed: 88 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pyproject.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ black = { version = ">=23.0" }
3636
ruff = { version = ">=0.4.0" }
3737
mypy = { version = ">=1.0.0" }
3838
isort = { version = ">=5.12.0" }
39+
pytest = { version = ">=8.4.0"}
3940

4041
[tool.black]
4142
line-length = 80
@@ -57,6 +58,6 @@ force_grid_wrap = 0
5758
use_parentheses = true
5859

5960
[tool.mypy]
60-
python_version = 3.8
61+
python_version = 3.9
6162
strict = true
6263
ignore_missing_imports = true

tests/_mock_robot.py

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
from armctl.templates import Commands
2+
from armctl.templates import SocketController as Socket
3+
import time
4+
5+
TEST_STRING_PREFIX = "MOCK!!"
6+
7+
def _format_mock_command(method_name: str, arg=None):
8+
if arg is not None:
9+
return f"{TEST_STRING_PREFIX} {method_name.upper()}: {arg}"
10+
return f"{TEST_STRING_PREFIX} {method_name.upper()}"
11+
12+
class MockRobot(Socket, Commands):
13+
def __init__(self, ip: str = "127.0.0.1", port: int = 8_000):
14+
super().__init__(ip, port)
15+
16+
def move_joints(self, pos) -> str:
17+
return self.send_command(_format_mock_command(self.move_joints.__name__, pos))
18+
19+
def move_cartesian(self, pose) -> str:
20+
return self.send_command(_format_mock_command(self.move_cartesian.__name__, pose))
21+
22+
def get_joint_positions(self) -> str:
23+
return self.send_command(_format_mock_command(self.get_joint_positions.__name__))
24+
25+
def get_cartesian_position(self) -> str:
26+
return self.send_command(_format_mock_command(self.get_cartesian_position.__name__))
27+
28+
def stop_motion(self) -> str:
29+
return self.send_command(_format_mock_command(self.stop_motion.__name__))
30+
31+
def get_robot_state(self) -> str:
32+
return self.send_command(_format_mock_command(self.get_robot_state.__name__))
33+
34+
def sleep(self, seconds):
35+
return time.sleep(seconds)

tests/test_logger.py

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import logging
2+
import pytest
3+
from armctl import Logger
4+
from armctl.templates import logger as logger_module
5+
6+
7+
def test_send_level_name():
8+
assert logging.getLevelName(logger_module.SEND_LEVEL) == "SEND"
9+
10+
11+
def test_receive_level_name():
12+
assert logging.getLevelName(logger_module.RECEIVE_LEVEL) == "RECV"
13+
14+
15+
def test_logger_send_and_receive_methods_exist():
16+
log = logging.getLogger("test_logger")
17+
assert hasattr(log, "send")
18+
assert hasattr(log, "receive")
19+
20+
21+
def test_logger_send_logs_message(caplog):
22+
log = logging.getLogger("test_logger_send")
23+
with caplog.at_level(logger_module.SEND_LEVEL):
24+
log.send("This is a SEND message")
25+
assert any("This is a SEND message" in m for m in caplog.messages)
26+
assert any(r.levelname == "SEND" for r in caplog.records)
27+
28+
29+
def test_logger_receive_logs_message(caplog):
30+
log = logging.getLogger("test_logger_receive")
31+
with caplog.at_level(logger_module.RECEIVE_LEVEL):
32+
log.receive("This is a RECV message")
33+
assert any("This is a RECV message" in m for m in caplog.messages)
34+
assert any(r.levelname == "RECV" for r in caplog.records)
35+
36+
37+
def test_logger_verbosity_and_enable_disable(caplog):
38+
with caplog.at_level(logging.INFO):
39+
Logger.enable() # Make sure logging is enabled
40+
logging.info("This should appear")
41+
assert "This should appear" in caplog.text
42+
Logger.disable()
43+
logging.info("This should NOT appear")
44+
assert "This should NOT appear" not in caplog.text
45+
Logger.enable() # Re-enable for other tests

tests/test_serial_robot.py

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import pytest
2+
import time
3+
4+
from tests._mock_robot import MockRobot, TEST_STRING_PREFIX
5+
6+
7+
8+
@pytest.fixture
9+
def mock_robot():
10+
class PatchedMockRobot(MockRobot):
11+
def send_command(self, cmd):
12+
return cmd
13+
14+
return PatchedMockRobot()
15+
16+
17+
def test_move_joints(mock_robot):
18+
pos = [1, 2, 3]
19+
expected = f"{TEST_STRING_PREFIX} MOVE_JOINTS: {pos}"
20+
assert mock_robot.move_joints(pos) == expected
21+
22+
23+
def test_move_cartesian(mock_robot):
24+
pose = [0.1, 0.2, 0.3]
25+
expected = f"{TEST_STRING_PREFIX} MOVE_CARTESIAN: {pose}"
26+
assert mock_robot.move_cartesian(pose) == expected
27+
28+
29+
def test_get_joint_positions(mock_robot):
30+
expected = f"{TEST_STRING_PREFIX} GET_JOINT_POSITIONS"
31+
assert mock_robot.get_joint_positions() == expected
32+
33+
34+
def test_get_cartesian_position(mock_robot):
35+
expected = f"{TEST_STRING_PREFIX} GET_CARTESIAN_POSITION"
36+
assert mock_robot.get_cartesian_position() == expected
37+
38+
39+
def test_stop_motion(mock_robot):
40+
expected = f"{TEST_STRING_PREFIX} STOP_MOTION"
41+
assert mock_robot.stop_motion() == expected
42+
43+
44+
def test_get_robot_state(mock_robot):
45+
expected = f"{TEST_STRING_PREFIX} GET_ROBOT_STATE"
46+
assert mock_robot.get_robot_state() == expected
47+
48+
49+
def test_sleep_duration(mock_robot):
50+
sleep_seconds = 0.2
51+
start = time.time()
52+
mock_robot.sleep(sleep_seconds)
53+
elapsed = time.time() - start
54+
assert elapsed >= sleep_seconds

0 commit comments

Comments
 (0)