Skip to content
Open
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
45 changes: 45 additions & 0 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
name: Tests

on:
push:
branches: [ main ]
pull_request:
branches: [ main ]

jobs:
test:
runs-on: ubuntu-24.04

env:
PYTHONPATH: ${{ github.workspace }}:${{ github.workspace }}/web

steps:
- name: Checkout code
uses: actions/checkout@v4

- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.10'

- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install pytest
pip install -r tests/requirements.txt
pip install -r web/requirements.txt

- name: Run tests
run: |
pytest tests/ -v --tb=short

- name: Run tests with coverage
run: |
pytest tests/ --cov=. --cov-report=term --cov-report=xml

- name: Upload coverage reports
uses: codecov/codecov-action@v4
if: success()
with:
file: ./coverage.xml
fail_ci_if_error: false
3 changes: 3 additions & 0 deletions pytest.ini
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
[pytest]
testpaths = tests
asyncio_mode = auto
5 changes: 5 additions & 0 deletions tests/requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@

pytest-asyncio==0.21.1
pytest-cov==4.1.0
httpx==0.25.2
pytest-mock==3.12.0
315 changes: 315 additions & 0 deletions tests/web/conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,315 @@
"""
Pytest configuration and shared fixtures for end-to-end tests.

This module provides fixtures for setting up test environment including:
- Test client with FastAPI app
- Mock git repository
- Predefined remotes.json to avoid version fetching
- Remote reload authentication tokens
"""
import os
import json
import tempfile
import shutil
from typing import Generator
from unittest.mock import Mock

import pytest
from fastapi.testclient import TestClient


# Sample remotes.json for testing - no version fetching needed
TEST_REMOTES_JSON = [
{
"name": "test-remote-1",
"url": "https://github.com/test/ardupilot.git",
"vehicles": [
{
"name": "Copter",
"releases": [
{
"release_type": "latest",
"version_number": "4.6.0",
"commit_reference": "refs/heads/master"
},
{
"release_type": "stable",
"version_number": "4.3.0",
"commit_reference": "refs/tags/Copter-4.3.0"
}
]
},
{
"name": "Plane",
"releases": [
{
"release_type": "latest",
"version_number": "4.5.0",
"commit_reference": "refs/heads/master"
}
]
}
]
},
{
"name": "test-remote-2",
"url": "https://github.com/another/ardupilot.git",
"vehicles": [
{
"name": "Rover",
"releases": [
{
"release_type": "Custom",
"version_number": "Custom",
"commit_reference": "refs/tags/Rover-4.2.0"
}
]
}
]
}
]


@pytest.fixture(scope="session")
def test_base_dir() -> Generator[str, None, None]:
"""
Create a temporary base directory structure for testing.

Yields:
str: Path to the temporary base directory
"""
temp_dir = tempfile.mkdtemp(prefix="custombuild_test_")

# Create required subdirectories
subdirs = ["artifacts", "configs", "workdir", "secrets", "ardupilot"]
for subdir in subdirs:
os.makedirs(os.path.join(temp_dir, subdir), exist_ok=True)

# Create remotes.json with test data
remotes_json_path = os.path.join(temp_dir, "configs", "remotes.json")
with open(remotes_json_path, "w") as f:
json.dump(TEST_REMOTES_JSON, f, indent=2)

# Create remote reload token file
token_file_path = os.path.join(temp_dir, "secrets", "reload_token")
with open(token_file_path, "w") as f:
f.write("test-remote-reload-token-12345")

yield temp_dir

# Cleanup
shutil.rmtree(temp_dir, ignore_errors=True)


@pytest.fixture(scope="session")
def test_remote_reload_token() -> str:
"""
Return the test remote reload token.

Returns:
str: Test remote reload authentication token
"""
return "test-remote-reload-token-12345"


@pytest.fixture
def mock_git_repo():
"""
Create a mock GitRepo object.

Returns:
Mock: Mock GitRepo instance
"""
mock_repo = Mock()
mock_repo.path = "/tmp/test/ardupilot"
mock_repo.get_current_commit_hash.return_value = "abc123def456"
mock_repo.checkout_commit.return_value = True
mock_repo.get_tags.return_value = ["Copter-4.3.0", "Copter-4.4.0"]
return mock_repo


@pytest.fixture
def mock_versions_fetcher(test_base_dir):
"""
Create a mock VersionsFetcher that doesn't actually fetch versions.

This allows tests to run without starting background threads or
making actual git operations.

Args:
test_base_dir: Test base directory fixture

Returns:
Mock: Mock VersionsFetcher instance
"""
from metadata_manager.versions_fetcher import RemoteInfo

mock_fetcher = Mock()

# Mock the reload_remotes_json method
mock_fetcher.reload_remotes_json = Mock(return_value=None)

# Mock get_all_remotes_info to return test remotes
test_remotes = [
RemoteInfo(name="test-remote-1", url="https://github.com/test/ardupilot.git"),
RemoteInfo(name="test-remote-2", url="https://github.com/another/ardupilot.git")
]
mock_fetcher.get_all_remotes_info = Mock(return_value=test_remotes)

# Mock start/stop methods (no-op for tests)
mock_fetcher.start = Mock()
mock_fetcher.stop = Mock()

return mock_fetcher


@pytest.fixture
def mock_build_manager():
"""
Create a mock BuildManager for testing.

Returns:
Mock: Mock BuildManager instance
"""
mock_manager = Mock()
mock_manager.submit_build = Mock(return_value="test-build-id-123")
mock_manager.get_build_progress = Mock(return_value={
"build_id": "test-build-id-123",
"status": "queued",
"progress": 0
})
return mock_manager


@pytest.fixture
def mock_vehicles_manager():
"""
Create a mock VehiclesManager for testing.

Returns:
Mock: Mock VehiclesManager instance
"""
mock_manager = Mock()
mock_manager.get_vehicle_names = Mock(return_value=["Copter", "Plane", "Rover"])
return mock_manager


@pytest.fixture
def mock_cleaner_and_updater():
"""
Create mock instances for BuildArtifactsCleaner and BuildProgressUpdater.

Returns:
tuple: (mock_cleaner, mock_updater)
"""
mock_cleaner = Mock()
mock_cleaner.start = Mock()
mock_cleaner.stop = Mock()

mock_updater = Mock()
mock_updater.start = Mock()
mock_updater.stop = Mock()

return mock_cleaner, mock_updater


@pytest.fixture
def app_with_mocked_dependencies(
test_base_dir,
mock_git_repo,
mock_versions_fetcher,
mock_build_manager,
mock_vehicles_manager,
mock_cleaner_and_updater
):
"""
Create a FastAPI app instance with mocked dependencies.

This fixture sets up the application without requiring actual:
- Git repository cloning
- Version fetching background tasks
- Redis connection
- Build artifacts

Args:
test_base_dir: Test base directory
mock_git_repo: Mock git repository
mock_versions_fetcher: Mock versions fetcher
mock_build_manager: Mock build manager
mock_vehicles_manager: Mock vehicles manager
mock_cleaner_and_updater: Mock cleaner and updater

Yields:
FastAPI: Configured FastAPI application instance
"""
from contextlib import asynccontextmanager
from fastapi import FastAPI
from api.v1 import router as v1_router

mock_cleaner, mock_updater = mock_cleaner_and_updater

# Set environment variables for test configuration
os.environ["CBS_BASEDIR"] = test_base_dir
os.environ["CBS_REDIS_HOST"] = "localhost"
os.environ["CBS_REDIS_PORT"] = "6379"
os.environ["CBS_ENABLE_INBUILT_BUILDER"] = "0" # Disable builder for tests

@asynccontextmanager
async def test_lifespan(app: FastAPI):
"""Test lifespan that doesn't start background tasks."""
# Setup: Attach mocked dependencies to app state
app.state.repo = mock_git_repo
app.state.versions_fetcher = mock_versions_fetcher
app.state.vehicles_manager = mock_vehicles_manager
app.state.build_manager = mock_build_manager

# Create mock AP source metadata fetcher
mock_ap_src_fetcher = Mock()
app.state.ap_src_metadata_fetcher = mock_ap_src_fetcher

# Don't start background tasks in test mode
# versions_fetcher.start() - SKIPPED
# cleaner.start() - SKIPPED
# progress_updater.start() - SKIPPED

yield

# Shutdown - also skipped for tests
# No cleanup needed since we didn't start anything

# Create app with test lifespan
app = FastAPI(title="CustomBuild Test API", lifespan=test_lifespan)

# Include routers
app.include_router(v1_router, prefix="/api")

return app


@pytest.fixture
def client(app_with_mocked_dependencies) -> Generator[TestClient, None, None]:
"""
Create a TestClient for making requests to the app.

Args:
app_with_mocked_dependencies: FastAPI app with mocked dependencies

Yields:
TestClient: Test client for making API requests
"""
with TestClient(app_with_mocked_dependencies) as test_client:
yield test_client


@pytest.fixture
def auth_headers(test_remote_reload_token) -> dict:
"""
Create authorization headers with bearer token.

Args:
test_remote_reload_token: Test remote reload token

Returns:
dict: Headers with Authorization bearer token
"""
return {"Authorization": f"Bearer {test_remote_reload_token}"}
Loading