diff --git a/AGENTS.md b/AGENTS.md index 81e8dfe64a6..6a51b26b12f 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -30,6 +30,7 @@ This document contains key context, nuances, and troubleshooting tips specifical - **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. ## 4. Local Testing constraints +- **Qualifier Working Directory**: Run resource-construction checks and qualifier resource tests from `monitoring/uss_qualifier` with the repository root on `PYTHONPATH`. External resource paths such as `file://./test_data/...` resolve relative to the current working directory, as they do in `run_locally.sh`. - **Docker Dependency**: Mock USS and DSS environments require active Docker containers. Standard testing commands are typically structured via bash scripts like `./monitoring/uss_qualifier/run_locally.sh `. If container-building fails due to `Authentication` or package registry issues in the agent's environment, gracefully halt and ask the human user to run the script instead. ## 5. Continuous Improvement of this Guide diff --git a/NEXT_RELEASE_NOTES.md b/NEXT_RELEASE_NOTES.md index 819a8640751..f5ebb9bdce1 100644 --- a/NEXT_RELEASE_NOTES.md +++ b/NEXT_RELEASE_NOTES.md @@ -40,6 +40,8 @@ The release notes should contain at least the following sections: * For any uss_qualifier test configurations using any of the test suites `suites.astm.utm.f3548_21`, `suites.faa.uft.message_signing`, `suites.uspace.flight_auth`, or `suites.uspace.required_services`, the following resources must be changed: * `priority_preemption_flights` was previously an optional `resources.flight_planning.FlightIntentsResource`. If this resource was previously provided to one of the above test suites, a new resource named `priority_preemption_flights_provider` must be provided instead. This new resource must be a `resources.ResourceProvidingResource[resources.flight_planning.FlightIntentsResource]`, and one suitable concrete implementation is a `resources.geospatial.TriangularCascadeSoutheastResource`. See [f3548_self_contained](./monitoring/uss_qualifier/configurations/dev/f3548_self_contained.yaml) for an example. +* (uncommon) For any uss_qualifier test configurations directly using (i.e., not via the f3548_21 test suite) the scenario `scenarios.flight_planning.prep_planners.PrepareFlightPlannersScenario` or the scenario `scenarios.astm.utm.prep_planners.PrepareFlightPlanners`, the `flight_intents_provider` resource must be replaced with a `flight_intents_environments` that is a `resources.environments.ResourceEnvironmentsGenerator`; see [the f3548_21 test suite](./monitoring/uss_qualifier/suites/astm/utm/f3548_21.yaml) for an example. + ## Optional migration tasks ## Important information diff --git a/monitoring/uss_qualifier/action_generators/resource_environments.py b/monitoring/uss_qualifier/action_generators/resource_environments.py new file mode 100644 index 00000000000..fa9f2302d1b --- /dev/null +++ b/monitoring/uss_qualifier/action_generators/resource_environments.py @@ -0,0 +1,80 @@ +from collections.abc import Iterator + +from implicitdict import ImplicitDict + +from monitoring.monitorlib.inspection import fullname +from monitoring.uss_qualifier.action_generators.action_generator import ActionGenerator +from monitoring.uss_qualifier.action_generators.documentation.definitions import ( + PotentialGeneratedAction, +) +from monitoring.uss_qualifier.action_generators.documentation.documentation import ( + list_potential_actions_for_action_declaration, +) +from monitoring.uss_qualifier.resources.definitions import ResourceID +from monitoring.uss_qualifier.resources.environments import ( + ResourceEnvironmentsGenerator, +) +from monitoring.uss_qualifier.resources.resource import ( + MissingResourceError, + ResourceType, +) +from monitoring.uss_qualifier.suites.definitions import TestSuiteActionDeclaration +from monitoring.uss_qualifier.suites.suite import TestSuiteAction + + +class ResourceEnvironmentsActionGeneratorSpecification(ImplicitDict): + action_to_repeat: TestSuiteActionDeclaration + """Test suite action to run for each resource environment""" + + resource_environments_source: ResourceID + """Resource providing different resource environments""" + + +class ResourceEnvironmentsActionGenerator( + ActionGenerator[ResourceEnvironmentsActionGeneratorSpecification] +): + _actions: list[TestSuiteAction] + _current_action: int + + @classmethod + def list_potential_actions( + cls, specification: ResourceEnvironmentsActionGeneratorSpecification | None + ) -> list[PotentialGeneratedAction]: + if specification is None: + raise ValueError(f"{cls.__name__} requires a specification") + return list_potential_actions_for_action_declaration( + specification.action_to_repeat + ) + + @classmethod + def get_name(cls) -> str: + return "For each resource environment" + + def __init__( + self, + specification: ResourceEnvironmentsActionGeneratorSpecification, + resources: dict[ResourceID, ResourceType], + ): + if specification.resource_environments_source not in resources: + raise MissingResourceError( + f"Resource ID {specification.resource_environments_source} specified as `resource_environments_source` was not present in the available resource pool", + specification.resource_environments_source, + ) + environments_source = resources[specification.resource_environments_source] + if not isinstance(environments_source, ResourceEnvironmentsGenerator): + raise ValueError( + f"Expected resource ID {specification.resource_environments_source} to be a {fullname(ResourceEnvironmentsGenerator)} but it was a {fullname(environments_source.__class__)} instead" + ) + + self._actions = [] + for resource_env in environments_source.get_environments(): + modified_resources = resources | resource_env + + self._actions.append( + TestSuiteAction(specification.action_to_repeat, modified_resources) + ) + + self._current_action = 0 + + def actions(self) -> Iterator[TestSuiteAction]: + yield from self._actions diff --git a/monitoring/uss_qualifier/resources/README.md b/monitoring/uss_qualifier/resources/README.md index e3eb1cad78d..00db1441b93 100644 --- a/monitoring/uss_qualifier/resources/README.md +++ b/monitoring/uss_qualifier/resources/README.md @@ -32,6 +32,8 @@ Resources for a given test configuration are all declared in a single global res 2. Every type of test resource must define a "resource specification", which is a serializable data type that fully defines how to create an instance of that resource type. 3. Every type of test resource must define how to create an instance of the test resource from an instance of the resource specification. +Dependency resource IDs may be suffixed with `?` to indicate that the dependency is optional. Explicit constructor parameters must have a default value to accept optional dependencies. Dependencies accepted through `**kwargs` are treated as having an implicit default of `None`. Missing optional dependencies are omitted from the constructor call, so they do not appear in the `kwargs` dictionary. + ## Resource-modifying resources diff --git a/monitoring/uss_qualifier/resources/combinations.py b/monitoring/uss_qualifier/resources/combinations.py new file mode 100644 index 00000000000..c5c60ccb774 --- /dev/null +++ b/monitoring/uss_qualifier/resources/combinations.py @@ -0,0 +1,89 @@ +from collections.abc import Iterator +from dataclasses import dataclass +from typing import Optional + +from implicitdict import ImplicitDict + +from monitoring.monitorlib.inspection import fullname +from monitoring.uss_qualifier.resources.definitions import ResourceID +from monitoring.uss_qualifier.resources.environments import ( + ResourceEnvironmentSelectorResource, + ResourceEnvironmentsGenerator, +) +from monitoring.uss_qualifier.resources.plural import PluralResource +from monitoring.uss_qualifier.resources.resource import Resource + + +class ResourceCombinationsSpecification(ImplicitDict): + pass + + +@dataclass +class _RoleCombination: + role: ResourceID + resources: PluralResource + index: int + + +class ResourceCombinationsResource( + ResourceEnvironmentsGenerator, Resource[ResourceCombinationsSpecification] +): + """Resource that produces combinations of each dependent PluralResource according to ResourceIDs assigned by dependent resource keys. + + For example, with a pre-existing dependent `flight_planners` FlightPlannersResource, a + ResourceCombinationsResource(tested_uss=flight_planners, control_uss=flight_planners) will produce each combination + of tested_uss=FP1, control_uss=FP2 for all FP1, FP2 combinations in flight_planners. + """ + + _resource_sources: dict[ResourceID, PluralResource] + _selector: Optional[ResourceEnvironmentSelectorResource] + + def __init__( + self, + specification: ResourceCombinationsSpecification, + resource_origin: str, + combination_selector: Optional[ResourceEnvironmentSelectorResource] = None, + **dependencies, + ): + super().__init__(specification, resource_origin) + self._selector = combination_selector + self._resource_sources = dict() + for k, v in dependencies.items(): + if not isinstance(v, PluralResource): + raise ValueError( + f"Dependent resources for ResourceCombinationsResource must be PluralResources; dependent resource '{k}' was instead a {fullname(v.__class__)}" + ) + if v.get_resource_instances_count() == 0: + # Can't fill this role because there are no resources to fill it with. + continue + self._resource_sources[ResourceID(k)] = v + + def get_environments(self) -> Iterator[dict[ResourceID, Resource]]: + """Get the combinations of resources fulfilling each specified role.""" + roles = [ + _RoleCombination(role=k, resources=v, index=0) + for k, v in self._resource_sources.items() + ] + + incremented = True + while incremented: + combination = { + role.role: role.resources.get_resource_instance(role.index) + for role in roles + } + if self._selector is None or self._selector.select_resource_environment( + combination + ): + yield combination + + incremented = False + for r in range(len(roles)): + if ( + roles[r].index + < roles[r].resources.get_resource_instances_count() - 1 + ): + roles[r].index += 1 + for r0 in range(r): + roles[r0].index = 0 + incremented = True + break diff --git a/monitoring/uss_qualifier/resources/definitions.py b/monitoring/uss_qualifier/resources/definitions.py index e0c182553b2..ef297284540 100644 --- a/monitoring/uss_qualifier/resources/definitions.py +++ b/monitoring/uss_qualifier/resources/definitions.py @@ -18,7 +18,7 @@ class ResourceDeclaration(ImplicitDict): """Type of resource, expressed as a Python class name qualified relative to this `resources` module (optionally with generic type arguments in brackets)""" dependencies: dict[ResourceID, ResourceID] = {} - """Mapping of dependency parameter (additional argument to concrete resource constructor) to `name` of resource to use (optionally suffixed with `?` to indicate an optional dependency when the constructor parameter has a default value)""" + """Mapping of dependency parameter (additional argument to concrete resource constructor) to `name` of resource to use (optionally suffixed with `?` to indicate an optional dependency when the constructor parameter has a default value or is accepted through **kwargs). Missing optional dependencies are omitted from the constructor call; dependencies accepted through **kwargs have an implicit default of None.""" specification: dict = {} """Specification of resource; format is the SpecificationType that corresponds to the `resource_type`""" diff --git a/monitoring/uss_qualifier/resources/environments.py b/monitoring/uss_qualifier/resources/environments.py new file mode 100644 index 00000000000..a353b41ab74 --- /dev/null +++ b/monitoring/uss_qualifier/resources/environments.py @@ -0,0 +1,126 @@ +from abc import ABC, abstractmethod +from collections.abc import Iterator +from typing import Optional + +from implicitdict import ImplicitDict + +from monitoring.monitorlib.inspection import fullname +from monitoring.uss_qualifier.resources.definitions import ResourceID +from monitoring.uss_qualifier.resources.resource import ( + Resource, + ResourceProvidingResource, + ValueResource, +) + + +class ResourceEnvironmentsGenerator(ABC): + @abstractmethod + def get_environments(self) -> Iterator[dict[ResourceID, Resource]]: + raise NotImplementedError() + + +class ResourceEnvironmentSelectorSpecification(ImplicitDict): + """Specification defining how resource environments should be selected from the available/generated environments.""" + + pass + + +class ResourceEnvironmentSelectorResource( + ValueResource[ResourceEnvironmentSelectorSpecification] +): + def select_resource_environment( + self, resource_environment: dict[ResourceID, Resource] + ) -> bool: + """Returns True when the specified environment should be selected/used.""" + return True + + +class EnvironmentAugmentationResourceSpecification(ImplicitDict): + # In the future, this specification could be enhanced to affect how and whether providers are exercised. + pass + + +class EnvironmentAugmentationResource( + ResourceEnvironmentsGenerator, + Resource[EnvironmentAugmentationResourceSpecification], +): + """Presents as a ResourceEnvironmentsGenerator which augments the environments from an input `environments` resource + with additional resources named according to additional ResourceProvidingResource resource dependencies.""" + + _environment_generator: ResourceEnvironmentsGenerator + _providers: dict[ResourceID, ResourceProvidingResource] + + def __init__( + self, + specification: EnvironmentAugmentationResourceSpecification, + resource_origin: str, + environments: ResourceEnvironmentsGenerator, + **providers, + ): + super().__init__(specification, resource_origin) + + if not isinstance(environments, ResourceEnvironmentsGenerator): + raise ValueError( + f"The `provider` resource dependency for an {self.__class__.__name__} must be a ResourceEnvironmentsGenerator; found instead a {fullname(environments.__class__)}" + ) + self._environment_generator = environments + + self._providers = dict() + for k, v in providers.items(): + if not isinstance(v, ResourceProvidingResource): + raise ValueError( + f"All non-`provider` resource dependencies for an {self.__class__.__name__} must be ResourceProvidingResources, but the '{k}' resource dependency was instead a {fullname(environments.__class__)}" + ) + self._providers[ResourceID(k)] = v + + def get_environments(self) -> Iterator[dict[ResourceID, Resource]]: + for i, environment in enumerate(self._environment_generator.get_environments()): + augmentation = dict() + for resource_id, provider in self._providers.items(): + augmentation[resource_id] = provider.provide_resource_for( + index=i, environment=environment + ) + yield environment | augmentation + + +class ConcatenatedResourceEnvironmentsResourceSpecification(ImplicitDict): + environments_sequence: Optional[list[ResourceID]] + """Sequence in which each dependent ResourceEnvironmentsGenerator resource's environments are enumerated.""" + + +class ConcatenatedResourceEnvironmentsResource( + ResourceEnvironmentsGenerator, + Resource[ConcatenatedResourceEnvironmentsResourceSpecification], +): + """Presents as a ResourceEnvironmentsGenerator which sequentially enumerates the environments from each dependent + resource in the specified sequence.""" + + _generators: list[ResourceEnvironmentsGenerator] + + def __init__( + self, + specification: ConcatenatedResourceEnvironmentsResourceSpecification, + resource_origin: str, + **generators, + ): + super().__init__(specification, resource_origin) + + for k, v in generators.items(): + if not isinstance(v, ResourceEnvironmentsGenerator): + raise ValueError( + f"All resource dependencies for a {fullname(self.__class__)} must be ResourceEnvironmentsGenerators, but the '{k}' resource dependency was instead a {fullname(v.__class__)}" + ) + + remaining_generators = generators.copy() + self._generators = [] + if specification.environments_sequence: + for resource_id in specification.environments_sequence: + if resource_id in remaining_generators: + self._generators.append(remaining_generators.pop(resource_id)) + + for generator in remaining_generators.values(): + self._generators.append(generator) + + def get_environments(self) -> Iterator[dict[ResourceID, Resource]]: + for generator in self._generators: + yield from generator.get_environments() diff --git a/monitoring/uss_qualifier/resources/flight_planning/flight_planners.py b/monitoring/uss_qualifier/resources/flight_planning/flight_planners.py index e8ffe455a0f..14067da2518 100644 --- a/monitoring/uss_qualifier/resources/flight_planning/flight_planners.py +++ b/monitoring/uss_qualifier/resources/flight_planning/flight_planners.py @@ -10,9 +10,14 @@ from monitoring.uss_qualifier.reports.report import ParticipantID from monitoring.uss_qualifier.resources.communications import AuthAdapterResource from monitoring.uss_qualifier.resources.definitions import ResourceID +from monitoring.uss_qualifier.resources.environments import ( + ResourceEnvironmentSelectorResource, + ResourceEnvironmentSelectorSpecification, +) from monitoring.uss_qualifier.resources.flight_planning.flight_planner import ( FlightPlannerConfiguration, ) +from monitoring.uss_qualifier.resources.plural import PluralResource from monitoring.uss_qualifier.resources.resource import Resource @@ -65,7 +70,9 @@ class FlightPlannersSpecification(ImplicitDict): flight_planners: list[FlightPlannerConfiguration] -class FlightPlannersResource(Resource[FlightPlannersSpecification]): +class FlightPlannersResource( + PluralResource[FlightPlannerResource], Resource[FlightPlannersSpecification] +): flight_planners: list[FlightPlannerResource] def __init__( @@ -86,11 +93,19 @@ def __init__( for i, p in enumerate(specification.flight_planners) ] + def get_resource_instances_count(self) -> int: + return len(self.flight_planners) + + def get_resource_instance(self, index: int) -> FlightPlannerResource: + return self.flight_planners[index] + def make_subset(self, select_indices: Iterable[int]) -> list[FlightPlannerResource]: return [self.flight_planners[i] for i in select_indices] -class FlightPlannerCombinationSelectorSpecification(ImplicitDict): +class FlightPlannerCombinationSelectorSpecification( + ResourceEnvironmentSelectorSpecification +): must_include: Optional[list[ParticipantID]] """The set of flight planners which must be included in every combination""" @@ -99,7 +114,8 @@ class FlightPlannerCombinationSelectorSpecification(ImplicitDict): class FlightPlannerCombinationSelectorResource( - Resource[FlightPlannerCombinationSelectorSpecification] + ResourceEnvironmentSelectorResource, + Resource[FlightPlannerCombinationSelectorSpecification], ): _specification: FlightPlannerCombinationSelectorSpecification @@ -111,6 +127,17 @@ def __init__( super().__init__(specification, resource_origin) self._specification = specification + def select_resource_environment( + self, resource_environment: dict[ResourceID, Resource] + ) -> bool: + return self.is_valid_combination( + { + resource_id: resource + for resource_id, resource in resource_environment.items() + if isinstance(resource, FlightPlannerResource) + } + ) + def is_valid_combination( self, flight_planners: dict[ResourceID, FlightPlannerResource] ): diff --git a/monitoring/uss_qualifier/resources/plural.py b/monitoring/uss_qualifier/resources/plural.py new file mode 100644 index 00000000000..01d8fdbfaa1 --- /dev/null +++ b/monitoring/uss_qualifier/resources/plural.py @@ -0,0 +1,17 @@ +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() diff --git a/monitoring/uss_qualifier/resources/resource.py b/monitoring/uss_qualifier/resources/resource.py index f62e54b8713..30d560165e0 100644 --- a/monitoring/uss_qualifier/resources/resource.py +++ b/monitoring/uss_qualifier/resources/resource.py @@ -287,6 +287,9 @@ def get_resource_types( name: substitute_typevars(t, typevar_map) for name, t in raw_signature.items() } init_params = inspect.signature(resource_type.__init__).parameters + accepts_keyword_dependencies = any( + param.kind == inspect.Parameter.VAR_KEYWORD for param in init_params.values() + ) specification_type = None for arg_name, arg_type in constructor_signature.items(): @@ -312,7 +315,15 @@ def get_resource_types( for arg_name, pool_source in declaration.dependencies.items(): if pool_source.endswith("?"): param = init_params.get(arg_name) - if param is None or param.default is inspect.Parameter.empty: + has_default = ( + param is not None and param.default is not inspect.Parameter.empty + ) + # Dependencies accepted through **kwargs have an implicit None default; + # absent optional dependencies are omitted from the constructor call. + is_keyword_dependency = accepts_keyword_dependencies and ( + param is None or param.kind == inspect.Parameter.VAR_KEYWORD + ) + if not has_default and not is_keyword_dependency: raise ValueError( f'Resource declaration for {declaration.resource_type} specifies optional dependency "{pool_source}" for parameter "{arg_name}", which has no default value in {resource_type.__name__}.__init__' ) diff --git a/monitoring/uss_qualifier/scenarios/astm/utm/prep_planners.md b/monitoring/uss_qualifier/scenarios/astm/utm/prep_planners.md index 6598e072c84..ff61e0a073a 100644 --- a/monitoring/uss_qualifier/scenarios/astm/utm/prep_planners.md +++ b/monitoring/uss_qualifier/scenarios/astm/utm/prep_planners.md @@ -34,9 +34,9 @@ FlightIntentsResource containing flight intents that will be used in subsequent (Optional) If more than one FlightIntentsResource will be used in subsequent tests, additional intents may be specified with this resource. -### flight_intents_provider +### flight_intents_environments -(Optional) ResourceProvidingResource providing FlightIntentsResource according to `index`. +(Optional) ResourceEnvironmentsGenerator resource providing resource environments, each containing a `flight_intents` FlightIntentsResource. ## Flight planners preparation test case diff --git a/monitoring/uss_qualifier/scenarios/astm/utm/prep_planners.py b/monitoring/uss_qualifier/scenarios/astm/utm/prep_planners.py index f6be888bf20..d2e2dea6864 100644 --- a/monitoring/uss_qualifier/scenarios/astm/utm/prep_planners.py +++ b/monitoring/uss_qualifier/scenarios/astm/utm/prep_planners.py @@ -3,15 +3,14 @@ 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.environments import ( + ResourceEnvironmentsGenerator, +) from monitoring.uss_qualifier.resources.flight_planning import ( FlightIntentsResource, FlightPlannersResource, ) -from monitoring.uss_qualifier.resources.flight_planning.flight_intent import ( - FlightIntentsSpecification, -) from monitoring.uss_qualifier.resources.interuss.mock_uss.client import MockUSSResource -from monitoring.uss_qualifier.resources.resource import ResourceProvidingResource from monitoring.uss_qualifier.scenarios.astm.utm.clear_area_validation import ( validate_clear_area, ) @@ -35,10 +34,7 @@ def __init__( flight_intents2: FlightIntentsResource | None = None, flight_intents3: FlightIntentsResource | None = None, flight_intents4: FlightIntentsResource | None = None, - flight_intents_provider: ResourceProvidingResource[ - FlightIntentsSpecification, FlightIntentsResource - ] - | None = None, + flight_intents_environments: ResourceEnvironmentsGenerator | None = None, ): super().__init__( flight_planners, @@ -47,7 +43,7 @@ def __init__( flight_intents2, flight_intents3, flight_intents4, - flight_intents_provider, + flight_intents_environments, ) self.dss = dss.get_instance( { diff --git a/monitoring/uss_qualifier/scenarios/flight_planning/prep_planners.md b/monitoring/uss_qualifier/scenarios/flight_planning/prep_planners.md index cd248668959..8eb0e311f4c 100644 --- a/monitoring/uss_qualifier/scenarios/flight_planning/prep_planners.md +++ b/monitoring/uss_qualifier/scenarios/flight_planning/prep_planners.md @@ -30,9 +30,9 @@ FlightIntentsResource containing flight intents that will be used in subsequent (Optional) If more than one FlightIntentsResource will be used in subsequent tests, additional intents may be specified with this resource. -### flight_intents_provider +### flight_intents_environments -(Optional) ResourceProvidingResource providing FlightIntentsResource according to `index`. +(Optional) ResourceEnvironmentsGenerator resource providing resource environments, each containing a `flight_intents` FlightIntentsResource. ## Preparation test case diff --git a/monitoring/uss_qualifier/scenarios/flight_planning/prep_planners.py b/monitoring/uss_qualifier/scenarios/flight_planning/prep_planners.py index 37331484527..3cec45ea145 100644 --- a/monitoring/uss_qualifier/scenarios/flight_planning/prep_planners.py +++ b/monitoring/uss_qualifier/scenarios/flight_planning/prep_planners.py @@ -7,17 +7,17 @@ PlanningActivityError, ) from monitoring.monitorlib.geotemporal import Volume4D, Volume4DCollection +from monitoring.monitorlib.inspection import fullname from monitoring.monitorlib.temporal import TestTimeContext, Time from monitoring.uss_qualifier.configurations.configuration import ParticipantID +from monitoring.uss_qualifier.resources.environments import ( + ResourceEnvironmentsGenerator, +) from monitoring.uss_qualifier.resources.flight_planning import ( FlightIntentsResource, FlightPlannersResource, ) -from monitoring.uss_qualifier.resources.flight_planning.flight_intent import ( - FlightIntentsSpecification, -) from monitoring.uss_qualifier.resources.interuss.mock_uss.client import MockUSSResource -from monitoring.uss_qualifier.resources.resource import ResourceProvidingResource from monitoring.uss_qualifier.scenarios.scenario import TestScenario MAX_TEST_DURATION = timedelta(minutes=45) @@ -36,10 +36,7 @@ def __init__( flight_intents2: FlightIntentsResource | None = None, flight_intents3: FlightIntentsResource | None = None, flight_intents4: FlightIntentsResource | None = None, - flight_intents_provider: ResourceProvidingResource[ - FlightIntentsSpecification, FlightIntentsResource - ] - | None = None, + flight_intents_environments: ResourceEnvironmentsGenerator | None = None, ): super().__init__() now = Time(arrow.utcnow().datetime) @@ -47,31 +44,24 @@ def __init__( later = now.offset(MAX_TEST_DURATION) times_later = TestTimeContext.all_times_are(later) self.areas = [] - if flight_intents_provider: - # TODO: use just the indices that will be used in the test run - # This can be accomplished by creating a - # ResourceCombinationsResource[T1: Resource](Resource[ResourceCombinationsSpecification]) - # whose `source` is a PluralResource[T1] abstract base class (implemented by, e.g., - # FlightPlannersResource). ResourceCombinationsResource will fill a specified number - # of roles with combinations of resources (producing list[dict[ResourceID, T1]]) - # just like, e.g. the FlightPlannerCombinations action generator currently. Then, - # FlightPlannerCombinations can be adjusted to accept a ResourceCombinationsResource - # with all the combinations pregenerated -- this achieves parity with today, just the - # combinations are produced in the ResourceCombinationsResource rather than the action - # generator. Then, we can have a PerCombinationResources[T2: Resource] that uses the - # ResourceCombinationsResource as a dependency and produces a FlightIntentsResource per - # combination. That PerCombinationResources will implement PluralResource[T2], and - # then this scenario can be adjusted to accept a PluralResource[FlightIntentsResource] - # instead of the ResourceProvidingResource[FlightIntentsResource]. - extra_intents = (flight_intents_provider.provide_resource_for(index=0),) - else: - extra_intents = tuple() - for intents in ( + extra_intents = [] + if flight_intents_environments: + for environment in flight_intents_environments.get_environments(): + flight_intents_resource = environment.get("flight_intents", None) + if not flight_intents_resource: + continue + if not isinstance(flight_intents_resource, FlightIntentsResource): + raise ValueError( + f"{fullname(self.__class__)} expects each environment in `flight_intents_environments` to contain a `flight_intents` resource of type {fullname(FlightIntentsResource)}, but instead found {fullname(flight_intents_resource.__class__)}" + ) + extra_intents.append(flight_intents_resource) + + for intents in [ flight_intents, flight_intents2, flight_intents3, flight_intents4, - ) + extra_intents: + ] + extra_intents: if intents is None: continue v4c = Volume4DCollection([]) diff --git a/monitoring/uss_qualifier/suites/astm/utm/f3548_21.md b/monitoring/uss_qualifier/suites/astm/utm/f3548_21.md index 7b7135cc137..cff82e310de 100644 --- a/monitoring/uss_qualifier/suites/astm/utm/f3548_21.md +++ b/monitoring/uss_qualifier/suites/astm/utm/f3548_21.md @@ -11,7 +11,7 @@ 1. Suite: [DSS testing for ASTM F3548-21](dss_probing.md) ([`suites.astm.utm.dss_probing`](dss_probing.yaml)) 5. Action generator: [`action_generators.flight_planning.FlightPlannerCombinations`](../../../action_generators/flight_planning/planner_combinations.py) 1. Scenario: [Validation of operational intents](../../../scenarios/astm/utm/flight_intent_validation/flight_intent_validation.md) ([`scenarios.astm.utm.FlightIntentValidation`](../../../scenarios/astm/utm/flight_intent_validation/flight_intent_validation.py)) -6. Action generator: [`action_generators.flight_planning.FlightPlannerCombinations`](../../../action_generators/flight_planning/planner_combinations.py) +6. Action generator: [`action_generators.resource_environments.ResourceEnvironmentsActionGenerator`](../../../action_generators/resource_environments.py) 1. Scenario: [Nominal planning: conflict with higher priority](../../../scenarios/astm/utm/nominal_planning/conflict_higher_priority/conflict_higher_priority.md) ([`scenarios.astm.utm.ConflictHigherPriority`](../../../scenarios/astm/utm/nominal_planning/conflict_higher_priority/conflict_higher_priority.py)) 7. Action generator: [`action_generators.flight_planning.FlightPlannerCombinations`](../../../action_generators/flight_planning/planner_combinations.py) 1. Scenario: [Nominal planning: not permitted conflict with equal priority](../../../scenarios/astm/utm/nominal_planning/conflict_equal_priority_not_permitted/conflict_equal_priority_not_permitted.md) ([`scenarios.astm.utm.ConflictEqualPriorityNotPermitted`](../../../scenarios/astm/utm/nominal_planning/conflict_equal_priority_not_permitted/conflict_equal_priority_not_permitted.py)) diff --git a/monitoring/uss_qualifier/suites/astm/utm/f3548_21.yaml b/monitoring/uss_qualifier/suites/astm/utm/f3548_21.yaml index 8a17c152878..e52ef8d556e 100644 --- a/monitoring/uss_qualifier/suites/astm/utm/f3548_21.yaml +++ b/monitoring/uss_qualifier/suites/astm/utm/f3548_21.yaml @@ -31,6 +31,24 @@ local_resources: resource_type: resources.versioning.SystemIdentityResource specification: system_identity: astm.f3548.v21 + priority_preemption_combinations: + resource_type: resources.combinations.ResourceCombinationsResource + dependencies: + uss1: flight_planners + uss2: flight_planners + combination_selector: priority_planning_selector? + specification: {} + priority_preemption_environments: + resource_type: resources.environments.EnvironmentAugmentationResource + dependencies: + environments: priority_preemption_combinations # provides uss1, uss2 + flight_intents: priority_preemption_flights_provider + specification: {} + flight_intents_environments: + resource_type: resources.environments.ConcatenatedResourceEnvironmentsResource + dependencies: + priority_preemption: priority_preemption_environments? + specification: {} actions: - test_scenario: scenario_type: scenarios.versioning.GetSystemVersions @@ -50,7 +68,7 @@ actions: flight_intents: invalid_flight_intents flight_intents2: conflicting_flights? flight_intents3: non_conflicting_flights? - flight_intents_provider: priority_preemption_flights_provider? + flight_intents_environments: flight_intents_environments? - action_generator: generator_type: action_generators.astm.f3548.ForEachDSS resources: @@ -95,34 +113,25 @@ actions: flight_intents: invalid_flight_intents tested_uss: uss1 dss: dss - combination_selector_source: flight_intent_validation_selector + combination_selector_source: flight_intent_validation_selector? flight_planners_source: flight_planners roles: - uss1 - action_generator: - generator_type: action_generators.flight_planning.FlightPlannerCombinations + generator_type: action_generators.resource_environments.ResourceEnvironmentsActionGenerator resources: - flight_planners: flight_planners - priority_planning_selector: priority_planning_selector? - priority_preemption_flights_provider: priority_preemption_flights_provider + priority_preemption_environments: priority_preemption_environments dss: dss specification: + resource_environments_source: priority_preemption_environments action_to_repeat: test_scenario: scenario_type: scenarios.astm.utm.ConflictHigherPriority resources: - flight_intents: priority_preemption_flights + flight_intents: flight_intents tested_uss: uss1 control_uss: uss2 dss: dss - combination_selector_source: priority_planning_selector - flight_planners_source: flight_planners - roles: - - uss1 - - uss2 - per_index_resources: - - resource_provider: priority_preemption_flights_provider - provided_resource: priority_preemption_flights - action_generator: generator_type: action_generators.flight_planning.FlightPlannerCombinations resources: @@ -248,7 +257,7 @@ actions: flight_intents: invalid_flight_intents flight_intents2: conflicting_flights? flight_intents3: non_conflicting_flights? - flight_intents_provider: priority_preemption_flights_provider? + flight_intents_environments: flight_intents_environments? - test_scenario: scenario_type: scenarios.astm.utm.make_uss_report.MakeUssReport resources: diff --git a/schemas/monitoring/uss_qualifier/action_generators/resource_environments/ResourceEnvironmentsActionGeneratorSpecification.json b/schemas/monitoring/uss_qualifier/action_generators/resource_environments/ResourceEnvironmentsActionGeneratorSpecification.json new file mode 100644 index 00000000000..2893a0eb01f --- /dev/null +++ b/schemas/monitoring/uss_qualifier/action_generators/resource_environments/ResourceEnvironmentsActionGeneratorSpecification.json @@ -0,0 +1,24 @@ +{ + "$id": "https://github.com/interuss/monitoring/blob/main/schemas/monitoring/uss_qualifier/action_generators/resource_environments/ResourceEnvironmentsActionGeneratorSpecification.json", + "$schema": "https://json-schema.org/draft/2020-12/schema", + "description": "monitoring.uss_qualifier.action_generators.resource_environments.ResourceEnvironmentsActionGeneratorSpecification, as defined in monitoring/uss_qualifier/action_generators/resource_environments.py", + "properties": { + "$ref": { + "description": "Path to content that replaces the $ref", + "type": "string" + }, + "action_to_repeat": { + "$ref": "../../suites/definitions/TestSuiteActionDeclaration.json", + "description": "Test suite action to run for each resource environment" + }, + "resource_environments_source": { + "description": "Resource providing different resource environments", + "type": "string" + } + }, + "required": [ + "action_to_repeat", + "resource_environments_source" + ], + "type": "object" +} \ No newline at end of file diff --git a/schemas/monitoring/uss_qualifier/resources/combinations/ResourceCombinationsSpecification.json b/schemas/monitoring/uss_qualifier/resources/combinations/ResourceCombinationsSpecification.json new file mode 100644 index 00000000000..bfbc4689f81 --- /dev/null +++ b/schemas/monitoring/uss_qualifier/resources/combinations/ResourceCombinationsSpecification.json @@ -0,0 +1,12 @@ +{ + "$id": "https://github.com/interuss/monitoring/blob/main/schemas/monitoring/uss_qualifier/resources/combinations/ResourceCombinationsSpecification.json", + "$schema": "https://json-schema.org/draft/2020-12/schema", + "description": "monitoring.uss_qualifier.resources.combinations.ResourceCombinationsSpecification, as defined in monitoring/uss_qualifier/resources/combinations.py", + "properties": { + "$ref": { + "description": "Path to content that replaces the $ref", + "type": "string" + } + }, + "type": "object" +} \ No newline at end of file diff --git a/schemas/monitoring/uss_qualifier/resources/definitions/ResourceDeclaration.json b/schemas/monitoring/uss_qualifier/resources/definitions/ResourceDeclaration.json index 3567e7d91df..6f88dd285c9 100644 --- a/schemas/monitoring/uss_qualifier/resources/definitions/ResourceDeclaration.json +++ b/schemas/monitoring/uss_qualifier/resources/definitions/ResourceDeclaration.json @@ -11,7 +11,7 @@ "additionalProperties": { "type": "string" }, - "description": "Mapping of dependency parameter (additional argument to concrete resource constructor) to `name` of resource to use (optionally suffixed with `?` to indicate an optional dependency when the constructor parameter has a default value)", + "description": "Mapping of dependency parameter (additional argument to concrete resource constructor) to `name` of resource to use (optionally suffixed with `?` to indicate an optional dependency when the constructor parameter has a default value or is accepted through **kwargs). Missing optional dependencies are omitted from the constructor call; dependencies accepted through **kwargs have an implicit default of None.", "properties": { "$ref": { "description": "Path to content that replaces the $ref", diff --git a/schemas/monitoring/uss_qualifier/resources/environments/ConcatenatedResourceEnvironmentsResourceSpecification.json b/schemas/monitoring/uss_qualifier/resources/environments/ConcatenatedResourceEnvironmentsResourceSpecification.json new file mode 100644 index 00000000000..3fa0f2a7a0f --- /dev/null +++ b/schemas/monitoring/uss_qualifier/resources/environments/ConcatenatedResourceEnvironmentsResourceSpecification.json @@ -0,0 +1,22 @@ +{ + "$id": "https://github.com/interuss/monitoring/blob/main/schemas/monitoring/uss_qualifier/resources/environments/ConcatenatedResourceEnvironmentsResourceSpecification.json", + "$schema": "https://json-schema.org/draft/2020-12/schema", + "description": "monitoring.uss_qualifier.resources.environments.ConcatenatedResourceEnvironmentsResourceSpecification, as defined in monitoring/uss_qualifier/resources/environments.py", + "properties": { + "$ref": { + "description": "Path to content that replaces the $ref", + "type": "string" + }, + "environments_sequence": { + "description": "Sequence in which each dependent ResourceEnvironmentsGenerator resource's environments are enumerated.", + "items": { + "type": "string" + }, + "type": [ + "array", + "null" + ] + } + }, + "type": "object" +} \ No newline at end of file diff --git a/schemas/monitoring/uss_qualifier/resources/environments/EnvironmentAugmentationResourceSpecification.json b/schemas/monitoring/uss_qualifier/resources/environments/EnvironmentAugmentationResourceSpecification.json new file mode 100644 index 00000000000..2f7362352d5 --- /dev/null +++ b/schemas/monitoring/uss_qualifier/resources/environments/EnvironmentAugmentationResourceSpecification.json @@ -0,0 +1,12 @@ +{ + "$id": "https://github.com/interuss/monitoring/blob/main/schemas/monitoring/uss_qualifier/resources/environments/EnvironmentAugmentationResourceSpecification.json", + "$schema": "https://json-schema.org/draft/2020-12/schema", + "description": "monitoring.uss_qualifier.resources.environments.EnvironmentAugmentationResourceSpecification, as defined in monitoring/uss_qualifier/resources/environments.py", + "properties": { + "$ref": { + "description": "Path to content that replaces the $ref", + "type": "string" + } + }, + "type": "object" +} \ No newline at end of file diff --git a/schemas/monitoring/uss_qualifier/resources/environments/ResourceEnvironmentSelectorSpecification.json b/schemas/monitoring/uss_qualifier/resources/environments/ResourceEnvironmentSelectorSpecification.json new file mode 100644 index 00000000000..e7792db61af --- /dev/null +++ b/schemas/monitoring/uss_qualifier/resources/environments/ResourceEnvironmentSelectorSpecification.json @@ -0,0 +1,12 @@ +{ + "$id": "https://github.com/interuss/monitoring/blob/main/schemas/monitoring/uss_qualifier/resources/environments/ResourceEnvironmentSelectorSpecification.json", + "$schema": "https://json-schema.org/draft/2020-12/schema", + "description": "Specification defining how resource environments should be selected from the available/generated environments.\n\nmonitoring.uss_qualifier.resources.environments.ResourceEnvironmentSelectorSpecification, as defined in monitoring/uss_qualifier/resources/environments.py", + "properties": { + "$ref": { + "description": "Path to content that replaces the $ref", + "type": "string" + } + }, + "type": "object" +} \ No newline at end of file diff --git a/schemas/monitoring/uss_qualifier/resources/flight_planning/flight_planners/FlightPlannerCombinationSelectorSpecification.json b/schemas/monitoring/uss_qualifier/resources/flight_planning/flight_planners/FlightPlannerCombinationSelectorSpecification.json index 266c94ec481..5a3515d4147 100644 --- a/schemas/monitoring/uss_qualifier/resources/flight_planning/flight_planners/FlightPlannerCombinationSelectorSpecification.json +++ b/schemas/monitoring/uss_qualifier/resources/flight_planning/flight_planners/FlightPlannerCombinationSelectorSpecification.json @@ -1,7 +1,7 @@ { "$id": "https://github.com/interuss/monitoring/blob/main/schemas/monitoring/uss_qualifier/resources/flight_planning/flight_planners/FlightPlannerCombinationSelectorSpecification.json", "$schema": "https://json-schema.org/draft/2020-12/schema", - "description": "monitoring.uss_qualifier.resources.flight_planning.flight_planners.FlightPlannerCombinationSelectorSpecification, as defined in monitoring/uss_qualifier/resources/flight_planning/flight_planners.py", + "description": "Specification defining how resource environments should be selected from the available/generated environments.\n\nmonitoring.uss_qualifier.resources.flight_planning.flight_planners.FlightPlannerCombinationSelectorSpecification, as defined in monitoring/uss_qualifier/resources/flight_planning/flight_planners.py", "properties": { "$ref": { "description": "Path to content that replaces the $ref",