diff --git a/.basedpyright/baseline.json b/.basedpyright/baseline.json index b19d7df903..6b8d299a48 100644 --- a/.basedpyright/baseline.json +++ b/.basedpyright/baseline.json @@ -18361,22 +18361,6 @@ } ], "./monitoring/uss_qualifier/scenarios/astm/utm/nominal_planning/conflict_equal_priority_not_permitted/conflict_equal_priority_not_permitted.py": [ - { - "code": "reportAttributeAccessIssue", - "range": { - "startColumn": 15, - "endColumn": 28, - "lineCount": 1 - } - }, - { - "code": "reportAttributeAccessIssue", - "range": { - "startColumn": 23, - "endColumn": 35, - "lineCount": 1 - } - }, { "code": "reportArgumentType", "range": { @@ -18533,14 +18517,6 @@ } ], "./monitoring/uss_qualifier/scenarios/astm/utm/nominal_planning/conflict_higher_priority/conflict_higher_priority.py": [ - { - "code": "reportAttributeAccessIssue", - "range": { - "startColumn": 23, - "endColumn": 35, - "lineCount": 1 - } - }, { "code": "reportArgumentType", "range": { 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 1a9c7c8604..3949dcd654 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 @@ -3,12 +3,8 @@ Scope, ) -from monitoring.monitorlib.clients.flight_planning.client import ( - FlightPlannerClient, -) from monitoring.monitorlib.clients.flight_planning.flight_info import ( AirspaceUsageState, - FlightInfo, UasState, ) from monitoring.monitorlib.clients.flight_planning.flight_info_template import ( @@ -18,22 +14,16 @@ FlightPlanStatus, PlanningActivityResult, ) -from monitoring.uss_qualifier.resources.astm.f3548.v21 import DSSInstanceResource -from monitoring.uss_qualifier.resources.astm.f3548.v21.dss import DSSInstance +from monitoring.uss_qualifier.resources.astm.f3548.v21.dss import DSSInstanceResource from monitoring.uss_qualifier.resources.flight_planning import FlightIntentsResource -from monitoring.uss_qualifier.resources.flight_planning.flight_intent import ( - FlightIntentID, -) from monitoring.uss_qualifier.resources.flight_planning.flight_intent_validation import ( ExpectedFlightIntent, - estimate_scenario_execution_max_extents, - validate_flight_intent_templates, ) from monitoring.uss_qualifier.resources.flight_planning.flight_planners import ( FlightPlannerResource, ) -from monitoring.uss_qualifier.scenarios.astm.utm.clear_area_validation import ( - validate_clear_area, +from monitoring.uss_qualifier.scenarios.astm.utm.nominal_planning.planning_sequence_scenario import ( + PlanningSequenceScenario, ) from monitoring.uss_qualifier.scenarios.astm.utm.test_steps import OpIntentValidator from monitoring.uss_qualifier.scenarios.flight_planning.prioritization_test_steps import ( @@ -44,19 +34,17 @@ ) from monitoring.uss_qualifier.scenarios.flight_planning.test_steps import ( activate_flight, - cleanup_flights, delete_flight, plan_flight, submit_flight, ) from monitoring.uss_qualifier.scenarios.scenario import ( ScenarioCannotContinueError, - TestScenario, ) from monitoring.uss_qualifier.suites.suite import ExecutionContext -class ConflictEqualPriorityNotPermitted(TestScenario): +class ConflictEqualPriorityNotPermitted(PlanningSequenceScenario): flight1_id: str | None = None flight1_planned: FlightInfoTemplate flight1_activated: FlightInfoTemplate @@ -65,27 +53,18 @@ class ConflictEqualPriorityNotPermitted(TestScenario): flight1c_activated: FlightInfoTemplate flight2_id: str | None = None - flight2m_planned: FlightInfoTemplate - flight2_planned: FlightInfoTemplate - flight2_activated: FlightInfoTemplate - flight2_nonconforming: FlightInfoTemplate - - tested_uss: FlightPlannerClient - control_uss: FlightPlannerClient - dss: DSSInstance - flight_intents_templates: dict[FlightIntentID, FlightInfoTemplate] + equal_prio_flight2m_planned: FlightInfoTemplate + equal_prio_flight2_planned: FlightInfoTemplate + equal_prio_flight2_activated: FlightInfoTemplate + equal_prio_flight2_nonconforming: FlightInfoTemplate def __init__( self, tested_uss: FlightPlannerResource, control_uss: FlightPlannerResource, dss: DSSInstanceResource, - flight_intents: FlightIntentsResource | None = None, + flight_intents: FlightIntentsResource, ): - super().__init__() - self.tested_uss = tested_uss.client - self.control_uss = control_uss.client - scopes = { Scope.StrategicCoordination: "search for operational intent references to verify outcomes of planning activities and retrieve operational intent details" } @@ -94,8 +73,6 @@ def __init__( "query for telemetry for off-nominal operational intents" ) - self.dss = dss.get_instance(scopes) - expected_flight_intents = [ ExpectedFlightIntent( "flight1_planned", @@ -164,54 +141,16 @@ def __init__( ), # Note: this intent expected to produce Nonconforming state, but this is hard to verify without telemetry. UAS state is not actually off-nominal. ] - self.flight_intents_templates = ( - flight_intents.get_flight_intents() if flight_intents else {} + super().__init__( + flight_intents=flight_intents, + expected_flight_intents=expected_flight_intents, + tested_uss=tested_uss, + control_uss=control_uss, + dss=dss, + scopes=scopes, ) - try: - validate_flight_intent_templates( - self.flight_intents_templates, expected_flight_intents - ) - except ValueError as e: - raise ValueError( - f"`{self.me()}` TestScenario requirements for flight_intents not met: {e}" - ) - - for efi in expected_flight_intents: - setattr( - self, - efi.intent_id.replace("equal_prio_", ""), - self.flight_intents_templates[efi.intent_id], - ) - - def resolve_flight(self, flight_template: FlightInfoTemplate) -> FlightInfo: - return flight_template.resolve(self.time_context.evaluate_now()) - - def run(self, context: ExecutionContext): - self.begin_test_scenario(context) - - self.record_note( - "Tested USS", - f"{self.tested_uss.participant_id}", - ) - self.record_note( - "Control USS", - f"{self.control_uss.participant_id}", - ) - - self.begin_test_case("Prerequisites check") - self.begin_test_step("Verify area is clear") - estimated_max_extents = estimate_scenario_execution_max_extents( - self.time_context, self.flight_intents_templates - ) - validate_clear_area( - self, - self.dss, - [estimated_max_extents], - ignore_self=False, - ) - self.end_test_step() - self.end_test_case() + def run_planning_sequence(self, context: ExecutionContext): self.begin_test_case("Attempt to plan flight into conflict") self._attempt_plan_flight_conflict() self.end_test_case() @@ -234,11 +173,9 @@ def run(self, context: ExecutionContext): self._modify_activated_flight_preexisting_conflict(flight_1_oi_ref) self.end_test_case() - self.end_test_scenario() - def _attempt_plan_flight_conflict(self): self.begin_test_step("Plan Flight 2") - flight2_planned = self.resolve_flight(self.flight2_planned) + flight2_planned = self.resolve_flight(self.equal_prio_flight2_planned) with OpIntentValidator( self, @@ -257,7 +194,7 @@ def _attempt_plan_flight_conflict(self): self.end_test_step() self.begin_test_step("Activate Flight 2") - flight2_activated = self.resolve_flight(self.flight2_activated) + flight2_activated = self.resolve_flight(self.equal_prio_flight2_activated) with OpIntentValidator( self, @@ -448,7 +385,7 @@ def _modify_activated_flight_preexisting_conflict( self.end_test_step() self.begin_test_step("Plan Flight 2m") - flight2m_planned = self.resolve_flight(self.flight2m_planned) + flight2m_planned = self.resolve_flight(self.equal_prio_flight2m_planned) with OpIntentValidator( self, @@ -468,7 +405,9 @@ def _modify_activated_flight_preexisting_conflict( self.end_test_step() self.begin_test_step("Declare Flight 2 non-conforming") - flight2_nonconforming = self.resolve_flight(self.flight2_nonconforming) + flight2_nonconforming = self.resolve_flight( + self.equal_prio_flight2_nonconforming + ) with OpIntentValidator( self, @@ -536,8 +475,3 @@ def _modify_activated_flight_preexisting_conflict( }: validator.expect_not_shared() self.end_test_step() - - def cleanup(self): - self.begin_cleanup() - cleanup_flights(self, (self.control_uss, self.tested_uss)) - self.end_cleanup() diff --git a/monitoring/uss_qualifier/scenarios/astm/utm/nominal_planning/conflict_higher_priority/conflict_higher_priority.md b/monitoring/uss_qualifier/scenarios/astm/utm/nominal_planning/conflict_higher_priority/conflict_higher_priority.md index a1bc95fa40..9056eaf9fc 100644 --- a/monitoring/uss_qualifier/scenarios/astm/utm/nominal_planning/conflict_higher_priority/conflict_higher_priority.md +++ b/monitoring/uss_qualifier/scenarios/astm/utm/nominal_planning/conflict_higher_priority/conflict_higher_priority.md @@ -88,6 +88,12 @@ FlightPlannerResource that will manage conflicting Flight 2. ### dss DSSInstanceResource that provides access to a DSS instance where flight creation/sharing can be verified. +## Prerequisites check test case + +### [Verify area is clear test step](../../clear_area_validation.md) + +While this scenario assumes that the area used is already clear of any pre-existing flights (using, for instance, PrepareFlightPlanners scenario) in order to avoid a large number of area-clearing operations, the scenario will not proceed correctly if the area was left in a dirty state following a previous scenario that was supposed to leave the area clear. This test step verifies that the area is clear. + ## Attempt to plan flight in conflict test case  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 9ad274b87c..229c97cddb 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 @@ -4,9 +4,6 @@ Scope, ) -from monitoring.monitorlib.clients.flight_planning.client import ( - FlightPlannerClient, -) from monitoring.monitorlib.clients.flight_planning.flight_info import ( AirspaceUsageState, FlightInfo, @@ -18,16 +15,17 @@ from monitoring.monitorlib.clients.flight_planning.planning import ( PlanningActivityResult, ) -from monitoring.uss_qualifier.resources.astm.f3548.v21 import DSSInstanceResource -from monitoring.uss_qualifier.resources.astm.f3548.v21.dss import DSSInstance +from monitoring.uss_qualifier.resources.astm.f3548.v21.dss import DSSInstanceResource from monitoring.uss_qualifier.resources.flight_planning import FlightIntentsResource from monitoring.uss_qualifier.resources.flight_planning.flight_intent_validation import ( ExpectedFlightIntent, - validate_flight_intent_templates, ) from monitoring.uss_qualifier.resources.flight_planning.flight_planners import ( FlightPlannerResource, ) +from monitoring.uss_qualifier.scenarios.astm.utm.nominal_planning.planning_sequence_scenario import ( + PlanningSequenceScenario, +) from monitoring.uss_qualifier.scenarios.astm.utm.notifications_to_operator.notification_checker import ( NotificationChecker, ) @@ -40,16 +38,14 @@ ) from monitoring.uss_qualifier.scenarios.flight_planning.test_steps import ( activate_flight, - cleanup_flights, delete_flight, modify_activated_flight, plan_flight, ) -from monitoring.uss_qualifier.scenarios.scenario import TestScenario from monitoring.uss_qualifier.suites.suite import ExecutionContext -class ConflictHigherPriority(TestScenario, NotificationChecker): +class ConflictHigherPriority(PlanningSequenceScenario, NotificationChecker): flight1_id: str | None = None flight1_planned: FlightInfoTemplate flight1m_planned: FlightInfoTemplate @@ -62,10 +58,6 @@ class ConflictHigherPriority(TestScenario, NotificationChecker): flight2_activated: FlightInfoTemplate flight2m_activated: FlightInfoTemplate - tested_uss: FlightPlannerClient - control_uss: FlightPlannerClient - dss: DSSInstance - def __init__( self, flight_intents: FlightIntentsResource, @@ -73,15 +65,9 @@ def __init__( control_uss: FlightPlannerResource, dss: DSSInstanceResource, ): - super().__init__() - self.tested_uss = tested_uss.client - self.control_uss = control_uss.client - self.dss = dss.get_instance( - { - Scope.StrategicCoordination: "search for operational intent references to verify outcomes of planning activities and retrieve operational intent details" - } - ) - + scopes = { + Scope.StrategicCoordination: "search for operational intent references to verify outcomes of planning activities and retrieve operational intent details" + } expected_flight_intents = [ ExpectedFlightIntent( "flight1_planned", @@ -149,32 +135,16 @@ def __init__( ), ] - templates = flight_intents.get_flight_intents() - try: - validate_flight_intent_templates(templates, expected_flight_intents) - except ValueError as e: - raise ValueError( - f"`{self.me()}` TestScenario requirements for flight_intents not met: {e}" - ) - - for efi in expected_flight_intents: - setattr(self, efi.intent_id, templates[efi.intent_id]) - - def resolve_flight(self, flight_template: FlightInfoTemplate) -> FlightInfo: - return flight_template.resolve(self.time_context.evaluate_now()) - - def run(self, context: ExecutionContext): - self.begin_test_scenario(context) - - self.record_note( - "Tested USS", - f"{self.tested_uss.participant_id}", - ) - self.record_note( - "Control USS", - f"{self.control_uss.participant_id}", + super().__init__( + flight_intents=flight_intents, + expected_flight_intents=expected_flight_intents, + tested_uss=tested_uss, + control_uss=control_uss, + dss=dss, + scopes=scopes, ) + def run_planning_sequence(self, context: ExecutionContext): self.begin_test_case("Attempt to plan flight in conflict") self._attempt_plan_flight_conflict() self.end_test_case() @@ -206,8 +176,6 @@ def run(self, context: ExecutionContext): ) self.end_test_case() - self.end_test_scenario() - def _attempt_plan_flight_conflict(self): self.begin_test_step("Plan Flight 2") flight2_planned = self.resolve_flight(self.flight2_planned) @@ -530,8 +498,3 @@ def _attempt_modify_activated_flight_conflict( ) validator.expect_not_shared() self.end_test_step() - - def cleanup(self): - self.begin_cleanup() - cleanup_flights(self, (self.control_uss, self.tested_uss)) - self.end_cleanup() 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 new file mode 100644 index 0000000000..4222170b49 --- /dev/null +++ b/monitoring/uss_qualifier/scenarios/astm/utm/nominal_planning/planning_sequence_scenario.py @@ -0,0 +1,129 @@ +from abc import ABC, abstractmethod + +from uas_standards.astm.f3548.v21.constants import Scope + +from monitoring.monitorlib.clients.flight_planning.client import FlightPlannerClient +from monitoring.monitorlib.clients.flight_planning.flight_info import FlightInfo +from monitoring.monitorlib.clients.flight_planning.flight_info_template import ( + FlightInfoTemplate, +) +from monitoring.uss_qualifier.resources.astm.f3548.v21.dss import ( + DSSInstance, + DSSInstanceResource, +) +from monitoring.uss_qualifier.resources.flight_planning import ( + FlightIntentsResource, + FlightPlannerResource, +) +from monitoring.uss_qualifier.resources.flight_planning.flight_intent import ( + FlightIntentID, +) +from monitoring.uss_qualifier.resources.flight_planning.flight_intent_validation import ( + ExpectedFlightIntent, + estimate_scenario_execution_max_extents, + validate_flight_intent_templates, +) +from monitoring.uss_qualifier.scenarios.astm.utm.clear_area_validation import ( + validate_clear_area, +) +from monitoring.uss_qualifier.scenarios.flight_planning.test_steps import ( + cleanup_flights, +) +from monitoring.uss_qualifier.scenarios.scenario import TestScenario +from monitoring.uss_qualifier.suites.suite import ExecutionContext + + +class PlanningSequenceScenario(TestScenario, ABC): + """Abstracts a test scenario where a sequence of planning actions are performed. + + The provided flight_intents will be validated against expected_flight_intents and the flight intent templates + extracted from flight_intents will be added as attributes to this scenario object according to their intent_ids. + """ + + tested_uss: FlightPlannerClient + control_uss: FlightPlannerClient + dss: DSSInstance + flight_intents_templates: dict[FlightIntentID, FlightInfoTemplate] + # Note: FlightInfoTemplate attributes named according to flight_intents' intent_ids will also be present + + def __init__( + self, + tested_uss: FlightPlannerResource, + control_uss: FlightPlannerResource, + dss: DSSInstanceResource, + flight_intents: FlightIntentsResource, + expected_flight_intents: list[ExpectedFlightIntent], + scopes: dict[Scope, str], + ): + super().__init__() + 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()}) + + self.flight_intents_templates = flight_intents.get_flight_intents() + try: + validate_flight_intent_templates( + self.flight_intents_templates, expected_flight_intents + ) + except ValueError as e: + raise ValueError( + f"`{self.me()}` TestScenario requirements for flight_intents not met: {e}" + ) + + for efi in expected_flight_intents: + setattr(self, efi.intent_id, self.flight_intents_templates[efi.intent_id]) + + self.flight_intents_templates = ( + flight_intents.get_flight_intents() if flight_intents else {} + ) + try: + validate_flight_intent_templates( + self.flight_intents_templates, expected_flight_intents + ) + except ValueError as e: + raise ValueError( + f"`{self.me()}` TestScenario requirements for flight_intents not met: {e}" + ) + + def resolve_flight(self, flight_template: FlightInfoTemplate) -> FlightInfo: + return flight_template.resolve(self.time_context.evaluate_now()) + + @abstractmethod + def run_planning_sequence(self, context: ExecutionContext): + """Run the main planning sequence of the test scenario, assuming the test scenario has already begun.""" + raise NotImplementedError() + + def run(self, context: ExecutionContext): + self.begin_test_scenario(context) + + self.record_note( + "Tested USS", + f"{self.tested_uss.participant_id}", + ) + self.record_note( + "Control USS", + f"{self.control_uss.participant_id}", + ) + + self.begin_test_case("Prerequisites check") + self.begin_test_step("Verify area is clear") + estimated_max_extents = estimate_scenario_execution_max_extents( + self.time_context, self.flight_intents_templates + ) + validate_clear_area( + self, + self.dss, + [estimated_max_extents], + ignore_self=False, + ) + self.end_test_step() + self.end_test_case() + + self.run_planning_sequence(context) + + self.end_test_scenario() + + def cleanup(self): + self.begin_cleanup() + cleanup_flights(self, (self.control_uss, self.tested_uss)) + self.end_cleanup() diff --git a/monitoring/uss_qualifier/scenarios/scenario.py b/monitoring/uss_qualifier/scenarios/scenario.py index 8ffdf0758d..b0ba71d633 100644 --- a/monitoring/uss_qualifier/scenarios/scenario.py +++ b/monitoring/uss_qualifier/scenarios/scenario.py @@ -764,7 +764,11 @@ def find_test_scenarios( for descendant in descendants: if descendant not in test_scenarios: test_scenarios.add(descendant) - elif inspect.isclass(member) and member is not TestScenario: + elif ( + inspect.isclass(member) + and member is not TestScenario + and not inspect.isabstract(member) + ): if issubclass(member, TestScenario): if member not in test_scenarios: test_scenarios.add(member) diff --git a/monitoring/uss_qualifier/scenarios/scenario_test/utils.py b/monitoring/uss_qualifier/scenarios/scenario_test/utils.py index e2babe4d9a..c04f92ae27 100644 --- a/monitoring/uss_qualifier/scenarios/scenario_test/utils.py +++ b/monitoring/uss_qualifier/scenarios/scenario_test/utils.py @@ -65,7 +65,8 @@ def run(self, context): pass class TestScenarioB(_TestScenario): - pass + def run(self, context): + pass class NotATestScenarioC: pass @@ -77,10 +78,12 @@ class NotATestScenarioC: fake_sub_module = _build_module("test.submodule") class TestScenarioSubA(_TestScenario): - pass + def run(self, context): + pass class TestScenarioSubB(_TestScenario): - pass + def run(self, context): + pass fake_sub_module.TestScenarioSubA = TestScenarioSubA fake_sub_module.TestScenarioSubB = TestScenarioSubB @@ -89,7 +92,8 @@ class TestScenarioSubB(_TestScenario): fake_subsub_module = _build_module("test.submodule.subsubmodule") class TestScenarioSubSubA(_TestScenario): - pass + def run(self, context): + pass fake_subsub_module.TestScenarioSubSubA = TestScenarioSubSubA diff --git a/monitoring/uss_qualifier/suites/astm/utm/f3548_21.md b/monitoring/uss_qualifier/suites/astm/utm/f3548_21.md index 51f7c5f4a2..59d9157a77 100644 --- a/monitoring/uss_qualifier/suites/astm/utm/f3548_21.md +++ b/monitoring/uss_qualifier/suites/astm/utm/f3548_21.md @@ -40,7 +40,7 @@