diff --git a/monitoring/uss_qualifier/scenarios/astm/utm/flight_intent_validation/flight_intent_validation.md b/monitoring/uss_qualifier/scenarios/astm/utm/flight_intent_validation/flight_intent_validation.md index f0eb4a2666..6223d806ec 100644 --- a/monitoring/uss_qualifier/scenarios/astm/utm/flight_intent_validation/flight_intent_validation.md +++ b/monitoring/uss_qualifier/scenarios/astm/utm/flight_intent_validation/flight_intent_validation.md @@ -17,6 +17,7 @@ FlightIntentsResource that provides the following flight intents: - `valid_flight`: a valid operational intent upon which other invalid ones are derived, in `Accepted` state - `valid_activated`: state mutation `Activated` - `invalid_too_far_away`: reference time mutation: reference time pulled back so that it is like the operational intent is attempted to be planned more than OiMaxPlanHorizon = 30 days ahead of time + - `invalid_recently_ended`: end time that is within 5 to 10 seconds in the past, relative to TimeOfEvaluation - `valid_conflict_tiny_overlap`: volumes mutation: has a volume that overlaps with `valid_op_intent` just above IntersectionMinimumPrecision = 1cm in a way that must result as a conflict Because the scenario involves activation of intents, all activated intents must be active during the execution of the @@ -46,6 +47,25 @@ to reject or accept the flight. If the USS indicates that the injection attempt #### [Validate flight intent too far ahead of time not planned](../validate_not_shared_operational_intent.md) +### Attempt to plan recently ended flight intent test step +The user flight intent that the test driver attempts to plan has a reference time that has recently ended by just slightly more than `TimeSyncMaxDifferentialSeconds`=5 seconds. +As such, if the USS synchronizes its time correctly, the planning attempt should be rejected. + +#### 🛑 Incorrectly planned check +If the USS successfully plans the flight or otherwise fails to indicate a rejection, it means that it failed to validate +that the intent provided was in the past. Therefore, this check will fail if the USS indicates success in creating the +flight from the user flight intent, per one of the following requirements: +- the USS does not implement properly the interface _getOperationalIntentDetails_ as required by **[astm.f3548.v21.USS0105](../../../../requirements/astm/f3548/v21.md)**, which specifies that _The end time may not be in the past_; or +- the USS did not synchronize its time within `TimeSyncMaxDifferentialSeconds`=5 seconds of an industry-recognized time source as required by **[astm.f3548.v21.GEN0100](../../../../requirements/astm/f3548/v21.md)**; or +- the USS did not use the synchronized time for the operational intent timestamps, as required by **[astm.f3548.v21.GEN0105](../../../../requirements/astm/f3548/v21.md)**. + +#### 🛑 Failure check +All flight intent data provided was complete and correct. It should have been processed successfully, allowing the USS +to reject or accept the flight. If the USS indicates that the injection attempt failed, this check will fail per +**[interuss.automated_testing.flight_planning.ExpectedBehavior](../../../../requirements/interuss/automated_testing/flight_planning.md)**. + +#### [Validate recently ended flight intent not planned](../validate_not_shared_operational_intent.md) + ## Validate transition to Ended state after cancellation test case ### Plan flight intent test step 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 c0819077e5..042d5c7bd8 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 @@ -1,9 +1,32 @@ +from datetime import timedelta +from typing import Dict + import arrow +from monitoring.monitorlib.clients.flight_planning.flight_info import ( + AirspaceUsageState, + UasState, +) +from monitoring.monitorlib.clients.flight_planning.flight_info_template import ( + FlightInfoTemplate, +) +from monitoring.monitorlib.clients.flight_planning.planning import ( + FlightPlanStatus, + PlanningActivityResult, +) from monitoring.monitorlib.geotemporal import Volume4DCollection +from monitoring.monitorlib.temporal import TimeDuringTest, Time +from monitoring.uss_qualifier.resources.flight_planning.flight_intent_validation import ( + ExpectedFlightIntent, + validate_flight_intent_templates, +) from monitoring.uss_qualifier.suites.suite import ExecutionContext from uas_standards.astm.f3548.v21.api import OperationalIntentState -from uas_standards.astm.f3548.v21.constants import OiMaxPlanHorizonDays, Scope +from uas_standards.astm.f3548.v21.constants import ( + OiMaxPlanHorizonDays, + Scope, + TimeSyncMaxDifferentialSeconds, +) from monitoring.uss_qualifier.resources.astm.f3548.v21 import DSSInstanceResource from monitoring.uss_qualifier.resources.astm.f3548.v21.dss import DSSInstance @@ -28,6 +51,7 @@ cleanup_flights, submit_flight_intent, delete_flight_intent, + submit_flight, ) from uas_standards.interuss.automated_testing.scd.v1.api import ( InjectFlightResponseResult, @@ -40,6 +64,7 @@ class FlightIntentValidation(TestScenario): valid_activated: FlightIntent invalid_too_far_away: FlightIntent + invalid_recently_ended_template: FlightInfoTemplate valid_conflict_tiny_overlap: FlightIntent @@ -60,6 +85,8 @@ def __init__( } ) + # parse intents in deprecated FlightIntent format + # TODO: adapt scenario to use FlightInfoTemplate everywhere instead _flight_intents = { k: FlightIntent.from_flight_info_template(v) for k, v in flight_intents.get_flight_intents().items() @@ -69,9 +96,6 @@ def __init__( for intent in _flight_intents.values(): extents.extend(intent.request.operational_intent.volumes) extents.extend(intent.request.operational_intent.off_nominal_volumes) - self._intents_extent = Volume4DCollection.from_interuss_scd_api( - extents - ).bounding_volume.to_f3548v21() try: ( @@ -143,7 +167,39 @@ def __init__( f"`{self.me()}` TestScenario requirements for flight_intents not met: {e}" ) + # parse intents in FlightInfoTemplate format + expected_flight_intents = [ + ExpectedFlightIntent( + "invalid_recently_ended", + "Recently Ended Flight", + usage_state=AirspaceUsageState.Planned, + uas_state=UasState.Nominal, + ), + ] + + 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: + intent = FlightIntent.from_flight_info_template(templates[efi.intent_id]) + extents.extend(intent.request.operational_intent.volumes) + extents.extend(intent.request.operational_intent.off_nominal_volumes) + setattr(self, f"{efi.intent_id}_template", templates[efi.intent_id]) + + self._intents_extent = Volume4DCollection.from_interuss_scd_api( + extents + ).bounding_volume.to_f3548v21() + def run(self, context: ExecutionContext): + times = { + TimeDuringTest.StartOfTestRun: Time(context.start_time), + TimeDuringTest.StartOfScenario: Time(arrow.utcnow().datetime), + } self.begin_test_scenario(context) self.record_note( "Tested USS", @@ -151,7 +207,7 @@ def run(self, context: ExecutionContext): ) self.begin_test_case("Attempt to plan invalid flight intents") - self._attempt_invalid() + self._attempt_invalid(times) self.end_test_case() self.begin_test_case("Validate transition to Ended state after cancellation") @@ -164,7 +220,7 @@ def run(self, context: ExecutionContext): self.end_test_scenario() - def _attempt_invalid(self): + def _attempt_invalid(self, times: Dict[TimeDuringTest, Time]): self.begin_test_step("Attempt to plan flight intent too far ahead of time") with OpIntentValidator( self, @@ -184,6 +240,46 @@ def _attempt_invalid(self): validator.expect_not_shared() self.end_test_step() + self.begin_test_step("Attempt to plan recently ended flight intent") + times[TimeDuringTest.TimeOfEvaluation] = Time(arrow.utcnow().datetime) + invalid_recently_ended = self.invalid_recently_ended_template.resolve(times) + + invalid_recently_ended_end_time = ( + invalid_recently_ended.basic_information.area.time_end.datetime + ) + recently_ended_min_end_time = times[ + TimeDuringTest.TimeOfEvaluation + ].datetime - timedelta(seconds=TimeSyncMaxDifferentialSeconds + 5) + recently_ended_max_end_time = times[ + TimeDuringTest.TimeOfEvaluation + ].datetime - timedelta(seconds=TimeSyncMaxDifferentialSeconds) + if invalid_recently_ended_end_time < recently_ended_min_end_time: + raise ValueError( + f"`{self.me()}` TestScenario requirements for flight_intents not met: invalid_recently_ended does not end recently (ends at {invalid_recently_ended_end_time}, minimum allowed {recently_ended_min_end_time})" + ) + if invalid_recently_ended_end_time > recently_ended_max_end_time: + raise ValueError( + f"`{self.me()}` TestScenario requirements for flight_intents not met: invalid_recently_ended does not end in the past (ends at {invalid_recently_ended_end_time}, maximum allowed {recently_ended_max_end_time})" + ) + + with OpIntentValidator( + self, + self.tested_uss, + self.dss, + self._intents_extent, + ) as validator: + submit_flight( + self, + "Incorrectly planned", + {(PlanningActivityResult.Rejected, FlightPlanStatus.NotPlanned)}, + {PlanningActivityResult.Failed: "Failure"}, + self.tested_uss.client, + invalid_recently_ended, + ) + + validator.expect_not_shared() + self.end_test_step() + def _validate_ended_cancellation(self): self.begin_test_step("Plan flight intent") with OpIntentValidator( diff --git a/monitoring/uss_qualifier/test_data/flight_intents/standard/invalid_flight_intents.yaml b/monitoring/uss_qualifier/test_data/flight_intents/standard/invalid_flight_intents.yaml index 61d41bfffa..7317b268f4 100644 --- a/monitoring/uss_qualifier/test_data/flight_intents/standard/invalid_flight_intents.yaml +++ b/monitoring/uss_qualifier/test_data/flight_intents/standard/invalid_flight_intents.yaml @@ -54,6 +54,25 @@ intents: time_during_test: StartOfTestRun offset: 32d + invalid_recently_ended: + delta: + source: valid_flight + mutation: + # end_time < now - TimeSyncMaxDifferentialSeconds = 5 seconds + basic_information: + area: + - start_time: + offset_from: + starting_from: + time_during_test: TimeOfEvaluation + offset: -1m + end_time: + offset_from: + starting_from: + time_during_test: TimeOfEvaluation + offset: -5s + duration: + valid_conflict_tiny_overlap: delta: source: valid_flight