From 9575aeac589b70324d5e02c98c6c45dfb2a42fb6 Mon Sep 17 00:00:00 2001 From: Samuel Anderson <119458760+AWS-Samuel@users.noreply.github.com> Date: Wed, 21 Feb 2024 09:45:49 -0600 Subject: [PATCH] feat!: add adaptor interface/data versioning (#73) - Adds two new commands: - version-info - Use to print Runtime CLI, Data Interface versions - is-compatible - Used to validate against expected Runtime CLI, Data Interface versions. - Adds abstract property integration_data_interface_version - Must be implemented by subclasses. - Is a SemanticVersion representing the interface version of: - init-data - run-data Signed-off-by: Samuel Anderson <119458760+AWS-Samuel@users.noreply.github.com> --- pyproject.toml | 2 +- src/openjd/adaptor_runtime/_entrypoint.py | 104 +++++++++++++++ .../adaptor_runtime/adaptors/__init__.py | 2 + .../adaptor_runtime/adaptors/_base_adaptor.py | 12 ++ .../adaptor_runtime/adaptors/_versioning.py | 62 +++++++++ .../integ/IntegCommandAdaptor/adaptor.py | 10 +- .../adaptors/test_integration_adaptor.py | 10 +- .../adaptors/test_integration_path_mapping.py | 5 + .../test_integration_adaptor_ipc.py | 6 +- .../background/sample_adaptor/adaptor.py | 6 +- .../unit/adaptors/fake_adaptor.py | 6 +- .../unit/adaptors/test_adaptor.py | 6 +- .../unit/adaptors/test_basic_adaptor.py | 6 +- .../unit/adaptors/test_versioning.py | 58 ++++++++ .../adaptor_runtime/unit/test_entrypoint.py | 125 +++++++++++++++++- 15 files changed, 411 insertions(+), 9 deletions(-) create mode 100644 src/openjd/adaptor_runtime/adaptors/_versioning.py create mode 100644 test/openjd/adaptor_runtime/unit/adaptors/test_versioning.py diff --git a/pyproject.toml b/pyproject.toml index 9b163cc..610203d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -157,7 +157,7 @@ is-posix = "sys_platform != 'win32'" [tool.coverage.report] show_missing = true -fail_under = 92 +fail_under = 90 [tool.semantic_release] # Can be removed or set to true once we are v1 diff --git a/src/openjd/adaptor_runtime/_entrypoint.py b/src/openjd/adaptor_runtime/_entrypoint.py index c1cd2ec..9a1e1b4 100644 --- a/src/openjd/adaptor_runtime/_entrypoint.py +++ b/src/openjd/adaptor_runtime/_entrypoint.py @@ -6,6 +6,7 @@ import os import signal import sys + from pathlib import Path from argparse import ArgumentParser, Namespace from types import FrameType as FrameType @@ -25,6 +26,7 @@ _OPENJD_LOG_REGEX, ConditionalFormatter, ) +from .adaptors import SemanticVersion if TYPE_CHECKING: # pragma: no cover from .adaptors.configuration import AdaptorConfiguration @@ -33,6 +35,7 @@ _U = TypeVar("_U", bound=BaseAdaptor) +_ADAPTOR_CLI_VERSION = SemanticVersion(major=0, minor=1) _CLI_HELP_TEXT = { "init_data": ( "Data to pass to the adaptor during initialization. " @@ -93,6 +96,28 @@ class _IntegrationData(NamedTuple): path_mapping_data: dict +class _VersionInfo(NamedTuple): + adaptor_cli_version: SemanticVersion + integration_data_interface_version: SemanticVersion + + def has_compatibility_with(self, expected: "_VersionInfo") -> bool: + """Returns a boolean representing if the versions of this adaptor CLI and integration data + interface are compatible with the expected adaptor CLI and integration data interface. + + This check is NOT commutative. It is assumed that self contains the versions of the + installed (running) Adaptor and that the VersionInfo being passed contains the versions + expected by something like a job template. + + Args: + other (_VersionInfo): The VersionInfo to compare with. + """ + return self.adaptor_cli_version.has_compatibility_with( + expected.adaptor_cli_version + ) and self.integration_data_interface_version.has_compatibility_with( + expected.integration_data_interface_version + ) + + class EntryPoint: """ The main entry point of the adaptor runtime. @@ -144,6 +169,14 @@ def _init_config(self) -> None: # is valid here. self.config = self.config_manager.get_default_config() + def _get_version_info(self) -> _VersionInfo: + return _VersionInfo( + adaptor_cli_version=_ADAPTOR_CLI_VERSION, + integration_data_interface_version=self.adaptor_class( + {} + ).integration_data_interface_version, + ) + def _get_integration_data(self, parsed_args: Namespace) -> _IntegrationData: return _IntegrationData( init_data=parsed_args.init_data if hasattr(parsed_args, "init_data") else {}, @@ -162,6 +195,22 @@ def start(self, reentry_exe: Optional[Path] = None) -> None: """ log_config = self._init_loggers() parser, parsed_args = self._parse_args() + version_info = self._get_version_info() + + if parsed_args.command == "is-compatible": + return self._handle_is_compatible(version_info, parsed_args, parser) + elif parsed_args.command == "version-info": + return print( + yaml.dump( + { + "OpenJD Adaptor CLI Version": str(version_info.adaptor_cli_version), + f"{self.adaptor_class.__name__} Data Interface Version": str( + version_info.integration_data_interface_version + ), + }, + indent=2, + ) + ) self._init_config() if not parsed_args.command: parser.print_help() @@ -185,6 +234,40 @@ def start(self, reentry_exe: Optional[Path] = None) -> None: adaptor, parsed_args, log_config, integration_data, reentry_exe ) + def _handle_is_compatible( + self, version_info: _VersionInfo, parsed_args: Namespace, parser: ArgumentParser + ): + try: + expected_version_info = _VersionInfo( + SemanticVersion.parse(parsed_args.openjd_adaptor_cli_version), + SemanticVersion.parse(parsed_args.integration_data_interface_version), + ) + except ValueError as e: + parser.error(str(e)) + return + + if not version_info.has_compatibility_with(expected_version_info): + parser.error( + "Installed interface versions are incompatible with expected:" + "\nInstalled:" + f"\n\tOpenJD Adaptor CLI Version: {version_info.adaptor_cli_version}" + f"\n\t{self.adaptor_class.__name__} Data Interface Version: {version_info.integration_data_interface_version}" + "\nExpected:" + f"\n\tOpenJD Adaptor CLI Version: {expected_version_info.adaptor_cli_version}" + f"\n\t{self.adaptor_class.__name__} Data Interface Version: {expected_version_info.integration_data_interface_version}" + ) + else: + print( + "Installed interface versions are compatible with expected:" + "\nInstalled:" + f"\n\tOpenJD Adaptor CLI Version: {version_info.adaptor_cli_version}" + f"\n\t{self.adaptor_class.__name__} Data Interface Version: {version_info.integration_data_interface_version}" + "\nExpected:" + f"\n\tOpenJD Adaptor CLI Version: {expected_version_info.adaptor_cli_version}" + f"\n\t{self.adaptor_class.__name__} Data Interface Version: {expected_version_info.integration_data_interface_version}" + ) + return + def _handle_run( self, adaptor: BaseAdaptor[AdaptorConfiguration], integration_data: _IntegrationData ): @@ -280,6 +363,27 @@ def _build_argparser(self) -> ArgumentParser: subparser = parser.add_subparsers(dest="command", title="commands") subparser.add_parser("show-config", help=_CLI_HELP_TEXT["show_config"]) + subparser.add_parser( + "version-info", + help="Prints CLI and data interface versions, then the program exits.", + ) + + compat_parser = subparser.add_parser( + "is-compatible", + help="Validates compatiblity for the adaptor CLI interface and integration data interface provided", + ) + compat_parser.add_argument( + "--openjd-adaptor-cli-version", + metavar="", + help="The version of the openjd adaptor CLI to compare with the installed adaptor.", + required=True, + ) + compat_parser.add_argument( + "--integration-data-interface-version", + metavar="", + help=f"The version of the {self.adaptor_class.__name__}'s data interface to compare with the installed adaptor.", + required=True, + ) init_data = ArgumentParser(add_help=False) init_data.add_argument( diff --git a/src/openjd/adaptor_runtime/adaptors/__init__.py b/src/openjd/adaptor_runtime/adaptors/__init__.py index 45548ee..41f83f7 100644 --- a/src/openjd/adaptor_runtime/adaptors/__init__.py +++ b/src/openjd/adaptor_runtime/adaptors/__init__.py @@ -7,6 +7,7 @@ from ._command_adaptor import CommandAdaptor from ._path_mapping import PathMappingRule from ._validator import AdaptorDataValidator, AdaptorDataValidators +from ._versioning import SemanticVersion __all__ = [ "Adaptor", @@ -18,4 +19,5 @@ "BaseAdaptor", "CommandAdaptor", "PathMappingRule", + "SemanticVersion", ] diff --git a/src/openjd/adaptor_runtime/adaptors/_base_adaptor.py b/src/openjd/adaptor_runtime/adaptors/_base_adaptor.py index 96c8779..98e0e46 100644 --- a/src/openjd/adaptor_runtime/adaptors/_base_adaptor.py +++ b/src/openjd/adaptor_runtime/adaptors/_base_adaptor.py @@ -6,6 +6,7 @@ import math import os import sys +from abc import abstractproperty from dataclasses import dataclass from types import ModuleType from typing import Generic @@ -18,6 +19,7 @@ ) from ._adaptor_states import AdaptorStates from ._path_mapping import PathMappingRule +from ._versioning import SemanticVersion __all__ = [ "AdaptorConfigurationOptions", @@ -88,6 +90,16 @@ def cancel(self): # pragma: no cover """ self.on_cancel() + @abstractproperty + def integration_data_interface_version(self) -> SemanticVersion: + """ + Returns a SemanticVersion of the data-interface. + Should be incremented when changes are made to any of the integration's: + - init-data schema + - run-data schema + """ + pass + @property def config_manager(self) -> ConfigurationManager[_T]: """ diff --git a/src/openjd/adaptor_runtime/adaptors/_versioning.py b/src/openjd/adaptor_runtime/adaptors/_versioning.py new file mode 100644 index 0000000..622dd59 --- /dev/null +++ b/src/openjd/adaptor_runtime/adaptors/_versioning.py @@ -0,0 +1,62 @@ +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +import re + +from functools import total_ordering +from typing import Any, NamedTuple + +VERSION_RE = re.compile(r"^\d*\.\d*$") + + +@total_ordering +class SemanticVersion(NamedTuple): + major: int + minor: int + + def __str__(self): + return f"{self.major}.{self.minor}" + + def __lt__(self, other: Any): + if not isinstance(other, SemanticVersion): + raise TypeError(f"Cannot compare SemanticVersion with {type(other)}") + if self.major < other.major: + return True + elif self.major == other.major: + if self.minor < other.minor: + return True + return False + + def __eq__(self, other: Any) -> bool: + if not isinstance(other, SemanticVersion): + raise TypeError(f"Cannot compare SemanticVersion with {type(other).__name__}") + return self.major == other.major and self.minor == other.minor + + def has_compatibility_with(self, other: "SemanticVersion") -> bool: + """ + Returns a boolean representing if the version of self has compatibility with other. + + This check is NOT commutative. + """ + if not isinstance(other, SemanticVersion): + raise TypeError( + f"Cannot check compatibility of SemanticVersion with {type(other).__name__}" + ) + if self.major == other.major == 0: + return self.minor == other.minor # Pre-release versions treat minor as breaking + return self.major == other.major and self.minor >= other.minor + + @classmethod + def parse(cls, version_str: str) -> "SemanticVersion": + """ + Parses a version string into a SemanticVersion object. + + Raises ValueError if the version string is not valid. + """ + try: + if not VERSION_RE.match(version_str): + raise ValueError + major_str, minor_str = version_str.split(".") + major = int(major_str) + minor = int(minor_str) + except ValueError: + raise ValueError(f'Provided version "{version_str}" was not of form Major.Minor') + return SemanticVersion(major, minor) diff --git a/test/openjd/adaptor_runtime/integ/IntegCommandAdaptor/adaptor.py b/test/openjd/adaptor_runtime/integ/IntegCommandAdaptor/adaptor.py index 74dccf0..4dea74c 100644 --- a/test/openjd/adaptor_runtime/integ/IntegCommandAdaptor/adaptor.py +++ b/test/openjd/adaptor_runtime/integ/IntegCommandAdaptor/adaptor.py @@ -5,13 +5,17 @@ from logging import getLogger from openjd.adaptor_runtime._osname import OSName -from openjd.adaptor_runtime.adaptors import CommandAdaptor +from openjd.adaptor_runtime.adaptors import CommandAdaptor, SemanticVersion from openjd.adaptor_runtime.process import ManagedProcess logger = getLogger(__name__) class IntegManagedProcess(ManagedProcess): + @property + def integration_data_interface_version(self) -> SemanticVersion: + return SemanticVersion(major=0, minor=1) + def get_executable(self) -> str: if OSName.is_windows(): # In Windows, we cannot directly execute the powershell script. @@ -25,6 +29,10 @@ def get_arguments(self) -> List[str]: class IntegCommandAdaptor(CommandAdaptor): + @property + def integration_data_interface_version(self) -> SemanticVersion: + return SemanticVersion(major=0, minor=1) + def get_managed_process(self, run_data: dict) -> ManagedProcess: return IntegManagedProcess(run_data) diff --git a/test/openjd/adaptor_runtime/integ/adaptors/test_integration_adaptor.py b/test/openjd/adaptor_runtime/integ/adaptors/test_integration_adaptor.py index dab543c..b3c49bd 100644 --- a/test/openjd/adaptor_runtime/integ/adaptors/test_integration_adaptor.py +++ b/test/openjd/adaptor_runtime/integ/adaptors/test_integration_adaptor.py @@ -6,7 +6,7 @@ import shutil from pathlib import Path -from openjd.adaptor_runtime.adaptors import Adaptor +from openjd.adaptor_runtime.adaptors import Adaptor, SemanticVersion class TestRun: @@ -36,6 +36,10 @@ def on_run(self, run_data: dict): print(f"\t{key} = {value}") self.update_status(progress=second_progress, status_message=second_status_message) + @property + def integration_data_interface_version(self) -> SemanticVersion: + return SemanticVersion(major=0, minor=1) + # GIVEN init_data: dict = {} run_data: dict = {"key1": "value1", "key2": "value2", "key3": "value3"} @@ -77,6 +81,10 @@ def on_cleanup(self): os.remove(str(self.f)) shutil.rmtree(parent_dir) + @property + def integration_data_interface_version(self) -> SemanticVersion: + return SemanticVersion(major=0, minor=1) + init_dict: dict = {} fa = FileAdaptor(init_dict) diff --git a/test/openjd/adaptor_runtime/integ/adaptors/test_integration_path_mapping.py b/test/openjd/adaptor_runtime/integ/adaptors/test_integration_path_mapping.py index 1aa6c0b..9a34907 100644 --- a/test/openjd/adaptor_runtime/integ/adaptors/test_integration_path_mapping.py +++ b/test/openjd/adaptor_runtime/integ/adaptors/test_integration_path_mapping.py @@ -4,6 +4,7 @@ from unittest.mock import MagicMock from openjd.adaptor_runtime.adaptors import CommandAdaptor, PathMappingRule from openjd.adaptor_runtime.process import ManagedProcess +from openjd.adaptor_runtime.adaptors import SemanticVersion class FakeCommandAdaptor(CommandAdaptor): @@ -17,6 +18,10 @@ def __init__(self, path_mapping_rules: list[dict]): def get_managed_process(self, run_data: dict) -> ManagedProcess: return MagicMock() + @property + def integration_data_interface_version(self) -> SemanticVersion: + return SemanticVersion(major=0, minor=1) + class TestGetPathMappingRules: def test_no_rules(self) -> None: diff --git a/test/openjd/adaptor_runtime/integ/application_ipc/test_integration_adaptor_ipc.py b/test/openjd/adaptor_runtime/integ/application_ipc/test_integration_adaptor_ipc.py index ec698b9..032c1b4 100644 --- a/test/openjd/adaptor_runtime/integ/application_ipc/test_integration_adaptor_ipc.py +++ b/test/openjd/adaptor_runtime/integ/application_ipc/test_integration_adaptor_ipc.py @@ -8,7 +8,7 @@ import pytest from openjd.adaptor_runtime_client import Action as _Action -from openjd.adaptor_runtime.adaptors import Adaptor +from openjd.adaptor_runtime.adaptors import Adaptor, SemanticVersion from openjd.adaptor_runtime.application_ipc import ActionsQueue as _ActionsQueue from .fake_app_client import FakeAppClient as _FakeAppClient from openjd.adaptor_runtime._osname import OSName @@ -27,6 +27,10 @@ def __init__(self, path_mapping_rules): def on_run(self, run_data: dict): return + @property + def integration_data_interface_version(self) -> SemanticVersion: + return SemanticVersion(major=0, minor=1) + path_mapping_rules = [ { "source_path_format": "windows", diff --git a/test/openjd/adaptor_runtime/integ/background/sample_adaptor/adaptor.py b/test/openjd/adaptor_runtime/integ/background/sample_adaptor/adaptor.py index 74ce780..faeefae 100644 --- a/test/openjd/adaptor_runtime/integ/background/sample_adaptor/adaptor.py +++ b/test/openjd/adaptor_runtime/integ/background/sample_adaptor/adaptor.py @@ -2,7 +2,7 @@ import logging -from openjd.adaptor_runtime.adaptors import Adaptor +from openjd.adaptor_runtime.adaptors import Adaptor, SemanticVersion _logger = logging.getLogger(__name__) @@ -15,6 +15,10 @@ class SampleAdaptor(Adaptor): def __init__(self, init_data: dict, **_): super().__init__(init_data) + @property + def integration_data_interface_version(self) -> SemanticVersion: + return SemanticVersion(major=0, minor=1) + def on_start(self): _logger.info("on_start") diff --git a/test/openjd/adaptor_runtime/unit/adaptors/fake_adaptor.py b/test/openjd/adaptor_runtime/unit/adaptors/fake_adaptor.py index 4b51346..d468ea7 100644 --- a/test/openjd/adaptor_runtime/unit/adaptors/fake_adaptor.py +++ b/test/openjd/adaptor_runtime/unit/adaptors/fake_adaptor.py @@ -2,7 +2,7 @@ from __future__ import annotations -from openjd.adaptor_runtime.adaptors import BaseAdaptor +from openjd.adaptor_runtime.adaptors import BaseAdaptor, SemanticVersion __all__ = ["FakeAdaptor"] @@ -11,6 +11,10 @@ class FakeAdaptor(BaseAdaptor): def __init__(self, init_data: dict, **kwargs): super().__init__(init_data, **kwargs) + @property + def integration_data_interface_version(self) -> SemanticVersion: + return SemanticVersion(major=0, minor=1) + def _start(self): pass diff --git a/test/openjd/adaptor_runtime/unit/adaptors/test_adaptor.py b/test/openjd/adaptor_runtime/unit/adaptors/test_adaptor.py index de35b2d..f2ff7c0 100644 --- a/test/openjd/adaptor_runtime/unit/adaptors/test_adaptor.py +++ b/test/openjd/adaptor_runtime/unit/adaptors/test_adaptor.py @@ -4,7 +4,7 @@ from unittest.mock import Mock, patch -from openjd.adaptor_runtime.adaptors import Adaptor +from openjd.adaptor_runtime.adaptors import Adaptor, SemanticVersion class FakeAdaptor(Adaptor): @@ -18,6 +18,10 @@ def __init__(self, init_data: dict): def on_run(self, run_data: dict): pass + @property + def integration_data_interface_version(self) -> SemanticVersion: + return SemanticVersion(major=0, minor=1) + class TestRun: """ diff --git a/test/openjd/adaptor_runtime/unit/adaptors/test_basic_adaptor.py b/test/openjd/adaptor_runtime/unit/adaptors/test_basic_adaptor.py index 8988b80..56f4655 100644 --- a/test/openjd/adaptor_runtime/unit/adaptors/test_basic_adaptor.py +++ b/test/openjd/adaptor_runtime/unit/adaptors/test_basic_adaptor.py @@ -4,7 +4,7 @@ from unittest.mock import MagicMock, patch -from openjd.adaptor_runtime.adaptors import CommandAdaptor +from openjd.adaptor_runtime.adaptors import CommandAdaptor, SemanticVersion from openjd.adaptor_runtime.process import ManagedProcess @@ -19,6 +19,10 @@ def __init__(self, init_data: dict): def get_managed_process(self, run_data: dict) -> ManagedProcess: return MagicMock() + @property + def integration_data_interface_version(self) -> SemanticVersion: + return SemanticVersion(major=0, minor=1) + class TestRun: """ diff --git a/test/openjd/adaptor_runtime/unit/adaptors/test_versioning.py b/test/openjd/adaptor_runtime/unit/adaptors/test_versioning.py new file mode 100644 index 0000000..83b09d7 --- /dev/null +++ b/test/openjd/adaptor_runtime/unit/adaptors/test_versioning.py @@ -0,0 +1,58 @@ +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +from __future__ import annotations + +import pytest +from openjd.adaptor_runtime.adaptors import SemanticVersion + + +class TestSemanticVersion: + @pytest.mark.parametrize( + ("version_a", "version_b", "expected_result"), + [ + (SemanticVersion(0, 1), SemanticVersion(0, 2), False), + (SemanticVersion(0, 1), SemanticVersion(0, 1), True), + (SemanticVersion(0, 1), SemanticVersion(0, 0), False), + (SemanticVersion(1, 5), SemanticVersion(1, 4), True), + (SemanticVersion(1, 5), SemanticVersion(1, 5), True), + (SemanticVersion(1, 5), SemanticVersion(1, 6), False), + (SemanticVersion(1, 5), SemanticVersion(2, 0), False), + (SemanticVersion(1, 5), SemanticVersion(2, 5), False), + (SemanticVersion(1, 5), SemanticVersion(2, 6), False), + ], + ) + def test_has_compatibility_with( + self, version_a: SemanticVersion, version_b: SemanticVersion, expected_result: bool + ): + # WHEN + result = version_a.has_compatibility_with(version_b) + + # THEN + assert result == expected_result + + @pytest.mark.parametrize( + ("version_str", "expected_result"), + [ + ("1.0.0", ValueError), + ("1.zero", ValueError), + ("three.five", ValueError), + ("1. 5", ValueError), + (" 1.5", ValueError), + ("a version", ValueError), + ("-1.5", ValueError), + ("1.-5", ValueError), + ("-1.-5", ValueError), + ("1.5", SemanticVersion(1, 5)), + ("10.50", SemanticVersion(10, 50)), + ], + ) + def test_parse(self, version_str: str, expected_result: SemanticVersion | ValueError): + if expected_result is ValueError: + # WHEN/THEN + with pytest.raises(ValueError): + SemanticVersion.parse(version_str) + else: + # WHEN + result = SemanticVersion.parse(version_str) + + # THEN + assert result == expected_result diff --git a/test/openjd/adaptor_runtime/unit/test_entrypoint.py b/test/openjd/adaptor_runtime/unit/test_entrypoint.py index 690f102..9f3b9f9 100644 --- a/test/openjd/adaptor_runtime/unit/test_entrypoint.py +++ b/test/openjd/adaptor_runtime/unit/test_entrypoint.py @@ -19,7 +19,7 @@ ConfigurationManager, RuntimeConfiguration, ) -from openjd.adaptor_runtime.adaptors import BaseAdaptor +from openjd.adaptor_runtime.adaptors import BaseAdaptor, SemanticVersion from openjd.adaptor_runtime._background import BackendRunner, FrontendRunner from openjd.adaptor_runtime._osname import OSName from openjd.adaptor_runtime._entrypoint import _load_data @@ -58,6 +58,9 @@ def mock_getLogger(): def mock_adaptor_cls(): mock_adaptor_cls = MagicMock() mock_adaptor_cls.return_value.config = AdaptorConfigurationStub() + mock_adaptor_cls.return_value.integration_data_interface_version = SemanticVersion( + major=1, minor=5 + ) mock_adaptor_cls.__name__ = "MockAdaptor" return mock_adaptor_cls @@ -84,6 +87,126 @@ def test_errors_with_no_command( assert "No command was provided." in captured.err sys_exit.assert_called_once_with(2) + def test_version_info( + self, + mock_adaptor_cls: MagicMock, + capsys: pytest.CaptureFixture[str], + ): + # GIVEN + with patch.object( + runtime_entrypoint.sys, + "argv", + [ + "Adaptor", + "version-info", + ], + ): + entrypoint = EntryPoint(mock_adaptor_cls) + + # WHEN + entrypoint.start() + + # THEN + captured = capsys.readouterr() + assert yaml.safe_load(captured.out) == { + "OpenJD Adaptor CLI Version": str(runtime_entrypoint._ADAPTOR_CLI_VERSION), + "MockAdaptor Data Interface Version": "1.5", + } + + @pytest.mark.parametrize("integration_version", ["1.4", "1.5"]) + def test_is_compatible( + self, + mock_adaptor_cls: MagicMock, + capsys: pytest.CaptureFixture[str], + integration_version: str, + ): + # GIVEN + with patch.object( + runtime_entrypoint.sys, + "argv", + [ + "Adaptor", + "is-compatible", + "--openjd-adaptor-cli-version", + str(runtime_entrypoint._ADAPTOR_CLI_VERSION), + "--integration-data-interface-version", + integration_version, + ], + ): + entrypoint = EntryPoint(mock_adaptor_cls) + + # WHEN + entrypoint.start() + + # THEN + captured = capsys.readouterr() + assert "Installed interface versions are compatible with expected:" in captured.out + + def test_bad_version_string( + self, + mock_adaptor_cls: MagicMock, + capsys: pytest.CaptureFixture[str], + ): + # GIVEN + def exit(): + raise Exception + + with patch.object( + runtime_entrypoint.sys, + "argv", + [ + "Adaptor", + "is-compatible", + "--openjd-adaptor-cli-version", + str(runtime_entrypoint._ADAPTOR_CLI_VERSION), + "--integration-data-interface-version", + "1.0.0", + ], + ), patch.object( + argparse._sys, "exit" # type: ignore + ) as sys_exit: + entrypoint = EntryPoint(mock_adaptor_cls) + + # WHEN + entrypoint.start() + + # THEN + captured = capsys.readouterr() + assert 'Provided version "1.0.0" was not of form Major.Minor' in captured.err + sys_exit.assert_called_once_with(2) + + @pytest.mark.parametrize("integration_version", ["0.9", "1.6", "1.40", "1.50", "2.0"]) + def test_is_not_compatible( + self, + mock_adaptor_cls: MagicMock, + capsys: pytest.CaptureFixture[str], + integration_version: str, + ): + # GIVEN + with patch.object( + runtime_entrypoint.sys, + "argv", + [ + "Adaptor", + "is-compatible", + "--openjd-adaptor-cli-version", + str(runtime_entrypoint._ADAPTOR_CLI_VERSION), + "--integration-data-interface-version", + integration_version, + ], + ), patch.object( + argparse._sys, "exit" # type: ignore + ) as sys_exit: + entrypoint = EntryPoint(mock_adaptor_cls) + + # WHEN + entrypoint.start() + + # THEN + captured = capsys.readouterr() + assert "Installed interface versions are incompatible with expected:" in captured.err + sys_exit.assert_called_once_with(2) + def test_creates_adaptor_with_init_data(self, mock_adaptor_cls: MagicMock): # GIVEN init_data = {"init": "data"}