diff --git a/monitoring/uss_qualifier/action_generators/action_generator.py b/monitoring/uss_qualifier/action_generators/action_generator.py index 71694d5547..5505dd8d04 100644 --- a/monitoring/uss_qualifier/action_generators/action_generator.py +++ b/monitoring/uss_qualifier/action_generators/action_generator.py @@ -1,7 +1,7 @@ from __future__ import annotations from abc import ABC, abstractmethod import inspect -from typing import Generic, Dict, Optional, TypeVar +from typing import Generic, Dict, Optional, TypeVar, List, Type from implicitdict import ImplicitDict from monitoring import uss_qualifier as uss_qualifier_module @@ -12,6 +12,10 @@ from monitoring.uss_qualifier.action_generators.definitions import ( ActionGeneratorSpecificationType, ActionGeneratorDefinition, + GeneratorTypeName, +) +from monitoring.uss_qualifier.action_generators.documentation.definitions import ( + PotentialGeneratedAction, ) from monitoring.uss_qualifier.reports.report import TestSuiteActionReport from monitoring.uss_qualifier.resources.definitions import ResourceID @@ -45,32 +49,35 @@ def run_next_action(self) -> Optional[TestSuiteActionReport]: "A concrete action generator must implement `actions` method" ) + @classmethod + def list_potential_actions( + cls, specification: Optional[ActionGeneratorSpecificationType] + ) -> List[PotentialGeneratedAction]: + """Enumerate the potential actions that may be run by an instance of this generator type. + + Concrete subclasses of ActionGenerator must implement a classmethod that shadows this one according to this + specification. + + Args: + specification: A serializable (subclass of implicitdict.ImplicitDict) specification for how to create the + action generator instance, or None if the action generator type does not need a specification. + + Returns: All potential actions that may be generated by this generator, depending on the resources provided. + """ + raise NotImplementedError( + "A concrete action generator must implement `list_potential_actions` classmethod" + ) + @staticmethod def make_from_definition( definition: ActionGeneratorDefinition, resources: Dict[ResourceID, ResourceType] ) -> ActionGeneratorType: - from monitoring.uss_qualifier import ( - action_generators as action_generators_module, + action_generator_type = action_generator_type_from_name( + definition.generator_type ) + specification_type = action_generator_specification_type(action_generator_type) - import_submodules(action_generators_module) - action_generator_type = get_module_object_by_name( - parent_module=uss_qualifier_module, - object_name=definition.generator_type, - ) - if not issubclass(action_generator_type, ActionGenerator): - raise NotImplementedError( - "Action generator type {} is not a subclass of the ActionGenerator base class".format( - action_generator_type.__name__ - ) - ) - constructor_signature = inspect.signature(action_generator_type.__init__) - specification_type = None constructor_args = {} - for arg_name, arg in constructor_signature.parameters.items(): - if arg_name == "specification": - specification_type = arg.annotation - break if specification_type is not None: constructor_args["specification"] = ImplicitDict.parse( definition.specification, specification_type @@ -82,3 +89,36 @@ def make_from_definition( ActionGeneratorType = TypeVar("ActionGeneratorType", bound=ActionGenerator) + + +def action_generator_type_from_name( + action_generator_type_name: GeneratorTypeName, +) -> Type[ActionGenerator]: + from monitoring.uss_qualifier import ( + action_generators as action_generators_module, + ) + + import_submodules(action_generators_module) + action_generator_type = get_module_object_by_name( + parent_module=uss_qualifier_module, + object_name=action_generator_type_name, + ) + if not issubclass(action_generator_type, ActionGenerator): + raise NotImplementedError( + "Action generator type {} is not a subclass of the ActionGenerator base class".format( + action_generator_type.__name__ + ) + ) + return action_generator_type + + +def action_generator_specification_type( + action_generator_type: Type[ActionGenerator], +) -> Optional[Type]: + constructor_signature = inspect.signature(action_generator_type.__init__) + specification_type = None + for arg_name, arg in constructor_signature.parameters.items(): + if arg_name == "specification": + specification_type = arg.annotation + break + return specification_type diff --git a/monitoring/uss_qualifier/action_generators/astm/f3411/for_each_dss.py b/monitoring/uss_qualifier/action_generators/astm/f3411/for_each_dss.py index 95c756d4c0..bbbee1a469 100644 --- a/monitoring/uss_qualifier/action_generators/astm/f3411/for_each_dss.py +++ b/monitoring/uss_qualifier/action_generators/astm/f3411/for_each_dss.py @@ -3,6 +3,12 @@ from implicitdict import ImplicitDict from monitoring.monitorlib.inspection import fullname +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.reports.report import TestSuiteActionReport from monitoring.uss_qualifier.resources.astm.f3411 import ( DSSInstanceResource, @@ -34,6 +40,14 @@ class ForEachDSS(ActionGenerator[ForEachDSSSpecification]): _current_action: int _failure_reaction: ReactionToFailure + @classmethod + def list_potential_actions( + cls, specification: ForEachDSSSpecification + ) -> List[PotentialGeneratedAction]: + return list_potential_actions_for_action_declaration( + specification.action_to_repeat + ) + def __init__( self, specification: ForEachDSSSpecification, diff --git a/monitoring/uss_qualifier/action_generators/definitions.py b/monitoring/uss_qualifier/action_generators/definitions.py index 8533a5ccc1..aca51c8bfa 100644 --- a/monitoring/uss_qualifier/action_generators/definitions.py +++ b/monitoring/uss_qualifier/action_generators/definitions.py @@ -5,7 +5,7 @@ GeneratorTypeName = str -"""This plain string represents a type of action generator, expressed as a Python class name qualified relative to the `uss_qualifier.action_generators` module""" +"""This plain string represents a type of action generator, expressed as a Python class name qualified relative to the `uss_qualifier` module""" ActionGeneratorSpecificationType = TypeVar( diff --git a/monitoring/uss_qualifier/action_generators/documentation/__init__.py b/monitoring/uss_qualifier/action_generators/documentation/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/monitoring/uss_qualifier/action_generators/documentation/definitions.py b/monitoring/uss_qualifier/action_generators/documentation/definitions.py new file mode 100644 index 0000000000..33bb951e80 --- /dev/null +++ b/monitoring/uss_qualifier/action_generators/documentation/definitions.py @@ -0,0 +1,40 @@ +from typing import Optional + +from implicitdict import ImplicitDict +from monitoring.uss_qualifier.action_generators.definitions import GeneratorTypeName +from monitoring.uss_qualifier.fileio import FileReference +from monitoring.uss_qualifier.scenarios.definitions import TestScenarioTypeName +from monitoring.uss_qualifier.suites.definitions import ActionType, TestSuiteDefinition + + +class PotentialTestScenarioAction(ImplicitDict): + scenario_type: TestScenarioTypeName + """Type of test scenario.""" + + +class PotentialTestSuiteAction(ImplicitDict): + suite_type: Optional[FileReference] + """Type/location of test suite. Usually expressed as the file name of the suite definition (without extension) qualified relative to the `uss_qualifier` folder""" + + suite_definition: Optional[TestSuiteDefinition] + """Definition of test suite internal to the configuration -- specified instead of `suite_type`.""" + + +class PotentialActionGeneratorAction(ImplicitDict): + generator_type: GeneratorTypeName + """Type of action generator.""" + + specification: dict + """Specification of action generator; format is the ActionGeneratorSpecificationType that corresponds to the `generator_type`""" + + +class PotentialGeneratedAction(ImplicitDict): + test_scenario: Optional[PotentialTestScenarioAction] + test_suite: Optional[PotentialTestSuiteAction] + action_generator: Optional[PotentialActionGeneratorAction] + + def get_action_type(self) -> ActionType: + matches = [v for v in ActionType if v in self and self[v]] + if len(matches) != 1: + ActionType.raise_invalid_action_declaration() + return ActionType(matches[0]) diff --git a/monitoring/uss_qualifier/action_generators/documentation/documentation.py b/monitoring/uss_qualifier/action_generators/documentation/documentation.py new file mode 100644 index 0000000000..ec1193f4c5 --- /dev/null +++ b/monitoring/uss_qualifier/action_generators/documentation/documentation.py @@ -0,0 +1,78 @@ +from typing import List, Union + +from implicitdict import ImplicitDict +from monitoring.uss_qualifier.action_generators.action_generator import ( + action_generator_type_from_name, + action_generator_specification_type, +) +from monitoring.uss_qualifier.action_generators.definitions import ( + ActionGeneratorDefinition, +) +from monitoring.uss_qualifier.action_generators.documentation.definitions import ( + PotentialGeneratedAction, + PotentialTestScenarioAction, + PotentialTestSuiteAction, + PotentialActionGeneratorAction, +) +from monitoring.uss_qualifier.suites.definitions import ( + TestSuiteActionDeclaration, + ActionType, +) + + +def list_potential_actions_for_action_generator_definition( + generator_def: Union[ActionGeneratorDefinition, PotentialActionGeneratorAction] +) -> List[PotentialGeneratedAction]: + action_generator_type = action_generator_type_from_name( + generator_def.generator_type + ) + specification_type = action_generator_specification_type(action_generator_type) + if specification_type is not None: + spec = ImplicitDict.parse(generator_def.specification, specification_type) + else: + spec = None + return action_generator_type.list_potential_actions(spec) + + +def list_potential_actions_for_action_declaration( + declaration: TestSuiteActionDeclaration, +) -> List[PotentialGeneratedAction]: + action_type = declaration.get_action_type() + if action_type == ActionType.TestScenario: + return [ + PotentialGeneratedAction( + test_scenario=PotentialTestScenarioAction( + scenario_type=declaration.test_scenario.scenario_type + ) + ) + ] + elif action_type == ActionType.TestSuite: + if "suite_type" in declaration.test_suite and declaration.test_suite.suite_type: + return [ + PotentialGeneratedAction( + test_suite=PotentialTestSuiteAction( + suite_type=declaration.test_suite.suite_type + ) + ) + ] + elif ( + "suite_definition" in declaration.test_suite + and declaration.test_suite.suite_definition + ): + return [ + PotentialGeneratedAction( + test_suite=PotentialTestSuiteAction( + suite_definition=declaration.test_suite.suite_definition + ) + ) + ] + elif action_type == ActionType.ActionGenerator: + return [ + PotentialGeneratedAction( + action_generator=PotentialActionGeneratorAction( + generator_type=declaration.action_generator.generator_type + ) + ) + ] + else: + raise NotImplementedError(f"Action type {action_type} is not supported") diff --git a/monitoring/uss_qualifier/action_generators/flight_planning/planner_combinations.py b/monitoring/uss_qualifier/action_generators/flight_planning/planner_combinations.py index f81899c677..38d2ddf54e 100644 --- a/monitoring/uss_qualifier/action_generators/flight_planning/planner_combinations.py +++ b/monitoring/uss_qualifier/action_generators/flight_planning/planner_combinations.py @@ -3,6 +3,17 @@ from implicitdict import ImplicitDict from monitoring.monitorlib.inspection import fullname +from monitoring.uss_qualifier.action_generators.action_generator import ( + action_generator_type_from_name, + action_generator_specification_type, +) +from monitoring.uss_qualifier.action_generators.documentation.definitions import ( + PotentialGeneratedAction, + PotentialTestScenarioAction, +) +from monitoring.uss_qualifier.action_generators.documentation.documentation import ( + list_potential_actions_for_action_declaration, +) from monitoring.uss_qualifier.reports.report import TestSuiteActionReport from monitoring.uss_qualifier.resources.definitions import ResourceID from monitoring.uss_qualifier.resources.flight_planning import FlightPlannersResource @@ -14,7 +25,10 @@ ResourceType, ) -from monitoring.uss_qualifier.suites.definitions import TestSuiteActionDeclaration +from monitoring.uss_qualifier.suites.definitions import ( + TestSuiteActionDeclaration, + ActionType, +) from monitoring.uss_qualifier.suites.suite import ( ActionGenerator, TestSuiteAction, @@ -43,6 +57,14 @@ class FlightPlannerCombinations( _current_action: int _failure_reaction: ReactionToFailure + @classmethod + def list_potential_actions( + cls, specification: FlightPlannerCombinationsSpecification + ) -> List[PotentialGeneratedAction]: + return list_potential_actions_for_action_declaration( + specification.action_to_repeat + ) + def __init__( self, specification: FlightPlannerCombinationsSpecification, diff --git a/monitoring/uss_qualifier/action_generators/repetition/repeat.py b/monitoring/uss_qualifier/action_generators/repetition/repeat.py index c282a73693..a2f3256af2 100644 --- a/monitoring/uss_qualifier/action_generators/repetition/repeat.py +++ b/monitoring/uss_qualifier/action_generators/repetition/repeat.py @@ -1,6 +1,12 @@ from typing import Dict, List, Optional from implicitdict import ImplicitDict +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.reports.report import TestSuiteActionReport from monitoring.uss_qualifier.resources.definitions import ResourceID from monitoring.uss_qualifier.resources.resource import ResourceType @@ -26,6 +32,14 @@ class Repeat(ActionGenerator[RepeatSpecification]): _current_action: int _failure_reaction: ReactionToFailure + @classmethod + def list_potential_actions( + cls, specification: RepeatSpecification + ) -> List[PotentialGeneratedAction]: + return list_potential_actions_for_action_declaration( + specification.action_to_repeat + ) + def __init__( self, specification: RepeatSpecification, diff --git a/monitoring/uss_qualifier/reports/report.py b/monitoring/uss_qualifier/reports/report.py index d6bb83e63c..cb4371d828 100644 --- a/monitoring/uss_qualifier/reports/report.py +++ b/monitoring/uss_qualifier/reports/report.py @@ -7,6 +7,7 @@ from implicitdict import ImplicitDict, StringBasedDateTime from monitoring.monitorlib import fetch, inspection +from monitoring.uss_qualifier.action_generators.definitions import GeneratorTypeName from monitoring.uss_qualifier.common_data_definitions import Severity from monitoring.uss_qualifier.configurations.configuration import ( TestConfiguration, @@ -18,6 +19,7 @@ JSONPathExpression, ) from monitoring.uss_qualifier.requirements.definitions import RequirementID +from monitoring.uss_qualifier.scenarios.definitions import TestScenarioTypeName class FailedCheck(ImplicitDict): @@ -211,7 +213,7 @@ class TestScenarioReport(ImplicitDict): name: str """Name of this test scenario""" - scenario_type: str + scenario_type: TestScenarioTypeName """Type of this test scenario""" documentation_url: str @@ -295,7 +297,7 @@ def participant_ids(self) -> Set[ParticipantID]: class ActionGeneratorReport(ImplicitDict): - generator_type: str + generator_type: GeneratorTypeName """Type of action generator""" actions: List[TestSuiteActionReport] diff --git a/monitoring/uss_qualifier/scenarios/definitions.py b/monitoring/uss_qualifier/scenarios/definitions.py index 5672c40b10..90d8c556fc 100644 --- a/monitoring/uss_qualifier/scenarios/definitions.py +++ b/monitoring/uss_qualifier/scenarios/definitions.py @@ -5,9 +5,13 @@ from monitoring.uss_qualifier.resources.definitions import ResourceID +TestScenarioTypeName = str +"""This plain string represents a type of test scenario, expressed as a Python class name qualified relative to the `uss_qualifier` module""" + + class TestScenarioDeclaration(ImplicitDict): - scenario_type: FileReference - """Type/location of test scenario. Usually expressed as the class name of the scenario module-qualified relative to the `uss_qualifier` folder""" + scenario_type: TestScenarioTypeName + """Type of test scenario.""" resources: Optional[Dict[ResourceID, ResourceID]] """Mapping of the ID a resource in the test scenario -> the ID a resource is known by in the parent test suite. diff --git a/monitoring/uss_qualifier/scenarios/scenario.py b/monitoring/uss_qualifier/scenarios/scenario.py index b3901e938e..c564045ba0 100644 --- a/monitoring/uss_qualifier/scenarios/scenario.py +++ b/monitoring/uss_qualifier/scenarios/scenario.py @@ -24,7 +24,10 @@ ParticipantID, PassedCheck, ) -from monitoring.uss_qualifier.scenarios.definitions import TestScenarioDeclaration +from monitoring.uss_qualifier.scenarios.definitions import ( + TestScenarioDeclaration, + TestScenarioTypeName, +) from monitoring.uss_qualifier.scenarios.documentation.definitions import ( TestScenarioDocumentation, TestCaseDocumentation, @@ -159,7 +162,7 @@ def record_passed( self._step_report.passed_checks.append(passed_check) -def get_scenario_type_by_name(scenario_type_name: str) -> Type: +def get_scenario_type_by_name(scenario_type_name: TestScenarioTypeName) -> Type: inspection.import_submodules(scenarios_module) scenario_type = inspection.get_module_object_by_name( parent_module=uss_qualifier_module, object_name=scenario_type_name diff --git a/monitoring/uss_qualifier/suites/astm/netrid/f3411_19.md b/monitoring/uss_qualifier/suites/astm/netrid/f3411_19.md index b91c6ea824..ec359caacf 100644 --- a/monitoring/uss_qualifier/suites/astm/netrid/f3411_19.md +++ b/monitoring/uss_qualifier/suites/astm/netrid/f3411_19.md @@ -4,6 +4,7 @@ ## Actions -1. Action generator: `action_generators.astm.f3411.ForEachDSS` +1. Action generator: [`action_generators.astm.f3411.ForEachDSS`](../../../action_generators/astm/f3411/for_each_dss.py) + 1. Suite: 2. Scenario: [ASTM F3411-19 NetRID DSS interoperability](../../../scenarios/astm/netrid/v19/dss_interoperability.md) ([`scenarios.astm.netrid.v19.DSSInteroperability`](../../../scenarios/astm/netrid/v19/dss_interoperability.py)) 3. Scenario: [ASTM NetRID nominal behavior](../../../scenarios/astm/netrid/v19/nominal_behavior.md) ([`scenarios.astm.netrid.v19.NominalBehavior`](../../../scenarios/astm/netrid/v19/nominal_behavior.py)) diff --git a/monitoring/uss_qualifier/suites/astm/netrid/f3411_22a.md b/monitoring/uss_qualifier/suites/astm/netrid/f3411_22a.md index 8dd20db5b5..38be47bdb5 100644 --- a/monitoring/uss_qualifier/suites/astm/netrid/f3411_22a.md +++ b/monitoring/uss_qualifier/suites/astm/netrid/f3411_22a.md @@ -4,6 +4,7 @@ ## Actions -1. Action generator: `action_generators.astm.f3411.ForEachDSS` +1. Action generator: [`action_generators.astm.f3411.ForEachDSS`](../../../action_generators/astm/f3411/for_each_dss.py) + 1. Suite: 2. Scenario: [ASTM F3411-22a NetRID DSS interoperability](../../../scenarios/astm/netrid/v22a/dss_interoperability.md) ([`scenarios.astm.netrid.v22a.DSSInteroperability`](../../../scenarios/astm/netrid/v22a/dss_interoperability.py)) 3. Scenario: [ASTM NetRID nominal behavior](../../../scenarios/astm/netrid/v22a/nominal_behavior.md) ([`scenarios.astm.netrid.v22a.NominalBehavior`](../../../scenarios/astm/netrid/v22a/nominal_behavior.py)) diff --git a/monitoring/uss_qualifier/suites/astm/utm/f3548_21.md b/monitoring/uss_qualifier/suites/astm/utm/f3548_21.md index 62a6f5c093..c83a1301f3 100644 --- a/monitoring/uss_qualifier/suites/astm/utm/f3548_21.md +++ b/monitoring/uss_qualifier/suites/astm/utm/f3548_21.md @@ -4,6 +4,9 @@ ## Actions -1. Action generator: `action_generators.flight_planning.FlightPlannerCombinations` -2. Action generator: `action_generators.flight_planning.FlightPlannerCombinations` -3. Action generator: `action_generators.flight_planning.FlightPlannerCombinations` +1. 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)) +2. Action generator: [`action_generators.flight_planning.FlightPlannerCombinations`](../../../action_generators/flight_planning/planner_combinations.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)) +3. 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/documentation/documentation.py b/monitoring/uss_qualifier/suites/documentation/documentation.py index ca453656d1..0b84e4bee2 100644 --- a/monitoring/uss_qualifier/suites/documentation/documentation.py +++ b/monitoring/uss_qualifier/suites/documentation/documentation.py @@ -1,18 +1,37 @@ import glob import inspect import os -from typing import Iterator, Optional +from typing import Iterator, Optional, List, Union from implicitdict import ImplicitDict +from monitoring.uss_qualifier.action_generators.action_generator import ( + action_generator_type_from_name, +) +from monitoring.uss_qualifier.action_generators.definitions import ( + ActionGeneratorDefinition, +) +from monitoring.uss_qualifier.action_generators.documentation.definitions import ( + PotentialGeneratedAction, + PotentialActionGeneratorAction, +) +from monitoring.uss_qualifier.action_generators.documentation.documentation import ( + list_potential_actions_for_action_generator_definition, +) from monitoring.uss_qualifier.fileio import ( load_dict_with_references, get_package_name, resolve_filename, + FileReference, ) +from monitoring.uss_qualifier.scenarios.definitions import TestScenarioTypeName from monitoring.uss_qualifier.scenarios.documentation.parsing import get_documentation from monitoring.uss_qualifier.scenarios.scenario import get_scenario_type_by_name -from monitoring.uss_qualifier.suites.definitions import TestSuiteDefinition, ActionType +from monitoring.uss_qualifier.suites.definitions import ( + TestSuiteDefinition, + ActionType, + TestSuiteActionDeclaration, +) def find_test_suites(start_path: Optional[str] = None) -> Iterator[str]: @@ -45,46 +64,91 @@ def make_test_suite_documentation(test_suite_yaml_file: str) -> str: lines.append("") base_path = os.path.dirname(test_suite_yaml_file) for i, action in enumerate(suite_def.actions): - action_type = action.get_action_type() - if action_type == ActionType.TestScenario: - scenario_type = get_scenario_type_by_name( - action.test_scenario.scenario_type - ) - py_rel_path = os.path.relpath(inspect.getfile(scenario_type), base_path) - scenario_doc = get_documentation(scenario_type) - doc_rel_path = os.path.relpath(scenario_doc.local_path, start=base_path) - lines.append( - f"{i + 1}. Scenario: [{scenario_doc.name}]({doc_rel_path}) ([`{action.test_scenario.scenario_type}`]({py_rel_path}))" - ) - elif action_type == ActionType.TestSuite: - if "suite_type" in action.test_suite and action.test_suite.suite_type: - suite_def = ImplicitDict.parse( - load_dict_with_references(action.test_suite.suite_type), - TestSuiteDefinition, - ) - suite_path = resolve_filename(action.test_suite.suite_type) - suite_rel_path = os.path.relpath(suite_path, start=base_path) - doc_path = os.path.splitext(suite_path)[0] + ".md" - doc_rel_path = os.path.relpath(doc_path, start=base_path) - lines.append( - f"{i + 1}. Suite: [{suite_def.name}]({doc_rel_path}) ([`{action.test_suite.suite_type}`]({suite_rel_path}))" - ) - elif "suite_definition" in action.test_suite and action.suite_definition: - # TODO: Generate additional test suite documentation for in-suite suite definition - lines.append(f"{i + 1}. Suite: ") - else: - raise ValueError( - f"Test suite action {i + 1} missing suite type or definition in {test_suite_yaml_file}" - ) - elif action_type == ActionType.ActionGenerator: - # TODO: Add documentation for action generators - lines.append( - f"{i + 1}. Action generator: `{action.action_generator.generator_type}`" - ) - else: - raise NotImplementedError( - f"Unsupported test suite action type: {action_type}" - ) + lines.extend(render_action(action, i + 1, base_path, 0)) lines.append("") return "\n".join(lines) + + +def render_scenario( + scenario_type_name: TestScenarioTypeName, i: int, base_path: str, indent: int +) -> List[str]: + lines = [] + scenario_type = get_scenario_type_by_name(scenario_type_name) + py_rel_path = os.path.relpath(inspect.getfile(scenario_type), base_path) + scenario_doc = get_documentation(scenario_type) + doc_rel_path = os.path.relpath(scenario_doc.local_path, start=base_path) + lines.append( + f"{' ' * indent}{i}. Scenario: [{scenario_doc.name}]({doc_rel_path}) ([`{scenario_type_name}`]({py_rel_path}))" + ) + return lines + + +def render_suite_by_type( + suite_type: FileReference, i: int, base_path: str, indent: int +) -> List[str]: + lines = [] + suite_def = ImplicitDict.parse( + load_dict_with_references(suite_type), + TestSuiteDefinition, + ) + suite_path = resolve_filename(suite_type) + suite_rel_path = os.path.relpath(suite_path, start=base_path) + doc_path = os.path.splitext(suite_path)[0] + ".md" + doc_rel_path = os.path.relpath(doc_path, start=base_path) + lines.append( + f"{' ' * indent}{i}. Suite: [{suite_def.name}]({doc_rel_path}) ([`{suite_type}`]({suite_rel_path}))" + ) + return lines + + +def render_action_generator( + generator_def: Union[ActionGeneratorDefinition, PotentialActionGeneratorAction], + i: int, + base_path: str, + indent: int, +) -> List[str]: + lines = [] + action_generator_type = action_generator_type_from_name( + generator_def.generator_type + ) + py_rel_path = os.path.relpath( + inspect.getfile(action_generator_type), start=base_path + ) + lines.append( + f"{' ' * indent}{i}. Action generator: [`{generator_def.generator_type}`]({py_rel_path})" + ) + potential_actions = list_potential_actions_for_action_generator_definition( + generator_def + ) + for j, potential_action in enumerate(potential_actions): + lines.extend(render_action(potential_action, j + 1, base_path, indent + 4)) + return lines + + +def render_action( + action: Union[TestSuiteActionDeclaration, PotentialGeneratedAction], + i: int, + base_path: str, + indent: int, +) -> List[str]: + action_type = action.get_action_type() + if action_type == ActionType.TestScenario: + return render_scenario(action.test_scenario.scenario_type, i, base_path, indent) + elif action_type == ActionType.TestSuite: + if "suite_type" in action.test_suite and action.test_suite.suite_type: + return render_suite_by_type( + action.test_suite.suite_type, i, base_path, indent + ) + elif ( + "suite_definition" in action.test_suite + and action.test_suite.suite_definition + ): + # TODO: Generate additional test suite documentation for in-suite suite definition + return [f"{' ' * indent}{i}. Suite: "] + else: + raise ValueError(f"Test suite action {i} missing suite type or definition") + elif action_type == ActionType.ActionGenerator: + return render_action_generator(action.action_generator, i, base_path, indent) + else: + raise NotImplementedError(f"Unsupported test suite action type: {action_type}") diff --git a/monitoring/uss_qualifier/suites/uspace/flight_auth.md b/monitoring/uss_qualifier/suites/uspace/flight_auth.md index 25bdb21bce..a98b01af1a 100644 --- a/monitoring/uss_qualifier/suites/uspace/flight_auth.md +++ b/monitoring/uss_qualifier/suites/uspace/flight_auth.md @@ -5,4 +5,5 @@ ## Actions 1. Suite: [ASTM F3548-21](../astm/utm/f3548_21.md) ([`suites.astm.utm.f3548_21`](../astm/utm/f3548_21.yaml)) -2. Action generator: `action_generators.flight_planning.FlightPlannerCombinations` +2. Action generator: [`action_generators.flight_planning.FlightPlannerCombinations`](../../action_generators/flight_planning/planner_combinations.py) + 1. Scenario: [Flight authorisation validation](../../scenarios/uspace/flight_auth/validation.md) ([`scenarios.uspace.flight_auth.Validation`](../../scenarios/uspace/flight_auth/validation.py)) diff --git a/schemas/monitoring/uss_qualifier/scenarios/definitions/TestScenarioDeclaration.json b/schemas/monitoring/uss_qualifier/scenarios/definitions/TestScenarioDeclaration.json index 60d2424977..26e32cbfaf 100644 --- a/schemas/monitoring/uss_qualifier/scenarios/definitions/TestScenarioDeclaration.json +++ b/schemas/monitoring/uss_qualifier/scenarios/definitions/TestScenarioDeclaration.json @@ -1,35 +1,35 @@ { + "$id": "https://github.com/interuss/monitoring/blob/main/schemas/monitoring/uss_qualifier/scenarios/definitions/TestScenarioDeclaration.json", "$schema": "https://json-schema.org/draft/2020-12/schema", - "type": "object", + "description": "monitoring.uss_qualifier.scenarios.definitions.TestScenarioDeclaration, as defined in monitoring/uss_qualifier/scenarios/definitions.py", "properties": { "$ref": { - "type": "string", - "description": "Path to content that replaces the $ref" + "description": "Path to content that replaces the $ref", + "type": "string" }, "resources": { - "type": [ - "object", - "null" - ], + "additionalProperties": { + "type": "string" + }, + "description": "Mapping of the ID a resource in the test scenario -> the ID a resource is known by in the parent test suite.\n\nThe additional argument to concrete test scenario constructor is supplied by the parent suite resource .", "properties": { "$ref": { - "type": "string", - "description": "Path to content that replaces the $ref" + "description": "Path to content that replaces the $ref", + "type": "string" } }, - "additionalProperties": { - "type": "string" - }, - "description": "Mapping of the ID a resource in the test scenario -> the ID a resource is known by in the parent test suite.\n\nThe additional argument to concrete test scenario constructor is supplied by the parent suite resource ." + "type": [ + "object", + "null" + ] }, "scenario_type": { - "type": "string", - "description": "Type/location of test scenario. Usually expressed as the class name of the scenario module-qualified relative to the `uss_qualifier` folder" + "description": "Type of test scenario.", + "type": "string" } }, - "$id": "https://github.com/interuss/monitoring/blob/main/schemas/monitoring/uss_qualifier/scenarios/definitions/TestScenarioDeclaration.json", - "description": "monitoring.uss_qualifier.scenarios.definitions.TestScenarioDeclaration, as defined in monitoring/uss_qualifier/scenarios/definitions.py", "required": [ "scenario_type" - ] + ], + "type": "object" } \ No newline at end of file