Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
c20d130
Simplify scenario building
atti92 Nov 22, 2025
60283a8
remove unused
atti92 Nov 22, 2025
79efbf9
fix scenario names
atti92 Nov 22, 2025
1141c24
Make sure github job fails if scenarios fail.
atti92 Nov 22, 2025
87f3afe
update
atti92 Nov 22, 2025
5d46019
typo
atti92 Nov 22, 2025
0d7b230
Fix
atti92 Nov 22, 2025
07d77f6
disable F5 failing test in github
atti92 Nov 22, 2025
79cac1e
change PR run type
atti92 Nov 22, 2025
42f287f
Add healthcheck for docker
atti92 Nov 22, 2025
97baf5a
Update src/openutm_verification/scenarios/test_airtraffic_data_openut…
atti92 Nov 23, 2025
badb42e
fix linting warnings
atti92 Nov 23, 2025
711627f
Apply suggestions from code review
atti92 Nov 23, 2025
a22ad24
remove unused class
atti92 Nov 23, 2025
e481ed2
Merge branch 'main' into simplify-scenarios
atti92 Nov 23, 2025
d8bc0a0
Fix decorator typing issues
atti92 Nov 23, 2025
c1eeb8a
Fix generic
atti92 Nov 23, 2025
135fdac
fix python 3.12
atti92 Nov 23, 2025
ad2b9d0
Update src/openutm_verification/core/clients/flight_blender/flight_bl…
atti92 Nov 23, 2025
228099f
Update src/openutm_verification/scenarios/test_add_flight_declaration.py
atti92 Nov 23, 2025
c732015
Update src/openutm_verification/scenarios/test_geo_fence_upload.py
atti92 Nov 23, 2025
7e36de4
Update src/openutm_verification/scenarios/test_f5_flow.py
atti92 Nov 23, 2025
8f4ab56
Update src/openutm_verification/scenarios/test_opensky_live_data.py
atti92 Nov 23, 2025
a9c5456
Update src/openutm_verification/core/reporting/reporting_models.py
atti92 Nov 23, 2025
5f0fa67
cancel previous workflows on commit
atti92 Nov 23, 2025
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
9 changes: 7 additions & 2 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,14 @@ name: Integration Test

on:
pull_request:
types: [ready_for_review]
types: [opened, synchronize, reopened]
branches:
- main

concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true

jobs:
docker:
timeout-minutes: 30
Expand All @@ -21,7 +25,7 @@ jobs:
version: latest

- name: Start Flight Blender and dependencies
run: docker compose --env-file .env.tests -f docker-compose.fb.yml up -d
run: docker compose --env-file .env.tests -f docker-compose.fb.yml up -d --wait
working-directory: ./tests

- name: Install uv
Expand All @@ -34,6 +38,7 @@ jobs:
run: uv run openutm-verify --debug --config config/default.yaml

- uses: actions/upload-artifact@v4
if: always()
with:
name: test-reports
path: reports/
Expand Down
23 changes: 12 additions & 11 deletions config/default.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -37,18 +37,19 @@ data_files:

# List of test scenario IDs to execute
scenarios:
# "F1_happy_path":
# telemetry: "config/bern/telemetry_f1.json"
# "F2_contingent_path":
# telemetry: "config/bern/telemetry_f2.json"
# "F3_non_conforming_path":
# telemetry: "config/bern/telemetry_f3.json"
# "F5_non_conforming_path":
F1_happy_path:
telemetry: "config/bern/telemetry_f1.json"
F2_contingent_path:
telemetry: "config/bern/telemetry_f2.json"
F3_non_conforming_path:
telemetry: "config/bern/telemetry_f3.json"
# F5_non_conforming_path:
# telemetry: "config/bern/telemetry_f5.json"
# "opensky_live_data":
# "add_flight_declaration":
# "geo_fence_upload":
# "sdsp_track_heartbeat":
opensky_live_data:
add_flight_declaration:
geo_fence_upload:
# sdsp_track:
# sdsp_heartbeat:
openutm_sim_air_traffic_data:

# Reporting configuration
Expand Down
4 changes: 3 additions & 1 deletion src/openutm_verification/cli/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
Command Line Interface for OpenUTM Verification Tool.
"""

import sys
from datetime import datetime, timezone
from pathlib import Path

Expand Down Expand Up @@ -41,12 +42,13 @@ def main():
log_file = setup_logging(output_dir, base_filename, config.reporting.formats, args.debug)

# Run verification scenarios
run_verification_scenarios(config, args.config)
failed = run_verification_scenarios(config, args.config)

if log_file:
from loguru import logger

logger.info(f"Log file saved to: {log_file}")
sys.exit(failed)


if __name__ == "__main__":
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import json
import time
import uuid
from contextlib import contextmanager
from dataclasses import asdict
from typing import Any, Dict, List, Optional

Expand All @@ -10,8 +11,12 @@
from openutm_verification.core.clients.flight_blender.base_client import (
BaseBlenderAPIClient,
)
from openutm_verification.core.execution.config_models import DataFiles
from openutm_verification.core.execution.scenario_runner import scenario_step
from openutm_verification.core.reporting.reporting_models import Status, StepResult
from openutm_verification.core.reporting.reporting_models import (
Status,
StepResult,
)
from openutm_verification.models import (
FlightBlenderError,
HeartbeatMessage,
Expand Down Expand Up @@ -75,6 +80,8 @@ def __init__(self, base_url: str, credentials: Dict[str, Any], request_timeout:
self.latest_geo_fence_id: Optional[str] = None
# Context: store the most recently created flight declaration id for teardown/steps
self.latest_flight_declaration_id: Optional[str] = None
# Context: store the generated telemetry states for the current scenario
self.telemetry_states: Optional[List[Dict[str, Any]]] = None
logger.debug(f"Initialized FlightBlenderClient with base_url={base_url}, request_timeout={request_timeout}")

def __exit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None:
Expand All @@ -89,11 +96,10 @@ def __exit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None:
return super().__exit__(exc_type, exc_val, exc_tb)

@scenario_step("Upload Geo Fence")
def upload_geo_fence(self, operation_id: Optional[str] = None, filename: Optional[str] = None) -> Dict[str, Any]:
def upload_geo_fence(self, filename: Optional[str] = None) -> Dict[str, Any]:
"""Upload an Area-of-Interest (Geo Fence) to Flight Blender.

Args:
operation_id: Not used for geo-fence upload (included for API consistency).
filename: Path to the GeoJSON file containing the geo-fence definition.

Returns:
Expand Down Expand Up @@ -121,12 +127,9 @@ def upload_geo_fence(self, operation_id: Optional[str] = None, filename: Optiona
return body

@scenario_step("Get Geo Fence")
def get_geo_fence(self, operation_id: Optional[str] = None) -> Dict[str, Any]:
def get_geo_fence(self) -> Dict[str, Any]:
"""Retrieve the details of the most recently uploaded geo-fence.

Args:
operation_id: Not used for geo-fence retrieval (included for API consistency).

Returns:
The JSON response from the API containing geo-fence details, or a dict
indicating skip if no geo-fence ID is available.
Expand Down Expand Up @@ -237,13 +240,12 @@ def upload_flight_declaration(self, declaration: str | Any) -> Dict[str, Any]:
return response_json

@scenario_step("Update Operation State")
def update_operation_state(self, operation_id: str, new_state: OperationState, duration_seconds: int = 0) -> Dict[str, Any]:
def update_operation_state(self, new_state: OperationState, duration_seconds: int = 0) -> Dict[str, Any]:
"""Update the state of a flight operation.

Posts the new state and optionally waits for the specified duration.

Args:
operation_id: The ID of the operation to update.
new_state: The new OperationState to set.
duration_seconds: Optional seconds to sleep after update (default 0).

Expand All @@ -253,12 +255,12 @@ def update_operation_state(self, operation_id: str, new_state: OperationState, d
Raises:
FlightBlenderError: If the update request fails.
"""
endpoint = f"/flight_declaration_ops/flight_declaration_state/{operation_id}"
logger.debug(f"Updating operation {operation_id} to state {new_state.name}")
endpoint = f"/flight_declaration_ops/flight_declaration_state/{self.latest_flight_declaration_id}"
logger.debug(f"Updating operation {self.latest_flight_declaration_id} to state {new_state.name}")
payload = {"state": new_state.value, "submitted_by": "hh@auth.com"}

response = self.put(endpoint, json=payload)
logger.info(f"Operation state updated for {operation_id} to {new_state.name}")
logger.info(f"Operation state updated for {self.latest_flight_declaration_id} to {new_state.name}")
if duration_seconds > 0:
logger.debug(f"Sleeping for {duration_seconds} seconds after state update")
time.sleep(duration_seconds)
Expand All @@ -281,11 +283,10 @@ def _load_telemetry_file(self, filename: str) -> List[Dict[str, Any]]:
rid_json = json.loads(rid_json_file.read())
return rid_json["current_states"]

def _submit_telemetry_states_impl(self, operation_id: str, states: List[Dict[str, Any]], duration_seconds: int = 0) -> Optional[Dict[str, Any]]:
def _submit_telemetry_states_impl(self, states: List[Dict[str, Any]], duration_seconds: int = 0) -> Optional[Dict[str, Any]]:
"""Internal implementation for submitting telemetry states.

Args:
operation_id: The ID of the operation for telemetry submission.
states: List of telemetry state dictionaries.
duration_seconds: Optional maximum duration in seconds to submit telemetry (default 0 for unlimited).

Expand All @@ -296,9 +297,9 @@ def _submit_telemetry_states_impl(self, operation_id: str, states: List[Dict[str
FlightBlenderError: If maximum waiting time is exceeded due to rate limits.
"""
endpoint = "/flight_stream/set_telemetry"
logger.debug(f"Submitting telemetry for operation {operation_id}")
logger.debug(f"Submitting telemetry for operation {self.latest_flight_declaration_id}")

rid_operator_details = _create_rid_operator_details(operation_id)
rid_operator_details = _create_rid_operator_details(self.latest_flight_declaration_id)

last_response = None
maximum_waiting_time = 10.0
Expand Down Expand Up @@ -337,14 +338,13 @@ def _submit_telemetry_states_impl(self, operation_id: str, states: List[Dict[str
return last_response

@scenario_step("Submit Telemetry (from file)")
def submit_telemetry_from_file(self, operation_id: str, filename: str, duration_seconds: int = 0) -> Optional[Dict[str, Any]]:
def submit_telemetry_from_file(self, filename: str, duration_seconds: int = 0) -> Optional[Dict[str, Any]]:
"""Submit telemetry data for a flight operation.

Loads telemetry states from file and submits them sequentially, with optional
duration limiting and error handling for rate limits.

Args:
operation_id: The ID of the operation for telemetry submission.
filename: Path to the JSON file containing telemetry data.
duration_seconds: Optional maximum duration in seconds to submit telemetry (default 0 for unlimited).

Expand All @@ -355,7 +355,7 @@ def submit_telemetry_from_file(self, operation_id: str, filename: str, duration_
FlightBlenderError: If maximum waiting time is exceeded due to rate limits.
"""
states = self._load_telemetry_file(filename)
return self._submit_telemetry_states_impl(operation_id, states, duration_seconds)
return self._submit_telemetry_states_impl(states, duration_seconds)

@scenario_step("Wait X seconds")
def wait_x_seconds(self, wait_time_seconds: int = 5) -> str:
Expand All @@ -366,15 +366,14 @@ def wait_x_seconds(self, wait_time_seconds: int = 5) -> str:
return f"Waited for Flight Blender to process {wait_time_seconds} seconds."

@scenario_step("Submit Telemetry")
def submit_telemetry(self, operation_id: str, states: List[Dict[str, Any]], duration_seconds: int = 0) -> Optional[Dict[str, Any]]:
def submit_telemetry(self, states: Optional[List[Dict[str, Any]]] = None, duration_seconds: int = 0) -> Optional[Dict[str, Any]]:
"""Submit telemetry data for a flight operation from in-memory states.

Submits telemetry states sequentially from the provided list, with optional
duration limiting and error handling for rate limits.

Args:
operation_id: The ID of the operation for telemetry submission.
states: List of telemetry state dictionaries.
states: List of telemetry state dictionaries. If None, uses the generated telemetry states from context.
duration_seconds: Optional maximum duration in seconds to submit telemetry (default 0 for unlimited).

Returns:
Expand All @@ -383,12 +382,15 @@ def submit_telemetry(self, operation_id: str, states: List[Dict[str, Any]], dura
Raises:
FlightBlenderError: If maximum waiting time is exceeded due to rate limits.
"""
return self._submit_telemetry_states_impl(operation_id, states, duration_seconds)
telemetry_states = states or self.telemetry_states
if telemetry_states is None:
raise ValueError("Telemetry states are required and could not be resolved from context.")

return self._submit_telemetry_states_impl(telemetry_states, duration_seconds)

@scenario_step("Check Operation State")
def check_operation_state(
self,
operation_id: str,
expected_state: OperationState,
duration_seconds: int = 0,
) -> str:
Expand All @@ -398,30 +400,27 @@ def check_operation_state(
and returns a success status.

Args:
operation_id: The ID of the operation to check.
expected_state: The expected OperationState.
duration_seconds: Seconds to wait for processing.

Returns:
A dictionary with the check result.
"""
logger.info(f"Checking operation state for {operation_id} (simulated)...")
logger.info(f"Checking operation state for {self.latest_flight_declaration_id} (simulated)...")
logger.info(f"Waiting for {duration_seconds} seconds for Flight Blender to process state...")
time.sleep(duration_seconds)
logger.info(f"Flight state check for {operation_id} completed (simulated).")
logger.info(f"Flight state check for {self.latest_flight_declaration_id} completed (simulated).")
return f"Waited for Flight Blender to process {expected_state} state."

@scenario_step("Check Operation State Connected")
def check_operation_state_connected(
self,
operation_id: str,
expected_state: OperationState,
duration_seconds: int = 0,
) -> Dict[str, Any]:
"""Check the operation state by polling the API until the expected state is reached.

Args:
operation_id: The ID of the operation to check.
expected_state: The expected OperationState.
duration_seconds: Maximum seconds to poll for the state.

Expand All @@ -431,36 +430,36 @@ def check_operation_state_connected(
Raises:
FlightBlenderError: If the expected state is not reached within the timeout.
"""
endpoint = f"/flight_declaration_ops/flight_declaration/{operation_id}"
logger.info(f"Checking operation state for {operation_id}, expecting {expected_state.name}")
endpoint = f"/flight_declaration_ops/flight_declaration/{self.latest_flight_declaration_id}"
logger.info(f"Checking operation state for {self.latest_flight_declaration_id}, expecting {expected_state.name}")
start_time = time.time()

while time.time() - start_time < duration_seconds:
response = self.get(endpoint)
data = response.json()
current_state_value = data.get("state")
logger.debug(f"Current state for {operation_id}: {current_state_value}")
logger.debug(f"Current state for {self.latest_flight_declaration_id}: {current_state_value}")
if current_state_value == expected_state.value:
logger.info(f"Operation {operation_id} reached expected state {expected_state.name}")
logger.info(f"Operation {self.latest_flight_declaration_id} reached expected state {expected_state.name}")
return data

time.sleep(1)

logger.error(f"Operation {operation_id} did not reach expected state {expected_state.name} within {duration_seconds} seconds")
raise FlightBlenderError(f"Operation {operation_id} did not reach expected state {expected_state.name} within {duration_seconds} seconds")
logger.error(
f"Operation {self.latest_flight_declaration_id} did not reach expected state {expected_state.name} within {duration_seconds} seconds"
)
raise FlightBlenderError(
f"Operation {self.latest_flight_declaration_id} did not reach expected state {expected_state.name} within {duration_seconds} seconds"
)

@scenario_step("Delete Flight Declaration")
def delete_flight_declaration(self, operation_id: Optional[str] = None) -> Dict[str, Any]:
def delete_flight_declaration(self) -> Dict[str, Any]:
"""Delete a flight declaration by ID.

Args:
operation_id: Optional ID of the flight declaration to delete. If not provided,
uses the latest uploaded flight declaration ID.

Returns:
A dictionary with deletion status, including whether it was successful.
"""
op_id = operation_id or self.latest_flight_declaration_id
op_id = self.latest_flight_declaration_id
if not op_id:
logger.warning("No flight declaration ID available for deletion")
return {
Expand Down Expand Up @@ -600,6 +599,7 @@ def initialize_heartbeat_websocket_connection(self, session_id: str) -> Any:
ws = self.create_websocket_connection(endpoint=endpoint)
return ws

@scenario_step("Verify SDSP Track")
def initialize_verify_sdsp_track(
self,
expected_heartbeat_interval_seconds: int,
Expand Down Expand Up @@ -672,6 +672,7 @@ def initialize_verify_sdsp_track(
duration=duration,
)

@scenario_step("Verify SDSP Heartbeat")
def initialize_verify_sdsp_heartbeat(
self,
expected_heartbeat_interval_seconds: int,
Expand Down Expand Up @@ -746,3 +747,31 @@ def initialize_verify_sdsp_heartbeat(

def close_heartbeat_websocket_connection(self, ws_connection: Any) -> None:
ws_connection.close()

@scenario_step("Setup Flight Declaration")
def setup_flight_declaration(self, flight_declaration_path: str, telemetry_path: str) -> None:
"""Generates data and uploads flight declaration."""
from openutm_verification.scenarios.common import (
generate_flight_declaration,
generate_telemetry,
)

flight_declaration = generate_flight_declaration(flight_declaration_path)
telemetry_states = generate_telemetry(telemetry_path)

self.telemetry_states = telemetry_states

upload_result = self.upload_flight_declaration(flight_declaration)

if upload_result.status == Status.FAIL:
Comment thread
atti92 marked this conversation as resolved.
logger.error(f"Flight declaration upload failed: {upload_result}")
raise FlightBlenderError("Failed to upload flight declaration during setup_flight_declaration")

@contextmanager
def flight_declaration(self, data_files: DataFiles):
"""Context manager to setup and teardown a flight operation based on scenario config."""
self.setup_flight_declaration(data_files.flight_declaration, data_files.telemetry)
try:
yield
finally:
self.delete_flight_declaration()
Comment on lines +776 to +777

Copilot AI Nov 23, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The context manager calls delete_flight_declaration() in the finally block without any error handling. If the deletion fails (e.g., due to network issues or if the declaration wasn't successfully created), it could mask the original error or cause confusion. Consider:

  1. Catching and logging exceptions in the finally block
  2. Only attempting deletion if self.latest_flight_declaration_id is set
  3. Not raising exceptions from the finally block to preserve the original error context

Copilot uses AI. Check for mistakes.
Loading