Skip to content
Merged
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
20 changes: 9 additions & 11 deletions .github/workflows/ruff_linter.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,27 +15,25 @@ jobs:

steps:
- uses: actions/checkout@v4

- name: Set up Python 3.10
uses: actions/setup-python@v3
with:
python-version: "3.10"

- name: Install dependencies
- name: Install uv
run: |
python -m pip install --upgrade pip
curl -LsSf https://astral.sh/uv/install.sh | sh
echo "${HOME}/.cargo/bin" >> $GITHUB_PATH

- name: Install Linter Ruff
- name: Create virtual environment and install dependencies
run: |
curl -LsSf https://astral.sh/uv/install.sh | sh
uv venv
uv pip install ruff pytest
uv pip install -e .[dev]
if [ -f requirements.txt ]; then uv pip install -r requirements.txt; fi
uv pip install ruff

- name: Run Ruff
run: |
source .venv/bin/activate
ruff check .

- name: Run Tests
run: |
source .venv/bin/activate
pytest tests/
uv run ruff check .
55 changes: 33 additions & 22 deletions .github/workflows/ubuntu.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,27 +13,38 @@ jobs:
strategy:
fail-fast: false
matrix:
python-version: ["3.10", "3.11"]
python-version: [ "3.10", "3.11" ]

steps:
- uses: actions/checkout@v4
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v3
with:
python-version: ${{ matrix.python-version }}

- name: Install dependencies
run: |
python -m pip install --upgrade pip
python -m pip install flake8 pytest
python -m pip install .[dev]
if [ -f requirements.txt ]; then pip install -r requirements.txt; fi

- name: Lint with flake8
run: |
flake8 drone_base/ --count --select=E9,F63,F7,F82 --show-source --statistics
flake8 drone_base/ --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics

- name: Run tests with pytest and coverage
run: |
pytest --cov=drone_base --cov-report=xml
- uses: actions/checkout@v4

- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v3
with:
python-version: ${{ matrix.python-version }}

- name: Install system dependencies for X11
run: |
sudo apt-get update
sudo apt-get install -y xvfb

- name: Install uv
run: |
curl -LsSf https://astral.sh/uv/install.sh | sh
echo "${HOME}/.cargo/bin" >> $GITHUB_PATH

- name: Create virtual environment and install dependencies
run: |
uv venv
uv pip install -e .[dev]
if [ -f requirements.txt ]; then uv pip install -r requirements.txt; fi
uv pip install flake8 pytest pytest-cov

- name: Lint with flake8
run: |
uv run flake8 drone_base/ --count --select=E9,F63,F7,F82 --show-source --statistics
uv run flake8 drone_base/ --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics

- name: Run tests with pytest and coverage
run: |
xvfb-run -a uv run pytest --cov=drone_base --cov-report=xml
12 changes: 6 additions & 6 deletions drone_base/config/logger.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,12 @@ def setup_logger(
"""
Set up a logger with both console and file handlers.

@param logger_name: Name of the logger.
@param log_file: Optional path to log file. If None, only console logging is set.
@param level: Overall logging level.
@param console_level: Logging level for console output.
@param file_level: Logging level for file output.
@return: Configured logger instance.
:param logger_name: Name of the logger.
:param log_file: Optional path to log file. If None, only console logging is set.
:param level: Overall logging level.
:param console_level: Logging level for console output.
:param file_level: Logging level for file output.
:return: Configured logger instance.
"""
logger = logging.getLogger(logger_name)
logger.setLevel(level)
Expand Down
8 changes: 0 additions & 8 deletions drone_base/config/video.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,11 +44,3 @@ def update_dimensions(self, width: int, height: int):
self.height = height
self.frame_center_x, self.frame_center_y = self.__compute_frame_center()
self.frame_area = self.__compute_frame_area()


if __name__ == '__main__':
v = VideoConfig(width=224, height=112)
print(f"Width: {v.width}, Height: {v.height}")
print(f"Frame center: {v.frame_center_x}, Frame center: {v.frame_center_y}")
print(f"Frame area: {v.frame_area}")
print(f"Camera mode: {v.cam_mode}")
6 changes: 3 additions & 3 deletions drone_base/control/drone_commander.py
Original file line number Diff line number Diff line change
Expand Up @@ -94,11 +94,11 @@ def tilt_camera(
"""
Tilts the gimbal camera for the pitch value.

@param pitch_deg: The pitch value in degrees.
:param pitch_deg: The pitch value in degrees.
Positive value will rotate the camera upwards.
Negative value will rotate the camera downwards.
@param control_mode: "position" or "velocity" control mode.
@param reference_type: An absolute reference type considers the camera to be parallel to the ground plane.
:param control_mode: "position" or "velocity" control mode.
:param reference_type: An absolute reference type considers the camera to be parallel to the ground plane.
A relative reference type takes into consideration the current position of the camera.
This parameter influences the pitch_deg value.
"""
Expand Down
4 changes: 2 additions & 2 deletions drone_base/control/manual/keyboard_controller.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,8 +61,8 @@ def __init__(
logger_dir: str | Path | None = None,
):
"""
@param commander: The DroneCommander instance that will handle all drone operations.
@param speed_config: The speed configuration class of the drone. Contains speed details,
:param commander: The DroneCommander instance that will handle all drone operations.
:param speed_config: The speed configuration class of the drone. Contains speed details,
if not given it will use default speeds.
"""
# TODO: find a way to work around drone commander. ISSUE: Using ANAFI-AI (works on ANAFI4K)
Expand Down
8 changes: 4 additions & 4 deletions drone_base/stream/processing/streaming_metadata.py
Original file line number Diff line number Diff line change
Expand Up @@ -89,10 +89,10 @@ def compute_forward_distance_hybrid(data: list[dict], distance_threshold: float
"""
Compute the total distance traveled using both GPS and orientation data.

@param data: The data from the drone logs.
@param distance_threshold: Minimum distance to consider in meters.
@param window_size: How many distances to consider.
@return: The total distance traveled over the forward displacement.
:param data: The data from the drone logs.
:param distance_threshold: Minimum distance to consider in meters.
:param window_size: How many distances to consider.
:return: The total distance traveled over the forward displacement.
"""
total_distance = 0

Expand Down
6 changes: 5 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -68,4 +68,8 @@ ignore = [
[tool.pytest.ini_options]
testpaths = ["tests"]
python_files = "test_*.py"
python_functions = "test_*"
python_functions = "test_*"

[tool.coverage.run]
source = ["pyfileman"]
omit = ["tests/*", "examples/*", "drone_base/_version.py"]
Empty file added tests/__init__.py
Empty file.
Empty file added tests/config/__init__.py
Empty file.
131 changes: 131 additions & 0 deletions tests/config/test_drone.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
import unittest

from drone_base.config.drone import DroneSpeed, GimbalType, DroneIp


class TestDroneSpeed(unittest.TestCase):
def test_default_initialization(self):
"""Test that DroneSpeed initializes with default values correctly"""
drone_speed = DroneSpeed()
expected_defaults = {
"x": 20,
"y": 20,
"z": 20,
"z_rot": 20,
"dt": 0.1
}

for attr, expected_value in expected_defaults.items():
with self.subTest(attr=attr):
self.assertEqual(getattr(drone_speed, attr), expected_value)

def test_valid_custom_values(self):
"""Test initialization with valid custom values"""
custom_values = {
"x": 50,
"y": 75,
"z": 25,
"z_rot": 100,
"dt": 0.5
}

drone_speed = DroneSpeed(**custom_values)

for attr, expected_value in custom_values.items():
with self.subTest(attr=attr):
self.assertEqual(getattr(drone_speed, attr), expected_value)

def test_invalid_range_values(self):
"""Test that values outside the valid range raise ValueError with correct message"""
range_attrs = ["x", "y", "z", "z_rot"]
test_cases = []

for attr in range_attrs:
test_cases.extend([
(attr, -1, "must be between [0, 100], got \"-1\""),
(attr, 101, "must be between [0, 100], got \"101\"")
])

for attr_name, invalid_value, expected_msg in test_cases:
with self.subTest(attr=attr_name, value=invalid_value):
kwargs = {attr_name: invalid_value}
with self.assertRaises(ValueError) as context:
DroneSpeed(**kwargs)
self.assertIn(expected_msg, str(context.exception))

def test_invalid_dt(self):
"""Test that negative dt raises ValueError"""
with self.assertRaises(ValueError) as context:
DroneSpeed(dt=-0.1)
self.assertIn("Time step must be positive, got \"-0.1\"", str(context.exception))

def test_boundary_values(self):
"""Test that boundary values are accepted"""
boundary_cases = [
# (test_name, kwargs)
("lower_bounds", {"x": 0, "y": 0, "z": 0, "z_rot": 0, "dt": 0}),
("upper_bounds", {"x": 100, "y": 100, "z": 100, "z_rot": 100, "dt": 100})
]

for test_name, kwargs in boundary_cases:
with self.subTest(case=test_name):
drone_speed = DroneSpeed(**kwargs)
for attr, value in kwargs.items():
self.assertEqual(getattr(drone_speed, attr), value)

def test_partial_initialization(self):
"""Test that partial initialization works with defaults"""
partial_values = {"x": 75, "z_rot": 90}
expected_values = {
"x": 75,
"y": 20, # default
"z": 20, # default
"z_rot": 90,
"dt": 0.1 # default
}

drone_speed = DroneSpeed(**partial_values)

for attr, expected_value in expected_values.items():
with self.subTest(attr=attr):
self.assertEqual(getattr(drone_speed, attr), expected_value)


class TestGimbalType(unittest.TestCase):
def test_enum_values(self):
self.assertEqual(GimbalType.REF_ABSOLUTE.value, "absolute")
self.assertEqual(GimbalType.REF_RELATIVE.value, "relative")
self.assertEqual(GimbalType.MODE_POSITION.value, "position")
self.assertEqual(GimbalType.MODE_VELOCITY.value, "velocity")

def test_str_method(self):
self.assertEqual(str(GimbalType.REF_ABSOLUTE), "absolute")
self.assertEqual(str(GimbalType.REF_RELATIVE), "relative")
self.assertEqual(str(GimbalType.MODE_POSITION), "position")
self.assertEqual(str(GimbalType.MODE_VELOCITY), "velocity")

def test_enum_members(self):
expected_values = {"absolute", "relative", "position", "velocity"}
enum_values = {member.value for member in GimbalType}
self.assertEqual(enum_values, expected_values)


class TestDroneIp(unittest.TestCase):
def test_enum_values(self):
self.assertEqual(DroneIp.CABLE.value, "192.168.53.1")
self.assertEqual(DroneIp.WIRELESS.value, "192.168.42.1")
self.assertEqual(DroneIp.SIMULATED.value, "10.202.0.1")

def test_str_method(self):
self.assertEqual(str(DroneIp.CABLE), "192.168.53.1")
self.assertEqual(str(DroneIp.WIRELESS), "192.168.42.1")
self.assertEqual(str(DroneIp.SIMULATED), "10.202.0.1")

def test_enum_members(self):
expected_values = {"192.168.53.1", "192.168.42.1", "10.202.0.1"}
enum_values = {member.value for member in DroneIp}
self.assertEqual(enum_values, expected_values)


if __name__ == "__main__":
unittest.main()
Loading