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
64 changes: 0 additions & 64 deletions .basedpyright/baseline.json
Original file line number Diff line number Diff line change
Expand Up @@ -12885,22 +12885,6 @@
"lineCount": 1
}
},
{
"code": "reportArgumentType",
"range": {
"startColumn": 52,
"endColumn": 68,
"lineCount": 1
}
},
{
"code": "reportArgumentType",
"range": {
"startColumn": 50,
"endColumn": 66,
"lineCount": 1
}
},
{
"code": "reportOptionalMemberAccess",
"range": {
Expand All @@ -12916,22 +12900,6 @@
"endColumn": 30,
"lineCount": 1
}
},
{
"code": "reportArgumentType",
"range": {
"startColumn": 50,
"endColumn": 66,
"lineCount": 1
}
},
{
"code": "reportUnusedExpression",
"range": {
"startColumn": 8,
"endColumn": 80,
"lineCount": 1
}
}
],
"./monitoring/uss_qualifier/scenarios/astm/utm/data_exchange_validation/test_steps/expected_interactions_test_steps.py": [
Expand Down Expand Up @@ -16997,14 +16965,6 @@
"lineCount": 1
}
},
{
"code": "reportArgumentType",
"range": {
"startColumn": 53,
"endColumn": 62,
"lineCount": 1
}
},
{
"code": "reportOptionalMemberAccess",
"range": {
Expand Down Expand Up @@ -17178,14 +17138,6 @@
"endColumn": 31,
"lineCount": 1
}
},
{
"code": "reportArgumentType",
"range": {
"startColumn": 46,
"endColumn": 61,
"lineCount": 1
}
}
],
"./monitoring/uss_qualifier/scenarios/astm/utm/nominal_planning/conflict_higher_priority/assets/make_assets.py": [
Expand Down Expand Up @@ -17319,14 +17271,6 @@
}
],
"./monitoring/uss_qualifier/scenarios/astm/utm/nominal_planning/conflict_higher_priority/conflict_higher_priority.py": [
{
"code": "reportArgumentType",
"range": {
"startColumn": 50,
"endColumn": 65,
"lineCount": 1
}
},
{
"code": "reportArgumentType",
"range": {
Expand All @@ -17335,14 +17279,6 @@
"lineCount": 1
}
},
{
"code": "reportArgumentType",
"range": {
"startColumn": 50,
"endColumn": 65,
"lineCount": 1
}
},
{
"code": "reportArgumentType",
"range": {
Expand Down
1 change: 1 addition & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ This document contains key context, nuances, and troubleshooting tips specifical
- **Documentation Traceability**: All documented test checks must trace back to exactly one or more requirements using a specific bold format, and feature a severity emoji prefix (e.g., `## 🛑 Correct operational intent details check`). You must refer to `monitoring/uss_qualifier/scenarios/README.md` for specific markup details before modifying test steps.

## 4. Local Testing constraints
- **Unit-test working directory**: Run `uss_qualifier` unit tests from `monitoring/uss_qualifier/` with the repository root on `PYTHONPATH` (for example, `PYTHONPATH=../.. ../../.venv/bin/python -m pytest scenarios/scenario_test resources/flight_planning`). Some resource fixtures resolve test-data paths relative to the working directory and fail when run from the repository root.
- **Qualifier Working Directory**: Run resource-construction checks and qualifier resource tests from `monitoring/uss_qualifier` with the repository root on `PYTHONPATH`. External resource paths such as `file://./test_data/...` resolve relative to the current working directory, as they do in `run_locally.sh`.
- **Docker Dependency**: Mock USS and DSS environments require active Docker containers. Standard testing commands are typically structured via bash scripts like `./monitoring/uss_qualifier/run_locally.sh <config>`. If container-building fails due to `Authentication` or package registry issues in the agent's environment, gracefully halt and ask the human user to run the script instead.

Expand Down
7 changes: 5 additions & 2 deletions monitoring/monitorlib/clients/flight_planning/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,11 +26,9 @@ class FlightPlannerClient(ABC):
"""Client to interact with a USS as a user performing flight planning activities and as the test director preparing for tests involving flight planning activities."""

participant_id: ParticipantID
created_flight_ids: set[FlightID]

def __init__(self, participant_id: ParticipantID):
self.participant_id = participant_id
self.created_flight_ids = set()

# ===== Emulation of user actions =====

Expand All @@ -40,9 +38,14 @@ def try_plan_flight(
flight_info: FlightInfo,
execution_style: ExecutionStyle,
additional_fields: dict | None = None,
*,
flight_id: FlightID | None = None,
) -> PlanningActivityResponse:
"""Instruct the USS to emulate a normal user trying to plan the described flight.

If flight_id is provided, use it for the new flight; otherwise generate an ID.
Callers can provide an ID to track the flight even if the request fails.

Raises:
* PlanningActivityError
"""
Expand Down
19 changes: 7 additions & 12 deletions monitoring/monitorlib/clients/flight_planning/client_scd.py
Original file line number Diff line number Diff line change
Expand Up @@ -150,15 +150,6 @@ def _inject(
# As a mitigation, if the USS responds with ReadyToFly for an off-nominal injected flight, we assume that the USS responded correctly.
response.flight_plan_status = FlightPlanStatus.OffNominal

created_status = [
FlightPlanStatus.Planned,
FlightPlanStatus.OkToFly,
FlightPlanStatus.OffNominal,
]
if response.activity_result == PlanningActivityResult.Completed:
if response.flight_plan_status in created_status:
self.created_flight_ids.add(flight_id)

if query.response.json and "as_planned" in query.response.json:
# Make best effort to interpret additional `as_planned` field according to flight_planning API as an ad-hoc
# retrofit to the legacy scd injection API
Expand All @@ -181,9 +172,14 @@ def try_plan_flight(
flight_info: FlightInfo,
execution_style: ExecutionStyle,
additional_fields: dict | None = None,
*,
flight_id: FlightID | None = None,
) -> PlanningActivityResponse:
return self._inject(
str(uuid.uuid4()), flight_info, execution_style, additional_fields
flight_id or str(uuid.uuid4()),
flight_info,
execution_style,
additional_fields,
)

def try_update_flight(
Expand Down Expand Up @@ -243,8 +239,7 @@ def try_end_flight(
notes=resp.notes if "notes" in resp else None,
)
if resp.result == scd_api.DeleteFlightResponseResult.Closed:
del self._plan_statuses[flight_id]
self.created_flight_ids.discard(flight_id)
self._plan_statuses.pop(flight_id, None)

else:
self._plan_statuses[flight_id] = response.flight_plan_status
Expand Down
24 changes: 6 additions & 18 deletions monitoring/monitorlib/clients/flight_planning/client_v1.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@
from monitoring.monitorlib.clients.flight_planning.planning import (
AdvisoryInclusion,
Conflict,
FlightPlanStatus,
PlanningActivityResponse,
QueryUserNotificationsResponse,
UserNotification,
Expand Down Expand Up @@ -55,13 +54,6 @@ def _inject(
for k, v in additional_fields.items():
req[k] = v

# We record the flight regardless of the outcome of the query that will be executed
# below. This should ward off unexpected exceptions, timeouts or error responses returned
# by the server despite the flight having been created.
# The cleanup logic supports cleanup attempts for flights that do not exist.
# In some case, the flight is removed later in this function, see comments bellow.
self.created_flight_ids.add(flight_plan_id)

op = api.OPERATIONS[api.OperationID.UpsertFlightPlan]
url = op.path.format(flight_plan_id=flight_plan_id)
query = query_and_describe(
Expand Down Expand Up @@ -103,24 +95,21 @@ def _inject(
if "as_planned" in resp and resp.as_planned:
response.as_planned = FlightInfo.from_flight_plan(resp.as_planned)

# If we know that the flight was successfully not created
# (the server explicitly refused to), we remove it from set of flights.
# That the only case when we do this, if we recieve no response after a
# timeout, the flight may still have been created (and cleanup_flights
# handle gracefully such cases).
if resp.flight_plan_status == FlightPlanStatus.NotPlanned:
self.created_flight_ids.remove(flight_plan_id)

return response

def try_plan_flight(
self,
flight_info: FlightInfo,
execution_style: ExecutionStyle,
additional_fields: dict | None = None,
*,
flight_id: FlightID | None = None,
) -> PlanningActivityResponse:
return self._inject(
str(uuid.uuid4()), flight_info, execution_style, additional_fields
flight_id or str(uuid.uuid4()),
flight_info,
execution_style,
additional_fields,
)

def try_update_flight(
Expand Down Expand Up @@ -167,7 +156,6 @@ def try_end_flight(
query,
)

self.created_flight_ids.discard(flight_id)
response = PlanningActivityResponse(
flight_id=flight_id,
queries=[query],
Expand Down
4 changes: 4 additions & 0 deletions monitoring/uss_qualifier/scenarios/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,10 @@ See [CONTRIBUTING.md](../../../CONTRIBUTING.md#ussqualifier-test-scenarios) for

Scenarios should avoid delays when possible as automated tests are more valuable when they run more quickly. When delays are necessary, only use `<TestScenario>.sleep` -- do not use `time.sleep` nor `monitorlib.delay.sleep` nor any other means to cause an intentional delay. The use of `<TestScenario>.sleep` allows delays to be more easily tracked and audited as they are often a prime target of interest when attempting to reduce automated testing run time.

### Flight planning state

Scenarios using flight planning helpers should create a separate (owned by scenario) `FlightPlanningState` instance and pass it to each helper via the `flight_planning_state` keyword argument, including `cleanup_flights`. The state tracks flights for all planners used by that scenario. Use the state's `plan_flight`, `update_flight`, and `end_flight` methods when issuing planning activities directly so cleanup can track flights even when requests fail.

## Resources

Most test scenarios will require [test resources](../resources/README.md) (like NetRID telemetry to inject, NetRID service providers under test, etc) usually customized to the ecosystem in which the tests are being performed. A test scenario declares what kind of resource(s) it requires, and a test suite identifies which available resources should be used to fulfill each test scenario's needs.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@
OpIntentValidationFailureType,
OpIntentValidator,
)
from monitoring.uss_qualifier.scenarios.flight_planning.state import FlightPlanningState
from monitoring.uss_qualifier.scenarios.flight_planning.test_steps import (
cleanup_flights,
delete_flight,
Expand Down Expand Up @@ -76,6 +77,7 @@ def __init__(
flight_intents: FlightIntentsResource | None = None,
):
super().__init__()
self.flight_planning_state = FlightPlanningState()
self.tested_uss_client = tested_uss.client
self.mock_uss = mock_uss.mock_uss
self.mock_uss_client = mock_uss.mock_uss.flight_planner
Expand Down Expand Up @@ -159,6 +161,7 @@ def _plan_successfully_test_case(self):
self,
self.mock_uss_client,
flight_2,
flight_planning_state=self.flight_planning_state,
)
# TODO(#1326): Validate that flight as planned still allows this scenario to proceed
flight_2 = as_planned
Expand All @@ -181,6 +184,7 @@ def _plan_successfully_test_case(self):
self,
self.tested_uss_client,
flight_1,
flight_planning_state=self.flight_planning_state,
)
# TODO(#1326): Validate that flight as planned still allows this scenario to proceed
flight_1 = as_planned
Expand Down Expand Up @@ -214,11 +218,23 @@ def _plan_successfully_test_case(self):
self.end_test_step()

self.begin_test_step("Delete tested_uss flight")
delete_flight(self, self.tested_uss_client, self.flight_1_id)
assert self.flight_1_id is not None
delete_flight(
self,
self.tested_uss_client,
self.flight_1_id,
flight_planning_state=self.flight_planning_state,
)
self.end_test_step()

self.begin_test_step("Delete mock_uss flight")
delete_flight(self, self.mock_uss_client, self.flight_2_id)
assert self.flight_2_id is not None
delete_flight(
self,
self.mock_uss_client,
self.flight_2_id,
flight_planning_state=self.flight_planning_state,
)
self.end_test_step()

def _plan_unsuccessfully_test_case(self):
Expand Down Expand Up @@ -251,6 +267,7 @@ def _plan_unsuccessfully_test_case(self):
self.mock_uss_client,
flight_info,
additional_fields,
flight_planning_state=self.flight_planning_state,
)
# TODO(#1326): Validate that flight as planned still allows this scenario to proceed
flight_info = as_planned
Expand Down Expand Up @@ -281,6 +298,7 @@ def _plan_unsuccessfully_test_case(self):
{},
self.tested_uss_client,
flight_1,
flight_planning_state=self.flight_planning_state,
)
validator.expect_not_shared()
self.end_test_step()
Expand Down Expand Up @@ -310,10 +328,20 @@ def _plan_unsuccessfully_test_case(self):
self.end_test_step()

self.begin_test_step("Delete mock_uss flight")
delete_flight(self, self.mock_uss_client, self.flight_2_id)
assert self.flight_2_id is not None
delete_flight(
self,
self.mock_uss_client,
self.flight_2_id,
flight_planning_state=self.flight_planning_state,
)
self.end_test_step()

def cleanup(self):
self.begin_cleanup()
(cleanup_flights(self, (self.mock_uss_client, self.tested_uss_client)),)
cleanup_flights(
self,
(self.mock_uss_client, self.tested_uss_client),
flight_planning_state=self.flight_planning_state,
)
self.end_cleanup()
Loading
Loading