diff --git a/AGENTS.md b/AGENTS.md
index 14454910b1..9b8181f947 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -18,6 +18,8 @@ This document contains key context, nuances, and troubleshooting tips specifical
## 2. Navigating Data Schemas
+- **Resource base-class imports**: `configurations.configuration` imports resource definitions through the resources package, whose initializer loads the `Resource` base class. Import `ParticipantID` from configuration under `TYPE_CHECKING` with postponed annotations in `resources/resource.py` to avoid a circular import when scenarios are imported first.
+- **Generic method annotations**: With postponed annotations, `typing.get_type_hints(SomeGenericClass.__init__)` cannot resolve class-scoped PEP 695 type parameters automatically (verified on Python 3.13 and 3.14). Use `monitoring.monitorlib.typing.get_method_type_hints` to supply the declaring class's namespace, including for inherited methods and generic return types. Regression tests should exercise resource construction and selector resolution with postponed annotations enabled.
- **Implicit Types**: Many schema objects inherit from `ImplicitDict`. This means that reading their raw Python class definitions may not reveal all their expected structure. Rely on their `__annotations__` or their OpenAPI documentation.
- **Optional `ImplicitDict` Fields**: `ImplicitDict` fields annotated with `| None` (or `Optional[...]`) indicate optional dictionary keys. In implicitdict 5.0.0, accessing a missing optional attribute returns `None` without inserting a key; use `"field" in obj` to distinguish absence from an explicitly stored null. Constructor keyword arguments set to `None` normally omit optional values, but class defaults may still populate the key; assigning `obj.field = None` or `obj["field"] = None` stores an explicit null. Use dictionary membership rather than `hasattr` when key presence matters.
- **implicitdict 5.0.0 Typing**: Its unannotated `__getattribute__` introduces `None` into the inferred type of undeclared attributes (`Unknown | Any | None` in basedpyright 1.40.1), while declared fields and properties retain their types. New optional-access diagnostics may expose a nonexistent member or the wrong schema class rather than a missing optional value. Check the receiver's declaration before adding a None guard or suppressing the diagnostic. An explicit `-> Any` return annotation on the accessor restores the previous permissive fallback without changing runtime behavior, but also hides these pre-existing member-access mistakes.
@@ -26,9 +28,10 @@ This document contains key context, nuances, and troubleshooting tips specifical
- **Datetime Types**: Fields annotated as `StringBasedDateTime` (or generic `Time` structures wrapping it) inherently expose a `.datetime` property mapping directly to Python's built-in `datetime.datetime` type. Always rely on this `.datetime` attribute for date-math instead of manually parsing string strings.
- **DSS Base URL Suffixes**: When configuring DSS instances, the base URL for ASTM F3411-22a (NetRID v2) requires the `/rid/v2` suffix path (e.g. `http://dss1.uss1.localutm/rid/v2`), whereas F3411-19 and SCD (F3548) typically use the host base URL directly without that suffix.
-## 3. `uss_qualifier` Test Development Rules
+## 3. `uss_qualifier` Development Rules
- **Markdown Documentation is Mandatory**: Every check (e.g., `self._scenario.check(...)`) added to a Python test scenario must be meticulously documented in the corresponding Markdown documentation file for that specific test scenario or fragment.
- **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.
+- **Resource name shortcuts**: The shortcuts for resource names added to __int__.py (e.g., `from .planning_area import PlanningAreaResource as PlanningAreaResource`) should not be continued. All future development should use only the canonical resource location (e.g., `resources.planning_area.PlanningAreaResource`).
## 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.
diff --git a/monitoring/monitorlib/typing.py b/monitoring/monitorlib/typing.py
index 872e42dcc4..f30bbddf62 100644
--- a/monitoring/monitorlib/typing.py
+++ b/monitoring/monitorlib/typing.py
@@ -1,5 +1,5 @@
import types
-from typing import TypeVar, Union, get_args, get_origin
+from typing import Any, TypeVar, Union, get_args, get_origin, get_type_hints
TypeName = str
"""Plain string representing a Python class/type name without generic type arguments (e.g., `resources.flight_planning.FlightPlannerResource`)."""
@@ -14,6 +14,20 @@
"""A type annotation as may appear on a constructor/function parameter or generic base class."""
+def get_method_type_hints(cls: type, method_name: str) -> dict[str, Any]:
+ """Resolve method annotations using the namespace of its declaring class.
+
+ Python 3.13's get_type_hints does not automatically include class-scoped
+ PEP 695 type parameters when evaluating postponed method annotations.
+ Inherited methods need their declaring class's parameters, even when the
+ subclass defines type parameters with the same names.
+ """
+ owner = next(base for base in cls.__mro__ if method_name in vars(base))
+ localns = dict(vars(owner))
+ localns.update((p.__name__, p) for p in getattr(owner, "__type_params__", ()))
+ return get_type_hints(getattr(cls, method_name), localns=localns)
+
+
def substitute_typevars(
tp: TypeAnnotation, mapping: dict[TypeVar, ResolvedType]
) -> TypeAnnotation:
diff --git a/monitoring/uss_qualifier/reports/report.py b/monitoring/uss_qualifier/reports/report.py
index 4945383da7..760dc3be5b 100644
--- a/monitoring/uss_qualifier/reports/report.py
+++ b/monitoring/uss_qualifier/reports/report.py
@@ -293,6 +293,9 @@ class TestScenarioReport(ImplicitDict):
resource_origins: Optional[dict[ResourceID, str]]
"""For each resource used by this test scenario, the place that resource originated."""
+ resource_participants: Optional[dict[ResourceID, list[ParticipantID]]]
+ """For each resource used by this test scenario that is associated with one or more participants, the participants associated with that resource."""
+
notes: Optional[dict[str, Note]]
"""Additional information about this scenario that may be useful"""
diff --git a/monitoring/uss_qualifier/reports/sequence_view/events.py b/monitoring/uss_qualifier/reports/sequence_view/events.py
index 4d308b6e2f..860da4cb23 100644
--- a/monitoring/uss_qualifier/reports/sequence_view/events.py
+++ b/monitoring/uss_qualifier/reports/sequence_view/events.py
@@ -274,6 +274,7 @@ def compute_tested_scenario(
if "resource_origins" in report and report.resource_origins is not None
else {}
),
+ resource_participants=report.get("resource_participants") or {},
)
action_indexer.index += 1
return scenario
diff --git a/monitoring/uss_qualifier/reports/sequence_view/summary_types.py b/monitoring/uss_qualifier/reports/sequence_view/summary_types.py
index f6d1d95e68..69085c5cb1 100644
--- a/monitoring/uss_qualifier/reports/sequence_view/summary_types.py
+++ b/monitoring/uss_qualifier/reports/sequence_view/summary_types.py
@@ -117,6 +117,7 @@ class TestedScenario:
participants: dict[ParticipantID, TestedParticipant]
execution_error: ErrorReport | None
resource_origins: dict[ResourceID, str]
+ resource_participants: dict[ResourceID, list[ParticipantID]]
@property
def rows(self) -> int:
diff --git a/monitoring/uss_qualifier/reports/templates/sequence_view/scenario.html b/monitoring/uss_qualifier/reports/templates/sequence_view/scenario.html
index 90a0cd4726..ce65a4d953 100644
--- a/monitoring/uss_qualifier/reports/templates/sequence_view/scenario.html
+++ b/monitoring/uss_qualifier/reports/templates/sequence_view/scenario.html
@@ -1,6 +1,15 @@
{% from "explorer.html" import explorer_header, explorer_content, explorer_footer %}
{% from "sequence_view/severity.html" import severity_symbol, severity_class with context %}
+{% macro participant_list(participants) -%}
+ {%- if participants|length == 1 -%}
+ {{ participants[0]|e }}
+ {%- elif participants|length == 2 -%}
+ {{ participants[0]|e }} and {{ participants[1]|e }}
+ {%- else -%}
+ {{ participants[:-1]|join(', ')|e }}, and {{ participants[-1]|e }}
+ {%- endif -%}
+{%- endmacro %}
{% set collapsible.queries = collapsible.queries + ["resource_origin_list"] %}
diff --git a/monitoring/uss_qualifier/resources/astm/f3411/dss.py b/monitoring/uss_qualifier/resources/astm/f3411/dss.py
index 3767561446..39bb66e009 100644
--- a/monitoring/uss_qualifier/resources/astm/f3411/dss.py
+++ b/monitoring/uss_qualifier/resources/astm/f3411/dss.py
@@ -70,6 +70,9 @@ def is_same_as(self, other: DSSInstance) -> bool:
class DSSInstanceResource(Resource[DSSInstanceSpecification]):
dss_instance: DSSInstance
+ def get_associated_participants(self) -> list[ParticipantID]:
+ return [self.dss_instance.participant_id]
+
def __init__(
self,
specification: DSSInstanceSpecification,
@@ -119,6 +122,9 @@ class DSSInstancesSpecification(ImplicitDict):
class DSSInstancesResource(Resource[DSSInstancesSpecification]):
dss_instances: list[DSSInstance]
+ def get_associated_participants(self) -> list[ParticipantID]:
+ return list(dict.fromkeys(d.participant_id for d in self.dss_instances))
+
def __init__(
self,
specification: DSSInstancesSpecification,
diff --git a/monitoring/uss_qualifier/resources/astm/f3548/v21/dss.py b/monitoring/uss_qualifier/resources/astm/f3548/v21/dss.py
index 864fdec533..dce3586fad 100644
--- a/monitoring/uss_qualifier/resources/astm/f3548/v21/dss.py
+++ b/monitoring/uss_qualifier/resources/astm/f3548/v21/dss.py
@@ -51,6 +51,7 @@
from monitoring.monitorlib.inspection import calling_function_name, fullname
from monitoring.monitorlib.mutate import scd as mutate
from monitoring.monitorlib.mutate.scd import MutatedSubscription
+from monitoring.uss_qualifier.configurations.configuration import ParticipantID
from monitoring.uss_qualifier.resources.astm.dss import NotificationIndexImplementation
from monitoring.uss_qualifier.resources.communications import AuthAdapterResource
from monitoring.uss_qualifier.resources.plural import PluralResource
@@ -75,7 +76,7 @@ class DSSInstanceSpecification(ImplicitDict):
notification_index_implementation: Optional[NotificationIndexImplementation]
"""Style of implementation this instance uses for notification index.
-
+
If not specified, TimeBased is assumed."""
def __init__(self, *args, **kwargs):
@@ -713,6 +714,9 @@ class DSSInstanceResource(Resource[DSSInstanceSpecification]):
_auth_adapter: AuthAdapterResource
_client: UTMClientSession
+ def get_associated_participants(self) -> list[ParticipantID]:
+ return [self.participant_id]
+
def __init__(
self,
specification: DSSInstanceSpecification,
diff --git a/monitoring/uss_qualifier/resources/communications/auth_adapter.py b/monitoring/uss_qualifier/resources/communications/auth_adapter.py
index c073ed55fd..e0641b72d3 100644
--- a/monitoring/uss_qualifier/resources/communications/auth_adapter.py
+++ b/monitoring/uss_qualifier/resources/communications/auth_adapter.py
@@ -36,6 +36,9 @@ class AuthAdapterResource(Resource[AuthAdapterSpecification]):
scopes: set[str]
participant_id: ParticipantID | None = None
+ def get_associated_participants(self) -> list[ParticipantID]:
+ return [self.participant_id] if self.participant_id else []
+
def __init__(self, specification: AuthAdapterSpecification, resource_origin: str):
super().__init__(specification, resource_origin)
if (
diff --git a/monitoring/uss_qualifier/resources/flight_planning/flight_planners.py b/monitoring/uss_qualifier/resources/flight_planning/flight_planners.py
index 14067da251..049ebb6ff8 100644
--- a/monitoring/uss_qualifier/resources/flight_planning/flight_planners.py
+++ b/monitoring/uss_qualifier/resources/flight_planning/flight_planners.py
@@ -29,6 +29,9 @@ class FlightPlannerResource(Resource[FlightPlannerSpecification]):
client: FlightPlannerClient
participant_id: ParticipantID
+ def get_associated_participants(self) -> list[ParticipantID]:
+ return [self.participant_id]
+
def __init__(
self,
specification: FlightPlannerSpecification,
diff --git a/monitoring/uss_qualifier/resources/geospatial_info/geospatial_info_providers.py b/monitoring/uss_qualifier/resources/geospatial_info/geospatial_info_providers.py
index 4482612efc..8f7d817f0d 100644
--- a/monitoring/uss_qualifier/resources/geospatial_info/geospatial_info_providers.py
+++ b/monitoring/uss_qualifier/resources/geospatial_info/geospatial_info_providers.py
@@ -54,6 +54,9 @@ class GeospatialInfoProviderResource(Resource[GeospatialInfoProviderSpecificatio
client: GeospatialInfoClient
participant_id: ParticipantID
+ def get_associated_participants(self) -> list[ParticipantID]:
+ return [self.participant_id]
+
def __init__(
self,
specification: GeospatialInfoProviderSpecification,
diff --git a/monitoring/uss_qualifier/resources/interuss/datastore/datastore.py b/monitoring/uss_qualifier/resources/interuss/datastore/datastore.py
index ece037ed3c..e0af25e2c7 100644
--- a/monitoring/uss_qualifier/resources/interuss/datastore/datastore.py
+++ b/monitoring/uss_qualifier/resources/interuss/datastore/datastore.py
@@ -7,6 +7,7 @@
import psycopg
from implicitdict import ImplicitDict
+from monitoring.uss_qualifier.configurations.configuration import ParticipantID
from monitoring.uss_qualifier.resources.resource import Resource
@@ -30,6 +31,9 @@ def __init__(self, *args, **kwargs):
class DatastoreDBNodeResource(Resource[DatastoreDBNodeSpecification]):
_specification: DatastoreDBNodeSpecification
+ def get_associated_participants(self) -> list[ParticipantID]:
+ return [self.participant_id]
+
def __init__(
self,
specification: DatastoreDBNodeSpecification,
@@ -66,6 +70,9 @@ class DatastoreDBClusterSpecification(ImplicitDict):
class DatastoreDBClusterResource(Resource[DatastoreDBClusterSpecification]):
nodes: list[DatastoreDBNodeResource]
+ def get_associated_participants(self) -> list[ParticipantID]:
+ return list(dict.fromkeys(n.participant_id for n in self.nodes))
+
def __init__(
self,
specification: DatastoreDBClusterSpecification,
diff --git a/monitoring/uss_qualifier/resources/interuss/mock_uss/client.py b/monitoring/uss_qualifier/resources/interuss/mock_uss/client.py
index 6d5594f196..7675bf3f1d 100644
--- a/monitoring/uss_qualifier/resources/interuss/mock_uss/client.py
+++ b/monitoring/uss_qualifier/resources/interuss/mock_uss/client.py
@@ -170,6 +170,9 @@ class MockUSSSpecification(ImplicitDict):
class MockUSSResource(Resource[MockUSSSpecification]):
mock_uss: MockUSSClient
+ def get_associated_participants(self) -> list[ParticipantID]:
+ return [self.mock_uss.participant_id]
+
def __init__(
self,
specification: MockUSSSpecification,
@@ -192,6 +195,9 @@ class MockUSSsSpecification(ImplicitDict):
class MockUSSsResource(Resource[MockUSSsSpecification]):
mock_uss_instances: list[MockUSSClient]
+ def get_associated_participants(self) -> list[ParticipantID]:
+ return list(dict.fromkeys(m.participant_id for m in self.mock_uss_instances))
+
def __init__(
self,
specification: MockUSSsSpecification,
diff --git a/monitoring/uss_qualifier/resources/interuss/uss_identification.py b/monitoring/uss_qualifier/resources/interuss/uss_identification.py
index c35fa1110c..487b18d4ae 100644
--- a/monitoring/uss_qualifier/resources/interuss/uss_identification.py
+++ b/monitoring/uss_qualifier/resources/interuss/uss_identification.py
@@ -48,6 +48,9 @@ def __init__(
super().__init__(specification, resource_origin)
self.identifiers = specification.uss_identifiers or {}
+ def get_associated_participants(self) -> list[ParticipantID]:
+ return list(self.identifiers)
+
def attribute_query_server(self, query: Query) -> None:
"""Identify the participant ID of the server responding to this query and mutate `query` accordingly, if possible"""
claims = query.request.token
diff --git a/monitoring/uss_qualifier/resources/netrid/observers.py b/monitoring/uss_qualifier/resources/netrid/observers.py
index 67def4c8ce..7e912a73b4 100644
--- a/monitoring/uss_qualifier/resources/netrid/observers.py
+++ b/monitoring/uss_qualifier/resources/netrid/observers.py
@@ -8,6 +8,7 @@
from monitoring.monitorlib import fetch, infrastructure
from monitoring.monitorlib.fetch import QueryType
+from monitoring.uss_qualifier.configurations.configuration import ParticipantID
from monitoring.uss_qualifier.resources.communications import AuthAdapterResource
from monitoring.uss_qualifier.resources.resource import Resource
@@ -96,6 +97,9 @@ class NetRIDObserversSpecification(ImplicitDict):
class NetRIDObserversResource(Resource[NetRIDObserversSpecification]):
observers: list[RIDSystemObserver]
+ def get_associated_participants(self) -> list[ParticipantID]:
+ return list(dict.fromkeys(o.participant_id for o in self.observers))
+
def __init__(
self,
specification: NetRIDObserversSpecification,
diff --git a/monitoring/uss_qualifier/resources/netrid/service_providers.py b/monitoring/uss_qualifier/resources/netrid/service_providers.py
index 4e2e55929a..5fd8531c23 100644
--- a/monitoring/uss_qualifier/resources/netrid/service_providers.py
+++ b/monitoring/uss_qualifier/resources/netrid/service_providers.py
@@ -12,6 +12,7 @@
SCOPE_RID_QUALIFIER_INJECT,
CreateTestParameters,
)
+from monitoring.uss_qualifier.configurations.configuration import ParticipantID
from monitoring.uss_qualifier.resources.communications import AuthAdapterResource
from monitoring.uss_qualifier.resources.resource import Resource
@@ -109,6 +110,9 @@ def get_user_notifications(
class NetRIDServiceProviders(Resource[NetRIDServiceProvidersSpecification]):
service_providers: list[NetRIDServiceProvider]
+ def get_associated_participants(self) -> list[ParticipantID]:
+ return list(dict.fromkeys(s.participant_id for s in self.service_providers))
+
def __init__(
self,
specification: NetRIDServiceProvidersSpecification,
diff --git a/monitoring/uss_qualifier/resources/plural.py b/monitoring/uss_qualifier/resources/plural.py
index 01d8fdbfaa..ee1b8047a6 100644
--- a/monitoring/uss_qualifier/resources/plural.py
+++ b/monitoring/uss_qualifier/resources/plural.py
@@ -1,17 +1,25 @@
-from abc import ABC, abstractmethod
-
-from monitoring.uss_qualifier.resources.resource import Resource
-
-
-class PluralResource[TSingularResource: Resource](Resource, ABC):
- """Resource that provides multiple instances of a singular resources."""
-
- @abstractmethod
- def get_resource_instances_count(self) -> int:
- """Get the number of singular resources this plural resource contains."""
- raise NotImplementedError()
-
- @abstractmethod
- def get_resource_instance(self, index: int) -> TSingularResource:
- """Get the specific singular resource at the specified index contained by this plural resource."""
- raise NotImplementedError()
+from abc import ABC, abstractmethod
+
+from monitoring.uss_qualifier.configurations.configuration import ParticipantID
+from monitoring.uss_qualifier.resources.resource import Resource
+
+
+class PluralResource[TSingularResource: Resource](Resource, ABC):
+ """Resource that provides multiple instances of a singular resources."""
+
+ def get_associated_participants(self) -> list[ParticipantID]:
+ participants = []
+ for i in range(self.get_resource_instances_count()):
+ resource = self.get_resource_instance(i)
+ participants.extend(resource.get_associated_participants())
+ return list(dict.fromkeys(participants))
+
+ @abstractmethod
+ def get_resource_instances_count(self) -> int:
+ """Get the number of singular resources this plural resource contains."""
+ raise NotImplementedError()
+
+ @abstractmethod
+ def get_resource_instance(self, index: int) -> TSingularResource:
+ """Get the specific singular resource at the specified index contained by this plural resource."""
+ raise NotImplementedError()
diff --git a/monitoring/uss_qualifier/resources/resource.py b/monitoring/uss_qualifier/resources/resource.py
index 528e974299..9ecbdebbd5 100644
--- a/monitoring/uss_qualifier/resources/resource.py
+++ b/monitoring/uss_qualifier/resources/resource.py
@@ -1,7 +1,9 @@
+from __future__ import annotations
+
import inspect
from abc import ABC, abstractmethod
from dataclasses import dataclass
-from typing import TypeVar, get_origin, get_type_hints
+from typing import TYPE_CHECKING, TypeVar, get_origin
from implicitdict import ImplicitDict
from loguru import logger
@@ -14,6 +16,7 @@
TypeAnnotation,
collect_typevars,
format_type_name,
+ get_method_type_hints,
infer_typevars_from_arg,
is_type_covariant_match,
substitute_typevars,
@@ -25,6 +28,9 @@
ResourceTypeName,
)
+if TYPE_CHECKING:
+ from monitoring.uss_qualifier.configurations.configuration import ParticipantID
+
SpecificationType = TypeVar("SpecificationType", bound=ImplicitDict)
@@ -65,7 +71,7 @@ def _get_typevar_map(self) -> dict[TypeVar, ResolvedType]:
params: tuple[TypeVar, ...] = getattr(self.__class__, "__parameters__", ())
if params:
try:
- hints = get_type_hints(self.__class__.__init__)
+ hints = get_method_type_hints(self.__class__, "__init__")
for arg_name, hint in hints.items():
if (
isinstance(hint, TypeVar)
@@ -108,7 +114,11 @@ def is_or_inherits(self, resource_type: ResourceTypeName) -> bool:
self._get_type_annotation(), specified_type, self._get_typevar_map()
)
- def resolve_resource(self) -> "Resource":
+ def get_associated_participants(self) -> list[ParticipantID]:
+ """Return distinct participant IDs associated with this resource, if any."""
+ return []
+
+ def resolve_resource(self) -> Resource:
"""Return the resource to expose in the resource pool after construction.
Selectors may return another resource. Its type metadata and origin are
@@ -176,7 +186,7 @@ def __init__(
def resolve_resource(self) -> SingularResourceType:
resource = self._provider.provide_resource_for(index=self._specification.index)
expected_type = substitute_typevars(
- get_type_hints(SingularResource.resolve_resource)["return"],
+ get_method_type_hints(SingularResource, "resolve_resource")["return"],
self._get_typevar_map(),
)
if not isinstance(resource, Resource) or not is_type_covariant_match(
@@ -329,7 +339,7 @@ def get_resource_types(
collect_typevars(resource_type, typevar_map)
- raw_signature = get_type_hints(resource_type.__init__)
+ raw_signature = get_method_type_hints(resource_type, "__init__")
constructor_signature: dict[str, TypeAnnotation] = {
name: substitute_typevars(t, typevar_map) for name, t in raw_signature.items()
}
diff --git a/monitoring/uss_qualifier/resources/versioning/client.py b/monitoring/uss_qualifier/resources/versioning/client.py
index 5ae7a0da36..fcdeb59912 100644
--- a/monitoring/uss_qualifier/resources/versioning/client.py
+++ b/monitoring/uss_qualifier/resources/versioning/client.py
@@ -34,6 +34,9 @@ class VersionProvidersSpecification(ImplicitDict):
class VersionProvidersResource(Resource[VersionProvidersSpecification]):
version_providers: list[VersioningClient]
+ def get_associated_participants(self) -> list[ParticipantID]:
+ return list(dict.fromkeys(v.participant_id for v in self.version_providers))
+
def __init__(
self,
specification: VersionProvidersSpecification,
diff --git a/monitoring/uss_qualifier/scenarios/scenario.py b/monitoring/uss_qualifier/scenarios/scenario.py
index 729d392d5a..dda4db475e 100644
--- a/monitoring/uss_qualifier/scenarios/scenario.py
+++ b/monitoring/uss_qualifier/scenarios/scenario.py
@@ -280,6 +280,9 @@ class GenericTestScenario(ABC):
resource_origins: dict[ResourceID, str]
"""Map between local resource name (as defined in test scenario) to where that resource originated."""
+ resource_participants: dict[ResourceID, list[ParticipantID]]
+ """Participants associated with each local resource that has participant associations."""
+
_phase: ScenarioPhase = ScenarioPhase.Undefined
_scenario_report: TestScenarioReport | None = None
_current_case: TestCaseDocumentation | None = None
@@ -300,6 +303,7 @@ def __init__(self):
self._phase = ScenarioPhase.NotStarted
self.time_context = TestTimeContext()
self.cache = {}
+ self.resource_participants = {}
@staticmethod
def make_test_scenario(
@@ -311,6 +315,7 @@ def make_test_scenario(
constructor_signature = inspect.signature(scenario_type.__init__)
constructor_args = {}
resource_origins = {}
+ resource_participants = {}
for arg_name, arg in constructor_signature.parameters.items():
if arg_name == "self":
continue
@@ -328,10 +333,14 @@ def make_test_scenario(
)
constructor_args[arg_name] = resource_pool[arg_name]
resource_origins[arg_name] = resource_pool[arg_name].resource_origin
+ participants = resource_pool[arg_name].get_associated_participants()
+ if participants:
+ resource_participants[arg_name] = participants
scenario = scenario_type(**constructor_args)
scenario.declaration = declaration
scenario.resource_origins = resource_origins
+ scenario.resource_participants = resource_participants
return scenario
@abstractmethod
@@ -395,6 +404,7 @@ def _make_scenario_report(self) -> None:
scenario_type=self.declaration.scenario_type,
documentation_url=self.documentation.url,
resource_origins=self.resource_origins,
+ resource_participants=self.resource_participants,
start_time=StringBasedDateTime(datetime.now(UTC)),
cases=[],
)
diff --git a/schemas/monitoring/uss_qualifier/reports/report/TestScenarioReport.json b/schemas/monitoring/uss_qualifier/reports/report/TestScenarioReport.json
index 074e8c7137..639724fb0d 100644
--- a/schemas/monitoring/uss_qualifier/reports/report/TestScenarioReport.json
+++ b/schemas/monitoring/uss_qualifier/reports/report/TestScenarioReport.json
@@ -94,6 +94,25 @@
"null"
]
},
+ "resource_participants": {
+ "additionalProperties": {
+ "items": {
+ "type": "string"
+ },
+ "type": "array"
+ },
+ "description": "For each resource used by this test scenario that is associated with one or more participants, the participants associated with that resource.",
+ "properties": {
+ "$ref": {
+ "description": "Path to content that replaces the $ref",
+ "type": "string"
+ }
+ },
+ "type": [
+ "object",
+ "null"
+ ]
+ },
"scenario_type": {
"description": "Type of this test scenario",
"type": "string"