From 963d778a6f5128b1a04a31f9c4f5f6e752992c44 Mon Sep 17 00:00:00 2001 From: Benjamin Pelletier Date: Thu, 8 Oct 2026 09:10:19 +0900 Subject: [PATCH] Move planned-flights state out of flight planner client --- .basedpyright/baseline.json | 64 --------------- AGENTS.md | 1 + .../clients/flight_planning/client.py | 7 +- .../clients/flight_planning/client_scd.py | 19 ++--- .../clients/flight_planning/client_v1.py | 24 ++---- monitoring/uss_qualifier/scenarios/README.md | 4 + .../get_op_data_validation.py | 36 +++++++- .../flight_intent_validation.py | 19 ++++- .../conflict_equal_priority_not_permitted.py | 27 +++++- .../conflict_higher_priority.py | 28 ++++++- .../planning_sequence_scenario.py | 8 +- .../astm/utm/off_nominal_planning/down_uss.py | 7 +- .../down_uss_equal_priority_not_permitted.py | 3 + .../receive_notifications_for_awareness.py | 12 ++- .../scd_authentication_validation.py | 9 +- .../prioritization_test_steps.py | 25 ++++++ .../scenarios/flight_planning/state.py | 82 +++++++++++++++++++ .../scenarios/flight_planning/test_steps.py | 66 +++++++++++---- .../general_flight_authorization.py | 12 ++- .../uspace/flight_auth/validation.py | 8 +- 20 files changed, 330 insertions(+), 131 deletions(-) create mode 100644 monitoring/uss_qualifier/scenarios/flight_planning/state.py diff --git a/.basedpyright/baseline.json b/.basedpyright/baseline.json index b7c5b45a2f8..0e966f8600b 100644 --- a/.basedpyright/baseline.json +++ b/.basedpyright/baseline.json @@ -12901,22 +12901,6 @@ "lineCount": 1 } }, - { - "code": "reportArgumentType", - "range": { - "startColumn": 52, - "endColumn": 68, - "lineCount": 1 - } - }, - { - "code": "reportArgumentType", - "range": { - "startColumn": 50, - "endColumn": 66, - "lineCount": 1 - } - }, { "code": "reportOptionalMemberAccess", "range": { @@ -12932,22 +12916,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": [ @@ -17013,14 +16981,6 @@ "lineCount": 1 } }, - { - "code": "reportArgumentType", - "range": { - "startColumn": 53, - "endColumn": 62, - "lineCount": 1 - } - }, { "code": "reportOptionalMemberAccess", "range": { @@ -17194,14 +17154,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": [ @@ -17335,14 +17287,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": { @@ -17351,14 +17295,6 @@ "lineCount": 1 } }, - { - "code": "reportArgumentType", - "range": { - "startColumn": 50, - "endColumn": 65, - "lineCount": 1 - } - }, { "code": "reportArgumentType", "range": { diff --git a/AGENTS.md b/AGENTS.md index 81e8dfe64a6..c86534dda8c 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -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. - **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 `. 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. ## 5. Continuous Improvement of this Guide diff --git a/monitoring/monitorlib/clients/flight_planning/client.py b/monitoring/monitorlib/clients/flight_planning/client.py index f4186f4e1bf..5bc1907d421 100644 --- a/monitoring/monitorlib/clients/flight_planning/client.py +++ b/monitoring/monitorlib/clients/flight_planning/client.py @@ -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 ===== @@ -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 """ diff --git a/monitoring/monitorlib/clients/flight_planning/client_scd.py b/monitoring/monitorlib/clients/flight_planning/client_scd.py index 85837d1849d..a2d61c2535b 100644 --- a/monitoring/monitorlib/clients/flight_planning/client_scd.py +++ b/monitoring/monitorlib/clients/flight_planning/client_scd.py @@ -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 @@ -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( @@ -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 diff --git a/monitoring/monitorlib/clients/flight_planning/client_v1.py b/monitoring/monitorlib/clients/flight_planning/client_v1.py index eedb47997c6..6dc07b03c2b 100644 --- a/monitoring/monitorlib/clients/flight_planning/client_v1.py +++ b/monitoring/monitorlib/clients/flight_planning/client_v1.py @@ -17,7 +17,6 @@ from monitoring.monitorlib.clients.flight_planning.planning import ( AdvisoryInclusion, Conflict, - FlightPlanStatus, PlanningActivityResponse, QueryUserNotificationsResponse, UserNotification, @@ -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( @@ -103,14 +95,6 @@ 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( @@ -118,9 +102,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( @@ -167,7 +156,6 @@ def try_end_flight( query, ) - self.created_flight_ids.discard(flight_id) response = PlanningActivityResponse( flight_id=flight_id, queries=[query], diff --git a/monitoring/uss_qualifier/scenarios/README.md b/monitoring/uss_qualifier/scenarios/README.md index f1a13bdfb8e..75ff19a2cd5 100644 --- a/monitoring/uss_qualifier/scenarios/README.md +++ b/monitoring/uss_qualifier/scenarios/README.md @@ -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 `.sleep` -- do not use `time.sleep` nor `monitorlib.delay.sleep` nor any other means to cause an intentional delay. The use of `.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. diff --git a/monitoring/uss_qualifier/scenarios/astm/utm/data_exchange_validation/get_op_data_validation.py b/monitoring/uss_qualifier/scenarios/astm/utm/data_exchange_validation/get_op_data_validation.py index bdeb6dc00d6..7f6aba72534 100644 --- a/monitoring/uss_qualifier/scenarios/astm/utm/data_exchange_validation/get_op_data_validation.py +++ b/monitoring/uss_qualifier/scenarios/astm/utm/data_exchange_validation/get_op_data_validation.py @@ -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, @@ -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 @@ -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 @@ -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 @@ -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): @@ -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 @@ -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() @@ -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() diff --git a/monitoring/uss_qualifier/scenarios/astm/utm/flight_intent_validation/flight_intent_validation.py b/monitoring/uss_qualifier/scenarios/astm/utm/flight_intent_validation/flight_intent_validation.py index f17615dc284..c1fba0e19a5 100644 --- a/monitoring/uss_qualifier/scenarios/astm/utm/flight_intent_validation/flight_intent_validation.py +++ b/monitoring/uss_qualifier/scenarios/astm/utm/flight_intent_validation/flight_intent_validation.py @@ -31,6 +31,7 @@ FlightPlannerResource, ) from monitoring.uss_qualifier.scenarios.astm.utm.test_steps import 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, @@ -65,6 +66,7 @@ def __init__( dss: DSSInstanceResource, ): super().__init__() + self.flight_planning_state = FlightPlanningState() self.tested_uss = tested_uss.client self.dss = dss.get_instance( { @@ -169,6 +171,7 @@ def _attempt_invalid(self): failed_checks={PlanningActivityResult.Failed: "Failure"}, flight_planner=self.tested_uss, flight_info=invalid_too_far_away, + flight_planning_state=self.flight_planning_state, ) validator.expect_not_shared() @@ -193,6 +196,7 @@ def _attempt_invalid(self): flight_planner=self.tested_uss, flight_info=invalid_recently_ended, may_end_in_past=True, + flight_planning_state=self.flight_planning_state, ) validator.expect_not_shared() @@ -212,6 +216,7 @@ def _validate_ended_cancellation(self): self, self.tested_uss, valid_flight, + flight_planning_state=self.flight_planning_state, ) # TODO(#1326): Validate that flight as planned still allows this scenario to proceed assert as_planned is not None @@ -220,13 +225,19 @@ def _validate_ended_cancellation(self): self.end_test_step() self.begin_test_step("Remove Valid Flight") + assert flight_id is not None with OpIntentValidator( self, self.tested_uss, self.dss, valid_flight, ) as cancelled_validator: - _ = delete_flight(self, self.tested_uss, flight_id) + _ = delete_flight( + self, + self.tested_uss, + flight_id, + flight_planning_state=self.flight_planning_state, + ) cancelled_validator.expect_removed(oi_ref.id) self.end_test_step() @@ -238,6 +249,7 @@ def _validate_precision_intersection(self): self, self.tested_uss, valid_flight, + flight_planning_state=self.flight_planning_state, ) # TODO(#1326): Validate that flight as planned still allows this scenario to proceed self.end_test_step() @@ -262,6 +274,7 @@ def _validate_precision_intersection(self): failed_checks={PlanningActivityResult.Failed: "Failure"}, flight_planner=self.tested_uss, flight_info=valid_conflict_tiny_overlap, + flight_planning_state=self.flight_planning_state, ) validator.expect_not_shared() @@ -269,5 +282,7 @@ def _validate_precision_intersection(self): def cleanup(self): self.begin_cleanup() - cleanup_flights(self, [self.tested_uss]) + cleanup_flights( + self, [self.tested_uss], flight_planning_state=self.flight_planning_state + ) self.end_cleanup() diff --git a/monitoring/uss_qualifier/scenarios/astm/utm/nominal_planning/conflict_equal_priority_not_permitted/conflict_equal_priority_not_permitted.py b/monitoring/uss_qualifier/scenarios/astm/utm/nominal_planning/conflict_equal_priority_not_permitted/conflict_equal_priority_not_permitted.py index 3949dcd6543..ad0c2a1d5aa 100644 --- a/monitoring/uss_qualifier/scenarios/astm/utm/nominal_planning/conflict_equal_priority_not_permitted/conflict_equal_priority_not_permitted.py +++ b/monitoring/uss_qualifier/scenarios/astm/utm/nominal_planning/conflict_equal_priority_not_permitted/conflict_equal_priority_not_permitted.py @@ -187,6 +187,7 @@ def _attempt_plan_flight_conflict(self): self, self.control_uss, flight2_planned, + flight_planning_state=self.flight_planning_state, ) # TODO(#1326): Validate that flight as planned still allows this scenario to proceed flight2_planned = as_planned @@ -208,6 +209,7 @@ def _attempt_plan_flight_conflict(self): self.control_uss, flight2_activated, self.flight2_id, + flight_planning_state=self.flight_planning_state, ) # TODO(#1326): Validate that flight as planned still allows this scenario to proceed flight2_activated = as_planned @@ -227,6 +229,7 @@ def _attempt_plan_flight_conflict(self): self, self.tested_uss, flight1_planned, + flight_planning_state=self.flight_planning_state, ) validator.expect_not_shared() self.end_test_step() @@ -246,6 +249,7 @@ def _attempt_activate_flight_conflict(self): self.tested_uss, flight1_activated, self.flight1_id, + flight_planning_state=self.flight_planning_state, ) validator.expect_not_shared() self.end_test_step() @@ -267,6 +271,7 @@ def _attempt_modify_planned_flight_conflict( self.tested_uss, flight1c_planned, nearby_potential_conflict=True, + flight_planning_state=self.flight_planning_state, ) # TODO(#1326): Validate that flight as planned still allows this scenario to proceed assert as_planned is not None @@ -289,6 +294,7 @@ def _attempt_modify_planned_flight_conflict( self.tested_uss, flight1_planned, self.flight1_id, + flight_planning_state=self.flight_planning_state, ) flight_1_oi_ref = validator.expect_shared( flight1c_planned, skip_if_not_found=True @@ -315,6 +321,7 @@ def _attempt_modify_activated_flight_conflict( self.tested_uss, flight1c_activated, self.flight1_id, + flight_planning_state=self.flight_planning_state, ) # TODO(#1326): Validate that flight as planned still allows this scenario to proceed assert as_planned is not None @@ -337,6 +344,7 @@ def _attempt_modify_activated_flight_conflict( self.tested_uss, flight1_activated, self.flight1_id, + flight_planning_state=self.flight_planning_state, ) validator.expect_not_shared() self.end_test_step() @@ -347,12 +355,23 @@ def _attempt_modify_activated_flight_conflict( ) if self.flight1_id is None: raise ValueError("flight1_id is None") - delete_flight(self, self.tested_uss, self.flight1_id) + delete_flight( + self, + self.tested_uss, + self.flight1_id, + flight_planning_state=self.flight_planning_state, + ) self.flight1_id = None self.end_test_step() self.begin_test_step("Delete Flight 2") - delete_flight(self, self.control_uss, self.flight2_id) + assert self.flight2_id is not None + delete_flight( + self, + self.control_uss, + self.flight2_id, + flight_planning_state=self.flight_planning_state, + ) self.flight2_id = None self.end_test_step() @@ -377,6 +396,7 @@ def _modify_activated_flight_preexisting_conflict( self.tested_uss, flight1_activated, self.flight1_id, + flight_planning_state=self.flight_planning_state, ) # TODO(#1326): Validate that flight as planned still allows this scenario to proceed assert as_planned is not None @@ -397,6 +417,7 @@ def _modify_activated_flight_preexisting_conflict( self, self.control_uss, flight2m_planned, + flight_planning_state=self.flight_planning_state, ) # TODO(#1326): Validate that flight as planned still allows this scenario to proceed assert as_planned is not None @@ -427,6 +448,7 @@ def _modify_activated_flight_preexisting_conflict( flight_planner=self.control_uss, flight_info=flight2_nonconforming, flight_id=self.flight2_id, + flight_planning_state=self.flight_planning_state, ) # TODO(#1326): Validate that flight as planned still allows this scenario to proceed flight2_nonconforming = as_planned @@ -463,6 +485,7 @@ def _modify_activated_flight_preexisting_conflict( flight_planner=self.tested_uss, flight_info=flight1m_activated, flight_id=self.flight1_id, + flight_planning_state=self.flight_planning_state, ) # TODO(#1326): Validate that flight as planned still allows this scenario to proceed flight1m_activated = as_planned diff --git a/monitoring/uss_qualifier/scenarios/astm/utm/nominal_planning/conflict_higher_priority/conflict_higher_priority.py b/monitoring/uss_qualifier/scenarios/astm/utm/nominal_planning/conflict_higher_priority/conflict_higher_priority.py index 229c97cddb3..7abd32228a8 100644 --- a/monitoring/uss_qualifier/scenarios/astm/utm/nominal_planning/conflict_higher_priority/conflict_higher_priority.py +++ b/monitoring/uss_qualifier/scenarios/astm/utm/nominal_planning/conflict_higher_priority/conflict_higher_priority.py @@ -190,6 +190,7 @@ def _attempt_plan_flight_conflict(self): self, self.control_uss, flight2_planned, + flight_planning_state=self.flight_planning_state, ) # TODO(#1326): Validate that flight as planned still allows this scenario to proceed flight2_planned = as_planned @@ -209,12 +210,19 @@ def _attempt_plan_flight_conflict(self): self, self.tested_uss, flight1_planned, + flight_planning_state=self.flight_planning_state, ) validator.expect_not_shared() self.end_test_step() self.begin_test_step("Delete Flight 2") - _ = delete_flight(self, self.control_uss, self.flight2_id) + assert self.flight2_id is not None + _ = delete_flight( + self, + self.control_uss, + self.flight2_id, + flight_planning_state=self.flight_planning_state, + ) self.flight2_id = None self.end_test_step() @@ -234,6 +242,7 @@ def _attempt_modify_planned_flight_conflict( self, self.tested_uss, flight1_planned, + flight_planning_state=self.flight_planning_state, ) # TODO(#1326): Validate that flight as planned still allows this scenario to proceed assert as_planned is not None @@ -258,6 +267,7 @@ def _attempt_modify_planned_flight_conflict( self, self.control_uss, flight2_planned, + flight_planning_state=self.flight_planning_state, ) # TODO(#1326): Validate that flight as planned still allows this scenario to proceed assert as_planned is not None @@ -291,6 +301,7 @@ def _attempt_modify_planned_flight_conflict( self.tested_uss, flight1m_planned, self.flight1_id, + flight_planning_state=self.flight_planning_state, ) flight_1_oi_ref = validator.expect_shared( flight1_planned, skip_if_not_found=True @@ -319,6 +330,7 @@ def _attempt_activate_flight_conflict( self.tested_uss, flight1_activated, self.flight1_id, + flight_planning_state=self.flight_planning_state, ) flight_1_oi_ref = validator.expect_shared( flight_1_intent, skip_if_not_found=True @@ -331,7 +343,13 @@ def _modify_activated_flight_conflict_preexisting( self, flight_1_oi_ref: OperationalIntentReference | None ) -> tuple[FlightInfo, OperationalIntentReference, OperationalIntentReference]: self.begin_test_step("Delete Flight 2") - _ = delete_flight(self, self.control_uss, self.flight2_id) + assert self.flight2_id is not None + _ = delete_flight( + self, + self.control_uss, + self.flight2_id, + flight_planning_state=self.flight_planning_state, + ) self.flight2_id = None self.end_test_step() @@ -350,6 +368,7 @@ def _modify_activated_flight_conflict_preexisting( self.tested_uss, flight1_activated, self.flight1_id, + flight_planning_state=self.flight_planning_state, ) # TODO(#1326): Validate that flight as planned still allows this scenario to proceed assert as_planned is not None @@ -370,6 +389,7 @@ def _modify_activated_flight_conflict_preexisting( self, self.control_uss, flight2_planned, + flight_planning_state=self.flight_planning_state, ) # TODO(#1326): Validate that flight as planned still allows this scenario to proceed flight2_planned = as_planned @@ -395,6 +415,7 @@ def _modify_activated_flight_conflict_preexisting( self.control_uss, flight2_activated, self.flight2_id, + flight_planning_state=self.flight_planning_state, ) # TODO(#1326): Validate that flight as planned still allows this scenario to proceed flight2_activated = as_planned @@ -430,6 +451,7 @@ def _modify_activated_flight_conflict_preexisting( flight1m_activated, self.flight1_id, preexisting_conflict=True, + flight_planning_state=self.flight_planning_state, ) # TODO(#1326): Validate that flight as planned still allows this scenario to proceed flight1m_activated = as_planned @@ -466,6 +488,7 @@ def _attempt_modify_activated_flight_conflict( self.control_uss, flight2m_activated, self.flight2_id, + flight_planning_state=self.flight_planning_state, ) # TODO(#1326): Validate that flight as planned still allows this scenario to proceed flight2m_activated = as_planned @@ -495,6 +518,7 @@ def _attempt_modify_activated_flight_conflict( self.tested_uss, flight1c_activated, self.flight1_id, + flight_planning_state=self.flight_planning_state, ) validator.expect_not_shared() self.end_test_step() diff --git a/monitoring/uss_qualifier/scenarios/astm/utm/nominal_planning/planning_sequence_scenario.py b/monitoring/uss_qualifier/scenarios/astm/utm/nominal_planning/planning_sequence_scenario.py index 4222170b49a..d1033eedc9c 100644 --- a/monitoring/uss_qualifier/scenarios/astm/utm/nominal_planning/planning_sequence_scenario.py +++ b/monitoring/uss_qualifier/scenarios/astm/utm/nominal_planning/planning_sequence_scenario.py @@ -26,6 +26,7 @@ from monitoring.uss_qualifier.scenarios.astm.utm.clear_area_validation import ( validate_clear_area, ) +from monitoring.uss_qualifier.scenarios.flight_planning.state import FlightPlanningState from monitoring.uss_qualifier.scenarios.flight_planning.test_steps import ( cleanup_flights, ) @@ -56,6 +57,7 @@ def __init__( scopes: dict[Scope, str], ): super().__init__() + self.flight_planning_state = FlightPlanningState() self.tested_uss = tested_uss.client self.control_uss = control_uss.client self.dss = dss.get_instance({k.value: v for k, v in scopes.items()}) @@ -125,5 +127,9 @@ def run(self, context: ExecutionContext): def cleanup(self): self.begin_cleanup() - cleanup_flights(self, (self.control_uss, self.tested_uss)) + cleanup_flights( + self, + (self.control_uss, self.tested_uss), + flight_planning_state=self.flight_planning_state, + ) self.end_cleanup() diff --git a/monitoring/uss_qualifier/scenarios/astm/utm/off_nominal_planning/down_uss.py b/monitoring/uss_qualifier/scenarios/astm/utm/off_nominal_planning/down_uss.py index 91bbe8fda33..d53b7328f25 100644 --- a/monitoring/uss_qualifier/scenarios/astm/utm/off_nominal_planning/down_uss.py +++ b/monitoring/uss_qualifier/scenarios/astm/utm/off_nominal_planning/down_uss.py @@ -42,6 +42,7 @@ set_uss_available, set_uss_down, ) +from monitoring.uss_qualifier.scenarios.flight_planning.state import FlightPlanningState from monitoring.uss_qualifier.scenarios.flight_planning.test_steps import ( cleanup_flights, submit_flight, @@ -72,6 +73,7 @@ def __init__( dss: DSSInstanceResource, ): super().__init__() + self.flight_planning_state = FlightPlanningState() self.dss_resource = dss self.tested_uss = tested_uss.client self.dss = dss.get_instance(self._dss_req_scopes) @@ -269,6 +271,7 @@ def _plan_flight_conflict_planned(self): failed_checks={PlanningActivityResult.Failed: "Failure"}, flight_planner=self.tested_uss, flight_info=flight1_planned, + flight_planning_state=self.flight_planning_state, ) # TODO(#1326): Validate that flight as planned still allows this scenario to proceed flight1_planned = as_planned @@ -333,7 +336,9 @@ def _clear_op_intents(self, area: Volume4D): def cleanup(self): self.begin_cleanup() set_uss_available(self, self.dss, self.uss_qualifier_sub) - cleanup_flights(self, [self.tested_uss]) + cleanup_flights( + self, [self.tested_uss], flight_planning_state=self.flight_planning_state + ) if self.scenario_execution_max_extents: self._clear_op_intents(self.scenario_execution_max_extents) diff --git a/monitoring/uss_qualifier/scenarios/astm/utm/off_nominal_planning/down_uss_equal_priority_not_permitted.py b/monitoring/uss_qualifier/scenarios/astm/utm/off_nominal_planning/down_uss_equal_priority_not_permitted.py index 2aa36f1a79d..920d992a963 100644 --- a/monitoring/uss_qualifier/scenarios/astm/utm/off_nominal_planning/down_uss_equal_priority_not_permitted.py +++ b/monitoring/uss_qualifier/scenarios/astm/utm/off_nominal_planning/down_uss_equal_priority_not_permitted.py @@ -132,6 +132,7 @@ def _plan_flight_conflict_activated(self) -> OperationalIntentReference: failed_checks={PlanningActivityResult.Failed: "Failure"}, flight_planner=self.tested_uss, flight_info=flight2_planned, + flight_planning_state=self.flight_planning_state, ) validator.expect_not_shared() @@ -175,6 +176,7 @@ def _plan_flight_conflict_nonconforming( failed_checks={PlanningActivityResult.Failed: "Failure"}, flight_planner=self.tested_uss, flight_info=flight2_planned, + flight_planning_state=self.flight_planning_state, ) validator.expect_not_shared() @@ -216,6 +218,7 @@ def _plan_flight_conflict_contingent(self, oi_ref: OperationalIntentReference): failed_checks={PlanningActivityResult.Failed: "Failure"}, flight_planner=self.tested_uss, flight_info=flight2_planned, + flight_planning_state=self.flight_planning_state, ) validator.expect_not_shared() diff --git a/monitoring/uss_qualifier/scenarios/astm/utm/subscription_notifications/receive_notifications_for_awareness/receive_notifications_for_awareness.py b/monitoring/uss_qualifier/scenarios/astm/utm/subscription_notifications/receive_notifications_for_awareness/receive_notifications_for_awareness.py index 57595bcf24b..eb1423965ba 100644 --- a/monitoring/uss_qualifier/scenarios/astm/utm/subscription_notifications/receive_notifications_for_awareness/receive_notifications_for_awareness.py +++ b/monitoring/uss_qualifier/scenarios/astm/utm/subscription_notifications/receive_notifications_for_awareness/receive_notifications_for_awareness.py @@ -28,6 +28,7 @@ expect_tested_uss_receives_notification_from_mock_uss, ) from monitoring.uss_qualifier.scenarios.astm.utm.test_steps import OpIntentValidator +from monitoring.uss_qualifier.scenarios.flight_planning.state import FlightPlanningState from monitoring.uss_qualifier.scenarios.flight_planning.test_steps import ( activate_flight, cleanup_flights, @@ -65,6 +66,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 @@ -162,6 +164,7 @@ def _receive_notification_successfully_when_activated_test_case(self): self, self.tested_uss_client, flight_1_planned, + flight_planning_state=self.flight_planning_state, ) # TODO(#1326): Validate that flight as planned still allows this scenario to proceed flight_1_planned = as_planned @@ -179,6 +182,7 @@ def _receive_notification_successfully_when_activated_test_case(self): self.tested_uss_client, flight_1_activated, self.flight_1_id, + flight_planning_state=self.flight_planning_state, ) # TODO(#1326): Validate that flight as planned still allows this scenario to proceed flight_1_activated = as_planned @@ -198,6 +202,7 @@ def _receive_notification_successfully_when_activated_test_case(self): self, self.mock_uss_client, flight_2_planned, + flight_planning_state=self.flight_planning_state, ) # TODO(#1326): Validate that flight as planned still allows this scenario to proceed flight_2_planned = as_planned @@ -237,6 +242,7 @@ def _receive_notification_successfully_when_activated_modified_test_case(self): self.mock_uss_client, flight_2_planned_modified, self.flight_2_id, + flight_planning_state=self.flight_planning_state, ) # TODO(#1326): Validate that flight as planned still allows this scenario to proceed flight_2_planned_modified = as_planned @@ -259,5 +265,9 @@ def _receive_notification_successfully_when_activated_modified_test_case(self): 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() diff --git a/monitoring/uss_qualifier/scenarios/astm/utm/uss_authentication/scd_authentication_validation.py b/monitoring/uss_qualifier/scenarios/astm/utm/uss_authentication/scd_authentication_validation.py index 5e8c05bf4d7..fd5386c18e8 100644 --- a/monitoring/uss_qualifier/scenarios/astm/utm/uss_authentication/scd_authentication_validation.py +++ b/monitoring/uss_qualifier/scenarios/astm/utm/uss_authentication/scd_authentication_validation.py @@ -47,6 +47,7 @@ from monitoring.uss_qualifier.scenarios.astm.utm.uss_authentication.endpoint_auth_validator import ( EndpointAuthValidator, ) +from monitoring.uss_qualifier.scenarios.flight_planning.state import FlightPlanningState from monitoring.uss_qualifier.scenarios.flight_planning.test_steps import ( cleanup_flights, plan_flight, @@ -73,6 +74,7 @@ def __init__( flight_intents: FlightIntentsResource, ): super().__init__() + self.flight_planning_state = FlightPlanningState() self.tested_uss = tested_uss self.utm_auth = utm_auth self.flight_1 = self._init_flight_template(flight_intents=flight_intents) @@ -150,6 +152,7 @@ def _resolve_oi_ref(self) -> OperationalIntentReference | None: self, self.tested_uss.client, flight, + flight_planning_state=self.flight_planning_state, ) return validator.expect_shared(as_planned) @@ -243,5 +246,9 @@ def _verify_make_uss_report(self, uss: USSInstance): def cleanup(self): self.begin_cleanup() - cleanup_flights(self, [self.tested_uss.client]) + cleanup_flights( + self, + [self.tested_uss.client], + flight_planning_state=self.flight_planning_state, + ) self.end_cleanup() diff --git a/monitoring/uss_qualifier/scenarios/flight_planning/prioritization_test_steps.py b/monitoring/uss_qualifier/scenarios/flight_planning/prioritization_test_steps.py index d2ae866cbde..8f60a1eb810 100644 --- a/monitoring/uss_qualifier/scenarios/flight_planning/prioritization_test_steps.py +++ b/monitoring/uss_qualifier/scenarios/flight_planning/prioritization_test_steps.py @@ -10,6 +10,7 @@ PlanningActivityResponse, PlanningActivityResult, ) +from monitoring.uss_qualifier.scenarios.flight_planning.state import FlightPlanningState from monitoring.uss_qualifier.scenarios.flight_planning.test_steps import ( expect_flight_intent_state, submit_flight, @@ -22,6 +23,8 @@ def plan_priority_conflict_flight( flight_planner: FlightPlannerClient, flight_info: FlightInfo, additional_fields: dict | None = None, + *, + flight_planning_state: FlightPlanningState, ) -> PlanningActivityResponse: """Attempt to plan a flight intent that should result in a conflict with a higher priority flight intent. @@ -48,6 +51,7 @@ def plan_priority_conflict_flight( flight_planner=flight_planner, flight_info=flight_info, additional_fields=additional_fields, + flight_planning_state=flight_planning_state, )[0] @@ -57,6 +61,8 @@ def modify_planned_priority_conflict_flight( flight_info: FlightInfo, flight_id: str, additional_fields: dict | None = None, + *, + flight_planning_state: FlightPlanningState, ) -> PlanningActivityResponse: """Attempt to modify a planned flight intent that should result in a conflict with a higher priority flight intent. @@ -91,6 +97,7 @@ def modify_planned_priority_conflict_flight( flight_info=flight_info, flight_id=flight_id, additional_fields=additional_fields, + flight_planning_state=flight_planning_state, )[0] @@ -100,6 +107,8 @@ def activate_priority_conflict_flight( flight_info: FlightInfo, flight_id: str | None = None, additional_fields: dict | None = None, + *, + flight_planning_state: FlightPlanningState, ) -> PlanningActivityResponse: """Attempt to activate a flight intent that should result in a conflict with a higher priority flight intent. @@ -134,6 +143,7 @@ def activate_priority_conflict_flight( flight_info=flight_info, flight_id=flight_id, additional_fields=additional_fields, + flight_planning_state=flight_planning_state, )[0] @@ -143,6 +153,8 @@ def modify_activated_priority_conflict_flight( flight_info: FlightInfo, flight_id: str, additional_fields: dict | None = None, + *, + flight_planning_state: FlightPlanningState, ) -> PlanningActivityResponse: """Attempt to modify an activated flight intent that should result in a conflict with a higher priority flight intent. @@ -173,6 +185,7 @@ def modify_activated_priority_conflict_flight( flight_info=flight_info, flight_id=flight_id, additional_fields=additional_fields, + flight_planning_state=flight_planning_state, )[0] @@ -181,6 +194,8 @@ def plan_conflict_flight( flight_planner: FlightPlannerClient, flight_info: FlightInfo, additional_fields: dict | None = None, + *, + flight_planning_state: FlightPlanningState, ) -> PlanningActivityResponse: """Attempt to plan a flight intent that should result in a non-permitted conflict with an equal priority flight intent. @@ -206,6 +221,7 @@ def plan_conflict_flight( flight_planner=flight_planner, flight_info=flight_info, additional_fields=additional_fields, + flight_planning_state=flight_planning_state, )[0] @@ -215,6 +231,8 @@ def modify_planned_conflict_flight( flight_info: FlightInfo, flight_id: str, additional_fields: dict | None = None, + *, + flight_planning_state: FlightPlanningState, ) -> PlanningActivityResponse: """Attempt to modify a planned flight intent that should result in a non-permitted conflict with an equal priority flight intent. @@ -249,6 +267,7 @@ def modify_planned_conflict_flight( flight_info=flight_info, flight_id=flight_id, additional_fields=additional_fields, + flight_planning_state=flight_planning_state, )[0] @@ -258,6 +277,8 @@ def activate_conflict_flight( flight_info: FlightInfo, flight_id: str | None = None, additional_fields: dict | None = None, + *, + flight_planning_state: FlightPlanningState, ) -> PlanningActivityResponse: """Attempt to activate a flight intent that should result in a non-permitted conflict with an equal priority flight intent. @@ -292,6 +313,7 @@ def activate_conflict_flight( flight_info=flight_info, flight_id=flight_id, additional_fields=additional_fields, + flight_planning_state=flight_planning_state, )[0] @@ -301,6 +323,8 @@ def modify_activated_conflict_flight( flight_info: FlightInfo, flight_id: str, additional_fields: dict | None = None, + *, + flight_planning_state: FlightPlanningState, ) -> PlanningActivityResponse: """Attempt to modify an activated flight intent that should result in a non-permitted conflict with an equal priority flight intent. @@ -331,4 +355,5 @@ def modify_activated_conflict_flight( flight_info=flight_info, flight_id=flight_id, additional_fields=additional_fields, + flight_planning_state=flight_planning_state, )[0] diff --git a/monitoring/uss_qualifier/scenarios/flight_planning/state.py b/monitoring/uss_qualifier/scenarios/flight_planning/state.py new file mode 100644 index 00000000000..1915b7287b1 --- /dev/null +++ b/monitoring/uss_qualifier/scenarios/flight_planning/state.py @@ -0,0 +1,82 @@ +import uuid + +from monitoring.monitorlib.clients.flight_planning.client import FlightPlannerClient +from monitoring.monitorlib.clients.flight_planning.flight_info import ( + ExecutionStyle, + FlightID, + FlightInfo, +) +from monitoring.monitorlib.clients.flight_planning.planning import ( + FlightPlanStatus, + PlanningActivityResponse, +) + + +class FlightPlanningState: + """Flights requiring cleanup for one scenario, across its flight planners. + + Each scenario must create its own instance even when sharing planner clients. + Record IDs before sending requests because a failed request may still create a + flight. Only a response confirming NotPlanned or Closed removes an ID. + """ + + def __init__(self): + self._created_flight_ids: dict[FlightPlannerClient, set[FlightID]] = {} + + def get_flight_ids(self, flight_planner: FlightPlannerClient) -> set[FlightID]: + """Return a copy of this scenario's flights needing cleanup by this planner.""" + return self._created_flight_ids.get(flight_planner, set()).copy() + + def forget_flight(self, flight_planner: FlightPlannerClient, flight_id: FlightID): + """Stop tracking a flight after closure or a cleanup attempt.""" + if flight_planner in self._created_flight_ids: + self._created_flight_ids[flight_planner].discard(flight_id) + + def _record_response( + self, flight_planner: FlightPlannerClient, response: PlanningActivityResponse + ): + if response.flight_plan_status in ( + FlightPlanStatus.NotPlanned, + FlightPlanStatus.Closed, + ): + self.forget_flight(flight_planner, response.flight_id) + + def plan_flight( + self, + flight_planner: FlightPlannerClient, + flight_info: FlightInfo, + execution_style: ExecutionStyle, + additional_fields: dict | None = None, + ) -> PlanningActivityResponse: + flight_id = str(uuid.uuid4()) + self._created_flight_ids.setdefault(flight_planner, set()).add(flight_id) + response = flight_planner.try_plan_flight( + flight_info, execution_style, additional_fields, flight_id=flight_id + ) + self._record_response(flight_planner, response) + return response + + def update_flight( + self, + flight_planner: FlightPlannerClient, + flight_id: FlightID, + flight_info: FlightInfo, + execution_style: ExecutionStyle, + additional_fields: dict | None = None, + ) -> PlanningActivityResponse: + self._created_flight_ids.setdefault(flight_planner, set()).add(flight_id) + response = flight_planner.try_update_flight( + flight_id, flight_info, execution_style, additional_fields + ) + self._record_response(flight_planner, response) + return response + + def end_flight( + self, + flight_planner: FlightPlannerClient, + flight_id: FlightID, + execution_style: ExecutionStyle, + ) -> PlanningActivityResponse: + response = flight_planner.try_end_flight(flight_id, execution_style) + self._record_response(flight_planner, response) + return response diff --git a/monitoring/uss_qualifier/scenarios/flight_planning/test_steps.py b/monitoring/uss_qualifier/scenarios/flight_planning/test_steps.py index 3b6a7d5c49c..bf043d593fd 100644 --- a/monitoring/uss_qualifier/scenarios/flight_planning/test_steps.py +++ b/monitoring/uss_qualifier/scenarios/flight_planning/test_steps.py @@ -7,8 +7,10 @@ FunctionalState, ) -from monitoring.monitorlib.clients.flight_planning.client import PlanningActivityError -from monitoring.monitorlib.clients.flight_planning.client_v1 import FlightPlannerClient +from monitoring.monitorlib.clients.flight_planning.client import ( + FlightPlannerClient, + PlanningActivityError, +) from monitoring.monitorlib.clients.flight_planning.flight_info import ( ExecutionStyle, FlightID, @@ -27,6 +29,7 @@ times_not_later_than_specified_or_now, values_exactly_equal, ) +from monitoring.uss_qualifier.scenarios.flight_planning.state import FlightPlanningState from monitoring.uss_qualifier.scenarios.scenario import ( ScenarioDidNotStopError, TestScenario, @@ -58,6 +61,8 @@ def plan_flight( flight_info: FlightInfo, additional_fields: dict | None = None, nearby_potential_conflict: bool = False, + *, + flight_planning_state: FlightPlanningState, ) -> tuple[PlanningActivityResponse, str | None, FlightInfo | None]: """Plan a flight intent that should result in success. @@ -81,6 +86,7 @@ def plan_flight( flight_planner=flight_planner, flight_info=flight_info, additional_fields=additional_fields, + flight_planning_state=flight_planning_state, ) if ( @@ -100,6 +106,8 @@ def modify_planned_flight( flight_info: FlightInfo, flight_id: str, additional_fields: dict | None = None, + *, + flight_planning_state: FlightPlanningState, ) -> tuple[PlanningActivityResponse, FlightInfo | None]: """Modify a planned flight intent that should result in success. @@ -132,6 +140,7 @@ def modify_planned_flight( flight_info=flight_info, flight_id=flight_id, additional_fields=additional_fields, + flight_planning_state=flight_planning_state, ) return resp, as_planned @@ -143,6 +152,8 @@ def modify_activated_flight( flight_id: str, preexisting_conflict: bool = False, additional_fields: dict | None = None, + *, + flight_planning_state: FlightPlanningState, ) -> tuple[PlanningActivityResponse, FlightInfo | None]: """Modify an activated flight intent that should result in success. @@ -175,6 +186,7 @@ def modify_activated_flight( flight_info=flight_info, flight_id=flight_id, additional_fields=additional_fields, + flight_planning_state=flight_planning_state, ) assert as_planned is not None @@ -208,6 +220,7 @@ def modify_activated_flight( flight_info=flight_info, flight_id=flight_id, additional_fields=additional_fields, + flight_planning_state=flight_planning_state, ) assert as_planned is not None @@ -220,6 +233,8 @@ def activate_flight( flight_info: FlightInfo, flight_id: str | None = None, additional_fields: dict | None = None, + *, + flight_planning_state: FlightPlanningState, ) -> tuple[PlanningActivityResponse, str | None, FlightInfo | None]: """Activate a flight intent that should result in success. @@ -240,6 +255,7 @@ def activate_flight( flight_info=flight_info, flight_id=flight_id, additional_fields=additional_fields, + flight_planning_state=flight_planning_state, ) @@ -254,6 +270,8 @@ def submit_flight( additional_fields: dict | None = None, skip_if_not_supported: bool = False, may_end_in_past: bool = False, + *, + flight_planning_state: FlightPlanningState, ) -> tuple[PlanningActivityResponse, str | None, FlightInfo | None]: """Submit a flight intent with an expected result. A check fail is considered by default of high severity and as such will raise an ScenarioCannotContinueError. @@ -284,7 +302,11 @@ def submit_flight( with scenario.check(success_check, [flight_planner.participant_id]) as check: try: resp, query, flight_id = request_flight( - flight_planner, flight_info, flight_id, additional_fields + flight_planner, + flight_info, + flight_id, + additional_fields, + flight_planning_state=flight_planning_state, ) scenario.record_query(query) except QueryError as e: @@ -363,6 +385,8 @@ def request_flight( flight_info: FlightInfo, flight_id: str | None, additional_fields: dict | None = None, + *, + flight_planning_state: FlightPlanningState, ) -> tuple[PlanningActivityResponse, Query, str]: """ Uses FlightPlannerClient to plan the flight @@ -374,16 +398,20 @@ def request_flight( """ if not flight_id: try: - resp = flight_planner.try_plan_flight( - flight_info, ExecutionStyle.IfAllowed, additional_fields + resp = flight_planning_state.plan_flight( + flight_planner, flight_info, ExecutionStyle.IfAllowed, additional_fields ) except PlanningActivityError as e: raise QueryError(str(e), e.queries) flight_id = resp.flight_id else: try: - resp = flight_planner.try_update_flight( - flight_id, flight_info, ExecutionStyle.IfAllowed + resp = flight_planning_state.update_flight( + flight_planner, + flight_id, + flight_info, + ExecutionStyle.IfAllowed, + additional_fields, ) except PlanningActivityError as e: raise QueryError(str(e), e.queries) @@ -395,6 +423,8 @@ def delete_flight( scenario: TestScenario, flight_planner: FlightPlannerClient, flight_id: FlightID, + *, + flight_planning_state: FlightPlanningState, ) -> PlanningActivityResponse: """Delete an existing flight intent that should result in success. A check fail is considered of high severity and as such will raise an ScenarioCannotContinueError. @@ -407,7 +437,9 @@ def delete_flight( "Successful deletion", [flight_planner.participant_id] ) as check: try: - resp = flight_planner.try_end_flight(flight_id, ExecutionStyle.IfAllowed) + resp = flight_planning_state.end_flight( + flight_planner, flight_id, ExecutionStyle.IfAllowed + ) except QueryError as e: scenario.record_queries(e.queries) @@ -425,7 +457,6 @@ def delete_flight( resp.activity_result == PlanningActivityResult.Completed and resp.flight_plan_status == FlightPlanStatus.Closed ): - flight_planner.created_flight_ids.discard(flight_id) return resp else: check.record_failed( @@ -439,24 +470,27 @@ def delete_flight( def cleanup_flights( - scenario: TestScenario, flight_planners: Iterable[FlightPlannerClient] + scenario: TestScenario, + flight_planners: Iterable[FlightPlannerClient], + *, + flight_planning_state: FlightPlanningState, ) -> None: """Remove flights during a cleanup test step. This function assumes: * `scenario` is currently cleaning up (cleanup has started) * "Successful flight deletion" check declared for cleanup phase in `scenario`'s documentation - * IDs of flights created have been added to each flight_planner.created_flight_ids + * flight_planning_state is owned by this scenario and tracks its flights """ for flight_planner in flight_planners: - to_remove = flight_planner.created_flight_ids.copy() + to_remove = flight_planning_state.get_flight_ids(flight_planner) for flight_id in to_remove: with scenario.check( "Successful flight deletion", [flight_planner.participant_id] ) as check: try: - resp = flight_planner.try_end_flight( - flight_id, ExecutionStyle.IfAllowed + resp = flight_planning_state.end_flight( + flight_planner, flight_id, ExecutionStyle.IfAllowed ) except QueryError as e: @@ -478,7 +512,7 @@ def cleanup_flights( ) # Do not attempt to clean up again as we would expect the same error - flight_planner.created_flight_ids.discard(flight_id) + flight_planning_state.forget_flight(flight_planner, flight_id) continue scenario.record_queries(resp.queries) @@ -496,4 +530,4 @@ def cleanup_flights( # Remove flight_id from created flights regardless of status. # If status was successful, the flight has been removed. If the status was unsuccessful, we would # expect the same error status if we were to try again. - flight_planner.created_flight_ids.discard(flight_id) + flight_planning_state.forget_flight(flight_planner, flight_id) diff --git a/monitoring/uss_qualifier/scenarios/interuss/flight_authorization/general_flight_authorization.py b/monitoring/uss_qualifier/scenarios/interuss/flight_authorization/general_flight_authorization.py index 1babc2b2656..32afa9f5b51 100644 --- a/monitoring/uss_qualifier/scenarios/interuss/flight_authorization/general_flight_authorization.py +++ b/monitoring/uss_qualifier/scenarios/interuss/flight_authorization/general_flight_authorization.py @@ -28,6 +28,7 @@ TestScenarioDocumentation, TestStepDocumentation, ) +from monitoring.uss_qualifier.scenarios.flight_planning.state import FlightPlanningState from monitoring.uss_qualifier.scenarios.scenario import TestScenario from monitoring.uss_qualifier.suites.suite import ExecutionContext @@ -58,6 +59,7 @@ def __init__( planner: FlightPlannerResource, ): super().__init__() + self.flight_planning_state = FlightPlanningState() self.table = table.table self.flight_planner = planner.client self.participant_id = planner.participant_id @@ -165,8 +167,8 @@ def _plan_flights(self): ) with self.check(_VALID_API_RESPONSE_NAME, [self.participant_id]) as check: try: - resp = self.flight_planner.try_plan_flight( - info, row.execution_style + resp = self.flight_planning_state.plan_flight( + self.flight_planner, info, row.execution_style ) except PlanningActivityError as e: for q in e.queries: @@ -272,8 +274,10 @@ def _plan_flights(self): _VALID_API_RESPONSE_NAME, [self.participant_id] ) as check: try: - del_resp = self.flight_planner.try_end_flight( - resp.flight_id, ExecutionStyle.IfAllowed + del_resp = self.flight_planning_state.end_flight( + self.flight_planner, + resp.flight_id, + ExecutionStyle.IfAllowed, ) except PlanningActivityError as e: for q in e.queries: diff --git a/monitoring/uss_qualifier/scenarios/uspace/flight_auth/validation.py b/monitoring/uss_qualifier/scenarios/uspace/flight_auth/validation.py index 2751e997ca7..36d165edf8e 100644 --- a/monitoring/uss_qualifier/scenarios/uspace/flight_auth/validation.py +++ b/monitoring/uss_qualifier/scenarios/uspace/flight_auth/validation.py @@ -19,6 +19,7 @@ from monitoring.uss_qualifier.resources.flight_planning.flight_planners import ( FlightPlannerResource, ) +from monitoring.uss_qualifier.scenarios.flight_planning.state import FlightPlanningState from monitoring.uss_qualifier.scenarios.flight_planning.test_steps import ( cleanup_flights, plan_flight, @@ -39,6 +40,7 @@ def __init__( flight_planner: FlightPlannerResource, ): super().__init__() + self.flight_planning_state = FlightPlanningState() self.ussp = flight_planner.client templates = flight_intents.get_flight_intents() @@ -118,6 +120,7 @@ def _attempt_invalid_flights(self) -> bool: failed_checks={PlanningActivityResult.Failed: "Failure"}, flight_planner=self.ussp, flight_info=flight_intent, + flight_planning_state=self.flight_planning_state, ) self.end_test_step() # Inject flight intents @@ -131,6 +134,7 @@ def _plan_valid_flight(self) -> bool: self, self.ussp, valid_flight_intent, + flight_planning_state=self.flight_planning_state, ) # TODO(#1326): Validate that flight as planned still allows this scenario to proceed if resp is None: @@ -140,5 +144,7 @@ def _plan_valid_flight(self) -> bool: def cleanup(self): self.begin_cleanup() - cleanup_flights(self, [self.ussp]) + cleanup_flights( + self, [self.ussp], flight_planning_state=self.flight_planning_state + ) self.end_cleanup()