Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .hooks/pre-push
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ black ${black_extra_args} $python_files

# Faster than pylint to check for issues
echo "Running ruff.."
ruff $python_files
ruff check $python_files

echo "Running pylint.."
pylint $python_files
Expand Down
7 changes: 3 additions & 4 deletions core/libs/commonwealth/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,13 @@ description = "BlueOS library to share common code."
readme = "README.md"
requires-python = ">=3.11"
dependencies = [
"aiohttp==3.7.4",
"aiohttp==3.12.13",
"appdirs==1.4.4",
"eclipse-zenoh==1.4.0",
"loguru==0.5.3",
"psutil==5.7.2",
"loguru==0.7.3",
"psutil==7.0.0",
"pykson==1.0.2",
"sentry-sdk==2.29.1",
"starlette==0.27.0",
]

[tool.uv.sources]
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import os
from unittest.mock import patch

import pytest

from commonwealth.mavlink_comm.MavlinkComm import MavlinkMessenger


class TestMavlinkMessenger:
"""Test cases for MavlinkMessenger class."""

def test_init_default_values(self) -> None:
"""Test initialization with default values."""
with patch.dict(os.environ, {"MAV_SYSTEM_ID": "1", "MAV_COMPONENT_ID_ONBOARD_COMPUTER4": "194"}):
messenger = MavlinkMessenger()
assert messenger.system_id == 1
assert messenger.component_id == 194
assert messenger.sequence == 0
assert messenger.m2r_address == "localhost:6040"

def test_set_system_id(self) -> None:
"""Test setting system ID."""
messenger = MavlinkMessenger()
messenger.set_system_id(10)
assert messenger.system_id == 10

def test_set_component_id(self) -> None:
"""Test setting component ID."""
messenger = MavlinkMessenger()
messenger.set_component_id(200)
assert messenger.component_id == 200

def test_set_sequence(self) -> None:
"""Test setting sequence."""
messenger = MavlinkMessenger()
messenger.set_sequence(5)
assert messenger.sequence == 5

def test_set_m2r_address_valid(self) -> None:
"""Test setting valid m2r address."""
messenger = MavlinkMessenger()
messenger.set_m2r_address("192.168.1.100:8080")
assert messenger.m2r_address == "192.168.1.100:8080"

def test_set_m2r_address_invalid(self) -> None:
"""Test setting invalid m2r address."""
messenger = MavlinkMessenger()
with pytest.raises(
ValueError, match="Invalid address. Valid address should follow the format 'localhost:6040'."
):
messenger.set_m2r_address("invalid_address")

def test_m2r_rest_url_property(self) -> None:
"""Test m2r_rest_url property."""
messenger = MavlinkMessenger()
assert messenger.m2r_rest_url == "http://localhost:6040/mavlink"
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ def load(self, file_path: pathlib.Path) -> None:

# Copy new content to settings class
try:
new = self.parse_obj(result)
new = self.model_validate(result)
self.__dict__.update(new.__dict__)
except ValidationError as e:
raise BadSettingsFile(f"Settings file contains invalid data: {e}") from e
Expand All @@ -109,7 +109,7 @@ def save(self, file_path: pathlib.Path) -> None:

# Prepare data prior to operation
logger.debug(f"Saving settings on: {file_path}")
json_data = self.dict()
json_data = self.model_dump()

# Create a temporary file in same directory, write and rename it to the original file
temp_file = file_path.with_suffix(".tmp")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ class SettingsV1(PydanticSettings):
name="bilica",
animal_type="dog",
parts=["finger", "eyes"],
animal_json=[JsonExample.parse_obj({"name": "Json!"})],
animal_json=[JsonExample.model_validate({"name": "Json!"})],
)
first_variable: int = 42

Expand Down
49 changes: 25 additions & 24 deletions core/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -24,26 +24,26 @@ dependencies = [

[dependency-groups]
dev = [
"coverage==7.5.1",
"coverage==7.9.2",
"nmeasim==1.1.1.0",
"pytest==7.4.2",
"pytest-cov==4.1.0",
"pyfakefs==5.2.4",
"pytest-asyncio==0.14.0",
"pytest-mock==3.10.0",
"pytest-timeout==2.1.0",
"pytest-xdist>=3.3.1",
"pyelftools==0.30",
"pytest==8.4.1",
"pytest-cov==6.2.1",
"pyfakefs==5.9.1",
"pytest-asyncio==1.0.0",
"pytest-mock==3.14.1",
"pytest-timeout==2.4.0",
"pytest-xdist==3.8.0",
"pyelftools==0.32",
]
lint = [
"black==22.3.0",
"isort==5.8",
"black==25.1.0",
"isort==5.13.2",
"pylint==3.0",
"mypy==1.4.1",
"ruff==0.0.288",
"pylint-pydantic==0.3.2",
"pylint-plugin-utils==0.8.2",
"types-requests==2.31.0.2",
"mypy==1.16.1",
"ruff==0.12.2",
"pylint-pydantic==0.3.5",
"pylint-plugin-utils==0.9.0",
"types-requests==2.32.4.20250611",
]

[tool.uv]
Expand Down Expand Up @@ -106,14 +106,15 @@ disable_error_code = [
]

[tool.ruff]
ignore = [
"A003", # BuiltinAttributeShadowing: Unnecessary restriction (not a source of confusion)
"E501", # LineTooLong: Let black take care of
"F401", # UnusedImport: We are using pylint 'ignore' comments for it
"F821", # UndefinedName: We are using pylint 'ignore' comments for it
"F841", # UnusedVariable: We are using pylint 'ignore' comments for it
"E402", # ModuleImportNotAtTopOfFile: noqa doesn't work and this is required at some places
]
[tool.ruff.lint]
ignore = [
"A003", # BuiltinAttributeShadowing: Unnecessary restriction (not a source of confusion)
"E501", # LineTooLong: Let black take care of
"F401", # UnusedImport: We are using pylint 'ignore' comments for it
"F821", # UndefinedName: We are using pylint 'ignore' comments for it
"F841", # UnusedVariable: We are using pylint 'ignore' comments for it
"E402", # ModuleImportNotAtTopOfFile: noqa doesn't work and this is required at some places
]

[tool.pylint]
[tool.pylint.master]
Expand Down
4 changes: 1 addition & 3 deletions core/services/ardupilot_manager/api/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,2 @@
# pylint: disable=W0406
# pylint: disable=import-self
from .app import application

__all__ = ["application"]
4 changes: 1 addition & 3 deletions core/services/ardupilot_manager/api/v1/routers/__init__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
# pylint: disable=W0406
# pylint: disable=import-self
from .endpoints import endpoints_router_v1
from .index import index_router_v1

__all__ = ["endpoints_router_v1", "index_router_v1"]
1 change: 0 additions & 1 deletion core/services/ardupilot_manager/api/v2/routers/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
# pylint: disable=W0406
from .index import index_router_v2

__all__ = ["index_router_v2"]
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ async def firmware_validation_wrapper() -> None:
if platform.system() != "Darwin":
# there are no SITL builds for MacOS
temporary_file = downloader.download(Vehicle.Sub, Platform.SITL, version="DEV")
board = FlightController(name="SITL", manufacturer="ArduPilot Team", platform=Platform.SITL)
board = FlightController(name="SITL", manufacturer="ArduPilot Team", platform=Platform.SITL, path=None)
await installer.install_firmware(temporary_file, board, pathlib.Path(f"{temporary_file}_dest"))

asyncio.run(firmware_validation_wrapper())
77 changes: 77 additions & 0 deletions core/services/ardupilot_manager/firmware/test_FirmwareUpload.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import pathlib
import subprocess
from unittest.mock import MagicMock, patch

import pytest

from exceptions import InvalidUploadTool, UploadToolNotFound
from firmware.FirmwareUpload import FirmwareUploader


class TestFirmwareUploader:
"""Test cases for FirmwareUploader class."""

def test_binary_name(self) -> None:
"""Test that binary_name returns the correct name."""
assert FirmwareUploader.binary_name() == "ardupilot_fw_uploader.py"

@patch("shutil.which")
def test_init_binary_not_found(self, mock_which: MagicMock) -> None:
"""Test that UploadToolNotFound is raised when binary is not found."""
mock_which.return_value = None
with pytest.raises(UploadToolNotFound, match="Uploader binary not found on system's PATH."):
FirmwareUploader()

@patch("shutil.which")
@patch("subprocess.check_output")
def test_init_binary_invalid(self, mock_check_output: MagicMock, mock_which: MagicMock) -> None:
"""Test that InvalidUploadTool is raised when binary validation fails."""
mock_which.return_value = "/fake/path/ardupilot_fw_uploader.py"
mock_check_output.side_effect = subprocess.CalledProcessError(1, "test")

with pytest.raises(InvalidUploadTool):
FirmwareUploader()

@patch("shutil.which")
@patch("subprocess.check_output")
def test_init_success(self, mock_check_output: MagicMock, mock_which: MagicMock) -> None:
"""Test successful initialization."""
mock_which.return_value = "/fake/path/ardupilot_fw_uploader.py"
mock_check_output.return_value = b"help output"

uploader = FirmwareUploader()
assert uploader.binary() == pathlib.Path("/fake/path/ardupilot_fw_uploader.py")

@patch("shutil.which")
@patch("subprocess.check_output")
def test_set_autopilot_port(self, mock_check_output: MagicMock, mock_which: MagicMock) -> None:
"""Test setting autopilot port."""
mock_which.return_value = "/fake/path/ardupilot_fw_uploader.py"
mock_check_output.return_value = b"help output"

uploader = FirmwareUploader()
new_port = pathlib.Path("/dev/ttyUSB0")
uploader.set_autopilot_port(new_port)
assert uploader._autopilot_port == new_port

@patch("shutil.which")
@patch("subprocess.check_output")
def test_set_baudrate_bootloader(self, mock_check_output: MagicMock, mock_which: MagicMock) -> None:
"""Test setting bootloader baudrate."""
mock_which.return_value = "/fake/path/ardupilot_fw_uploader.py"
mock_check_output.return_value = b"help output"

uploader = FirmwareUploader()
uploader.set_baudrate_bootloader(921600)
assert uploader._baudrate_bootloader == 921600

@patch("shutil.which")
@patch("subprocess.check_output")
def test_set_baudrate_flightstack(self, mock_check_output: MagicMock, mock_which: MagicMock) -> None:
"""Test setting flightstack baudrate."""
mock_which.return_value = "/fake/path/ardupilot_fw_uploader.py"
mock_check_output.return_value = b"help output"

uploader = FirmwareUploader()
uploader.set_baudrate_flightstack(115200)
assert uploader._baudrate_flightstack == 115200
13 changes: 10 additions & 3 deletions core/services/ardupilot_manager/mavlink_proxy/Endpoint.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
from typing import Any, Dict, Iterable, Optional, Type

import validators
from pydantic import constr, root_validator
from pydantic import constr, model_validator
from pydantic.dataclasses import dataclass


Expand Down Expand Up @@ -32,10 +32,17 @@ class Endpoint:
enabled: Optional[bool] = True
overwrite_settings: Optional[bool] = False

@root_validator
@model_validator(mode="before")
@classmethod
def is_mavlink_endpoint(cls: Type["Endpoint"], values: Any) -> Any:
connection_type, place, argument = (values.get("connection_type"), values.get("place"), values.get("argument"))
if isinstance(values, dict):
connection_type, place, argument = (
values.get("connection_type"),
values.get("place"),
values.get("argument"),
)
else:
return values

if connection_type in [
EndpointType.UDPServer,
Expand Down
7 changes: 5 additions & 2 deletions core/services/ardupilot_manager/mavlink_proxy/test_all.py
Original file line number Diff line number Diff line change
Expand Up @@ -118,8 +118,11 @@ def test_endpoint() -> None:
assert endpoint.place == "0.0.0.0", "Connection place does not match."
assert endpoint.argument == 14550, "Connection argument does not match."
assert str(endpoint) == "udpout:0.0.0.0:14550", "Connection string does not match."
assert endpoint.as_dict() == {
"__pydantic_initialised__": True,
endpoint_dict = endpoint.as_dict()
# Remove any Pydantic internal fields that might be present
endpoint_dict.pop("__pydantic_initialised__", None)
endpoint_dict.pop("__initialised__", None)
assert endpoint_dict == {
"name": "Test endpoint",
"owner": "pytest",
"connection_type": EndpointType.UDPClient.value,
Expand Down
22 changes: 11 additions & 11 deletions core/services/ardupilot_manager/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,19 +7,19 @@ requires-python = ">=3.11"
dependencies = [
"appdirs==1.4.4",
"commonwealth==0.1.0",
"fastapi==0.105.0",
"fastapi-versioning==0.9.1",
"loguru==0.5.3",
"packaging==20.4",
"psutil==5.7.2",
"pydantic==1.10.12",
"pyelftools==0.30",
"fastapi==0.115.14",
"fastapi-versioning==0.10.0",
"loguru==0.7.3",
"packaging==25.0",
"psutil==7.0.0",
"pydantic==2.11.7",
"pyelftools==0.32",
"pyserial==3.5",
# This dependency needs to be locked since it is used by fastapi
"python-multipart==0.0.5",
"smbus2==0.3.0",
"uvicorn==0.18.0",
"validators==0.18.2",
"python-multipart==0.0.20",
"smbus2==0.5.0",
"uvicorn==0.35.0",
"validators==0.35.0",
]

[tool.uv]
Expand Down
6 changes: 3 additions & 3 deletions core/services/ardupilot_manager/typedefs.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
from platform import machine
from typing import Any, Dict, List, Optional

from pydantic import BaseModel, validator
from pydantic import BaseModel, field_validator


class SITLFrame(str, Enum):
Expand Down Expand Up @@ -183,14 +183,14 @@ class Serial(BaseModel):
port: str
endpoint: str

@validator("port")
@field_validator("port")
@classmethod
def valid_letter(cls: Any, value: str) -> str:
if value in "BCDEFGH" and len(value) == 1:
return value
raise ValueError(f"Invalid serial port: {value}. These must be between B and H. A is reserved.")

@validator("endpoint")
@field_validator("endpoint")
@classmethod
def valid_endpoint(cls: Any, value: str) -> str:
if Path(value).exists():
Expand Down
Loading
Loading