Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions monitoring/uss_qualifier/resources/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,3 +31,15 @@ Resources for a given test configuration are all declared in a single global res
* Example: `netrid.flight_data.nominal_flights`, which would provide flight data for nominal flights which could be injected into Service Providers under test.
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.


## Resource modifiers

A `ResourceModifier` is a resource that wraps another resource and produces variants of it based on an integer index. This is useful when a test scenario needs multiple unique-but-related instances of a resource (e.g., distinct flights derived from a single base flight).

To use a `ResourceModifier`:

1. Declare it like any other resource, with its `base_resource` dependency pointing to the resource to be modified.
2. When need, call `adjust(index)` to obtain a modified copy of the base resource. Different `index` values produce different (unique) variants; the same `index` produces equivalent results.

The base resource itself remains available as `base_resource` on the modifier.
2 changes: 2 additions & 0 deletions monitoring/uss_qualifier/resources/dev/__init__.py
Original file line number Diff line number Diff line change
@@ -1,2 +1,4 @@
from .noop import NoOpResource as NoOpResource
from .test_exclusions import TestExclusionsResource as TestExclusionsResource
from .test_modifier import TestModifierModifierResource as TestModifierModifierResource
from .test_modifier import TestModifierResource as TestModifierResource
45 changes: 45 additions & 0 deletions monitoring/uss_qualifier/resources/dev/test_modifier.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
from implicitdict import ImplicitDict

from monitoring.uss_qualifier.resources.resource import Resource, ResourceModifier


class TestModifierSpecification(ImplicitDict):
base_id: int


class TestModifierResource(Resource[TestModifierSpecification]):
"""TestModifierResource is a simple resource returing 10 number, starting from base_id. Used for unit tests."""

_spec: TestModifierSpecification

def __init__(
self,
specification: TestModifierSpecification,
resource_origin: str,
):
super().__init__(specification, resource_origin)
self._spec = specification

def build_ids(self) -> list[int]:
return list(range(self._spec.base_id, self._spec.base_id + 10))


class TestModifierModifierSpecification(ImplicitDict):
shift_interval: int


class TestModifierModifierResource(
ResourceModifier[TestModifierModifierSpecification, TestModifierResource]
):
"""Modifier for a TestModifierResource. Used for unit tests."""

def adjust(self, index: int) -> TestModifierResource:

# 'Clone' the resource with new specs
return TestModifierResource(
TestModifierSpecification(
base_id=self.base_resource._spec.base_id
+ self._spec.shift_interval * index,
),
resource_origin=self.base_resource.resource_origin,
)
48 changes: 46 additions & 2 deletions monitoring/uss_qualifier/resources/resource.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
from abc import ABC
from typing import TypeVar, get_type_hints
from abc import ABC, abstractmethod
from typing import TypeVar, get_args, get_origin, get_type_hints

from implicitdict import ImplicitDict
from loguru import logger
Expand Down Expand Up @@ -43,6 +43,36 @@ def is_type(self, resource_type: str) -> bool:
ResourceType = TypeVar("ResourceType", bound=Resource)


class ResourceModifier[SpecificationType: ImplicitDict, ResourceType](
Resource[SpecificationType], ABC
):
"""A specifc type of resources that can return adjusted an resource that shall unique based on a specifc 'index'.
The underlying resource shall be a dependency named 'base_resource'.

Concrete subclass must implement 'adjust' as needed.
"""

_spec: SpecificationType
base_resource: ResourceType

def __init__(
self,
specification: SpecificationType,
resource_origin: str,
base_resource: ResourceType,
):
super().__init__(specification, resource_origin)
self._spec = specification
self.base_resource = base_resource

@abstractmethod
def adjust(self, index: int) -> ResourceType:
"""
Return a new instance of the base resource, modified to be unique based on 'index' value.
"""
pass


class MissingResourceError(ValueError):
missing_resource_name: str

Expand Down Expand Up @@ -156,6 +186,20 @@ def get_resource_types(
)

constructor_signature = get_type_hints(resource_type.__init__)

# Resolve generic type vars
typevar_map: dict = {}
for base in getattr(resource_type, "__orig_bases__", ()):
params = getattr(get_origin(base), "__type_params__", None) or getattr(
get_origin(base), "__parameters__", ()
)
for param, arg in zip(params, get_args(base)):
if not isinstance(arg, TypeVar):
typevar_map[param] = arg
constructor_signature = {
name: typevar_map.get(t, t) for name, t in constructor_signature.items()
}

specification_type = None
for arg_name, arg_type in constructor_signature.items():
if arg_name == "return":
Expand Down
130 changes: 130 additions & 0 deletions monitoring/uss_qualifier/resources/resources_test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
import unittest

from monitoring.uss_qualifier.resources.definitions import (
ResourceDeclaration,
ResourceID,
)
from monitoring.uss_qualifier.resources.dev.test_modifier import (
TestModifierModifierSpecification,
TestModifierSpecification,
)
from monitoring.uss_qualifier.resources.resource import create_resources


class TestResourceModifier(unittest.TestCase):
def _build_test_modifier_declaration(
self, base_id
) -> dict[ResourceID, ResourceDeclaration]:
return {
"test": ResourceDeclaration(
resource_type="resources.dev.TestModifierResource",
specification=TestModifierSpecification(base_id=base_id),
)
}

def _build_test_modifier_modifier_declaration(
self, base_id, shift_interval
) -> dict[ResourceID, ResourceDeclaration]:
return {
"test": self._build_test_modifier_declaration(base_id)["test"],
"test_modifier": ResourceDeclaration(
resource_type="resources.dev.TestModifierModifierResource",
specification=TestModifierModifierSpecification(
shift_interval=shift_interval
),
dependencies={
"base_resource": "test",
},
),
}

def test_base_resource(self):
"""Test basic usage of the resource"""
declaration = self._build_test_modifier_declaration(42)

resources = create_resources(declaration, "test", True)
assert "test" in resources

resource = resources["test"]

assert resource.build_ids() == [42, 43, 44, 45, 46, 47, 48, 49, 50, 51]

def test_base_resource_base_id(self):
"""Test that base id works as expected"""

declaration = self._build_test_modifier_declaration(52)

resources = create_resources(declaration, "test", True)
assert "test" in resources

resource = resources["test"]

assert resource.build_ids() == [52, 53, 54, 55, 56, 57, 58, 59, 60, 61]

def test_modifier_resource(self):
"""Test basic usage of the resource modifier"""
declaration = self._build_test_modifier_modifier_declaration(42, 10)

resources = create_resources(declaration, "test", True)
assert "test_modifier" in resources

resource = resources["test_modifier"]

assert resource.adjust(0).build_ids() == [
42,
43,
44,
45,
46,
47,
48,
49,
50,
51,
]
assert resource.adjust(1).build_ids() == [
52,
53,
54,
55,
56,
57,
58,
59,
60,
61,
]

def test_modifier_resource_shift(self):
"""Test shift usage of the resource modifier"""
declaration = self._build_test_modifier_modifier_declaration(42, 20)

resources = create_resources(declaration, "test", True)
assert "test_modifier" in resources

resource = resources["test_modifier"]

assert resource.adjust(0).build_ids() == [
42,
43,
44,
45,
46,
47,
48,
49,
50,
51,
]
assert resource.adjust(1).build_ids() == [
62,
63,
64,
65,
66,
67,
68,
69,
70,
71,
]
2 changes: 2 additions & 0 deletions schemas/manage_type_schemas.py
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,8 @@ def _find_specifications(
_find_specifications(member, repo, already_checked)
elif inspect.isclass(member):
if issubclass(member, Resource) and member != Resource:
if inspect.isabstract(member):
continue
spec_type = get_args(member.__orig_bases__[0])[0]
repo[fullname(spec_type)] = spec_type
elif issubclass(member, ActionGenerator) and member != ActionGenerator:
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
{
"$id": "https://github.com/interuss/monitoring/blob/main/schemas/monitoring/uss_qualifier/resources/dev/test_modifier/TestModifierModifierSpecification.json",
"$schema": "https://json-schema.org/draft/2020-12/schema",
"description": "monitoring.uss_qualifier.resources.dev.test_modifier.TestModifierModifierSpecification, as defined in monitoring/uss_qualifier/resources/dev/test_modifier.py",
"properties": {
"$ref": {
"description": "Path to content that replaces the $ref",
"type": "string"
},
"shift_interval": {
"type": "integer"
}
},
"required": [
"shift_interval"
],
"type": "object"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
{
"$id": "https://github.com/interuss/monitoring/blob/main/schemas/monitoring/uss_qualifier/resources/dev/test_modifier/TestModifierSpecification.json",
"$schema": "https://json-schema.org/draft/2020-12/schema",
"description": "monitoring.uss_qualifier.resources.dev.test_modifier.TestModifierSpecification, as defined in monitoring/uss_qualifier/resources/dev/test_modifier.py",
"properties": {
"$ref": {
"description": "Path to content that replaces the $ref",
"type": "string"
},
"base_id": {
"type": "integer"
}
},
"required": [
"base_id"
],
"type": "object"
}
Loading