diff --git a/.github/workflows/goth.yml b/.github/workflows/goth.yml index 55c07be..c0d29f3 100644 --- a/.github/workflows/goth.yml +++ b/.github/workflows/goth.yml @@ -27,15 +27,13 @@ jobs: with: python-version: '3.8' - - name: Configure poetry - uses: Gr1N/setup-poetry@v8 - with: - poetry-version: 1.2.2 + - name: Install and configure Poetry + run: python -m pip install -U pip setuptools poetry==1.3.2 - name: Install dependencies run: | poetry env use python3.8 - poetry install -vvv --with tests_integration + poetry install -vvv - name: Disconnect Docker containers from default network continue-on-error: true @@ -56,12 +54,13 @@ jobs: - name: Log in to GitHub Docker repository run: echo ${{ secrets.GITHUB_TOKEN }} | docker login docker.pkg.github.com -u ${{github.actor}} --password-stdin + - name: Initialize the test suite + run: poetry run poe tests_integration_init + - name: Run test suite env: GITHUB_API_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: | - poetry run poe tests_integration_assets - poetry run poe tests_integration + run: poetry run poe tests_integration - name: Upload test logs uses: actions/upload-artifact@v2 @@ -79,6 +78,6 @@ jobs: - name: Remove poetry virtual env if: always() # Python version below should agree with the version set up by this job. - # In future we'll be able to use the `--all` flag here to remove envs for + # In the future we'll be able to use the `--all` flag here to remove envs for # all Python versions (https://github.com/python-poetry/poetry/issues/3208). run: poetry env remove python3.8 diff --git a/dapp_runner/descriptor/__init__.py b/dapp_runner/descriptor/__init__.py index d0c60e0..244e1db 100644 --- a/dapp_runner/descriptor/__init__.py +++ b/dapp_runner/descriptor/__init__.py @@ -1,6 +1,6 @@ -from .base import DescriptorError from .config import Config from .dapp import DappDescriptor +from .error import DescriptorError __all__ = ( "DappDescriptor", diff --git a/dapp_runner/descriptor/base.py b/dapp_runner/descriptor/base.py deleted file mode 100644 index 67bd468..0000000 --- a/dapp_runner/descriptor/base.py +++ /dev/null @@ -1,120 +0,0 @@ -"""Dapp runner descriptor base classes.""" -from dataclasses import Field, dataclass, fields -from typing import Any, Dict, Generic, List, Type, TypeVar, Union - - -class DescriptorError(Exception): - """Error while loading a Dapp Runner descriptor.""" - - -DescriptorType = TypeVar("DescriptorType", bound="BaseDescriptor") - - -@dataclass -class BaseDescriptor(Generic[DescriptorType]): - """Base dapp runner descriptor class. - - Descriptor classes serve as factories of the entities defined in the specific - part of the descriptor tree. - """ - - @classmethod - def _instantiate_value(cls, desc: str, f: Field, value_type, value): - try: - if type(value_type) is type and issubclass(value_type, BaseDescriptor): - return value_type.load(value) - elif f.metadata.get("factory"): - return f.metadata["factory"](value) - elif type(value) is dict: - return value_type(**value) - else: - return value_type(value) - except Exception as e: - raise DescriptorError(f"{cls.__name__}.{desc}: {e.__class__.__name__}: {str(e)}") - - @classmethod - def _load_dict(cls, f: Field, field_type, descriptor_value: Dict[str, Any]) -> Dict[str, Any]: - try: - entry_type = getattr(field_type, "__args__", None)[1] # type: ignore [index] # noqa - except (TypeError, IndexError): - entry_type = None - - # is the dict's value type defined as a simple type? - if type(entry_type) is type: - out = {} - for k, v in descriptor_value.items(): - out[k] = cls._instantiate_value(f"{f.name}[{k}]", f, entry_type, v) - return out - return descriptor_value - - @classmethod - def _load_list(cls, f: Field, field_type, descriptor_value: List[Any]) -> List[Any]: - try: - entry_type = getattr(field_type, "__args__", None)[0] # type: ignore [index] # noqa - except (TypeError, IndexError): - entry_type = None - - # is the list's value type defined as a simple type? - if type(entry_type) is type: - out = [] - for i in range(len(descriptor_value)): - v = descriptor_value[i] - out.append(cls._instantiate_value(f"{f.name}[{i}]", f, entry_type, v)) - return out - return descriptor_value - - @classmethod - def _resolve_field(cls, f: Field, descriptor_value: Any, field_type=None): - # field has a load function defined, so we're delegating the responsibility - if f.metadata.get("load"): - return f.metadata["load"](descriptor_value) - - elif not field_type: - field_type = f.type - - # field is a simple type (i.e. not a `typing` type hint) - if type(field_type) is type: - return cls._instantiate_value(f.name, f, field_type, descriptor_value) - - # field is an Optional -> Union[..., NoneType] - elif ( - getattr(field_type, "__origin__", None) is Union - and len(field_type.__args__) == 2 - and field_type.__args__[1] is type(None) # noqa - ): - return cls._resolve_field(f, descriptor_value, field_type.__args__[0]) - - # field is a `Dict` - elif getattr(field_type, "__origin__", None) is dict: - return cls._load_dict(f, field_type, descriptor_value) - - # field is a `List` - elif getattr(field_type, "__origin__", None) is list: - return cls._load_list(f, field_type, descriptor_value) - - else: - raise NotImplementedError( - f"{cls.__name__}.{f.name}: Unimplemented handler for {field_type}" - ) - - @classmethod - def load(cls: Type[DescriptorType], descriptor_dict: Dict[str, Any]) -> DescriptorType: - """Create a new descriptor object from its dictionary representation.""" - resolved_kwargs: Dict[str, Any] = {} - for f in fields(cls): - # skip non-init fields - if not f.init: - continue - - # if the fields value is not provided in the descriptor, we're leaving - # that to the instantiated class' `__init__` to warn about that - if f.name not in descriptor_dict.keys(): - continue - - descriptor_value = descriptor_dict.get(f.name) - resolved_kwargs[f.name] = cls._resolve_field(f, descriptor_value) - - unexpected_keys = set(descriptor_dict.keys()) - set(f.name for f in fields(cls)) - if unexpected_keys: - raise DescriptorError(f"Unexpected keys: `{unexpected_keys}` for `{cls.__name__}`") - return cls(**resolved_kwargs) diff --git a/dapp_runner/descriptor/config.py b/dapp_runner/descriptor/config.py index e78f6d1..544713c 100644 --- a/dapp_runner/descriptor/config.py +++ b/dapp_runner/descriptor/config.py @@ -1,55 +1,63 @@ """Class definitions for the Dapp Runner's configuration descriptor.""" import os -from dataclasses import dataclass, field from typing import Optional -from .base import BaseDescriptor +from pydantic import BaseModel, Field, validator -@dataclass -class YagnaConfig(BaseDescriptor["YagnaConfig"]): +class YagnaConfig(BaseModel): """Yagna daemon configuration properties. Properties describing the local requestor daemon configuration that the Dapp Runner will use to run services on Golem. """ - def __app_key__factory(value: str): # type: ignore [misc] # noqa + subnet_tag: Optional[str] + api_url: Optional[str] = None + gsb_url: Optional[str] = None + app_key: Optional[str] = None + + class Config: # noqa: D106 + extra = "forbid" + + @validator("app_key", always=True) + def __app_key__extrapolate(cls, v): # TODO this should be applied uniformly across any fields, # for now, making an exception for the app key - if value and value.startswith("$"): - return os.environ[value[1:]] - - return value + if v and v.startswith("$"): + return os.environ[v[1:]] - subnet_tag: Optional[str] - api_url: Optional[str] = None - gsb_url: Optional[str] = None - app_key: Optional[str] = field(metadata={"factory": __app_key__factory}, default=None) + return v -@dataclass -class PaymentConfig: +class PaymentConfig(BaseModel): """Requestor's payment config.""" budget: float driver: str network: str + class Config: # noqa: D106 + extra = "forbid" -@dataclass -class LimitsConfig: + +class LimitsConfig(BaseModel): """Limits of the running app.""" startup_timeout: Optional[int] = None # seconds max_running_time: Optional[int] = None # seconds + class Config: # noqa: D106 + extra = "forbid" + -@dataclass -class Config(BaseDescriptor["Config"]): +class Config(BaseModel): """Root configuration descriptor for the Dapp Runner.""" yagna: YagnaConfig payment: PaymentConfig - limits: LimitsConfig = field(default_factory=LimitsConfig) + limits: LimitsConfig = Field(default_factory=LimitsConfig) + + class Config: # noqa: D106 + extra = "forbid" diff --git a/dapp_runner/descriptor/dapp.py b/dapp_runner/descriptor/dapp.py index eb6b415..6903877 100644 --- a/dapp_runner/descriptor/dapp.py +++ b/dapp_runner/descriptor/dapp.py @@ -1,13 +1,14 @@ """Class definitions for the Dapp Runner's dapp descriptor.""" import logging -from dataclasses import dataclass, field, fields -from typing import Any, Dict, Final, List, Optional, Tuple +import re +from typing import Any, Dict, Final, List, Optional, Tuple, Union import networkx +from pydantic import BaseModel, Field, PrivateAttr, validator from yapapi.payload import vm -from .base import BaseDescriptor, DescriptorError +from .error import DescriptorError NETWORK_DEFAULT_NAME: Final[str] = "default" @@ -26,124 +27,154 @@ logger = logging.getLogger(__name__) -@dataclass -class PayloadDescriptor: +class PayloadDescriptor(BaseModel): """Yapapi Payload descriptor.""" runtime: str - params: Dict[str, Any] = field(default_factory=dict) + params: Dict[str, Any] = Field(default_factory=dict) + class Config: # noqa: D106 + extra = "forbid" -@dataclass -class PortMapping: + +class PortMapping(BaseModel): """Port mapping for a http proxy.""" remote_port: int local_port: Optional[int] = None + class Config: # noqa: D106 + extra = "forbid" + -@dataclass -class ProxyDescriptor(BaseDescriptor["ProxyDescriptor"]): +class ProxyDescriptor(BaseModel): """Proxy descriptor.""" - def __ports_factory(value: str) -> PortMapping: # type: ignore [misc] # noqa - ports = [int(p) for p in value.split(":")] - port_mappping = PortMapping(remote_port=ports.pop()) - if ports: - port_mappping.local_port = ports.pop() - return port_mappping + ports: List[PortMapping] + + class Config: # noqa: D106 + extra = "forbid" - ports: List[PortMapping] = field(metadata={"factory": __ports_factory}) + @validator("ports", pre=True, each_item=True) + def __ports__preprocess(cls, v): + if isinstance(v, PortMapping): + return v + + try: + return re.match( + r"^(?:(?P\d+)\:)?(?P\d+)$", v + ).groupdict() # type: ignore [union-attr] + except AttributeError: + raise ValueError("Expected format: `remote_port` or `local_port:remote_port`.") -@dataclass class HttpProxyDescriptor(ProxyDescriptor): """HTTP proxy descriptor.""" -@dataclass class SocketProxyDescriptor(ProxyDescriptor): """TCP socket proxy descriptor.""" -@dataclass -class CommandDescriptor: +class CommandDescriptor(BaseModel): """Exeunit command descriptor.""" cmd: str = EXEUNIT_CMD_RUN - params: Dict[str, Any] = field(default_factory=dict) + params: Dict[str, Any] = Field(default_factory=dict) + + class Config: # noqa: D106 + extra = "forbid" @classmethod - def load(cls, c): - """Load a command descriptor from its serialized representation.""" - if isinstance(c, list): + def canonize_input(cls, value: Union[str, List, Dict]): + """Convert a single command descriptor to its canonical form. + + Supported formats: + + ```yaml + init: + - test: + args: ["/bin/rm", "/var/log/nginx/access.log", "/var/log/nginx/error.log"] + from: "aa" + to: "b" + - aaa: + args: ["/bin/rm", "/var/log/nginx/access.log", "/var/log/nginx/error.log"] + from: "aa" + to: "b" + ``` + + ```yaml + init: ["/docker-entrypoint.sh"] + ``` + + ```yaml + init: + - run: + args: + - "/docker-entrypoint.sh" + ``` + + ```yaml + init: + - ["/docker-entrypoint.sh"] + - ["/bin/chmod", "a+x", "/"] + - ["/bin/sh", "-c", 'echo "Hello from inside Golem!" > /usr/share/nginx/html/index.html'] # noqa + ``` + + """ + if isinstance(value, list): # assuming it's a `run` - return cls(params={"args": c}) - elif isinstance(c, dict) and len(c.keys()) == 1: + return {"cmd": EXEUNIT_CMD_RUN, "params": {"args": value}} + elif isinstance(value, dict) and len(value.keys()) == 1: # we don't want to support malformed entries # where multiple commands are present in a single dictionary - for cmd, params in c.items(): + for cmd, params in value.items(): if cmd == EXEUNIT_CMD_RUN and isinstance(params, list): # support shorthand `run` notation: # - run: # - ["/golem/run/simulate_observations_ctl.py", "--start"] params = {"args": params} - return CommandDescriptor(cmd=cmd, params=params) + return {"cmd": cmd, "params": params} else: - raise DescriptorError(f"Cannot parse the command descriptor `{c}`.") - - -class _CommandDescriptorList: - """Preprocessor for the exescript commands.""" - - def _process_command(self, c): - self.commands.append(CommandDescriptor.load(c)) - - def __init__(self): - self.commands = list() + raise DescriptorError(f"Cannot parse the command descriptor `{value}`.") - @classmethod - def load_commands(cls, value) -> List[CommandDescriptor]: - """Load the contents of the commands list.""" - commands_list = cls() - - if len(value) > 0 and isinstance(value[0], str): - # support single line definitions, e.g. `init: ["/docker-entrypoint.sh"]` - value = [value] - - for c in value: - commands_list._process_command(c) - - return commands_list.commands - -@dataclass -class ServiceDescriptor(BaseDescriptor["ServiceDescriptor"]): +class ServiceDescriptor(BaseModel): """Yapapi Service descriptor.""" payload: str - init: List[CommandDescriptor] = field( - metadata={"load": _CommandDescriptorList.load_commands}, default_factory=list - ) + init: List[CommandDescriptor] = Field(default_factory=list) network: Optional[str] = None - ip: List[str] = field(default_factory=list) + ip: List[str] = Field(default_factory=list) http_proxy: Optional[HttpProxyDescriptor] = None tcp_proxy: Optional[SocketProxyDescriptor] = None - depends_on: List[str] = field(default_factory=list) + depends_on: List[str] = Field(default_factory=list) + + class Config: # noqa: D106 + extra = "forbid" + + @validator("init", pre=True) + def __init__canonize_commands(cls, v): + if len(v) and isinstance(v[0], str): + # support single line definitions, e.g. `init: ["/docker-entrypoint.sh"]` + v = [v] + return [CommandDescriptor.canonize_input(v) for v in v] -@dataclass -class NetworkDescriptor(BaseDescriptor["NetworkDescriptor"]): + +class NetworkDescriptor(BaseModel): """Yapapi network descriptor.""" - ip: str = field(default="192.168.0.0/24") + ip: str = "192.168.0.0/24" owner_ip: Optional[str] = None mask: Optional[str] = None gateway: Optional[str] = None + class Config: # noqa: D106 + extra = "forbid" + -@dataclass -class MetaDescriptor: +class MetaDescriptor(BaseModel): """Meta descriptor for the app. Silently ignores unknown fields. @@ -154,24 +185,27 @@ class MetaDescriptor: author: str = "" version: str = "" - def __init__(self, **kwargs): - for f in fields(self): - if f.name in kwargs: - setattr(self, f.name, f.type(kwargs.pop(f.name))) - if kwargs: - logger.debug("Unrecognized `meta` fields: %s", kwargs) - -@dataclass -class DappDescriptor(BaseDescriptor["DappDescriptor"]): +class DappDescriptor(BaseModel): """Root dapp descriptor for the Dapp Runner.""" payloads: Dict[str, PayloadDescriptor] nodes: Dict[str, ServiceDescriptor] - networks: Dict[str, NetworkDescriptor] = field(default_factory=dict) - meta: MetaDescriptor = field(default_factory=MetaDescriptor) + networks: Dict[str, NetworkDescriptor] = Field(default_factory=dict) + meta: MetaDescriptor = Field(default_factory=MetaDescriptor) + + _dependency_graph: networkx.DiGraph = PrivateAttr() - _dependency_graph: networkx.DiGraph = field(init=False) + class Config: # noqa: D106 + extra = "forbid" + + def __init__(self, **kwargs): + super().__init__(**kwargs) + self.__validate_nodes() + self.__implicit_proxy_init() + self.__implicit_vpn() + self.__implicit_manifest_support() + self._resolve_dependencies() def __validate_nodes(self): """Ensure that required payloads and optional networks are defined.""" @@ -244,10 +278,3 @@ def nodes_prioritized(self) -> List[Tuple[str, ServiceDescriptor]]: for name in reversed(list(networkx.topological_sort(self._dependency_graph))) if name != DEPENDENCY_ROOT ] - - def __post_init__(self): - self.__validate_nodes() - self.__implicit_proxy_init() - self.__implicit_vpn() - self.__implicit_manifest_support() - self._resolve_dependencies() diff --git a/dapp_runner/descriptor/error.py b/dapp_runner/descriptor/error.py new file mode 100644 index 0000000..f42ec78 --- /dev/null +++ b/dapp_runner/descriptor/error.py @@ -0,0 +1,5 @@ +"""Dapp runner descriptor error classes.""" + + +class DescriptorError(Exception): + """Error while loading a Dapp Runner descriptor.""" diff --git a/dapp_runner/runner/__init__.py b/dapp_runner/runner/__init__.py index 694ce9d..b9879f3 100644 --- a/dapp_runner/runner/__init__.py +++ b/dapp_runner/runner/__init__.py @@ -40,8 +40,8 @@ async def _run_app( silent=False, ): """Run the dapp using the Runner.""" - config = Config.load(config_dict) - dapp = DappDescriptor.load(dapp_dict) + config = Config(**config_dict) + dapp = DappDescriptor(**dapp_dict) r = Runner(config=config, dapp=dapp) _print_env_info(r.golem) @@ -160,7 +160,7 @@ def start_runner( def verify_dapp(dapp_dict: dict): """Verify the passed app descriptor schema and report any encountered errors.""" try: - dapp = DappDescriptor.load(dapp_dict) + dapp = DappDescriptor(**dapp_dict) print(dapp) except DescriptorError as e: print(e) diff --git a/dapp_runner/runner/runner.py b/dapp_runner/runner/runner.py index c1063f2..e4fa8e8 100644 --- a/dapp_runner/runner/runner.py +++ b/dapp_runner/runner/runner.py @@ -1,7 +1,6 @@ """Main Dapp Runner module.""" import asyncio import logging -from dataclasses import asdict from datetime import datetime from typing import Dict, Final, List, Optional @@ -82,7 +81,7 @@ def __init__(self, config: Config, dapp: DappDescriptor): async def _create_networks(self): for name, desc in self.dapp.networks.items(): - self._networks[name] = await self.golem.create_network(**asdict(desc)) + self._networks[name] = await self.golem.create_network(**desc.dict()) async def _load_payloads(self): for name, desc in self.dapp.payloads.items(): @@ -340,7 +339,7 @@ async def _listen_incoming_command_queue(self): continue logger.debug("Creating runtime command: %s", cmd_def) - cmd = CommandDescriptor.load(cmd_def) + cmd = CommandDescriptor(**cmd_def) service.command_queue.put_nowait(cmd) def _is_cluster_state(self, cluster_id: str, state: ServiceState) -> bool: diff --git a/pyproject.toml b/pyproject.toml index 1633b1e..7a3ab9e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -37,7 +37,8 @@ pyyaml = "^5.0" # requires bump to goth's dependencies https://github.com/golem shortuuid = "^1.0" ansicolors = "^1.1.8" networkx = "^2.8" -yapapi = "^0.10.0" +yapapi = { git = "https://github.com/golemfactory/yapapi.git", branch = "master" } +pydantic = "^1.10.5" [tool.poetry.group.dev.dependencies] setuptools = "*" # implicitly required by liccehck @@ -58,13 +59,6 @@ pytest-mock = "^3.10.0" factory-boy = "^3.2.1" isort = "^5.11.4" -[tool.poetry.group.tests_integration] -optional = true - -[tool.poetry.group.tests_integration.dependencies] -goth = "^0.14.0" -requests = "^2.28.2" - [tool.poetry.scripts] dapp-runner = "dapp_runner.__main__:_cli" @@ -78,14 +72,21 @@ checks_typing = {cmd = "mypy --install-types --non-interactive --ignore-missing checks_license = {sequence = ["_checks_license_export", "_checks_license_verify"], help = "Run only license compatibility checks"} _checks_license_export = "poetry export -f requirements.txt -o .requirements.txt" _checks_license_verify = "liccheck -r .requirements.txt" + format = {sequence = ["_format_autoflake", "_format_isort", "_format_black"], help = "Run code auto formatting"} _format_autoflake = "autoflake ." _format_isort = "isort ." _format_black = "black ." + tests = {sequence = ["tests_unit"], help = "Run all available tests"} tests_unit = {cmd = "pytest --cov --cov-report html --cov-report term -sv tests/unit", help = "Run only unit tests"} -tests_integration_assets = "python -m goth create-assets tests/goth_tests/assets" -tests_integration = "pytest -svx tests/goth_tests --disable-pytest-warnings --config-override docker-compose.build-environment.use-prerelease=true --config-path tests/goth_tests/assets/goth-config-testing.yml" + +tests_integration_init = ["_tests_integration_env", "_tests_integration_requirements", "_tests_integration_assets"] +_tests_integration_env = "python -m venv .envs/goth" +_tests_integration_requirements = ".envs/goth/bin/pip install --extra-index-url https://test.pypi.org/simple/ goth==0.14.1 pytest pytest-asyncio pexpect" +_tests_integration_assets = ".envs/goth/bin/python -m goth create-assets tests/goth_tests/assets" +tests_integration = ".envs/goth/bin/python -m pytest -svx tests/goth_tests --disable-pytest-warnings --config-override docker-compose.build-environment.use-prerelease=true --config-path tests/goth_tests/assets/goth-config-testing.yml" + [tool.liccheck] authorized_licenses = [ diff --git a/tests/factories/descriptor/config.py b/tests/factories/descriptor/config.py index 347cc8e..7ec304d 100644 --- a/tests/factories/descriptor/config.py +++ b/tests/factories/descriptor/config.py @@ -11,6 +11,7 @@ class Meta: # noqa model = YagnaConfig subnet_tag = "public" + app_key = factory.Faker("pystr") class PaymentConfigFactory(factory.Factory): diff --git a/tests/unit/conftest.py b/tests/unit/conftest.py index 102bb48..70c685c 100644 --- a/tests/unit/conftest.py +++ b/tests/unit/conftest.py @@ -1,5 +1,6 @@ """Pytest configuration file containing the utilities for Dapp Runner unit tests.""" import asyncio +from typing import Tuple from unittest import mock import pytest @@ -11,7 +12,7 @@ class Utils: """Utilities for Dapp Runner tests.""" @staticmethod - def verify_error(expected_error, actual_error): + def verify_error(expected_error: Tuple[type, str], actual_error): """Verify expected error vs an actual error. Example usage: @@ -32,8 +33,8 @@ def test_error(params, expected_error, test_utils): if actual_error: if not expected_error: raise - assert str(expected_error) in str(actual_error) - assert type(actual_error) == type(expected_error) + assert expected_error[1] in str(actual_error) + assert type(actual_error) == expected_error[0] @pytest.fixture diff --git a/tests/unit/descriptor/test_config.py b/tests/unit/descriptor/test_config.py index 869ec18..67f7c9d 100644 --- a/tests/unit/descriptor/test_config.py +++ b/tests/unit/descriptor/test_config.py @@ -1,7 +1,7 @@ """Tests for the Config descriptor.""" import pytest +from pydantic import ValidationError -from dapp_runner.descriptor import DescriptorError from dapp_runner.descriptor.config import Config, PaymentConfig, YagnaConfig @@ -23,24 +23,33 @@ { "yagna": {"subnet_tag": "devnet-beta"}, }, - TypeError("__init__() missing"), + (ValidationError, "payment"), ), ( { + "yagna": {"subnet_tag": "devnet-beta"}, + "payment": { + "budget": 1.0, + "driver": "erc20", + "network": "rinkeby", + }, "foo": "bar", }, - DescriptorError("Unexpected keys: `{'foo'}"), + (ValidationError, "extra fields not permitted"), ), ], ) def test_config_descriptor(descriptor_dict, error): """Test whether the Config descriptor loads properly.""" try: - config = Config.load(descriptor_dict) + config = Config(**descriptor_dict) assert isinstance(config.yagna, YagnaConfig) assert isinstance(config.payment, PaymentConfig) + # raise Exception(config.payment) + except Exception as e: # noqa if not error: raise - assert str(error) in str(e) - assert type(e) == type(error) + + assert str(error[1]) in str(e) + assert type(e) == error[0] diff --git a/tests/unit/descriptor/test_dapp.py b/tests/unit/descriptor/test_dapp.py index 8fbb929..a122335 100644 --- a/tests/unit/descriptor/test_dapp.py +++ b/tests/unit/descriptor/test_dapp.py @@ -1,5 +1,6 @@ """Tests for the Dapp descriptor.""" import pytest +from pydantic import ValidationError from dapp_runner.descriptor import DappDescriptor, DescriptorError from dapp_runner.descriptor.dapp import ( @@ -98,7 +99,7 @@ } }, }, - DescriptorError("Undefined payload: `other`"), + (DescriptorError, "Undefined payload: `other`"), ), ( { @@ -111,7 +112,7 @@ } }, }, - TypeError("__init__() missing "), + (ValidationError, "payloads\n field required"), ), ( { @@ -131,20 +132,20 @@ } }, }, - DescriptorError("Undefined network: `missing`"), + (DescriptorError, "Undefined network: `missing`"), ), ( { "unsupported": {}, }, - DescriptorError("Unexpected keys: `{'unsupported'}"), + (ValidationError, "unsupported\n extra fields not permitted"), ), ], ) def test_dapp_descriptor(descriptor_dict, error, test_utils): """Test whether the Dapp descriptor loads correctly.""" try: - dapp = DappDescriptor.load(descriptor_dict) + dapp = DappDescriptor(**descriptor_dict) payload = list(dapp.payloads.values())[0] service = list(dapp.nodes.values())[0] assert isinstance(payload, PayloadDescriptor) @@ -168,7 +169,7 @@ def _test_proxy_descriptor( implicit_vpn, ): try: - dapp = DappDescriptor.load(descriptor_dict) + dapp = DappDescriptor(**descriptor_dict) service = list(dapp.nodes.values())[0] proxy = getattr(service, proxy_property) assert isinstance(proxy, proxy_class) @@ -208,7 +209,7 @@ def _test_proxy_descriptor( } }, }, - [PortMapping(25, 2525)], + [PortMapping(remote_port=25, local_port=2525)], None, False, ), @@ -228,7 +229,7 @@ def _test_proxy_descriptor( } }, }, - [PortMapping(80)], + [PortMapping(remote_port=80)], None, False, ), @@ -244,7 +245,7 @@ def _test_proxy_descriptor( } }, }, - [PortMapping(80), PortMapping(1234)], + [PortMapping(remote_port=80), PortMapping(remote_port=1234)], None, False, ), @@ -264,7 +265,7 @@ def _test_proxy_descriptor( } }, }, - [PortMapping(80)], + [PortMapping(remote_port=80)], None, True, ), @@ -302,7 +303,7 @@ def test_http_proxy_descriptor(test_utils, descriptor_dict, port_mappings, error } }, }, - [PortMapping(25, 2525)], + [PortMapping(remote_port=25, local_port=2525)], None, False, ), @@ -322,7 +323,7 @@ def test_http_proxy_descriptor(test_utils, descriptor_dict, port_mappings, error } }, }, - [PortMapping(80)], + [PortMapping(remote_port=80)], None, False, ), @@ -338,7 +339,7 @@ def test_http_proxy_descriptor(test_utils, descriptor_dict, port_mappings, error } }, }, - [PortMapping(80), PortMapping(1234)], + [PortMapping(remote_port=80), PortMapping(remote_port=1234)], None, False, ), @@ -358,7 +359,7 @@ def test_http_proxy_descriptor(test_utils, descriptor_dict, port_mappings, error } }, }, - [PortMapping(80)], + [PortMapping(remote_port=80)], None, True, ), @@ -425,7 +426,7 @@ def test_tcp_proxy_descriptor(test_utils, descriptor_dict, port_mappings, error, ) def test_manifest_payload(descriptor_dict, implicit_manifest): """Test whether `manifest_support` is implicitly added to the capabilities list.""" - dapp = DappDescriptor.load(descriptor_dict) + dapp = DappDescriptor(**descriptor_dict) payload = list(dapp.payloads.values())[0] if implicit_manifest: assert VM_PAYLOAD_CAPS_KWARG in payload.params @@ -445,7 +446,7 @@ def test_manifest_payload(descriptor_dict, implicit_manifest): "payload": "foo", "init": ["test", "blah"], }, - [CommandDescriptor("run", {"args": ["test", "blah"]})], + [CommandDescriptor(cmd="run", params={"args": ["test", "blah"]})], None, ), # check the empty init default @@ -463,8 +464,8 @@ def test_manifest_payload(descriptor_dict, implicit_manifest): "init": [["test", "blah"], ["other command"]], }, [ - CommandDescriptor("run", {"args": ["test", "blah"]}), - CommandDescriptor("run", {"args": ["other command"]}), + CommandDescriptor(cmd="run", params={"args": ["test", "blah"]}), + CommandDescriptor(cmd="run", params={"args": ["other command"]}), ], None, ), @@ -475,7 +476,7 @@ def test_manifest_payload(descriptor_dict, implicit_manifest): "init": [{"run": ["test", "blah"]}], }, [ - CommandDescriptor("run", {"args": ["test", "blah"]}), + CommandDescriptor(cmd="run", params={"args": ["test", "blah"]}), ], None, ), @@ -491,7 +492,7 @@ def test_manifest_payload(descriptor_dict, implicit_manifest): ], }, None, - DescriptorError("Cannot parse the command descriptor"), + (DescriptorError, "Cannot parse the command descriptor"), ), # check the regular syntax ( @@ -503,8 +504,8 @@ def test_manifest_payload(descriptor_dict, implicit_manifest): ], }, [ - CommandDescriptor("deploy", {"kwargs": {"foo": "bar"}}), - CommandDescriptor("run", {"args": ["test", "command"]}), + CommandDescriptor(cmd="deploy", params={"kwargs": {"foo": "bar"}}), + CommandDescriptor(cmd="run", params={"args": ["test", "command"]}), ], None, ), @@ -513,7 +514,7 @@ def test_manifest_payload(descriptor_dict, implicit_manifest): def test_service_init(test_utils, descriptor_dict, expected_init, error): """Test the ServiceDescriptor's init field.""" try: - service = ServiceDescriptor.load(descriptor_dict) + service = ServiceDescriptor(**descriptor_dict) assert isinstance(service, ServiceDescriptor) assert service.init == expected_init @@ -576,7 +577,7 @@ def test_service_init(test_utils, descriptor_dict, expected_init, error): "payloads": {"foo": {"runtime": "vm"}}, "nodes": {"http": {"payload": "foo", "init": [], "depends_on": ["bar"]}}, }, - DescriptorError('Unmet `depends_on`: "bar" in service: "http"'), + (DescriptorError, 'Unmet `depends_on`: "bar" in service: "http"'), [], ), ( @@ -587,7 +588,7 @@ def test_service_init(test_utils, descriptor_dict, expected_init, error): "db": {"payload": "foo", "init": [], "depends_on": ["http"]}, }, }, - DescriptorError("Service definition contains a circular `depends_on`."), + (DescriptorError, "Service definition contains a circular `depends_on`."), [], ), ), @@ -595,7 +596,7 @@ def test_service_init(test_utils, descriptor_dict, expected_init, error): def test_depends_on(test_utils, descriptor_dict, error, expected_priority): """Test the `depends_on` parameter.""" try: - dapp = DappDescriptor.load(descriptor_dict) + dapp = DappDescriptor(**descriptor_dict) nodes_priority = [name for name, service in dapp.nodes_prioritized()] assert nodes_priority == expected_priority except Exception as e: # noqa diff --git a/tests/unit/runner/test_service.py b/tests/unit/runner/test_service.py index c7a6d43..7a2b3b9 100644 --- a/tests/unit/runner/test_service.py +++ b/tests/unit/runner/test_service.py @@ -102,12 +102,12 @@ async def test_service_init(mock_work_context, init, expected_script): "remote_port": 666, }, {}, - TypeError("__init__() missing 1 required positional argument: 'init'"), + (TypeError, "__init__() missing 1 required positional argument: 'init'"), ), ( {"init": [], "unknown_kwarg": "im-invalid"}, {}, - TypeError("__init__() got an unexpected keyword argument 'unknown_kwarg'"), + (TypeError, "__init__() got an unexpected keyword argument 'unknown_kwarg'"), ), ], ) @@ -158,7 +158,7 @@ def test_proxy_dapp_service(test_utils, service_kwargs, expected_attrs, expected {}, None, {}, - RunnerError('Undefined payload: "foo"'), + (RunnerError, 'Undefined payload: "foo"'), ), # missing network definition ( @@ -167,7 +167,7 @@ def test_proxy_dapp_service(test_utils, service_kwargs, expected_attrs, expected {}, None, {}, - RunnerError('Undefined network: "bar"'), + (RunnerError, 'Undefined network: "bar"'), ), # network service with an http proxy ( @@ -175,7 +175,7 @@ def test_proxy_dapp_service(test_utils, service_kwargs, expected_attrs, expected init=[], payload="foo", network="bar", - http_proxy=HttpProxyDescriptor(ports=[PortMapping(80)]), + http_proxy=HttpProxyDescriptor(ports=[PortMapping(remote_port=80)]), ), {"foo": SOME_PAYLOAD}, {"bar": SOME_NETWORK}, diff --git a/tests/unit/util/test_util.py b/tests/unit/util/test_util.py index c938815..6c3e8c9 100644 --- a/tests/unit/util/test_util.py +++ b/tests/unit/util/test_util.py @@ -29,7 +29,7 @@ def test_get_free_port_exceeded(test_utils): with pytest.raises(RuntimeError) as e: FreePortProvider().get_free_port() test_utils.verify_error( - RuntimeError("No free ports found. range_start=8080, range_end=9090"), e + (RuntimeError, "No free ports found. range_start=8080, range_end=9090"), e )