Skip to content

Commit e2e41c2

Browse files
authored
Merge pull request #229 from JdeRobot/testing
Implement RAM Transitions Tests
2 parents 965f115 + 104316c commit e2e41c2

21 files changed

Lines changed: 1479 additions & 43 deletions
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
name: Pytest with Coverage
2+
3+
on:
4+
pull_request:
5+
branches: [humble-devel]
6+
push:
7+
branches: [humble-devel, testing]
8+
jobs:
9+
test:
10+
runs-on: ubuntu-latest
11+
12+
steps:
13+
- name: Checkout code
14+
uses: actions/checkout@v4
15+
16+
- name: Set up Python
17+
uses: actions/setup-python@v5
18+
with:
19+
python-version: '3.10.12'
20+
21+
- name: Install dependencies
22+
run: |
23+
python -m pip install --upgrade pip
24+
pip install -r test/requirements.txt
25+
pip install pytest pytest-cov
26+
27+
- name: Run tests with coverage
28+
run: |
29+
PYTHONPATH=. pytest --cov=manager/manager --cov-report=term

.gitignore

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,8 @@ __pycache__/
33
/.idea
44

55
# IDEs
6-
.vscode
6+
.vscode
7+
8+
# Log Files
9+
*.log
10+
*.coverage

manager/comms/new_consumer.py

Lines changed: 55 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,9 @@
1+
"""
2+
WebSocket consumer module for the Robotics Application Manager (RAM).
3+
4+
Handles client connections, message processing, and communication with manager queue.
5+
"""
6+
17
import json
28
import logging
39
from queue import Queue
@@ -8,25 +14,37 @@
814
ManagerConsumerMessageException,
915
ManagerConsumerMessage,
1016
)
11-
from manager.comms.websocker_server import WebsocketServer
17+
from manager.comms.websocket_server import WebsocketServer
1218
from manager.ram_logging.log_manager import LogManager
1319

1420

1521
class Client:
22+
"""Represents a client connected to the WebSocket server."""
23+
1624
def __init__(self, **kwargs):
25+
"""Initialize a Client instance with id, handler, and address."""
1726
self.id = kwargs["id"]
1827
self.handler = kwargs["handler"]
1928
self.address = kwargs["address"]
2029

2130

2231
class ManagerConsumer:
2332
"""
24-
Websocket server consumer for new Robotics Application Manager aka. RAM
33+
Websocket server consumer for new Robotics Application Manager aka: RAM.
34+
2535
Supports single client connection to RAM
2636
TODO: Better handling of single client connections, closing and redirecting
2737
"""
2838

2939
def __init__(self, host, port, manager_queue: Queue):
40+
"""
41+
Initialize the ManagerConsumer with host, port, and manager_queue.
42+
43+
Args:
44+
host (str): The host address for the WebSocket server.
45+
port (int): The port number for the WebSocket server.
46+
manager_queue (Queue): The queue for communication with the manager.
47+
"""
3048
self.host = host
3149
self.port = port
3250
self.server = WebsocketServer(host=host, port=port, loglevel=logging.INFO)
@@ -37,7 +55,8 @@ def __init__(self, host, port, manager_queue: Queue):
3755
ws_logger.setLevel(logging.INFO)
3856
ws_logger.handlers.clear()
3957
ws_formatter = logging.Formatter(
40-
"%(asctime)s [%(threadName)-12.12s] [%(levelname)-5.5s] (%(name)s) %(message)s",
58+
"%(asctime)s [%(threadName)-12.12s] [%(levelname)-5.5s] "
59+
"(%(name)s) %(message)s",
4160
"%H:%M:%S",
4261
)
4362
ws_console_handler = logging.StreamHandler()
@@ -51,11 +70,25 @@ def __init__(self, host, port, manager_queue: Queue):
5170
self.manager_queue = manager_queue
5271

5372
def handle_client_new(self, client, server):
73+
"""
74+
Handle a new client connection event.
75+
76+
Args:
77+
client: The client object representing the connected client.
78+
server: The WebSocket server instance.
79+
"""
5480
LogManager.logger.info(f"client connected: {client}")
5581
self.client = client
5682
self.server.deny_new_connections()
5783

5884
def handle_client_disconnect(self, client, server):
85+
"""
86+
Handle a client disconnection event.
87+
88+
Args:
89+
client: The client object representing the disconnected client.
90+
server: The WebSocket server instance.
91+
"""
5992
if client is None:
6093
return
6194
LogManager.logger.info(f"client disconnected: {client}")
@@ -70,6 +103,14 @@ def handle_client_disconnect(self, client, server):
70103
self.server.allow_new_connections()
71104

72105
def handle_message_received(self, client, server, websocket_message):
106+
"""
107+
Handle a message received from a client.
108+
109+
Args:
110+
client: The client object that sent the message.
111+
server: The WebSocket server instance.
112+
websocket_message (str): The message received from the client.
113+
"""
73114
LogManager.logger.info(
74115
f"message received length: {len(websocket_message)} from client {client}"
75116
)
@@ -90,6 +131,15 @@ def handle_message_received(self, client, server, websocket_message):
90131
raise e
91132

92133
def send_message(self, message_data, command=None):
134+
"""
135+
Send a message to the connected client.
136+
137+
Args:
138+
message_data: The message data to send, can be a ManagerConsumerMessage,
139+
ManagerConsumerMessageException, or other data.
140+
command (str, optional): The command associated with the message,
141+
used if message_data is not a ManagerConsumerMessage.
142+
"""
93143
if self.client is not None and self.server is not None:
94144
if isinstance(message_data, ManagerConsumerMessage):
95145
message = message_data
@@ -103,7 +153,9 @@ def send_message(self, message_data, command=None):
103153
self.server.send_message(self.client, str(message))
104154

105155
def start(self):
156+
"""Start the WebSocket server in a separate thread."""
106157
self.server.run_forever(threaded=True)
107158

108159
def stop(self):
160+
"""Stop the WebSocket server gracefully."""
109161
self.server.shutdown_gracefully()

manager/libs/launch_world_model.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
"""Module for managing and validating robotics application launch world configs."""
2+
13
from dataclasses import dataclass
24
from typing import Optional
35
from pydantic import BaseModel, ValidationError
@@ -6,6 +8,8 @@
68

79

810
class ConfigurationModel(BaseModel):
11+
"""Pydantic model for robotics application world type and launch file config."""
12+
913
type: str
1014
launch_file_path: str
1115

@@ -15,10 +19,23 @@ class ConfigurationModel(BaseModel):
1519

1620
@dataclass
1721
class ConfigurationManager:
22+
"""Manager for robotics application configuration validation and storage."""
23+
1824
configuration: ConfigurationModel
1925

2026
@staticmethod
2127
def validate(configuration: dict):
28+
"""Validate the given configuration dictionary using the ConfigurationModel.
29+
30+
Args:
31+
configuration (dict): The configuration data to validate.
32+
33+
Returns:
34+
ConfigurationModel: The validated configuration model.
35+
36+
Raises:
37+
ValueError: If the configuration is invalid.
38+
"""
2239
try:
2340
return ConfigurationModel(**configuration)
2441
except ValidationError as e:

manager/manager/launcher/launcher_robot.py

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
"""LauncherRobot module for managing robot launchers in different simulation worlds."""
2+
13
from typing import Optional
24
from pydantic import BaseModel
35

@@ -66,6 +68,8 @@
6668

6769

6870
class LauncherRobot(BaseModel):
71+
"""Class for managing robot launchers in different simulation worlds."""
72+
6973
type: str
7074
launch_file_path: str
7175
module: str = ".".join(__name__.split(".")[:-1])
@@ -74,7 +78,8 @@ class LauncherRobot(BaseModel):
7478
start_pose: Optional[list] = []
7579

7680
def run(self, start_pose=None):
77-
if start_pose != None:
81+
"""Run the robot launcher with an optional start pose."""
82+
if start_pose is not None:
7883
self.start_pose = start_pose
7984
for module in worlds[self.type][str(self.ros_version)]:
8085
module["launch_file"] = self.launch_file_path
@@ -83,13 +88,16 @@ def run(self, start_pose=None):
8388
LogManager.logger.info(self.launchers)
8489

8590
def terminate(self):
91+
"""Terminate all robot launchers and clear the launchers list."""
8692
LogManager.logger.info("Terminating robot launcher")
8793
if self.launchers:
8894
for launcher in self.launchers:
8995
launcher.terminate()
9096
self.launchers = []
9197

9298
def launch_module(self, configuration):
99+
"""Launch a robot module based on the provided configuration."""
100+
93101
def process_terminated(name, exit_code):
94102
LogManager.logger.info(
95103
f"LauncherEngine: {name} exited with code {exit_code}"
@@ -98,17 +106,24 @@ def process_terminated(name, exit_code):
98106
self.terminated_callback(name, exit_code)
99107

100108
launcher_module_name = configuration["module"]
101-
launcher_module = f"{self.module}.launcher_{launcher_module_name}.Launcher{class_from_module(launcher_module_name)}"
109+
launcher_module = (
110+
f"{self.module}.launcher_{launcher_module_name}."
111+
f"Launcher{class_from_module(launcher_module_name)}"
112+
)
102113
launcher_class = get_class(launcher_module)
103114
launcher = launcher_class.from_config(launcher_class, configuration)
104115

105116
launcher.run(self.start_pose, process_terminated)
106117
return launcher
107118

108119
def launch_command(self, configuration):
120+
"""Launch a robot command based on the provided configuration."""
109121
pass
110122

111123

112124
class LauncherRobotException(Exception):
125+
"""Exception class for errors related to LauncherRobot."""
126+
113127
def __init__(self, message):
128+
"""Initialize the LauncherRobotException with a message."""
114129
super(LauncherRobotException, self).__init__(message)

manager/manager/lint/linter.py

Lines changed: 56 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,28 @@
1+
"""Linter module for evaluating and cleaning Python code using pylint."""
2+
13
import glob
24
import re
3-
import os
5+
import os # noqa: F401
46
import subprocess
57
import tempfile
68

79

810
class Lint:
11+
"""Class for evaluating and cleaning Python code using pylint."""
12+
913
def clean_pylint_output(self, result, warnings=False):
14+
"""
15+
Clean the output from pylint.
16+
17+
By removing unwanted messages and formatting errors.
18+
19+
Args:
20+
result (str): The output string from pylint.
21+
warnings (bool): Whether to include warnings in the output.
22+
23+
Returns:
24+
str: The cleaned and formatted output.
25+
"""
1026

1127
# result = result.replace(os.path.basename(code_file_name), 'user_code')
1228
# Define the patterns to remove
@@ -16,13 +32,15 @@ def clean_pylint_output(self, result, warnings=False):
1632
r":[0-9]+:[0-9]+: R[0-9]{4}:.*", # Refactor messages
1733
r":[0-9]+:[0-9]+: error.*EOF.*", # Unexpected EOF error
1834
r":[0-9]+:[0-9]+: E1101:.*Module 'ompl.*", # ompl E1101 error
19-
r":[0-9]+:[0-9]+:.*value.*argument.*unbound.*method.*", # No value for argument 'self' error
35+
(
36+
r":[0-9]+:[0-9]+:.*value.*argument.*unbound.*method.*"
37+
), # No value for argument 'self' error
2038
r":[0-9]+:[0-9]+: E1111:.*", # Assignment from no return error
2139
r":[0-9]+:[0-9]+: E1136:.*", # E1136 until issue is resolved
2240
]
2341

2442
if not warnings:
25-
# Remove convention, refactor, and warning messages if warnings are not desired
43+
# Remove convention, refactor, and warning msgs if warnings are not desired
2644
for pattern in patterns[:3]:
2745
result = re.sub(r"^[^:]*" + pattern, "", result, flags=re.MULTILINE)
2846

@@ -43,6 +61,15 @@ def clean_pylint_output(self, result, warnings=False):
4361
return result
4462

4563
def append_rating_if_missing(self, result):
64+
"""
65+
Append a default rating message to the result if it is missing.
66+
67+
Args:
68+
result (str): The output string from pylint.
69+
70+
Returns:
71+
str: The result string with the rating message appended if necessary.
72+
"""
4673
rating_message = (
4774
"-----------------------------------\nYour code has been rated at 0.00/10"
4875
)
@@ -58,13 +85,28 @@ def append_rating_if_missing(self, result):
5885
def evaluate_code(
5986
self, code, ros_version, warnings=False, py_lint_source="pylint_checker.py"
6087
):
88+
"""
89+
Evaluate the provided Python code using pylint and return the cleaned output.
90+
91+
Args:
92+
code (str): The Python code to evaluate.
93+
ros_version (str): The ROS version to determine environment settings.
94+
warnings (bool, optional): Whether to include warnings in the output.
95+
Defaults to False.
96+
py_lint_source (str, optional): The pylint checker source file.
97+
Defaults to "pylint_checker.py".
98+
99+
Returns:
100+
str: The cleaned and formatted pylint output.
101+
"""
61102
try:
62103
code = re.sub(r"from HAL import HAL", "from hal import HAL", code)
63104
code = re.sub(r"from GUI import GUI", "from gui import GUI", code)
64105
code = re.sub(r"from MAP import MAP", "from map import MAP", code)
65106
code = re.sub(r"\nimport cv2\n", "\nfrom cv2 import cv2\n", code)
66107

67-
# Avoids EOF error when iterative code is empty (which prevents other errors from showing)
108+
# Avoids EOF error when iterative code is empty
109+
# (which prevents other errors from showing)
68110
while_position = re.search(
69111
r"[^ ]while\s*\(\s*True\s*\)\s*:|[^ ]while\s*True\s*:|[^ ]while\s*1\s*:|[^ ]while\s*\(\s*1\s*\)\s*:",
70112
code,
@@ -89,9 +131,17 @@ def evaluate_code(
89131

90132
command = ""
91133
if "humble" in str(ros_version):
92-
command = f"export PYTHONPATH=$PYTHONPATH:/workspace/code; python3 /RoboticsApplicationManager/manager/manager/lint/{py_lint_source}"
134+
command = (
135+
f"export PYTHONPATH=$PYTHONPATH:/workspace/code; "
136+
f"python3 /RoboticsApplicationManager/manager/manager/lint/"
137+
f"{py_lint_source}"
138+
)
93139
else:
94-
command = f"export PYTHONPATH=$PYTHONPATH:/workspace/code; python3 /RoboticsApplicationManager/manager/manager/lint/{py_lint_source}"
140+
command = (
141+
f"export PYTHONPATH=$PYTHONPATH:/workspace/code; "
142+
f"python3 /RoboticsApplicationManager/manager/manager/lint/"
143+
f"{py_lint_source}"
144+
)
95145

96146
ret = subprocess.run(command, capture_output=True, text=True, shell=True)
97147

0 commit comments

Comments
 (0)