diff --git a/monitoring/monitorlib/geotemporal.py b/monitoring/monitorlib/geotemporal.py index 541b674402..2dd75fd1ec 100644 --- a/monitoring/monitorlib/geotemporal.py +++ b/monitoring/monitorlib/geotemporal.py @@ -2,8 +2,9 @@ import math from datetime import datetime, timedelta -from typing import Optional, List, Tuple, Dict +from typing import Optional, List, Tuple, Dict, Union +import arrow from implicitdict import ImplicitDict, StringBasedTimeDelta import s2sphere as s2sphere @@ -427,3 +428,31 @@ def has_active_volume(self, time_ref: datetime) -> bool: class Volume4DTemplateCollection(List[Volume4DTemplate]): pass + + +def end_time_of( + volume_or_volumes: Union[ + f3548v21.Volume4D, + Volume4D, + List[Union[f3548v21.Volume4D, Volume4D]], + Volume4DCollection, + ] +) -> Optional[Time]: + """Retrieve the end time of a volume or list of volumes.""" + if isinstance(volume_or_volumes, f3548v21.Volume4D): + if "time_end" in volume_or_volumes and volume_or_volumes.time_end: + return Time(volume_or_volumes.time_end.value) + else: + return None + elif isinstance(volume_or_volumes, Volume4D): + return volume_or_volumes.time_end + elif isinstance(volume_or_volumes, Volume4DCollection): + return volume_or_volumes.time_end + elif isinstance(volume_or_volumes, list): + time_ends = [end_time_of(v) for v in volume_or_volumes] + if not time_ends: + return None + elif any(t is None for t in time_ends): + return None + else: + return max(time_ends) 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 042d5c7bd8..478d4c606b 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 @@ -275,6 +275,7 @@ def _attempt_invalid(self, times: Dict[TimeDuringTest, Time]): {PlanningActivityResult.Failed: "Failure"}, self.tested_uss.client, invalid_recently_ended, + may_end_in_past=True, ) validator.expect_not_shared() diff --git a/monitoring/uss_qualifier/scenarios/flight_planning/test_steps.py b/monitoring/uss_qualifier/scenarios/flight_planning/test_steps.py index 1bfe527547..269f57a40a 100644 --- a/monitoring/uss_qualifier/scenarios/flight_planning/test_steps.py +++ b/monitoring/uss_qualifier/scenarios/flight_planning/test_steps.py @@ -1,6 +1,9 @@ import inspect from typing import Optional, Tuple, Iterable, Set, Dict, Union +import arrow + +from monitoring.monitorlib.geotemporal import end_time_of from uas_standards.interuss.automated_testing.flight_planning.v1.api import ( BasicFlightPlanInformationUsageState, BasicFlightPlanInformationUasState, @@ -23,8 +26,6 @@ ) from monitoring.monitorlib.fetch import QueryError, Query -from uas_standards.astm.f3548.v21.api import OperationalIntentState - from uas_standards.interuss.automated_testing.scd.v1.api import ( InjectFlightRequest, InjectFlightResponseResult, @@ -260,6 +261,12 @@ def submit_flight_intent( f"expected and unexpected results overlap: {expected_results.intersection(failed_checks.keys())}" ) + intent_end_time = end_time_of(flight_intent.operational_intent.volumes) + if intent_end_time and intent_end_time.datetime < arrow.utcnow(): + raise ValueError( + f"attempt to submit invalid flight intent: end time is in the past: {intent_end_time}" + ) + with scenario.check(success_check, [flight_planner.participant_id]) as check: try: resp, query, flight_id, advisories = flight_planner.request_flight( @@ -492,12 +499,15 @@ def submit_flight( flight_id: Optional[str] = None, additional_fields: Optional[dict] = None, skip_if_not_supported: bool = False, + may_end_in_past: bool = False, ) -> Tuple[PlanningActivityResponse, Optional[str]]: """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. The severity of each failed check may be overridden if needed. If skip_if_not_supported=True and the USS responds that the operation is not supported, the check is skipped without failing. + If may_end_in_past=True, this function won't raise an error if the flight intent's end time is in the past. + This function does not directly implement a test step. Returns: @@ -509,6 +519,13 @@ def submit_flight( f"expected and unexpected results overlap: {expected_results.intersection(failed_checks.keys())}" ) + if not may_end_in_past: + intent_end_time = end_time_of(flight_info.basic_information.area) + if intent_end_time and intent_end_time.datetime < arrow.utcnow(): + raise ValueError( + f"attempt to submit invalid flight intent: end time is in the past: {intent_end_time}" + ) + with scenario.check(success_check, [flight_planner.participant_id]) as check: try: resp, query, flight_id = request_flight( diff --git a/monitoring/uss_qualifier/test_data/che/flight_intents/invalid_flight_auths.yaml b/monitoring/uss_qualifier/test_data/che/flight_intents/invalid_flight_auths.yaml index 8011174983..e2a6463e8c 100644 --- a/monitoring/uss_qualifier/test_data/che/flight_intents/invalid_flight_auths.yaml +++ b/monitoring/uss_qualifier/test_data/che/flight_intents/invalid_flight_auths.yaml @@ -25,7 +25,7 @@ intents: start_time: offset_from: starting_from: - time_during_test: StartOfTestRun + time_during_test: StartOfScenario offset: -1s duration: 5m 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 7317b268f4..bf648362b8 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 @@ -24,9 +24,9 @@ intents: start_time: offset_from: starting_from: - time_during_test: StartOfTestRun + time_during_test: StartOfScenario offset: -1s - duration: 5m + duration: 10m astm_f3548_21: priority: 0 @@ -51,7 +51,7 @@ intents: - start_time: offset_from: starting_from: - time_during_test: StartOfTestRun + time_during_test: StartOfScenario offset: 32d invalid_recently_ended: diff --git a/monitoring/uss_qualifier/test_data/flight_intents/standard/non_conflicting.yaml b/monitoring/uss_qualifier/test_data/flight_intents/standard/non_conflicting.yaml index d9b1b14f36..dd75e21f4b 100644 --- a/monitoring/uss_qualifier/test_data/flight_intents/standard/non_conflicting.yaml +++ b/monitoring/uss_qualifier/test_data/flight_intents/standard/non_conflicting.yaml @@ -24,7 +24,7 @@ intents: start_time: offset_from: starting_from: - time_during_test: StartOfTestRun + time_during_test: StartOfScenario offset: -1s duration: 5m @@ -59,7 +59,7 @@ intents: start_time: offset_from: starting_from: - time_during_test: StartOfTestRun + time_during_test: StartOfScenario offset: -1s duration: 5m