diff --git a/monitoring/monitorlib/fetch/rid.py b/monitoring/monitorlib/fetch/rid.py index eb7cb44b4c..73f3b29c40 100644 --- a/monitoring/monitorlib/fetch/rid.py +++ b/monitoring/monitorlib/fetch/rid.py @@ -315,6 +315,21 @@ def timestamp(self) -> Optional[StringBasedDateTime]: f"Cannot retrieve speed using RID version {self.rid_version}" ) + @property + def timestamp_accuracy(self) -> Optional[float]: + if self.rid_version == RIDVersion.f3411_19: + if not self.v19_value.has_field_with_value("current_state"): + return None + return self.v19_value.current_state.timestamp_accuracy + elif self.rid_version == RIDVersion.f3411_22a: + if not self.v22a_value.has_field_with_value("current_state"): + return None + return self.v22a_value.current_state.timestamp_accuracy + else: + raise NotImplementedError( + f"Cannot retrieve speed using RID version {self.rid_version}" + ) + def errors(self) -> List[str]: try: rid_version = self.rid_version diff --git a/monitoring/uss_qualifier/scenarios/astm/netrid/display_data_evaluator.py b/monitoring/uss_qualifier/scenarios/astm/netrid/display_data_evaluator.py index 93946e08db..694ace955f 100644 --- a/monitoring/uss_qualifier/scenarios/astm/netrid/display_data_evaluator.py +++ b/monitoring/uss_qualifier/scenarios/astm/netrid/display_data_evaluator.py @@ -62,6 +62,16 @@ def id(self) -> str: def most_recent_position(self) -> Optional[Position]: return self.query.flights[self.flight].most_recent_position + # TODO we may rather want to expose the whole flight object (self.query.flights[self.flight]) + # and let callers access subfields directly (will be handled in separate PR) + @property + def timestamp_accuracy(self) -> Optional[float]: + return self.query.flights[self.flight].timestamp_accuracy + + @property + def raw_flight(self) -> dict: + return self.query.query.response.json["flights"][self.flight] + ObservationType = Union[Flight, DPObservedFlight] @@ -892,13 +902,14 @@ def _evaluate_normal_sp_observation( participants=[mapping.injected_flight.uss_participant_id], ) - # Check that altitudes match for any observed flights matching injected flights + # Check that required fields are present and match for any observed flights matching injected flights for mapping in mappings.values(): injected_telemetry = mapping.injected_flight.flight.telemetry[ mapping.telemetry_index ] observed_position = mapping.observed_flight.most_recent_position injected_position = injected_telemetry.position + if "alt" in observed_position: with self._test_scenario.check( "Service Provider altitude", @@ -914,6 +925,44 @@ def _evaluate_normal_sp_observation( details=f"{mapping.injected_flight.uss_participant_id}'s flight with injection ID {mapping.injected_flight.flight.injection_id} in test {mapping.injected_flight.test_id} had telemetry index {mapping.telemetry_index} at {injected_telemetry.timestamp} with lat={injected_telemetry.position.lat}, lng={injected_telemetry.position.lng}, alt={injected_telemetry.position.alt}, but Service Provider reported lat={observed_position.lat}, lng={observed_position.lng}, alt={observed_position.alt} at {mapping.observed_flight.query.query.request.initiated_at}", ) + # Due to how implicit dicts are deserialized and the timestamp_accuracy is specified in the OpenAPI file, we need to look into + # the raw JSON response to determine if the field is present: + # + # Because the spec requires the field to be present while also specifying a default value, the implicit dict based class derived + # from the spec may not catch that a field is missing at deserialization time (because the RIDAircraftState class specifies a default + # value for the field, no ValueError will be thrown when ImplicitDict.parse() is called). + # + # This means that a missing json field will neither raise an exception nor cause the field to be set to None when we access it, + # meaning this part of the logic cannot rely on the deserialized value to determine if the field was present or not. + raw_flight = mapping.observed_flight.raw_flight + raw_state = ( + raw_flight["current_state"] if "current_state" in raw_flight else {} + ) + with self._test_scenario.check( + "Service Provider timestamp accuracy is present", + [mapping.injected_flight.uss_participant_id], + ) as check: + if "timestamp_accuracy" not in raw_state: + check.record_failed( + "Timestamp accuracy not present in Service Provider response", + details=f"Timestamp accuracy not present in Service Provider {mapping.injected_flight.uss_participant_id}'s response for flight with injection ID {mapping.injected_flight.flight.injection_id} in test {mapping.injected_flight.test_id} with telemetry index {mapping.telemetry_index}", + ) + + if "timestamp_accuracy" in raw_state: + # From this point on we can use the 'timestamp_accuracy' field of the deserialized object + with self._test_scenario.check( + "Service Provider timestamp accuracy is correct", + [mapping.injected_flight.uss_participant_id], + ) as check: + if ( + mapping.observed_flight.timestamp_accuracy + != injected_telemetry.timestamp_accuracy + ): + check.record_failed( + "Timestamp accuracy in Service Provider response is incorrect", + details=f"Timestamp accuracy in Service Provider {mapping.injected_flight.uss_participant_id}'s response for flight with injection ID {mapping.injected_flight.flight.injection_id} in test {mapping.injected_flight.test_id} with telemetry index {mapping.telemetry_index} is {mapping.observed_flight.timestamp_accuracy} which is not equal to the injected value of {injected_telemetry.timestamp_accuracy}", + ) + # Verify that flight details queries succeeded and returned correctly-formatted data for mapping in mappings.values(): details_queries = [ diff --git a/monitoring/uss_qualifier/scenarios/astm/netrid/v19/nominal_behavior.md b/monitoring/uss_qualifier/scenarios/astm/netrid/v19/nominal_behavior.md index 1795378ab3..b0d211f2ce 100644 --- a/monitoring/uss_qualifier/scenarios/astm/netrid/v19/nominal_behavior.md +++ b/monitoring/uss_qualifier/scenarios/astm/netrid/v19/nominal_behavior.md @@ -88,6 +88,14 @@ The identity of flights is determined by precisely matching the known injected p **[astm.f3411.v19.NET0260,Table1,11](../../../../requirements/astm/f3411/v19.md)** requires that relevant Remote ID data, consistent with the common data dictionary, be reported by the Service Provider. Injected flight data had known altitudes, but the altitude reported by the Service Provider did not match those known altitudes. +#### ⚠️ Service Provider timestamp accuracy is present check + +**[astm.f3411.v19.NET0260,Table1,5](../../../../requirements/astm/f3411/v19.md)** requires that relevant Remote ID data, consistent with the common data dictionary, be reported by the Service Provider. Timestamp accuracy is a required field, but it was not present. + +#### ⚠️ Service Provider timestamp accuracy is correct check + +**[astm.f3411.v19.NET0260,Table1,5](../../../../requirements/astm/f3411/v19.md)** requires that relevant Remote ID data, consistent with the common data dictionary, be reported by the Service Provider. Injected flight data had known timestamp accuracy, but the accuracy reported by the Service Provider did not match it. + #### Successful flight details query check **[astm.f3411.v19.NET0710,2](../../../../requirements/astm/f3411/v19.md)** and **[astm.f3411.v19.NET0340](../../../../requirements/astm/f3411/v19.md)** require a Service Provider to implement the GET flight details endpoint. This check will fail if uss_qualifier cannot query that endpoint (specified in the ISA present in the DSS) successfully. diff --git a/monitoring/uss_qualifier/scenarios/astm/netrid/v22a/nominal_behavior.md b/monitoring/uss_qualifier/scenarios/astm/netrid/v22a/nominal_behavior.md index f7a1c174e7..726ee2385f 100644 --- a/monitoring/uss_qualifier/scenarios/astm/netrid/v22a/nominal_behavior.md +++ b/monitoring/uss_qualifier/scenarios/astm/netrid/v22a/nominal_behavior.md @@ -88,6 +88,14 @@ The identity of flights is determined by precisely matching the known injected p **[astm.f3411.v22a.NET0260,Table1,12](../../../../requirements/astm/f3411/v22a.md)** requires that relevant Remote ID data, consistent with the common data dictionary, be reported by the Service Provider. Injected flight data had known altitudes, but the altitude reported by the Service Provider did not match those known altitudes. +#### ⚠️ Service Provider timestamp accuracy is present check + +**[astm.f3411.v22a.NET0260,Table1,5](../../../../requirements/astm/f3411/v22a.md)** requires that relevant Remote ID data, consistent with the common data dictionary, be reported by the Service Provider. Timestamp accuracy is a required field, but it was not present. + +#### ⚠️ Service Provider timestamp accuracy is correct check + +**[astm.f3411.v22a.NET0260,Table1,5](../../../../requirements/astm/f3411/v22a.md)** requires that relevant Remote ID data, consistent with the common data dictionary, be reported by the Service Provider. Injected flight data had known timestamp accuracy, but the accuracy reported by the Service Provider did not match it. + #### Successful flight details query check **[astm.f3411.v22a.NET0710,2](../../../../requirements/astm/f3411/v22a.md)** and **[astm.f3411.v22a.NET0340](../../../../requirements/astm/f3411/v22a.md) require a Service Provider to implement the GET flight details endpoint. This check will fail if uss_qualifier cannot query that endpoint (specified in the ISA present in the DSS) successfully. diff --git a/monitoring/uss_qualifier/suites/astm/netrid/f3411_19.md b/monitoring/uss_qualifier/suites/astm/netrid/f3411_19.md index 89bab93543..53c730c3f0 100644 --- a/monitoring/uss_qualifier/suites/astm/netrid/f3411_19.md +++ b/monitoring/uss_qualifier/suites/astm/netrid/f3411_19.md @@ -21,7 +21,7 @@