From df3b79404f66bc02580b1e1881ff589b113f2779 Mon Sep 17 00:00:00 2001 From: MarcoFavorito Date: Tue, 4 May 2021 17:04:17 +0200 Subject: [PATCH 001/147] test: skip ganache/oef tests when running in CI --- .github/workflows/workflow.yml | 4 ++-- tests/common/utils.py | 1 + tests/conftest.py | 1 + 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/.github/workflows/workflow.yml b/.github/workflows/workflow.yml index db1d24562e..63ecbe4915 100644 --- a/.github/workflows/workflow.yml +++ b/.github/workflows/workflow.yml @@ -293,7 +293,7 @@ jobs: run: | tox -e py3.8 -- -m 'sync' # --aea-loop sync - name: Async integration tests - run: tox -e py3.8 -- -m 'integration and not unstable and not ledger' + run: tox -e py3.8 -- -m 'integration and not unstable and not ledger and not skip_in_ci' integration_checks_ledger: continue-on-error: True @@ -349,7 +349,7 @@ jobs: fetchcli config indent true fetchcli config broadcast-mode block - name: Integration tests - run: tox -e py3.8 -- -m 'integration and not unstable and ledger' + run: tox -e py3.8 -- -m 'integration and not unstable and ledger and not skip_in_ci' platform_checks: continue-on-error: True diff --git a/tests/common/utils.py b/tests/common/utils.py index 30b1720057..ba21020c0b 100644 --- a/tests/common/utils.py +++ b/tests/common/utils.py @@ -395,6 +395,7 @@ def run_aea_subprocess(*args, cwd: str = ".") -> Tuple[subprocess.Popen, str, st return result, stdout.decode("utf-8"), stderr.decode("utf-8") +@pytest.mark.skip_in_ci @pytest.mark.integration class UseOef: # pylint: disable=too-few-public-methods """Inherit from this class to launch an OEF node.""" diff --git a/tests/conftest.py b/tests/conftest.py index 2f94ad344d..0a0387aa47 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1402,6 +1402,7 @@ def make_uri(addr: str, port: int): return f"{addr}:{port}" +@pytest.mark.skip_in_ci @pytest.mark.integration class UseGanache: """Inherit from this class to use Ganache.""" From 4e9f3434369dafb8d7363568a4c165be83e21408 Mon Sep 17 00:00:00 2001 From: MarcoFavorito Date: Tue, 4 May 2021 17:47:59 +0200 Subject: [PATCH 002/147] test: skip tests if they use OEF or Ganache Update the 'check_skip' method of the utility test class 'DockerImage' such that the pytest test is skipped if: - docker is not available in the OS system path - docker --version fails - the version is greater than 19.0.0 (e.g. the default on ubuntu 20.04) --- .github/workflows/workflow.yml | 4 +-- .../aea-ledger-ethereum/tests/docker_image.py | 33 ++++++++++++++++++- tests/common/docker_image.py | 31 +++++++++++++++++ tests/common/utils.py | 1 - tests/conftest.py | 1 - 5 files changed, 65 insertions(+), 5 deletions(-) diff --git a/.github/workflows/workflow.yml b/.github/workflows/workflow.yml index 63ecbe4915..db1d24562e 100644 --- a/.github/workflows/workflow.yml +++ b/.github/workflows/workflow.yml @@ -293,7 +293,7 @@ jobs: run: | tox -e py3.8 -- -m 'sync' # --aea-loop sync - name: Async integration tests - run: tox -e py3.8 -- -m 'integration and not unstable and not ledger and not skip_in_ci' + run: tox -e py3.8 -- -m 'integration and not unstable and not ledger' integration_checks_ledger: continue-on-error: True @@ -349,7 +349,7 @@ jobs: fetchcli config indent true fetchcli config broadcast-mode block - name: Integration tests - run: tox -e py3.8 -- -m 'integration and not unstable and ledger and not skip_in_ci' + run: tox -e py3.8 -- -m 'integration and not unstable and ledger' platform_checks: continue-on-error: True diff --git a/plugins/aea-ledger-ethereum/tests/docker_image.py b/plugins/aea-ledger-ethereum/tests/docker_image.py index 2d5ade1314..ce8f63538d 100644 --- a/plugins/aea-ledger-ethereum/tests/docker_image.py +++ b/plugins/aea-ledger-ethereum/tests/docker_image.py @@ -19,11 +19,15 @@ """This module contains testing utilities.""" import logging +import re +import shutil +import subprocess import time from abc import ABC, abstractmethod from typing import Dict, List, Optional import docker +import pytest import requests from docker import DockerClient from docker.models.containers import Container @@ -35,7 +39,9 @@ class DockerImage(ABC): - """A class to wrap interaction with a Docker image.""" + """A class to wrap interatction with a Docker image.""" + + MINIMUM_DOCKER_VERSION = (19, 0, 0) def __init__(self, client: docker.DockerClient): """Initialize.""" @@ -47,6 +53,31 @@ def check_skip(self): By default, nothing happens. """ + self._check_docker_binary_available() + + def _check_docker_binary_available(self): + """Check the 'Docker' CLI tool is in the OS PATH.""" + result = shutil.which("docker") + if result is None: + pytest.skip("Docker not in the OS Path; skipping the test") + + result = subprocess.run( # nosec + ["docker", "--version"], stdout=subprocess.PIPE, stderr=subprocess.PIPE + ) + if result.returncode != 0: + pytest.skip(f"'docker --version' failed with exit code {result.returncode}") + + match = re.search( + r"Docker version ([0-9]+)\.([0-9]+)\.([0-9]+)", + result.stdout.decode("utf-8"), + ) + if match is None: + pytest.skip(f"cannot read version from the output of 'docker --version'") + version = (int(match.group(1)), int(match.group(2)), int(match.group(3))) + if version < self.MINIMUM_DOCKER_VERSION: + pytest.skip( + f"expected Docker version to be at least {'.'.join(self.MINIMUM_DOCKER_VERSION)}, found {'.'.join(version)}" + ) @property @abstractmethod diff --git a/tests/common/docker_image.py b/tests/common/docker_image.py index 77d8b0a835..7e08bb5bd6 100644 --- a/tests/common/docker_image.py +++ b/tests/common/docker_image.py @@ -21,6 +21,9 @@ import asyncio import logging import os +import re +import shutil +import subprocess # nosec import sys import tempfile import time @@ -45,6 +48,8 @@ class DockerImage(ABC): """A class to wrap interatction with a Docker image.""" + MINIMUM_DOCKER_VERSION = (19, 0, 0) + def __init__(self, client: docker.DockerClient): """Initialize.""" self._client = client @@ -55,6 +60,31 @@ def check_skip(self): By default, nothing happens. """ + self._check_docker_binary_available() + + def _check_docker_binary_available(self): + """Check the 'Docker' CLI tool is in the OS PATH.""" + result = shutil.which("docker") + if result is None: + pytest.skip("Docker not in the OS Path; skipping the test") + + result = subprocess.run( # nosec + ["docker", "--version"], stdout=subprocess.PIPE, stderr=subprocess.PIPE + ) + if result.returncode != 0: + pytest.skip(f"'docker --version' failed with exit code {result.returncode}") + + match = re.search( + r"Docker version ([0-9]+)\.([0-9]+)\.([0-9]+)", + result.stdout.decode("utf-8"), + ) + if match is None: + pytest.skip(f"cannot read version from the output of 'docker --version'") + version = (int(match.group(1)), int(match.group(2)), int(match.group(3))) + if version < self.MINIMUM_DOCKER_VERSION: + pytest.skip( + f"expected Docker version to be at least {'.'.join(self.MINIMUM_DOCKER_VERSION)}, found {'.'.join(version)}" + ) @property @abstractmethod @@ -186,6 +216,7 @@ def tag(self) -> str: def check_skip(self): """Check if the test should be skipped.""" + super().check_skip() if sys.version_info < (3, 7): pytest.skip("Python version < 3.7 not supported by the OEF.") return diff --git a/tests/common/utils.py b/tests/common/utils.py index ba21020c0b..30b1720057 100644 --- a/tests/common/utils.py +++ b/tests/common/utils.py @@ -395,7 +395,6 @@ def run_aea_subprocess(*args, cwd: str = ".") -> Tuple[subprocess.Popen, str, st return result, stdout.decode("utf-8"), stderr.decode("utf-8") -@pytest.mark.skip_in_ci @pytest.mark.integration class UseOef: # pylint: disable=too-few-public-methods """Inherit from this class to launch an OEF node.""" diff --git a/tests/conftest.py b/tests/conftest.py index 0a0387aa47..2f94ad344d 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1402,7 +1402,6 @@ def make_uri(addr: str, port: int): return f"{addr}:{port}" -@pytest.mark.skip_in_ci @pytest.mark.integration class UseGanache: """Inherit from this class to use Ganache.""" From 4eb90bde21ff05062631d70be91e1eff821d36e6 Mon Sep 17 00:00:00 2001 From: MarcoFavorito Date: Tue, 4 May 2021 19:18:43 +0200 Subject: [PATCH 003/147] remove calls to 'action_for_platform' with 'Linux' superseded by changes in 4e9f343 --- tests/conftest.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index 2f94ad344d..da88ad5dfc 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -645,7 +645,6 @@ def apply_aea_loop(request) -> None: @pytest.fixture(scope="session") -@action_for_platform("Linux", skip=False) def network_node( oef_addr, oef_port, pytestconfig, timeout: float = 2.0, max_attempts: int = 10 ): @@ -706,7 +705,6 @@ def update_default_ethereum_ledger_api(ethereum_testnet_config): @pytest.mark.integration @pytest.mark.ledger @pytest.fixture(scope="session") -@action_for_platform("Linux", skip=False) def ganache( ganache_configuration, ganache_addr, From 2e98ec7a2b5bb2e3b5b0970b25990bc6207cd47e Mon Sep 17 00:00:00 2001 From: David Minarsch Date: Wed, 5 May 2021 22:27:37 +0100 Subject: [PATCH 004/147] feat: update tac deployments --- examples/tac_deploy/Dockerfile | 2 +- examples/tac_deploy/README.md | 7 ++++--- examples/tac_deploy/data/.gitignore | 0 examples/tac_deploy/entrypoint.sh | 4 ++-- examples/tac_deploy/tac-deployment.yaml | 6 +++--- packages/fetchai/connections/p2p_libp2p/connection.py | 2 +- packages/fetchai/connections/p2p_libp2p/connection.yaml | 2 +- packages/hashes.csv | 2 +- scripts/bump_aea_version.py | 1 + 9 files changed, 14 insertions(+), 12 deletions(-) delete mode 100644 examples/tac_deploy/data/.gitignore diff --git a/examples/tac_deploy/Dockerfile b/examples/tac_deploy/Dockerfile index 6bb96e7290..65c40aa990 100644 --- a/examples/tac_deploy/Dockerfile +++ b/examples/tac_deploy/Dockerfile @@ -16,7 +16,7 @@ RUN apk add --no-cache go # aea installation RUN python -m pip install --upgrade pip -RUN pip install --upgrade --force-reinstall aea[all]==1.0.0 +RUN pip install --upgrade --force-reinstall aea[all]==1.0.1 # directories and aea cli config COPY /.aea /home/.aea diff --git a/examples/tac_deploy/README.md b/examples/tac_deploy/README.md index f26151add3..692e7c619e 100644 --- a/examples/tac_deploy/README.md +++ b/examples/tac_deploy/README.md @@ -29,19 +29,19 @@ GCloud should be configured first! Tag the image first with the latest tag: ``` bash -docker image tag tac-deploy gcr.io/fetch-ai-sandbox/tac_deploy:0.0.7 +docker image tag tac-deploy gcr.io/fetch-ai-sandbox/tac_deploy:0.0.10 ``` Push it to remote repo: ``` bash -docker push gcr.io/fetch-ai-sandbox/tac_deploy:0.0.7 +docker push gcr.io/fetch-ai-sandbox/tac_deploy:0.0.10 ``` ### Run it manually Run it ``` bash -kubectl run tac-deploy-{SOMETHING} --image=gcr.io/fetch-ai-sandbox/tac_deploy:0.0.7 --env="PARTICIPANTS_AMOUNT=5" --attach +kubectl run tac-deploy-{SOMETHING} --image=gcr.io/fetch-ai-sandbox/tac_deploy:0.0.10 --env="PARTICIPANTS_AMOUNT=5" --attach ``` Or simply restart existing deployment and latest image will be used with default configurations (see below): @@ -96,6 +96,7 @@ grep -rl 'TAKE CARE! Circumventing controller identity check!' output_dir/ | wc grep -rnw 'SOEF Network Connection Error' output_dir/ | wc -l grep -rnw 'SOEF Server Bad Response Error' output_dir/ | wc -l grep -rnw 'Failure during pipe closing.' output_dir/ | wc -l +grep -rnw "Couldn't connect to libp2p process within timeout" output_dir/ | wc -l grep -rnw 'Exception' output_dir/ | wc -l grep -rnw 'connect to libp2p process within timeout' output_dir/ | wc -l grep -rnw 'handling valid transaction' output_dir/tac_controller/ | wc -l diff --git a/examples/tac_deploy/data/.gitignore b/examples/tac_deploy/data/.gitignore deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/examples/tac_deploy/entrypoint.sh b/examples/tac_deploy/entrypoint.sh index 4028903bff..40c10884c3 100755 --- a/examples/tac_deploy/entrypoint.sh +++ b/examples/tac_deploy/entrypoint.sh @@ -52,7 +52,7 @@ echo CLEANUP_INTERVAL $CLEANUP_INTERVAL if [ -z "$NODE_CONNECTION_TIMEOUT" ]; then - NODE_CONNECTION_TIMEOUT=20 + NODE_CONNECTION_TIMEOUT=30 fi echo NODE_CONNECTION_TIMEOUT $NODE_CONNECTION_TIMEOUT @@ -115,7 +115,7 @@ function set_agent(){ key_file_name=$(generate_key $LEDGER $name $agent_data_dir 1) aea add-key fetchai $key_file_name --connection aea issue-certificates - json=$(printf '{"log_file": "%s", "delegate_uri": null, "entry_peers": ["%s"], "local_uri": "127.0.0.1:%s", "public_uri": null}' "$agent_data_dir/libp2p_node.log" "$PEER" "$port") + json=$(printf '{"log_file": "%s", "delegate_uri": null, "entry_peers": ["%s"], "local_uri": "127.0.0.1:%s", "public_uri": null, "node_connection_timeout": '%i'}' "$agent_data_dir/libp2p_node.log" "$PEER" "$port" "$(($NODE_CONNECTION_TIMEOUT))") aea config set --type dict vendor.fetchai.connections.p2p_libp2p.config "$json" log_file=$agent_data_dir/$name.log json=$(printf '{"version": 1, "formatters": {"standard": {"format": ""}}, "handlers": {"console": {"class": "logging.StreamHandler", "formatter": "standard", "level": "%s"}, "file": {"class": "logging.FileHandler", "filename": "%s", "mode": "w", "level": "%s", "formatter": "standard"}}, "loggers": {"aea": {"level": "%s", "handlers": ["file"]}}}' "$LOG_LEVEL" "$log_file" "$LOG_LEVEL" "$LOG_LEVEL") diff --git a/examples/tac_deploy/tac-deployment.yaml b/examples/tac_deploy/tac-deployment.yaml index 3a2bc579bf..cb8ffc8f46 100644 --- a/examples/tac_deploy/tac-deployment.yaml +++ b/examples/tac_deploy/tac-deployment.yaml @@ -18,7 +18,7 @@ spec: kubernetes.io/os: linux containers: - name: tac-deploy-container - image: gcr.io/fetch-ai-sandbox/tac_deploy:0.0.7 + image: gcr.io/fetch-ai-sandbox/tac_deploy:0.0.10 env: - name: PARTICIPANTS_AMOUNT value: "70" @@ -35,13 +35,13 @@ spec: - name: CLEANUP_INTERVAL value: "1800" - name: NODE_CONNECTION_TIMEOUT - value: "20" + value: "30" - name: LOG_LEVEL value: "INFO" - name: CLEAR_LOG_DATA_ON_LAUNCH value: "true" - name: CLEAR_KEY_DATA_ON_LAUNCH - value: "false" + value: "true" volumeMounts: - name: tac-deploy-data-vol mountPath: /data diff --git a/packages/fetchai/connections/p2p_libp2p/connection.py b/packages/fetchai/connections/p2p_libp2p/connection.py index b7a80d822e..7e39d56e2f 100644 --- a/packages/fetchai/connections/p2p_libp2p/connection.py +++ b/packages/fetchai/connections/p2p_libp2p/connection.py @@ -338,7 +338,7 @@ def get_client(self) -> NodeClient: return NodeClient(self.pipe) - def _child_watcher_callback(self, *_) -> None: # type:ignore + def _child_watcher_callback(self, *_) -> None: # pragma: nocover # type: ignore """Log if process was terminated before stop was called.""" if self._is_on_stop: return diff --git a/packages/fetchai/connections/p2p_libp2p/connection.yaml b/packages/fetchai/connections/p2p_libp2p/connection.yaml index 8335d1a390..2198c8f425 100644 --- a/packages/fetchai/connections/p2p_libp2p/connection.yaml +++ b/packages/fetchai/connections/p2p_libp2p/connection.yaml @@ -11,7 +11,7 @@ fingerprint: README.md: QmWcd2zHiRZLgXCSGw9gZ35WfcKsMeNSQouqNAaZnPBDDR __init__.py: QmYQuLNyQ8WTjgRYAoKAzoJEb7ocKXvM2hTyK4hsGch5D6 check_dependencies.py: QmP14nkQ8senwzdPdrZJLsA6EQ7zaKKEaLGDELhT42gp1P - connection.py: Qmcu4UDCoJZrd6jkDwHwZ9B863SpdAnWTjg5gssn7C1gjo + connection.py: QmadFUy2C1igQHWwfgi5p18CMqKh9RAq42imuiEc62bAV1 libp2p_node/README.md: QmSNJEQQwh25QSHgQPM4C84xKU6FczRpmJdN7HkCQyDPuC libp2p_node/aea/api.go: QmdFR5Rmkk2FGVDwzgHtjobjAKyLejqk2CAYBCvcF23AG7 libp2p_node/aea/envelope.pb.go: QmRfUNGpCeVJfsW3H1MzCN4pwDWgumfyWufVFp6xvUjjug diff --git a/packages/hashes.csv b/packages/hashes.csv index fc6270513b..a60234e214 100644 --- a/packages/hashes.csv +++ b/packages/hashes.csv @@ -41,7 +41,7 @@ fetchai/connections/http_server,QmZqiszQJuG7XA6LFWsMqXYSmViTrUkDfpkHwgYtDMbyXy fetchai/connections/ledger,QmT7ffwPzJ3isCMhN2qoj6NRyqinE2RkpSpUKNRFRXxpes fetchai/connections/local,QmUxLhmeE98S8BcuRDB7W7pRsJzpC3wVJV5ELLxVeEkoKC fetchai/connections/oef,QmaHQhxryQLaBk5TvC4iJFZtFvkPp4CoHxHg1iLnh2PAdm -fetchai/connections/p2p_libp2p,QmP8QtMAxuFHrPuwqg9pxnVzGcBs4fCL47pjkSDdK5fZna +fetchai/connections/p2p_libp2p,QmaW6tC7aQww1XbDrhcPXTMtJkcEPUMRsVANUP9heY1eR7 fetchai/connections/p2p_libp2p_client,QmYq2U83xTTnv1WDy1quvEz47QaV2uw1ZmB6sFHoCL31GC fetchai/connections/p2p_stub,QmToCExj3ZpdxUu3vYSMo4jZRJiMTkXMbyhbh18Hq6Nc4b fetchai/connections/prometheus,QmeRmgrccxRh8UG4AfTMct6D5wSuK9BXYoLwnohut2LJaB diff --git a/scripts/bump_aea_version.py b/scripts/bump_aea_version.py index fa2a905705..c5154138ca 100644 --- a/scripts/bump_aea_version.py +++ b/scripts/bump_aea_version.py @@ -71,6 +71,7 @@ def update_version_for_files(current_version: str, new_version: str) -> None: Path("deploy-image", "Dockerfile"), Path("develop-image", "docker-env.sh"), Path("docs", "quickstart.md"), + Path("examples", "tac_deploy", "Dockerfile"), Path("scripts", "install.ps1"), Path("scripts", "install.sh"), Path("tests", "test_docs", "test_bash_yaml", "md_files", "bash-quickstart.md"), From b4406fb721f3e975f6dade561a760dd94b3f21f3 Mon Sep 17 00:00:00 2001 From: David Minarsch Date: Thu, 6 May 2021 09:39:59 +0100 Subject: [PATCH 005/147] fix: static lint issue --- packages/fetchai/connections/p2p_libp2p/connection.py | 2 +- packages/fetchai/connections/p2p_libp2p/connection.yaml | 2 +- packages/hashes.csv | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/fetchai/connections/p2p_libp2p/connection.py b/packages/fetchai/connections/p2p_libp2p/connection.py index 7e39d56e2f..825be23bb3 100644 --- a/packages/fetchai/connections/p2p_libp2p/connection.py +++ b/packages/fetchai/connections/p2p_libp2p/connection.py @@ -338,7 +338,7 @@ def get_client(self) -> NodeClient: return NodeClient(self.pipe) - def _child_watcher_callback(self, *_) -> None: # pragma: nocover # type: ignore + def _child_watcher_callback(self, *_) -> None: # type: ignore # pragma: nocover """Log if process was terminated before stop was called.""" if self._is_on_stop: return diff --git a/packages/fetchai/connections/p2p_libp2p/connection.yaml b/packages/fetchai/connections/p2p_libp2p/connection.yaml index 2198c8f425..f9e7eb46d7 100644 --- a/packages/fetchai/connections/p2p_libp2p/connection.yaml +++ b/packages/fetchai/connections/p2p_libp2p/connection.yaml @@ -11,7 +11,7 @@ fingerprint: README.md: QmWcd2zHiRZLgXCSGw9gZ35WfcKsMeNSQouqNAaZnPBDDR __init__.py: QmYQuLNyQ8WTjgRYAoKAzoJEb7ocKXvM2hTyK4hsGch5D6 check_dependencies.py: QmP14nkQ8senwzdPdrZJLsA6EQ7zaKKEaLGDELhT42gp1P - connection.py: QmadFUy2C1igQHWwfgi5p18CMqKh9RAq42imuiEc62bAV1 + connection.py: QmecxEkWQijRvcaaok2pJdEatvLBTad6Vi9f721VLnvdks libp2p_node/README.md: QmSNJEQQwh25QSHgQPM4C84xKU6FczRpmJdN7HkCQyDPuC libp2p_node/aea/api.go: QmdFR5Rmkk2FGVDwzgHtjobjAKyLejqk2CAYBCvcF23AG7 libp2p_node/aea/envelope.pb.go: QmRfUNGpCeVJfsW3H1MzCN4pwDWgumfyWufVFp6xvUjjug diff --git a/packages/hashes.csv b/packages/hashes.csv index a60234e214..1f8ad50875 100644 --- a/packages/hashes.csv +++ b/packages/hashes.csv @@ -41,7 +41,7 @@ fetchai/connections/http_server,QmZqiszQJuG7XA6LFWsMqXYSmViTrUkDfpkHwgYtDMbyXy fetchai/connections/ledger,QmT7ffwPzJ3isCMhN2qoj6NRyqinE2RkpSpUKNRFRXxpes fetchai/connections/local,QmUxLhmeE98S8BcuRDB7W7pRsJzpC3wVJV5ELLxVeEkoKC fetchai/connections/oef,QmaHQhxryQLaBk5TvC4iJFZtFvkPp4CoHxHg1iLnh2PAdm -fetchai/connections/p2p_libp2p,QmaW6tC7aQww1XbDrhcPXTMtJkcEPUMRsVANUP9heY1eR7 +fetchai/connections/p2p_libp2p,Qme5ykdVah3sDnDkEXUycctPay1LtbRPwMBsQR2sErVJVk fetchai/connections/p2p_libp2p_client,QmYq2U83xTTnv1WDy1quvEz47QaV2uw1ZmB6sFHoCL31GC fetchai/connections/p2p_stub,QmToCExj3ZpdxUu3vYSMo4jZRJiMTkXMbyhbh18Hq6Nc4b fetchai/connections/prometheus,QmeRmgrccxRh8UG4AfTMct6D5wSuK9BXYoLwnohut2LJaB From ecc182907d038ded78d1b27f359d50f6ec4b338c Mon Sep 17 00:00:00 2001 From: David Minarsch Date: Thu, 6 May 2021 10:54:22 +0100 Subject: [PATCH 006/147] feat: extend tac deploy to support p2p client --- examples/tac_deploy/.env | 3 ++- examples/tac_deploy/build.sh | 18 ++++++++++++++++++ examples/tac_deploy/entrypoint.sh | 7 +++++-- 3 files changed, 25 insertions(+), 3 deletions(-) diff --git a/examples/tac_deploy/.env b/examples/tac_deploy/.env index 30db21cc04..41ee901589 100644 --- a/examples/tac_deploy/.env +++ b/examples/tac_deploy/.env @@ -1,2 +1,3 @@ PARTICIPANTS_AMOUNT=2 -LOG_LEVEL=DEBUG \ No newline at end of file +LOG_LEVEL=DEBUG +USE_CLIENT=true \ No newline at end of file diff --git a/examples/tac_deploy/build.sh b/examples/tac_deploy/build.sh index b92558f735..300d4441f1 100644 --- a/examples/tac_deploy/build.sh +++ b/examples/tac_deploy/build.sh @@ -1,17 +1,35 @@ #!/bin/bash set -e +if [ -z "$USE_CLIENT" ]; +then + USE_CLIENT=false +fi +echo USE_CLIENT $USE_CLIENT + mkdir /data # setup the agent aea fetch fetchai/tac_controller:latest cd tac_controller +if [[ "$USE_CLIENT" ]] +then + aea remove connection fetchai/p2p_libp2p + aea add connection fetchai/p2p_libp2p_client + aea config set agent.default_connection fetchai/p2p_libp2p_client:0.18.0 +fi aea install aea build cd .. aea fetch fetchai/tac_participant:latest --alias tac_participant_template cd tac_participant_template +if [[ "$USE_CLIENT" ]] +then + aea remove connection fetchai/p2p_libp2p + aea add connection fetchai/p2p_libp2p_client + aea config set agent.default_connection fetchai/p2p_libp2p_client:0.18.0 +fi aea install aea build cd .. diff --git a/examples/tac_deploy/entrypoint.sh b/examples/tac_deploy/entrypoint.sh index 40c10884c3..bbd0024fdc 100755 --- a/examples/tac_deploy/entrypoint.sh +++ b/examples/tac_deploy/entrypoint.sh @@ -114,9 +114,12 @@ function set_agent(){ aea add-key fetchai $key_file_name key_file_name=$(generate_key $LEDGER $name $agent_data_dir 1) aea add-key fetchai $key_file_name --connection + if [[ ! "$USE_CLIENT" ]] + then + json=$(printf '{"log_file": "%s", "delegate_uri": null, "entry_peers": ["%s"], "local_uri": "127.0.0.1:%s", "public_uri": null, "node_connection_timeout": '%i'}' "$agent_data_dir/libp2p_node.log" "$PEER" "$port" "$(($NODE_CONNECTION_TIMEOUT))") + aea config set --type dict vendor.fetchai.connections.p2p_libp2p.config "$json" + fi aea issue-certificates - json=$(printf '{"log_file": "%s", "delegate_uri": null, "entry_peers": ["%s"], "local_uri": "127.0.0.1:%s", "public_uri": null, "node_connection_timeout": '%i'}' "$agent_data_dir/libp2p_node.log" "$PEER" "$port" "$(($NODE_CONNECTION_TIMEOUT))") - aea config set --type dict vendor.fetchai.connections.p2p_libp2p.config "$json" log_file=$agent_data_dir/$name.log json=$(printf '{"version": 1, "formatters": {"standard": {"format": ""}}, "handlers": {"console": {"class": "logging.StreamHandler", "formatter": "standard", "level": "%s"}, "file": {"class": "logging.FileHandler", "filename": "%s", "mode": "w", "level": "%s", "formatter": "standard"}}, "loggers": {"aea": {"level": "%s", "handlers": ["file"]}}}' "$LOG_LEVEL" "$log_file" "$LOG_LEVEL" "$LOG_LEVEL") aea config set --type dict agent.logging_config "$json" From 4eaf4f32f33f63b0c9bc568f3150cc18941ebe96 Mon Sep 17 00:00:00 2001 From: "Yuri (solarw) Turchenkov" Date: Thu, 6 May 2021 15:08:34 +0300 Subject: [PATCH 007/147] libp2p connection tcpsocket usage for every platform --- examples/tac_deploy/Dockerfile | 3 + examples/tac_deploy/build.sh | 10 +-- .../aea/{pipe_windows.go => pipe.go} | 2 +- libs/go/libp2p_node/aea/pipe_unix.go | 90 ------------------- .../connections/p2p_libp2p/connection.py | 6 +- .../connections/p2p_libp2p/connection.yaml | 5 +- .../aea/{pipe_windows.go => pipe.go} | 2 +- .../p2p_libp2p/libp2p_node/aea/pipe_unix.go | 90 ------------------- packages/hashes.csv | 2 +- 9 files changed, 16 insertions(+), 194 deletions(-) rename libs/go/libp2p_node/aea/{pipe_windows.go => pipe.go} (98%) delete mode 100644 libs/go/libp2p_node/aea/pipe_unix.go rename packages/fetchai/connections/p2p_libp2p/libp2p_node/aea/{pipe_windows.go => pipe.go} (98%) delete mode 100644 packages/fetchai/connections/p2p_libp2p/libp2p_node/aea/pipe_unix.go diff --git a/examples/tac_deploy/Dockerfile b/examples/tac_deploy/Dockerfile index 65c40aa990..74c0c73e1b 100644 --- a/examples/tac_deploy/Dockerfile +++ b/examples/tac_deploy/Dockerfile @@ -2,6 +2,9 @@ FROM python:3.8-alpine USER root +ARG USE_CLIENT +ENV USE_CLIENT=$USE_CLIENT + RUN apk add --no-cache make git bash # cryptography: https://cryptography.io/en/latest/installation/#alpine diff --git a/examples/tac_deploy/build.sh b/examples/tac_deploy/build.sh index 300d4441f1..447274f352 100644 --- a/examples/tac_deploy/build.sh +++ b/examples/tac_deploy/build.sh @@ -1,7 +1,7 @@ #!/bin/bash set -e -if [ -z "$USE_CLIENT" ]; +if [ -z "$USE_CLIENT" ]; then USE_CLIENT=false fi @@ -10,9 +10,9 @@ echo USE_CLIENT $USE_CLIENT mkdir /data # setup the agent -aea fetch fetchai/tac_controller:latest +aea fetch --local fetchai/tac_controller:latest cd tac_controller -if [[ "$USE_CLIENT" ]] +if [[ "$USE_CLIENT" == "true" ]] then aea remove connection fetchai/p2p_libp2p aea add connection fetchai/p2p_libp2p_client @@ -22,9 +22,9 @@ aea install aea build cd .. -aea fetch fetchai/tac_participant:latest --alias tac_participant_template +aea fetch --local fetchai/tac_participant:latest --alias tac_participant_template cd tac_participant_template -if [[ "$USE_CLIENT" ]] +if [[ "$USE_CLIENT" == "true" ]] then aea remove connection fetchai/p2p_libp2p aea add connection fetchai/p2p_libp2p_client diff --git a/libs/go/libp2p_node/aea/pipe_windows.go b/libs/go/libp2p_node/aea/pipe.go similarity index 98% rename from libs/go/libp2p_node/aea/pipe_windows.go rename to libs/go/libp2p_node/aea/pipe.go index 54efa060c7..b020d2b444 100644 --- a/libs/go/libp2p_node/aea/pipe_windows.go +++ b/libs/go/libp2p_node/aea/pipe.go @@ -1,4 +1,4 @@ -// +build windows !linux !darwin +// +build windows linux darwin /* -*- coding: utf-8 -*- * ------------------------------------------------------------------------------ diff --git a/libs/go/libp2p_node/aea/pipe_unix.go b/libs/go/libp2p_node/aea/pipe_unix.go deleted file mode 100644 index 9b03e165da..0000000000 --- a/libs/go/libp2p_node/aea/pipe_unix.go +++ /dev/null @@ -1,90 +0,0 @@ -// +build linux darwin !windows - -/* -*- coding: utf-8 -*- -* ------------------------------------------------------------------------------ -* -* Copyright 2018-2019 Fetch.AI Limited -* -* Licensed under the Apache License, Version 2.0 (the "License"); -* you may not use this file except in compliance with the License. -* You may obtain a copy of the License at -* -* http://www.apache.org/licenses/LICENSE-2.0 -* -* Unless required by applicable law or agreed to in writing, software -* distributed under the License is distributed on an "AS IS" BASIS, -* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -* See the License for the specific language governing permissions and -* limitations under the License. -* -* ------------------------------------------------------------------------------ - */ - -package aea - -import ( - "encoding/binary" - "errors" - "math" - "os" -) - -type UnixPipe struct { - msgin_path string - msgout_path string - msgin *os.File - msgout *os.File -} - -func (pipe *UnixPipe) Connect() error { - // open pipes - var erro, erri error - pipe.msgout, erro = os.OpenFile(pipe.msgout_path, os.O_WRONLY, os.ModeNamedPipe) - pipe.msgin, erri = os.OpenFile(pipe.msgin_path, os.O_RDONLY, os.ModeNamedPipe) - - if erri != nil || erro != nil { - if erri != nil { - return erri - } - return erro - } - - return nil -} - -func (pipe *UnixPipe) Read() ([]byte, error) { - buf := make([]byte, 4) - _, err := pipe.msgin.Read(buf) - if err != nil { - return buf, errors.New("while receiving size" + err.Error()) - } - size := binary.BigEndian.Uint32(buf) - - buf = make([]byte, size) - _, err = pipe.msgin.Read(buf) - return buf, err - -} - -func (pipe *UnixPipe) Write(data []byte) error { - if len(data) > math.MaxInt32 { - return errors.New("value too large") - } - size := uint32(len(data)) - buf := make([]byte, 4, 4+size) - binary.BigEndian.PutUint32(buf, size) - buf = append(buf, data...) - _, err := pipe.msgout.Write(buf) - logger.Debug().Msgf("wrote data to pipe: %d bytes", size) - return err -} - -func (pipe *UnixPipe) Close() error { - pipe.msgin.Close() - pipe.msgout.Close() - return nil -} - -func NewPipe(msgin_path string, msgout_path string) Pipe { - return &UnixPipe{msgin_path: msgin_path, msgout_path: msgout_path, msgin: nil, msgout: nil} -} diff --git a/packages/fetchai/connections/p2p_libp2p/connection.py b/packages/fetchai/connections/p2p_libp2p/connection.py index 825be23bb3..32ec7eb3b1 100644 --- a/packages/fetchai/connections/p2p_libp2p/connection.py +++ b/packages/fetchai/connections/p2p_libp2p/connection.py @@ -37,7 +37,7 @@ from aea.helpers.acn.agent_record import AgentRecord from aea.helpers.acn.uri import Uri from aea.helpers.multiaddr.base import MultiAddr -from aea.helpers.pipe import IPCChannel, make_ipc_channel +from aea.helpers.pipe import IPCChannel, TCPSocketChannel from aea.mail.base import Envelope @@ -364,8 +364,8 @@ async def start(self) -> None: self._log_file_desc.write("test") self._log_file_desc.flush() - # setup fifos or tcp socket on windows - self.pipe = make_ipc_channel(logger=self.logger) + # tcp socket on every platform + self.pipe = TCPSocketChannel(logger=self.logger) env_file_data = self._make_env_file( pipe_in_path=self.pipe.in_path, pipe_out_path=self.pipe.out_path diff --git a/packages/fetchai/connections/p2p_libp2p/connection.yaml b/packages/fetchai/connections/p2p_libp2p/connection.yaml index f9e7eb46d7..787e0c7fdb 100644 --- a/packages/fetchai/connections/p2p_libp2p/connection.yaml +++ b/packages/fetchai/connections/p2p_libp2p/connection.yaml @@ -11,13 +11,12 @@ fingerprint: README.md: QmWcd2zHiRZLgXCSGw9gZ35WfcKsMeNSQouqNAaZnPBDDR __init__.py: QmYQuLNyQ8WTjgRYAoKAzoJEb7ocKXvM2hTyK4hsGch5D6 check_dependencies.py: QmP14nkQ8senwzdPdrZJLsA6EQ7zaKKEaLGDELhT42gp1P - connection.py: QmecxEkWQijRvcaaok2pJdEatvLBTad6Vi9f721VLnvdks + connection.py: Qma7YF3frWSKL92FaScmGtR1QmQgvM9QPBdWZPPgHRcTzh libp2p_node/README.md: QmSNJEQQwh25QSHgQPM4C84xKU6FczRpmJdN7HkCQyDPuC libp2p_node/aea/api.go: QmdFR5Rmkk2FGVDwzgHtjobjAKyLejqk2CAYBCvcF23AG7 libp2p_node/aea/envelope.pb.go: QmRfUNGpCeVJfsW3H1MzCN4pwDWgumfyWufVFp6xvUjjug libp2p_node/aea/envelope.proto: QmVuvesmfgzj5aKnbFoCocoGEv3T9MR7u6KWn7CT5yfjGi - libp2p_node/aea/pipe_unix.go: QmYC7pExTkBBNHnbBeyd4HKmj9fDTnouS4apbwZoGXNWJz - libp2p_node/aea/pipe_windows.go: QmNcuPLMsbdWqUn1SFx2V2RTkujjtvBfYQmavz6QiZx6av + libp2p_node/aea/pipe.go: QmWLBjZNRCcg3BSM9Cxb95yAXdT6wPjgw8DSYHxMuux4JA libp2p_node/dht/dhtclient/dhtclient.go: QmYFj4YghhVA7E976xLNQHRTGrik1uHLc6ZQ9Tw1p9ECzk libp2p_node/dht/dhtclient/dhtclient_test.go: QmS9SiLAojXYobqV9m5yeRpyPzt6JcSbQD78RNvSp6LPx6 libp2p_node/dht/dhtclient/options.go: QmaoJiavHescgx98inQjVUipPFRvcFFrodrScZ3fvrXJ4z diff --git a/packages/fetchai/connections/p2p_libp2p/libp2p_node/aea/pipe_windows.go b/packages/fetchai/connections/p2p_libp2p/libp2p_node/aea/pipe.go similarity index 98% rename from packages/fetchai/connections/p2p_libp2p/libp2p_node/aea/pipe_windows.go rename to packages/fetchai/connections/p2p_libp2p/libp2p_node/aea/pipe.go index 54efa060c7..b020d2b444 100644 --- a/packages/fetchai/connections/p2p_libp2p/libp2p_node/aea/pipe_windows.go +++ b/packages/fetchai/connections/p2p_libp2p/libp2p_node/aea/pipe.go @@ -1,4 +1,4 @@ -// +build windows !linux !darwin +// +build windows linux darwin /* -*- coding: utf-8 -*- * ------------------------------------------------------------------------------ diff --git a/packages/fetchai/connections/p2p_libp2p/libp2p_node/aea/pipe_unix.go b/packages/fetchai/connections/p2p_libp2p/libp2p_node/aea/pipe_unix.go deleted file mode 100644 index 9b03e165da..0000000000 --- a/packages/fetchai/connections/p2p_libp2p/libp2p_node/aea/pipe_unix.go +++ /dev/null @@ -1,90 +0,0 @@ -// +build linux darwin !windows - -/* -*- coding: utf-8 -*- -* ------------------------------------------------------------------------------ -* -* Copyright 2018-2019 Fetch.AI Limited -* -* Licensed under the Apache License, Version 2.0 (the "License"); -* you may not use this file except in compliance with the License. -* You may obtain a copy of the License at -* -* http://www.apache.org/licenses/LICENSE-2.0 -* -* Unless required by applicable law or agreed to in writing, software -* distributed under the License is distributed on an "AS IS" BASIS, -* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -* See the License for the specific language governing permissions and -* limitations under the License. -* -* ------------------------------------------------------------------------------ - */ - -package aea - -import ( - "encoding/binary" - "errors" - "math" - "os" -) - -type UnixPipe struct { - msgin_path string - msgout_path string - msgin *os.File - msgout *os.File -} - -func (pipe *UnixPipe) Connect() error { - // open pipes - var erro, erri error - pipe.msgout, erro = os.OpenFile(pipe.msgout_path, os.O_WRONLY, os.ModeNamedPipe) - pipe.msgin, erri = os.OpenFile(pipe.msgin_path, os.O_RDONLY, os.ModeNamedPipe) - - if erri != nil || erro != nil { - if erri != nil { - return erri - } - return erro - } - - return nil -} - -func (pipe *UnixPipe) Read() ([]byte, error) { - buf := make([]byte, 4) - _, err := pipe.msgin.Read(buf) - if err != nil { - return buf, errors.New("while receiving size" + err.Error()) - } - size := binary.BigEndian.Uint32(buf) - - buf = make([]byte, size) - _, err = pipe.msgin.Read(buf) - return buf, err - -} - -func (pipe *UnixPipe) Write(data []byte) error { - if len(data) > math.MaxInt32 { - return errors.New("value too large") - } - size := uint32(len(data)) - buf := make([]byte, 4, 4+size) - binary.BigEndian.PutUint32(buf, size) - buf = append(buf, data...) - _, err := pipe.msgout.Write(buf) - logger.Debug().Msgf("wrote data to pipe: %d bytes", size) - return err -} - -func (pipe *UnixPipe) Close() error { - pipe.msgin.Close() - pipe.msgout.Close() - return nil -} - -func NewPipe(msgin_path string, msgout_path string) Pipe { - return &UnixPipe{msgin_path: msgin_path, msgout_path: msgout_path, msgin: nil, msgout: nil} -} diff --git a/packages/hashes.csv b/packages/hashes.csv index 1f8ad50875..9e87b675e6 100644 --- a/packages/hashes.csv +++ b/packages/hashes.csv @@ -41,7 +41,7 @@ fetchai/connections/http_server,QmZqiszQJuG7XA6LFWsMqXYSmViTrUkDfpkHwgYtDMbyXy fetchai/connections/ledger,QmT7ffwPzJ3isCMhN2qoj6NRyqinE2RkpSpUKNRFRXxpes fetchai/connections/local,QmUxLhmeE98S8BcuRDB7W7pRsJzpC3wVJV5ELLxVeEkoKC fetchai/connections/oef,QmaHQhxryQLaBk5TvC4iJFZtFvkPp4CoHxHg1iLnh2PAdm -fetchai/connections/p2p_libp2p,Qme5ykdVah3sDnDkEXUycctPay1LtbRPwMBsQR2sErVJVk +fetchai/connections/p2p_libp2p,QmSLPfHt1ow8EDGBtbRvDh7TetXeXpS5ju2eThd5WR3bF4 fetchai/connections/p2p_libp2p_client,QmYq2U83xTTnv1WDy1quvEz47QaV2uw1ZmB6sFHoCL31GC fetchai/connections/p2p_stub,QmToCExj3ZpdxUu3vYSMo4jZRJiMTkXMbyhbh18Hq6Nc4b fetchai/connections/prometheus,QmeRmgrccxRh8UG4AfTMct6D5wSuK9BXYoLwnohut2LJaB From 1bc62c0b80dd611c46bc0b71d18edda4b25bf2c7 Mon Sep 17 00:00:00 2001 From: "Yuri (solarw) Turchenkov" Date: Thu, 6 May 2021 15:53:26 +0300 Subject: [PATCH 008/147] fix for build.sh of tac deployment example --- examples/tac_deploy/build.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/tac_deploy/build.sh b/examples/tac_deploy/build.sh index 447274f352..21e74af0eb 100644 --- a/examples/tac_deploy/build.sh +++ b/examples/tac_deploy/build.sh @@ -10,7 +10,7 @@ echo USE_CLIENT $USE_CLIENT mkdir /data # setup the agent -aea fetch --local fetchai/tac_controller:latest +aea fetch fetchai/tac_controller:latest cd tac_controller if [[ "$USE_CLIENT" == "true" ]] then @@ -22,7 +22,7 @@ aea install aea build cd .. -aea fetch --local fetchai/tac_participant:latest --alias tac_participant_template +aea fetch fetchai/tac_participant:latest --alias tac_participant_template cd tac_participant_template if [[ "$USE_CLIENT" == "true" ]] then From d25e39b9c4155b838fdf5caaca6501b11eb8de83 Mon Sep 17 00:00:00 2001 From: David Minarsch Date: Thu, 6 May 2021 18:02:00 +0100 Subject: [PATCH 009/147] fix: add nil check for stream in p2p node --- libs/go/libp2p_node/utils/utils.go | 78 +++---------------- .../connections/p2p_libp2p/connection.yaml | 2 +- .../p2p_libp2p/libp2p_node/utils/utils.go | 78 +++---------------- packages/hashes.csv | 2 +- 4 files changed, 20 insertions(+), 140 deletions(-) diff --git a/libs/go/libp2p_node/utils/utils.go b/libs/go/libp2p_node/utils/utils.go index c5cb0d3719..92a01b0648 100644 --- a/libs/go/libp2p_node/utils/utils.go +++ b/libs/go/libp2p_node/utils/utils.go @@ -669,6 +669,9 @@ func ReadEnvelopeConn(conn net.Conn) (*aea.Envelope, error) { // ReadBytes from a network stream func ReadBytes(s network.Stream) ([]byte, error) { + if s == nil { + panic("GOTCHAAAAAAA 1, can not write to nil stream") + } rstream := bufio.NewReader(s) buf := make([]byte, 4) @@ -703,6 +706,9 @@ func WriteBytes(s network.Stream, data []byte) error { return nil } + if s == nil { + panic("GOTCHAAAAAAA 1, can not write to nil stream") + } wstream := bufio.NewWriter(s) size := uint32(len(data)) @@ -724,6 +730,9 @@ func WriteBytes(s network.Stream, data []byte) error { Msg("Error on data write") return err } + if s == nil { + panic("GOTCHAAAAAAA 2, can not write to nil stream") + } err = wstream.Flush() if err != nil { logger.Error(). @@ -733,72 +742,3 @@ func WriteBytes(s network.Stream, data []byte) error { } return err } - -// ReadString from a network stream -func ReadString(s network.Stream) (string, error) { - data, err := ReadBytes(s) - return string(data), err -} - -// WriteEnvelope to a network stream -func WriteEnvelope(envel *aea.Envelope, s network.Stream) error { - wstream := bufio.NewWriter(s) - data, err := proto.Marshal(envel) - if err != nil { - return err - } - size := uint32(len(data)) - - buf := make([]byte, 4) - binary.BigEndian.PutUint32(buf, size) - //log.Println("DEBUG writing size:", size, buf) - _, err = wstream.Write(buf) - if err != nil { - return err - } - - //log.Println("DEBUG writing data:", data) - _, err = wstream.Write(data) - if err != nil { - return err - } - - err = wstream.Flush() - if err != nil { - return err - } - return nil -} - -// ReadEnvelope from a network stream -func ReadEnvelope(s network.Stream) (*aea.Envelope, error) { - envel := &aea.Envelope{} - rstream := bufio.NewReader(s) - - buf := make([]byte, 4) - _, err := io.ReadFull(rstream, buf) - - if err != nil { - logger.Error(). - Str("err", err.Error()). - Msg("while reading size") - return envel, err - } - - size := binary.BigEndian.Uint32(buf) - if size > maxMessageSizeDelegateConnection { - return nil, errors.New("Expected message size larger than maximum allowed") - } - //logger.Debug().Msgf("received size: %d %x", size, buf) - buf = make([]byte, size) - _, err = io.ReadFull(rstream, buf) - if err != nil { - logger.Error(). - Str("err", err.Error()). - Msg("while reading data") - return envel, err - } - - err = proto.Unmarshal(buf, envel) - return envel, err -} diff --git a/packages/fetchai/connections/p2p_libp2p/connection.yaml b/packages/fetchai/connections/p2p_libp2p/connection.yaml index 787e0c7fdb..13bf2ff4b9 100644 --- a/packages/fetchai/connections/p2p_libp2p/connection.yaml +++ b/packages/fetchai/connections/p2p_libp2p/connection.yaml @@ -36,7 +36,7 @@ fingerprint: libp2p_node/go.mod: QmT2LQXkSen6Th4QLS2RU3tRVxJSLjdZTxRULUcnQFXfEL libp2p_node/go.sum: QmZc6oiDqQhB6ZKVojN26JxoTNH1QetyuYd9zjSksQnTFt libp2p_node/libp2p_node.go: QmSu21WBmAAfBbZFi3MZZgTrUMK5FYruSunxghpeUhxBMA - libp2p_node/utils/utils.go: QmZQAY5CMRsqK36Zgy4ukgGzu7W5EVCQJGFhf6gJ9XCYNg + libp2p_node/utils/utils.go: QmYKmPHnv9RHzw1XbyBvTN57jPojxGiTM3v23rdKxKFW2R fingerprint_ignore_patterns: [] build_entrypoint: check_dependencies.py connections: [] diff --git a/packages/fetchai/connections/p2p_libp2p/libp2p_node/utils/utils.go b/packages/fetchai/connections/p2p_libp2p/libp2p_node/utils/utils.go index c5cb0d3719..92a01b0648 100644 --- a/packages/fetchai/connections/p2p_libp2p/libp2p_node/utils/utils.go +++ b/packages/fetchai/connections/p2p_libp2p/libp2p_node/utils/utils.go @@ -669,6 +669,9 @@ func ReadEnvelopeConn(conn net.Conn) (*aea.Envelope, error) { // ReadBytes from a network stream func ReadBytes(s network.Stream) ([]byte, error) { + if s == nil { + panic("GOTCHAAAAAAA 1, can not write to nil stream") + } rstream := bufio.NewReader(s) buf := make([]byte, 4) @@ -703,6 +706,9 @@ func WriteBytes(s network.Stream, data []byte) error { return nil } + if s == nil { + panic("GOTCHAAAAAAA 1, can not write to nil stream") + } wstream := bufio.NewWriter(s) size := uint32(len(data)) @@ -724,6 +730,9 @@ func WriteBytes(s network.Stream, data []byte) error { Msg("Error on data write") return err } + if s == nil { + panic("GOTCHAAAAAAA 2, can not write to nil stream") + } err = wstream.Flush() if err != nil { logger.Error(). @@ -733,72 +742,3 @@ func WriteBytes(s network.Stream, data []byte) error { } return err } - -// ReadString from a network stream -func ReadString(s network.Stream) (string, error) { - data, err := ReadBytes(s) - return string(data), err -} - -// WriteEnvelope to a network stream -func WriteEnvelope(envel *aea.Envelope, s network.Stream) error { - wstream := bufio.NewWriter(s) - data, err := proto.Marshal(envel) - if err != nil { - return err - } - size := uint32(len(data)) - - buf := make([]byte, 4) - binary.BigEndian.PutUint32(buf, size) - //log.Println("DEBUG writing size:", size, buf) - _, err = wstream.Write(buf) - if err != nil { - return err - } - - //log.Println("DEBUG writing data:", data) - _, err = wstream.Write(data) - if err != nil { - return err - } - - err = wstream.Flush() - if err != nil { - return err - } - return nil -} - -// ReadEnvelope from a network stream -func ReadEnvelope(s network.Stream) (*aea.Envelope, error) { - envel := &aea.Envelope{} - rstream := bufio.NewReader(s) - - buf := make([]byte, 4) - _, err := io.ReadFull(rstream, buf) - - if err != nil { - logger.Error(). - Str("err", err.Error()). - Msg("while reading size") - return envel, err - } - - size := binary.BigEndian.Uint32(buf) - if size > maxMessageSizeDelegateConnection { - return nil, errors.New("Expected message size larger than maximum allowed") - } - //logger.Debug().Msgf("received size: %d %x", size, buf) - buf = make([]byte, size) - _, err = io.ReadFull(rstream, buf) - if err != nil { - logger.Error(). - Str("err", err.Error()). - Msg("while reading data") - return envel, err - } - - err = proto.Unmarshal(buf, envel) - return envel, err -} diff --git a/packages/hashes.csv b/packages/hashes.csv index 9e87b675e6..9ca813ca11 100644 --- a/packages/hashes.csv +++ b/packages/hashes.csv @@ -41,7 +41,7 @@ fetchai/connections/http_server,QmZqiszQJuG7XA6LFWsMqXYSmViTrUkDfpkHwgYtDMbyXy fetchai/connections/ledger,QmT7ffwPzJ3isCMhN2qoj6NRyqinE2RkpSpUKNRFRXxpes fetchai/connections/local,QmUxLhmeE98S8BcuRDB7W7pRsJzpC3wVJV5ELLxVeEkoKC fetchai/connections/oef,QmaHQhxryQLaBk5TvC4iJFZtFvkPp4CoHxHg1iLnh2PAdm -fetchai/connections/p2p_libp2p,QmSLPfHt1ow8EDGBtbRvDh7TetXeXpS5ju2eThd5WR3bF4 +fetchai/connections/p2p_libp2p,QmZ2BpAKYyuRno8diqN8sbWEzHkjnWgLGg2kWbcEDdWkEi fetchai/connections/p2p_libp2p_client,QmYq2U83xTTnv1WDy1quvEz47QaV2uw1ZmB6sFHoCL31GC fetchai/connections/p2p_stub,QmToCExj3ZpdxUu3vYSMo4jZRJiMTkXMbyhbh18Hq6Nc4b fetchai/connections/prometheus,QmeRmgrccxRh8UG4AfTMct6D5wSuK9BXYoLwnohut2LJaB From 66ba2729a4d24398f82c37f3f340bade73afdb96 Mon Sep 17 00:00:00 2001 From: David Minarsch Date: Thu, 6 May 2021 20:49:31 +0100 Subject: [PATCH 010/147] fix: deployment tac configs --- examples/tac_deploy/.env | 2 +- examples/tac_deploy/README.md | 6 +++--- examples/tac_deploy/entrypoint.sh | 5 ++++- examples/tac_deploy/tac-deployment.yaml | 2 +- 4 files changed, 9 insertions(+), 6 deletions(-) diff --git a/examples/tac_deploy/.env b/examples/tac_deploy/.env index 41ee901589..ee6efd8383 100644 --- a/examples/tac_deploy/.env +++ b/examples/tac_deploy/.env @@ -1,3 +1,3 @@ PARTICIPANTS_AMOUNT=2 LOG_LEVEL=DEBUG -USE_CLIENT=true \ No newline at end of file +USE_CLIENT=false \ No newline at end of file diff --git a/examples/tac_deploy/README.md b/examples/tac_deploy/README.md index 692e7c619e..e0e6083689 100644 --- a/examples/tac_deploy/README.md +++ b/examples/tac_deploy/README.md @@ -29,19 +29,19 @@ GCloud should be configured first! Tag the image first with the latest tag: ``` bash -docker image tag tac-deploy gcr.io/fetch-ai-sandbox/tac_deploy:0.0.10 +docker image tag tac-deploy gcr.io/fetch-ai-sandbox/tac_deploy:0.0.13 ``` Push it to remote repo: ``` bash -docker push gcr.io/fetch-ai-sandbox/tac_deploy:0.0.10 +docker push gcr.io/fetch-ai-sandbox/tac_deploy:0.0.13 ``` ### Run it manually Run it ``` bash -kubectl run tac-deploy-{SOMETHING} --image=gcr.io/fetch-ai-sandbox/tac_deploy:0.0.10 --env="PARTICIPANTS_AMOUNT=5" --attach +kubectl run tac-deploy-{SOMETHING} --image=gcr.io/fetch-ai-sandbox/tac_deploy:0.0.13 --env="PARTICIPANTS_AMOUNT=5" --attach ``` Or simply restart existing deployment and latest image will be used with default configurations (see below): diff --git a/examples/tac_deploy/entrypoint.sh b/examples/tac_deploy/entrypoint.sh index bbd0024fdc..c30f46dea9 100755 --- a/examples/tac_deploy/entrypoint.sh +++ b/examples/tac_deploy/entrypoint.sh @@ -6,6 +6,9 @@ PEER="/dns4/acn.fetch.ai/tcp/9001/p2p/16Uiu2HAmVWnopQAqq4pniYLw44VRvYxBUoRHqjz1H TAC_NAME='some_tac_id' BASE_PORT=10000 BASE_DIR=/data +OLD_DIR=/$(date "+%d_%m_%Y_%H%M") + +cp -R "$BASE_DIR" "$OLD_DIR" if [ -z "$COMPETITION_TIMEOUT" ]; then @@ -114,7 +117,7 @@ function set_agent(){ aea add-key fetchai $key_file_name key_file_name=$(generate_key $LEDGER $name $agent_data_dir 1) aea add-key fetchai $key_file_name --connection - if [[ ! "$USE_CLIENT" ]] + if [ "$USE_CLIENT" == false ]; then json=$(printf '{"log_file": "%s", "delegate_uri": null, "entry_peers": ["%s"], "local_uri": "127.0.0.1:%s", "public_uri": null, "node_connection_timeout": '%i'}' "$agent_data_dir/libp2p_node.log" "$PEER" "$port" "$(($NODE_CONNECTION_TIMEOUT))") aea config set --type dict vendor.fetchai.connections.p2p_libp2p.config "$json" diff --git a/examples/tac_deploy/tac-deployment.yaml b/examples/tac_deploy/tac-deployment.yaml index cb8ffc8f46..8b82f70e63 100644 --- a/examples/tac_deploy/tac-deployment.yaml +++ b/examples/tac_deploy/tac-deployment.yaml @@ -18,7 +18,7 @@ spec: kubernetes.io/os: linux containers: - name: tac-deploy-container - image: gcr.io/fetch-ai-sandbox/tac_deploy:0.0.10 + image: gcr.io/fetch-ai-sandbox/tac_deploy:0.0.13 env: - name: PARTICIPANTS_AMOUNT value: "70" From 9cd58ddcb0351988a82b598d21c64e23bf28975c Mon Sep 17 00:00:00 2001 From: David Minarsch Date: Fri, 7 May 2021 10:57:07 +0100 Subject: [PATCH 011/147] fix: add nil check for stream fix: linter errors from staticcheck --- examples/tac_deploy/.env | 7 ++++--- libs/go/libp2p_node/README.md | 1 + .../go/libp2p_node/dht/dhtclient/dhtclient.go | 5 +++-- libs/go/libp2p_node/dht/dhtnode/utils.go | 6 +++--- libs/go/libp2p_node/dht/dhtpeer/dhtpeer.go | 1 - .../libp2p_node/dht/dhtpeer/dhtpeer_test.go | 1 - libs/go/libp2p_node/dht/monitoring/file.go | 1 - libs/go/libp2p_node/go.mod | 3 +-- libs/go/libp2p_node/go.sum | 6 ++++++ libs/go/libp2p_node/libp2p_node.go | 1 + libs/go/libp2p_node/utils/utils.go | 16 +++++++-------- .../connections/p2p_libp2p/connection.yaml | 20 +++++++++---------- .../p2p_libp2p/libp2p_node/README.md | 1 + .../libp2p_node/dht/dhtclient/dhtclient.go | 5 +++-- .../libp2p_node/dht/dhtnode/utils.go | 6 +++--- .../libp2p_node/dht/dhtpeer/dhtpeer.go | 1 - .../libp2p_node/dht/dhtpeer/dhtpeer_test.go | 1 - .../libp2p_node/dht/monitoring/file.go | 1 - .../connections/p2p_libp2p/libp2p_node/go.mod | 3 +-- .../connections/p2p_libp2p/libp2p_node/go.sum | 6 ++++++ .../p2p_libp2p/libp2p_node/libp2p_node.go | 1 + .../p2p_libp2p/libp2p_node/utils/utils.go | 16 +++++++-------- packages/hashes.csv | 2 +- 23 files changed, 61 insertions(+), 50 deletions(-) diff --git a/examples/tac_deploy/.env b/examples/tac_deploy/.env index ee6efd8383..0fd448f2f1 100644 --- a/examples/tac_deploy/.env +++ b/examples/tac_deploy/.env @@ -1,3 +1,4 @@ -PARTICIPANTS_AMOUNT=2 -LOG_LEVEL=DEBUG -USE_CLIENT=false \ No newline at end of file +PARTICIPANTS_AMOUNT=10 +LOG_LEVEL=INFO +USE_CLIENT=false +CLEAR_KEY_DATA_ON_LAUNCH=true \ No newline at end of file diff --git a/libs/go/libp2p_node/README.md b/libs/go/libp2p_node/README.md index d95ba19985..0d084a56dd 100644 --- a/libs/go/libp2p_node/README.md +++ b/libs/go/libp2p_node/README.md @@ -20,6 +20,7 @@ To lint: ``` bash golines . -w golangci-lint run +staticcheck ./... ``` ## Messaging patterns diff --git a/libs/go/libp2p_node/dht/dhtclient/dhtclient.go b/libs/go/libp2p_node/dht/dhtclient/dhtclient.go index e627a21684..e9f6278e27 100644 --- a/libs/go/libp2p_node/dht/dhtclient/dhtclient.go +++ b/libs/go/libp2p_node/dht/dhtclient/dhtclient.go @@ -306,7 +306,6 @@ func (dhtClient *DHTClient) bootstrapLoopUntilTimeout() error { ) case <-ctx.Done(): sleepTime = 0 - break } if sleepTime == 0 { break @@ -338,12 +337,14 @@ func (dhtClient *DHTClient) newStreamLoopUntilTimeout( stream, err = dhtClient.routedHost.NewStream(ctx, peerID, streamType) case <-ctx.Done(): sleepTime = 0 - break } if sleepTime == 0 { break } } + if stream == nil { + return stream, errors.New("stream nil" + err.Error()) + } // register again in case of disconnection if disconnected { err = dhtClient.registerAgentAddress() diff --git a/libs/go/libp2p_node/dht/dhtnode/utils.go b/libs/go/libp2p_node/dht/dhtnode/utils.go index bdd237a75b..acf9781075 100644 --- a/libs/go/libp2p_node/dht/dhtnode/utils.go +++ b/libs/go/libp2p_node/dht/dhtnode/utils.go @@ -67,7 +67,7 @@ func IsValidProofOfRepresentation( // check public key matches if record.PeerPublicKey != representativePeerPubKey { - err := errors.New("Wrong peer public key, expected " + representativePeerPubKey) + err := errors.New("wrong peer public key, expected " + representativePeerPubKey) response := &Status{Code: Status_ERROR_WRONG_PUBLIC_KEY, Msgs: []string{err.Error()}} return response, err } @@ -76,7 +76,7 @@ func IsValidProofOfRepresentation( addrFromPubKey, err := utils.AgentAddressFromPublicKey(record.LedgerId, record.PublicKey) if err != nil || addrFromPubKey != record.Address { if err == nil { - err = errors.New("Agent address and public key don't match") + err = errors.New("agent address and public key don't match") } response := &Status{Code: Status_ERROR_WRONG_AGENT_ADDRESS} return response, err @@ -91,7 +91,7 @@ func IsValidProofOfRepresentation( ) if !ok || err != nil { if err == nil { - err = errors.New("Signature is not valid") + err = errors.New("signature is not valid") } response := &Status{Code: Status_ERROR_INVALID_PROOF} return response, err diff --git a/libs/go/libp2p_node/dht/dhtpeer/dhtpeer.go b/libs/go/libp2p_node/dht/dhtpeer/dhtpeer.go index f562cd3859..13b8bb4ca5 100644 --- a/libs/go/libp2p_node/dht/dhtpeer/dhtpeer.go +++ b/libs/go/libp2p_node/dht/dhtpeer/dhtpeer.go @@ -629,7 +629,6 @@ func (dhtPeer *DHTPeer) handleDelegateService(ready *sync.WaitGroup) { go dhtPeer.handleNewDelegationConnection(conn) } case <-dhtPeer.closing: - break } } } diff --git a/libs/go/libp2p_node/dht/dhtpeer/dhtpeer_test.go b/libs/go/libp2p_node/dht/dhtpeer/dhtpeer_test.go index d7686e3e4d..07705ca253 100644 --- a/libs/go/libp2p_node/dht/dhtpeer/dhtpeer_test.go +++ b/libs/go/libp2p_node/dht/dhtpeer/dhtpeer_test.go @@ -1959,7 +1959,6 @@ func ensureAddressAnnounced(peers ...*DHTPeer) { for !peer.addressAnnounced { select { case <-ctx.Done(): - break case <-time.After(5 * time.Millisecond): } } diff --git a/libs/go/libp2p_node/dht/monitoring/file.go b/libs/go/libp2p_node/dht/monitoring/file.go index 7c8248db77..ca2b39d591 100644 --- a/libs/go/libp2p_node/dht/monitoring/file.go +++ b/libs/go/libp2p_node/dht/monitoring/file.go @@ -176,7 +176,6 @@ func (fm *FileMonitoring) Start() { select { case <-fm.closing: file.Close() - break default: ignore(file.Truncate(0)) _, err := file.Seek(0, 0) diff --git a/libs/go/libp2p_node/go.mod b/libs/go/libp2p_node/go.mod index e06f56bec4..a5d16c054d 100644 --- a/libs/go/libp2p_node/go.mod +++ b/libs/go/libp2p_node/go.mod @@ -27,8 +27,7 @@ require ( github.com/sirupsen/logrus v1.7.0 // indirect golang.org/x/crypto v0.0.0-20201208171446-5f87f3452ae9 golang.org/x/mod v0.4.0 // indirect - golang.org/x/sys v0.0.0-20201214210602-f9fddec55a1e // indirect golang.org/x/term v0.0.0-20201210144234-2321bbc49cbf // indirect - golang.org/x/tools v0.0.0-20201215171152-6307297f4651 // indirect google.golang.org/protobuf v1.25.0 + honnef.co/go/tools v0.1.4 // indirect ) diff --git a/libs/go/libp2p_node/go.sum b/libs/go/libp2p_node/go.sum index 4b233f3a87..cb99823bb5 100644 --- a/libs/go/libp2p_node/go.sum +++ b/libs/go/libp2p_node/go.sum @@ -748,6 +748,8 @@ golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201214210602-f9fddec55a1e h1:AyodaIpKjppX+cBfTASF2E1US3H2JFBj920Ot3rtDjs= golang.org/x/sys v0.0.0-20201214210602-f9fddec55a1e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4 h1:myAQVi0cGEoqQVR5POX+8RR2mrocKqNN1hmeMqhX27k= +golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221 h1:/ZHdbVpdR/jk3g30/d4yUL0JU9kksj8+F/bnQUVLGDM= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= golang.org/x/term v0.0.0-20201210144234-2321bbc49cbf h1:MZ2shdL+ZM/XzY3ZGOnh4Nlpnxz5GSOhOmtHo3iPU6M= @@ -781,6 +783,8 @@ golang.org/x/tools v0.0.0-20200117012304-6edc0a871e69/go.mod h1:TB2adYChydJhpapK golang.org/x/tools v0.0.0-20200509030707-2212a7e161a5/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20201215171152-6307297f4651 h1:bdfqbHwYVvhLEIkESR524rqSsmV06Og3Fgz60LE7vZc= golang.org/x/tools v0.0.0-20201215171152-6307297f4651/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.1.0 h1:po9/4sTYwZU9lPhi1tOrb4hCv3qrhiQ77LZfGa2OjwY= +golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898 h1:/atklqdjdhuosWIl6AIbOeHJjicWYPqR9bpxqxYG2pA= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= @@ -839,3 +843,5 @@ honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWh honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.1-2019.2.3 h1:3JgtbtFHMiCmsznwGVTUWbgGov+pVqnlf1dEJTNAXeM= honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= +honnef.co/go/tools v0.1.4 h1:SadWOkti5uVN1FAMgxn165+Mw00fuQKyk4Gyn/inxNQ= +honnef.co/go/tools v0.1.4/go.mod h1:NgwopIslSNH47DimFoV78dnkksY2EFtX0ajyb3K/las= diff --git a/libs/go/libp2p_node/libp2p_node.go b/libs/go/libp2p_node/libp2p_node.go index c92151c514..f36712df43 100644 --- a/libs/go/libp2p_node/libp2p_node.go +++ b/libs/go/libp2p_node/libp2p_node.go @@ -121,6 +121,7 @@ func main() { opts = append(opts, dhtpeer.EnablePrometheusMonitoring(nodePortMonitoring)) } if registrationDelay != 0 { + //lint:ignore ST1011 don't use unit-specific suffix "Seconds" durationSeconds := time.Duration(registrationDelay) opts = append(opts, dhtpeer.WithRegistrationDelay(durationSeconds*1000000*time.Microsecond)) } diff --git a/libs/go/libp2p_node/utils/utils.go b/libs/go/libp2p_node/utils/utils.go index 92a01b0648..573ad8f004 100644 --- a/libs/go/libp2p_node/utils/utils.go +++ b/libs/go/libp2p_node/utils/utils.go @@ -325,7 +325,7 @@ func VerifyLedgerSignature( if found { return verifySignature(message, signature, pubkey) } - return false, errors.New("Unsupported ledger") + return false, errors.New("unsupported ledger") } // VerifyFetchAISignatureBTC verify the RFC6967 string-encoded signature of message using FetchAI public key @@ -433,7 +433,7 @@ func VerifyEthereumSignatureETH(message []byte, signature string, pubkey string) } if recoveredAddress != expectedAddress { - return false, errors.New("Recovered and expected addresses don't match") + return false, errors.New("recovered and expected addresses don't match") } return true, nil @@ -461,7 +461,7 @@ func AgentAddressFromPublicKey(ledgerId string, publicKey string) (string, error if addressFromPublicKey, found := addressFromPublicKeyTable[ledgerId]; found { return addressFromPublicKey(publicKey) } - return "", errors.New("Unsupported ledger " + ledgerId) + return "", errors.New("unsupported ledger " + ledgerId) } // FetchAIAddressFromPublicKey get wallet address from hex encoded secp256k1 public key @@ -639,7 +639,7 @@ func ReadBytesConn(conn net.Conn) ([]byte, error) { size := binary.BigEndian.Uint32(buf) if size > maxMessageSizeDelegateConnection { - return nil, errors.New("Expected message size larger than maximum allowed") + return nil, errors.New("expected message size larger than maximum allowed") } buf = make([]byte, size) @@ -670,7 +670,7 @@ func ReadEnvelopeConn(conn net.Conn) (*aea.Envelope, error) { // ReadBytes from a network stream func ReadBytes(s network.Stream) ([]byte, error) { if s == nil { - panic("GOTCHAAAAAAA 1, can not write to nil stream") + panic("CRITICAL can not write to nil stream") } rstream := bufio.NewReader(s) @@ -685,7 +685,7 @@ func ReadBytes(s network.Stream) ([]byte, error) { size := binary.BigEndian.Uint32(buf) if size > maxMessageSizeDelegateConnection { - return nil, errors.New("Expected message size larger than maximum allowed") + return nil, errors.New("expected message size larger than maximum allowed") } //logger.Debug().Msgf("expecting %d", size) @@ -707,7 +707,7 @@ func WriteBytes(s network.Stream, data []byte) error { } if s == nil { - panic("GOTCHAAAAAAA 1, can not write to nil stream") + panic("CRITICAL, can not write to nil stream") } wstream := bufio.NewWriter(s) @@ -731,7 +731,7 @@ func WriteBytes(s network.Stream, data []byte) error { return err } if s == nil { - panic("GOTCHAAAAAAA 2, can not write to nil stream") + panic("CRITICAL, can not flush nil stream") } err = wstream.Flush() if err != nil { diff --git a/packages/fetchai/connections/p2p_libp2p/connection.yaml b/packages/fetchai/connections/p2p_libp2p/connection.yaml index 13bf2ff4b9..716fb33f4e 100644 --- a/packages/fetchai/connections/p2p_libp2p/connection.yaml +++ b/packages/fetchai/connections/p2p_libp2p/connection.yaml @@ -12,31 +12,31 @@ fingerprint: __init__.py: QmYQuLNyQ8WTjgRYAoKAzoJEb7ocKXvM2hTyK4hsGch5D6 check_dependencies.py: QmP14nkQ8senwzdPdrZJLsA6EQ7zaKKEaLGDELhT42gp1P connection.py: Qma7YF3frWSKL92FaScmGtR1QmQgvM9QPBdWZPPgHRcTzh - libp2p_node/README.md: QmSNJEQQwh25QSHgQPM4C84xKU6FczRpmJdN7HkCQyDPuC + libp2p_node/README.md: Qmak56XnWfarVxasiaGqYQWJaNVnEAh2hsLWstuFVND98w libp2p_node/aea/api.go: QmdFR5Rmkk2FGVDwzgHtjobjAKyLejqk2CAYBCvcF23AG7 libp2p_node/aea/envelope.pb.go: QmRfUNGpCeVJfsW3H1MzCN4pwDWgumfyWufVFp6xvUjjug libp2p_node/aea/envelope.proto: QmVuvesmfgzj5aKnbFoCocoGEv3T9MR7u6KWn7CT5yfjGi libp2p_node/aea/pipe.go: QmWLBjZNRCcg3BSM9Cxb95yAXdT6wPjgw8DSYHxMuux4JA - libp2p_node/dht/dhtclient/dhtclient.go: QmYFj4YghhVA7E976xLNQHRTGrik1uHLc6ZQ9Tw1p9ECzk + libp2p_node/dht/dhtclient/dhtclient.go: Qmf2x9NZNr7gM1VJfTgzp8Wdjfu2w1krTgLt1PgTgjrY3m libp2p_node/dht/dhtclient/dhtclient_test.go: QmS9SiLAojXYobqV9m5yeRpyPzt6JcSbQD78RNvSp6LPx6 libp2p_node/dht/dhtclient/options.go: QmaoJiavHescgx98inQjVUipPFRvcFFrodrScZ3fvrXJ4z libp2p_node/dht/dhtnode/dhtnode.go: QmbyhgbCSAbQ1QsDw7FM7Nt5sZcvhbupA1jv5faxutbV7N libp2p_node/dht/dhtnode/message.pb.go: QmYT92e2BzAqS4VHmvDeVPs2ymToysmT9rtfwMBMtRg8nn libp2p_node/dht/dhtnode/message.proto: QmbgFcGVbxx4CP53cefW7JhXLYha6meSYqZ9hZJgBpcinF libp2p_node/dht/dhtnode/streams.go: Qmc2JcyiU4wHsgDj6aUunMAp4c5yMzo2ixeqRZHSW5PVwo - libp2p_node/dht/dhtnode/utils.go: QmcHGWgCXh1mtZCL9S9yQ2qjgKB2AbsYiG5sajrSBjCGuF + libp2p_node/dht/dhtnode/utils.go: QmcSEvmhU5TwL92qB1Nu3E28Lhs6UZ1GRnHwQ9knMdiyGx libp2p_node/dht/dhtpeer/benchmarks_test.go: QmX2KWsaFaVd9KGjvgYNgkLtgnu1CUhBPtTe9abKYndq4C - libp2p_node/dht/dhtpeer/dhtpeer.go: QmUrp42Kty9Rt4p2hY3FutRMNKKdRXQGjGBoSd1YiSsAaH - libp2p_node/dht/dhtpeer/dhtpeer_test.go: QmXySdeKevKC6jWweDX77ncGA1G5nyYNyfTgFB4S1y4kw8 + libp2p_node/dht/dhtpeer/dhtpeer.go: QmUS4bDXngfGL5X4CTPbb7qk2uN9rc2txerqdAuEi2QKQg + libp2p_node/dht/dhtpeer/dhtpeer_test.go: QmT8KpPtxwAFA9rHLJjh4wboGQ7bmLGFowQ2U94adzhWUL libp2p_node/dht/dhtpeer/options.go: QmRbB1dnA5TEeJZQpKBJQoxFNHSPLRiEtwtkK6ZJDZdjAX libp2p_node/dht/dhttests/dhttests.go: QmWTwAqXy4xPBWx49dvg91pBfaeyh79bgbh4yYc3u6kGhg - libp2p_node/dht/monitoring/file.go: QmfSw8prwiuxqcaTrEkWAL4dZYi7EzWGA4ZCgPgQ99ZnGN + libp2p_node/dht/monitoring/file.go: Qmb8U557xYB3gA3xRXSsjtZNTn5Cyn6VyHhDEseFfHpuTL libp2p_node/dht/monitoring/prometheus.go: QmQvXjEozVPMvRjda5WGRAU5b7cfUcRZUACQkTESG7Aewu libp2p_node/dht/monitoring/service.go: QmT47y2LHZECYcoE2uJ9QCGh3Kq8ePhYedo8dQE7X7v6YV - libp2p_node/go.mod: QmT2LQXkSen6Th4QLS2RU3tRVxJSLjdZTxRULUcnQFXfEL - libp2p_node/go.sum: QmZc6oiDqQhB6ZKVojN26JxoTNH1QetyuYd9zjSksQnTFt - libp2p_node/libp2p_node.go: QmSu21WBmAAfBbZFi3MZZgTrUMK5FYruSunxghpeUhxBMA - libp2p_node/utils/utils.go: QmYKmPHnv9RHzw1XbyBvTN57jPojxGiTM3v23rdKxKFW2R + libp2p_node/go.mod: QmU2MJhhvwTCfyztCei5R9LKStadFuFVUDb16s3tZwrZP8 + libp2p_node/go.sum: QmVpr925Kp8ZS7Z1iLfbjDB6NxG5BA7JczhhckzhmbYqzT + libp2p_node/libp2p_node.go: QmPgMQ3g93Jqu4GAv8e7fTWbrGK8hjSp7BDrKj1EuR1WcS + libp2p_node/utils/utils.go: QmUSw5SD3i7WhkPCVHc5ha6BhJbzk4Cf8HbyysEwUVz5zt fingerprint_ignore_patterns: [] build_entrypoint: check_dependencies.py connections: [] diff --git a/packages/fetchai/connections/p2p_libp2p/libp2p_node/README.md b/packages/fetchai/connections/p2p_libp2p/libp2p_node/README.md index d95ba19985..0d084a56dd 100644 --- a/packages/fetchai/connections/p2p_libp2p/libp2p_node/README.md +++ b/packages/fetchai/connections/p2p_libp2p/libp2p_node/README.md @@ -20,6 +20,7 @@ To lint: ``` bash golines . -w golangci-lint run +staticcheck ./... ``` ## Messaging patterns diff --git a/packages/fetchai/connections/p2p_libp2p/libp2p_node/dht/dhtclient/dhtclient.go b/packages/fetchai/connections/p2p_libp2p/libp2p_node/dht/dhtclient/dhtclient.go index e627a21684..e9f6278e27 100644 --- a/packages/fetchai/connections/p2p_libp2p/libp2p_node/dht/dhtclient/dhtclient.go +++ b/packages/fetchai/connections/p2p_libp2p/libp2p_node/dht/dhtclient/dhtclient.go @@ -306,7 +306,6 @@ func (dhtClient *DHTClient) bootstrapLoopUntilTimeout() error { ) case <-ctx.Done(): sleepTime = 0 - break } if sleepTime == 0 { break @@ -338,12 +337,14 @@ func (dhtClient *DHTClient) newStreamLoopUntilTimeout( stream, err = dhtClient.routedHost.NewStream(ctx, peerID, streamType) case <-ctx.Done(): sleepTime = 0 - break } if sleepTime == 0 { break } } + if stream == nil { + return stream, errors.New("stream nil" + err.Error()) + } // register again in case of disconnection if disconnected { err = dhtClient.registerAgentAddress() diff --git a/packages/fetchai/connections/p2p_libp2p/libp2p_node/dht/dhtnode/utils.go b/packages/fetchai/connections/p2p_libp2p/libp2p_node/dht/dhtnode/utils.go index bdd237a75b..acf9781075 100644 --- a/packages/fetchai/connections/p2p_libp2p/libp2p_node/dht/dhtnode/utils.go +++ b/packages/fetchai/connections/p2p_libp2p/libp2p_node/dht/dhtnode/utils.go @@ -67,7 +67,7 @@ func IsValidProofOfRepresentation( // check public key matches if record.PeerPublicKey != representativePeerPubKey { - err := errors.New("Wrong peer public key, expected " + representativePeerPubKey) + err := errors.New("wrong peer public key, expected " + representativePeerPubKey) response := &Status{Code: Status_ERROR_WRONG_PUBLIC_KEY, Msgs: []string{err.Error()}} return response, err } @@ -76,7 +76,7 @@ func IsValidProofOfRepresentation( addrFromPubKey, err := utils.AgentAddressFromPublicKey(record.LedgerId, record.PublicKey) if err != nil || addrFromPubKey != record.Address { if err == nil { - err = errors.New("Agent address and public key don't match") + err = errors.New("agent address and public key don't match") } response := &Status{Code: Status_ERROR_WRONG_AGENT_ADDRESS} return response, err @@ -91,7 +91,7 @@ func IsValidProofOfRepresentation( ) if !ok || err != nil { if err == nil { - err = errors.New("Signature is not valid") + err = errors.New("signature is not valid") } response := &Status{Code: Status_ERROR_INVALID_PROOF} return response, err diff --git a/packages/fetchai/connections/p2p_libp2p/libp2p_node/dht/dhtpeer/dhtpeer.go b/packages/fetchai/connections/p2p_libp2p/libp2p_node/dht/dhtpeer/dhtpeer.go index f562cd3859..13b8bb4ca5 100644 --- a/packages/fetchai/connections/p2p_libp2p/libp2p_node/dht/dhtpeer/dhtpeer.go +++ b/packages/fetchai/connections/p2p_libp2p/libp2p_node/dht/dhtpeer/dhtpeer.go @@ -629,7 +629,6 @@ func (dhtPeer *DHTPeer) handleDelegateService(ready *sync.WaitGroup) { go dhtPeer.handleNewDelegationConnection(conn) } case <-dhtPeer.closing: - break } } } diff --git a/packages/fetchai/connections/p2p_libp2p/libp2p_node/dht/dhtpeer/dhtpeer_test.go b/packages/fetchai/connections/p2p_libp2p/libp2p_node/dht/dhtpeer/dhtpeer_test.go index d7686e3e4d..07705ca253 100644 --- a/packages/fetchai/connections/p2p_libp2p/libp2p_node/dht/dhtpeer/dhtpeer_test.go +++ b/packages/fetchai/connections/p2p_libp2p/libp2p_node/dht/dhtpeer/dhtpeer_test.go @@ -1959,7 +1959,6 @@ func ensureAddressAnnounced(peers ...*DHTPeer) { for !peer.addressAnnounced { select { case <-ctx.Done(): - break case <-time.After(5 * time.Millisecond): } } diff --git a/packages/fetchai/connections/p2p_libp2p/libp2p_node/dht/monitoring/file.go b/packages/fetchai/connections/p2p_libp2p/libp2p_node/dht/monitoring/file.go index 7c8248db77..ca2b39d591 100644 --- a/packages/fetchai/connections/p2p_libp2p/libp2p_node/dht/monitoring/file.go +++ b/packages/fetchai/connections/p2p_libp2p/libp2p_node/dht/monitoring/file.go @@ -176,7 +176,6 @@ func (fm *FileMonitoring) Start() { select { case <-fm.closing: file.Close() - break default: ignore(file.Truncate(0)) _, err := file.Seek(0, 0) diff --git a/packages/fetchai/connections/p2p_libp2p/libp2p_node/go.mod b/packages/fetchai/connections/p2p_libp2p/libp2p_node/go.mod index e06f56bec4..a5d16c054d 100644 --- a/packages/fetchai/connections/p2p_libp2p/libp2p_node/go.mod +++ b/packages/fetchai/connections/p2p_libp2p/libp2p_node/go.mod @@ -27,8 +27,7 @@ require ( github.com/sirupsen/logrus v1.7.0 // indirect golang.org/x/crypto v0.0.0-20201208171446-5f87f3452ae9 golang.org/x/mod v0.4.0 // indirect - golang.org/x/sys v0.0.0-20201214210602-f9fddec55a1e // indirect golang.org/x/term v0.0.0-20201210144234-2321bbc49cbf // indirect - golang.org/x/tools v0.0.0-20201215171152-6307297f4651 // indirect google.golang.org/protobuf v1.25.0 + honnef.co/go/tools v0.1.4 // indirect ) diff --git a/packages/fetchai/connections/p2p_libp2p/libp2p_node/go.sum b/packages/fetchai/connections/p2p_libp2p/libp2p_node/go.sum index 4b233f3a87..cb99823bb5 100644 --- a/packages/fetchai/connections/p2p_libp2p/libp2p_node/go.sum +++ b/packages/fetchai/connections/p2p_libp2p/libp2p_node/go.sum @@ -748,6 +748,8 @@ golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201214210602-f9fddec55a1e h1:AyodaIpKjppX+cBfTASF2E1US3H2JFBj920Ot3rtDjs= golang.org/x/sys v0.0.0-20201214210602-f9fddec55a1e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4 h1:myAQVi0cGEoqQVR5POX+8RR2mrocKqNN1hmeMqhX27k= +golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221 h1:/ZHdbVpdR/jk3g30/d4yUL0JU9kksj8+F/bnQUVLGDM= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= golang.org/x/term v0.0.0-20201210144234-2321bbc49cbf h1:MZ2shdL+ZM/XzY3ZGOnh4Nlpnxz5GSOhOmtHo3iPU6M= @@ -781,6 +783,8 @@ golang.org/x/tools v0.0.0-20200117012304-6edc0a871e69/go.mod h1:TB2adYChydJhpapK golang.org/x/tools v0.0.0-20200509030707-2212a7e161a5/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20201215171152-6307297f4651 h1:bdfqbHwYVvhLEIkESR524rqSsmV06Og3Fgz60LE7vZc= golang.org/x/tools v0.0.0-20201215171152-6307297f4651/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.1.0 h1:po9/4sTYwZU9lPhi1tOrb4hCv3qrhiQ77LZfGa2OjwY= +golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898 h1:/atklqdjdhuosWIl6AIbOeHJjicWYPqR9bpxqxYG2pA= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= @@ -839,3 +843,5 @@ honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWh honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.1-2019.2.3 h1:3JgtbtFHMiCmsznwGVTUWbgGov+pVqnlf1dEJTNAXeM= honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= +honnef.co/go/tools v0.1.4 h1:SadWOkti5uVN1FAMgxn165+Mw00fuQKyk4Gyn/inxNQ= +honnef.co/go/tools v0.1.4/go.mod h1:NgwopIslSNH47DimFoV78dnkksY2EFtX0ajyb3K/las= diff --git a/packages/fetchai/connections/p2p_libp2p/libp2p_node/libp2p_node.go b/packages/fetchai/connections/p2p_libp2p/libp2p_node/libp2p_node.go index c92151c514..f36712df43 100644 --- a/packages/fetchai/connections/p2p_libp2p/libp2p_node/libp2p_node.go +++ b/packages/fetchai/connections/p2p_libp2p/libp2p_node/libp2p_node.go @@ -121,6 +121,7 @@ func main() { opts = append(opts, dhtpeer.EnablePrometheusMonitoring(nodePortMonitoring)) } if registrationDelay != 0 { + //lint:ignore ST1011 don't use unit-specific suffix "Seconds" durationSeconds := time.Duration(registrationDelay) opts = append(opts, dhtpeer.WithRegistrationDelay(durationSeconds*1000000*time.Microsecond)) } diff --git a/packages/fetchai/connections/p2p_libp2p/libp2p_node/utils/utils.go b/packages/fetchai/connections/p2p_libp2p/libp2p_node/utils/utils.go index 92a01b0648..573ad8f004 100644 --- a/packages/fetchai/connections/p2p_libp2p/libp2p_node/utils/utils.go +++ b/packages/fetchai/connections/p2p_libp2p/libp2p_node/utils/utils.go @@ -325,7 +325,7 @@ func VerifyLedgerSignature( if found { return verifySignature(message, signature, pubkey) } - return false, errors.New("Unsupported ledger") + return false, errors.New("unsupported ledger") } // VerifyFetchAISignatureBTC verify the RFC6967 string-encoded signature of message using FetchAI public key @@ -433,7 +433,7 @@ func VerifyEthereumSignatureETH(message []byte, signature string, pubkey string) } if recoveredAddress != expectedAddress { - return false, errors.New("Recovered and expected addresses don't match") + return false, errors.New("recovered and expected addresses don't match") } return true, nil @@ -461,7 +461,7 @@ func AgentAddressFromPublicKey(ledgerId string, publicKey string) (string, error if addressFromPublicKey, found := addressFromPublicKeyTable[ledgerId]; found { return addressFromPublicKey(publicKey) } - return "", errors.New("Unsupported ledger " + ledgerId) + return "", errors.New("unsupported ledger " + ledgerId) } // FetchAIAddressFromPublicKey get wallet address from hex encoded secp256k1 public key @@ -639,7 +639,7 @@ func ReadBytesConn(conn net.Conn) ([]byte, error) { size := binary.BigEndian.Uint32(buf) if size > maxMessageSizeDelegateConnection { - return nil, errors.New("Expected message size larger than maximum allowed") + return nil, errors.New("expected message size larger than maximum allowed") } buf = make([]byte, size) @@ -670,7 +670,7 @@ func ReadEnvelopeConn(conn net.Conn) (*aea.Envelope, error) { // ReadBytes from a network stream func ReadBytes(s network.Stream) ([]byte, error) { if s == nil { - panic("GOTCHAAAAAAA 1, can not write to nil stream") + panic("CRITICAL can not write to nil stream") } rstream := bufio.NewReader(s) @@ -685,7 +685,7 @@ func ReadBytes(s network.Stream) ([]byte, error) { size := binary.BigEndian.Uint32(buf) if size > maxMessageSizeDelegateConnection { - return nil, errors.New("Expected message size larger than maximum allowed") + return nil, errors.New("expected message size larger than maximum allowed") } //logger.Debug().Msgf("expecting %d", size) @@ -707,7 +707,7 @@ func WriteBytes(s network.Stream, data []byte) error { } if s == nil { - panic("GOTCHAAAAAAA 1, can not write to nil stream") + panic("CRITICAL, can not write to nil stream") } wstream := bufio.NewWriter(s) @@ -731,7 +731,7 @@ func WriteBytes(s network.Stream, data []byte) error { return err } if s == nil { - panic("GOTCHAAAAAAA 2, can not write to nil stream") + panic("CRITICAL, can not flush nil stream") } err = wstream.Flush() if err != nil { diff --git a/packages/hashes.csv b/packages/hashes.csv index 9ca813ca11..13bcfabc3c 100644 --- a/packages/hashes.csv +++ b/packages/hashes.csv @@ -41,7 +41,7 @@ fetchai/connections/http_server,QmZqiszQJuG7XA6LFWsMqXYSmViTrUkDfpkHwgYtDMbyXy fetchai/connections/ledger,QmT7ffwPzJ3isCMhN2qoj6NRyqinE2RkpSpUKNRFRXxpes fetchai/connections/local,QmUxLhmeE98S8BcuRDB7W7pRsJzpC3wVJV5ELLxVeEkoKC fetchai/connections/oef,QmaHQhxryQLaBk5TvC4iJFZtFvkPp4CoHxHg1iLnh2PAdm -fetchai/connections/p2p_libp2p,QmZ2BpAKYyuRno8diqN8sbWEzHkjnWgLGg2kWbcEDdWkEi +fetchai/connections/p2p_libp2p,QmZLQXjJT2kFEDjrGHquBzAAWactzhRxPQjhZfocTXhf27 fetchai/connections/p2p_libp2p_client,QmYq2U83xTTnv1WDy1quvEz47QaV2uw1ZmB6sFHoCL31GC fetchai/connections/p2p_stub,QmToCExj3ZpdxUu3vYSMo4jZRJiMTkXMbyhbh18Hq6Nc4b fetchai/connections/prometheus,QmeRmgrccxRh8UG4AfTMct6D5wSuK9BXYoLwnohut2LJaB From 516b1988d89c141b3090e3014ceb9438a37969a1 Mon Sep 17 00:00:00 2001 From: David Minarsch Date: Fri, 7 May 2021 13:04:10 +0100 Subject: [PATCH 012/147] fix: break implemented properly --- examples/tac_deploy/entrypoint.sh | 4 +++- libs/go/libp2p_node/dht/dhtclient/dhtclient.go | 14 ++++---------- libs/go/libp2p_node/dht/dhtpeer/dhtpeer_test.go | 2 ++ libs/go/libp2p_node/dht/monitoring/file.go | 2 ++ .../fetchai/connections/p2p_libp2p/connection.yaml | 6 +++--- .../libp2p_node/dht/dhtclient/dhtclient.go | 14 ++++---------- .../libp2p_node/dht/dhtpeer/dhtpeer_test.go | 2 ++ .../p2p_libp2p/libp2p_node/dht/monitoring/file.go | 2 ++ packages/hashes.csv | 2 +- 9 files changed, 23 insertions(+), 25 deletions(-) diff --git a/examples/tac_deploy/entrypoint.sh b/examples/tac_deploy/entrypoint.sh index c30f46dea9..fcd5460d8a 100755 --- a/examples/tac_deploy/entrypoint.sh +++ b/examples/tac_deploy/entrypoint.sh @@ -3,7 +3,8 @@ set -e LEDGER=fetchai PEER="/dns4/acn.fetch.ai/tcp/9001/p2p/16Uiu2HAmVWnopQAqq4pniYLw44VRvYxBUoRHqjz1Hh2SoCyjbyRW" -TAC_NAME='some_tac_id' +TAC_NAME='v'$((10 + $RANDOM % 1000)) +TAC_SERVICE=tac_service_$TAC_NAME BASE_PORT=10000 BASE_DIR=/data OLD_DIR=/$(date "+%d_%m_%Y_%H%M") @@ -148,6 +149,7 @@ function set_participant(){ set_agent $agent_name $(expr $BASE_PORT + $agent_id) aea config set vendor.fetchai.skills.tac_negotiation.behaviours.clean_up.args.tick_interval $CLEANUP_INTERVAL aea config set vendor.fetchai.skills.tac_negotiation.behaviours.tac_negotiation.args.search_interval $SEARCH_INTERVAL_TRADING + aea config set vendor.fetchai.skills.tac_negotiation.models.strategy.args.service_key $TAC_SERVICE aea config set vendor.fetchai.skills.tac_participation.behaviours.tac_search.args.tick_interval $SEARCH_INTERVAL_GAME aea config set vendor.fetchai.skills.tac_participation.models.game.args.search_query.search_value $TAC_NAME cd .. diff --git a/libs/go/libp2p_node/dht/dhtclient/dhtclient.go b/libs/go/libp2p_node/dht/dhtclient/dhtclient.go index e9f6278e27..9c69206ea9 100644 --- a/libs/go/libp2p_node/dht/dhtclient/dhtclient.go +++ b/libs/go/libp2p_node/dht/dhtclient/dhtclient.go @@ -305,10 +305,7 @@ func (dhtClient *DHTClient) bootstrapLoopUntilTimeout() error { dhtClient.bootstrapPeers, ) case <-ctx.Done(): - sleepTime = 0 - } - if sleepTime == 0 { - break + err = errors.New("bootstrap connect timeout reached") } } return err @@ -336,14 +333,11 @@ func (dhtClient *DHTClient) newStreamLoopUntilTimeout( sleepTime = sleepTime * sleepTimeIncreaseMFactor stream, err = dhtClient.routedHost.NewStream(ctx, peerID, streamType) case <-ctx.Done(): - sleepTime = 0 - } - if sleepTime == 0 { - break + err = errors.New("new stream loop timeout reached") } } - if stream == nil { - return stream, errors.New("stream nil" + err.Error()) + if stream == nil && err == nil { + return nil, errors.New("stream nil and err nil") } // register again in case of disconnection if disconnected { diff --git a/libs/go/libp2p_node/dht/dhtpeer/dhtpeer_test.go b/libs/go/libp2p_node/dht/dhtpeer/dhtpeer_test.go index 07705ca253..8aaf6ce8c8 100644 --- a/libs/go/libp2p_node/dht/dhtpeer/dhtpeer_test.go +++ b/libs/go/libp2p_node/dht/dhtpeer/dhtpeer_test.go @@ -1956,9 +1956,11 @@ func ensureAddressAnnounced(peers ...*DHTPeer) { for _, peer := range peers { ctx, cancel := context.WithTimeout(context.Background(), DHTPeerSetupTimeout) defer cancel() + L: for !peer.addressAnnounced { select { case <-ctx.Done(): + break L case <-time.After(5 * time.Millisecond): } } diff --git a/libs/go/libp2p_node/dht/monitoring/file.go b/libs/go/libp2p_node/dht/monitoring/file.go index ca2b39d591..2bd027110f 100644 --- a/libs/go/libp2p_node/dht/monitoring/file.go +++ b/libs/go/libp2p_node/dht/monitoring/file.go @@ -172,10 +172,12 @@ func (fm *FileMonitoring) Start() { fm.closing = make(chan struct{}) file, _ := os.OpenFile(fm.path, os.O_WRONLY|os.O_CREATE, 0666) +L: for { select { case <-fm.closing: file.Close() + break L default: ignore(file.Truncate(0)) _, err := file.Seek(0, 0) diff --git a/packages/fetchai/connections/p2p_libp2p/connection.yaml b/packages/fetchai/connections/p2p_libp2p/connection.yaml index 716fb33f4e..426b25b37b 100644 --- a/packages/fetchai/connections/p2p_libp2p/connection.yaml +++ b/packages/fetchai/connections/p2p_libp2p/connection.yaml @@ -17,7 +17,7 @@ fingerprint: libp2p_node/aea/envelope.pb.go: QmRfUNGpCeVJfsW3H1MzCN4pwDWgumfyWufVFp6xvUjjug libp2p_node/aea/envelope.proto: QmVuvesmfgzj5aKnbFoCocoGEv3T9MR7u6KWn7CT5yfjGi libp2p_node/aea/pipe.go: QmWLBjZNRCcg3BSM9Cxb95yAXdT6wPjgw8DSYHxMuux4JA - libp2p_node/dht/dhtclient/dhtclient.go: Qmf2x9NZNr7gM1VJfTgzp8Wdjfu2w1krTgLt1PgTgjrY3m + libp2p_node/dht/dhtclient/dhtclient.go: QmQpfFiHT7vwrdt6o7EuzgAbweXzP5rpndUiD3UAH2Naw7 libp2p_node/dht/dhtclient/dhtclient_test.go: QmS9SiLAojXYobqV9m5yeRpyPzt6JcSbQD78RNvSp6LPx6 libp2p_node/dht/dhtclient/options.go: QmaoJiavHescgx98inQjVUipPFRvcFFrodrScZ3fvrXJ4z libp2p_node/dht/dhtnode/dhtnode.go: QmbyhgbCSAbQ1QsDw7FM7Nt5sZcvhbupA1jv5faxutbV7N @@ -27,10 +27,10 @@ fingerprint: libp2p_node/dht/dhtnode/utils.go: QmcSEvmhU5TwL92qB1Nu3E28Lhs6UZ1GRnHwQ9knMdiyGx libp2p_node/dht/dhtpeer/benchmarks_test.go: QmX2KWsaFaVd9KGjvgYNgkLtgnu1CUhBPtTe9abKYndq4C libp2p_node/dht/dhtpeer/dhtpeer.go: QmUS4bDXngfGL5X4CTPbb7qk2uN9rc2txerqdAuEi2QKQg - libp2p_node/dht/dhtpeer/dhtpeer_test.go: QmT8KpPtxwAFA9rHLJjh4wboGQ7bmLGFowQ2U94adzhWUL + libp2p_node/dht/dhtpeer/dhtpeer_test.go: QmbygeomT8dmpo6qbj1VyqMujgqfnmvN9kbQKYh77HcvdK libp2p_node/dht/dhtpeer/options.go: QmRbB1dnA5TEeJZQpKBJQoxFNHSPLRiEtwtkK6ZJDZdjAX libp2p_node/dht/dhttests/dhttests.go: QmWTwAqXy4xPBWx49dvg91pBfaeyh79bgbh4yYc3u6kGhg - libp2p_node/dht/monitoring/file.go: Qmb8U557xYB3gA3xRXSsjtZNTn5Cyn6VyHhDEseFfHpuTL + libp2p_node/dht/monitoring/file.go: Qmc4QpKtjXaEFqGPeunV6TR4qR5RcMzoy8atzJH4ouBkfH libp2p_node/dht/monitoring/prometheus.go: QmQvXjEozVPMvRjda5WGRAU5b7cfUcRZUACQkTESG7Aewu libp2p_node/dht/monitoring/service.go: QmT47y2LHZECYcoE2uJ9QCGh3Kq8ePhYedo8dQE7X7v6YV libp2p_node/go.mod: QmU2MJhhvwTCfyztCei5R9LKStadFuFVUDb16s3tZwrZP8 diff --git a/packages/fetchai/connections/p2p_libp2p/libp2p_node/dht/dhtclient/dhtclient.go b/packages/fetchai/connections/p2p_libp2p/libp2p_node/dht/dhtclient/dhtclient.go index e9f6278e27..9c69206ea9 100644 --- a/packages/fetchai/connections/p2p_libp2p/libp2p_node/dht/dhtclient/dhtclient.go +++ b/packages/fetchai/connections/p2p_libp2p/libp2p_node/dht/dhtclient/dhtclient.go @@ -305,10 +305,7 @@ func (dhtClient *DHTClient) bootstrapLoopUntilTimeout() error { dhtClient.bootstrapPeers, ) case <-ctx.Done(): - sleepTime = 0 - } - if sleepTime == 0 { - break + err = errors.New("bootstrap connect timeout reached") } } return err @@ -336,14 +333,11 @@ func (dhtClient *DHTClient) newStreamLoopUntilTimeout( sleepTime = sleepTime * sleepTimeIncreaseMFactor stream, err = dhtClient.routedHost.NewStream(ctx, peerID, streamType) case <-ctx.Done(): - sleepTime = 0 - } - if sleepTime == 0 { - break + err = errors.New("new stream loop timeout reached") } } - if stream == nil { - return stream, errors.New("stream nil" + err.Error()) + if stream == nil && err == nil { + return nil, errors.New("stream nil and err nil") } // register again in case of disconnection if disconnected { diff --git a/packages/fetchai/connections/p2p_libp2p/libp2p_node/dht/dhtpeer/dhtpeer_test.go b/packages/fetchai/connections/p2p_libp2p/libp2p_node/dht/dhtpeer/dhtpeer_test.go index 07705ca253..8aaf6ce8c8 100644 --- a/packages/fetchai/connections/p2p_libp2p/libp2p_node/dht/dhtpeer/dhtpeer_test.go +++ b/packages/fetchai/connections/p2p_libp2p/libp2p_node/dht/dhtpeer/dhtpeer_test.go @@ -1956,9 +1956,11 @@ func ensureAddressAnnounced(peers ...*DHTPeer) { for _, peer := range peers { ctx, cancel := context.WithTimeout(context.Background(), DHTPeerSetupTimeout) defer cancel() + L: for !peer.addressAnnounced { select { case <-ctx.Done(): + break L case <-time.After(5 * time.Millisecond): } } diff --git a/packages/fetchai/connections/p2p_libp2p/libp2p_node/dht/monitoring/file.go b/packages/fetchai/connections/p2p_libp2p/libp2p_node/dht/monitoring/file.go index ca2b39d591..2bd027110f 100644 --- a/packages/fetchai/connections/p2p_libp2p/libp2p_node/dht/monitoring/file.go +++ b/packages/fetchai/connections/p2p_libp2p/libp2p_node/dht/monitoring/file.go @@ -172,10 +172,12 @@ func (fm *FileMonitoring) Start() { fm.closing = make(chan struct{}) file, _ := os.OpenFile(fm.path, os.O_WRONLY|os.O_CREATE, 0666) +L: for { select { case <-fm.closing: file.Close() + break L default: ignore(file.Truncate(0)) _, err := file.Seek(0, 0) diff --git a/packages/hashes.csv b/packages/hashes.csv index 13bcfabc3c..800564ab3e 100644 --- a/packages/hashes.csv +++ b/packages/hashes.csv @@ -41,7 +41,7 @@ fetchai/connections/http_server,QmZqiszQJuG7XA6LFWsMqXYSmViTrUkDfpkHwgYtDMbyXy fetchai/connections/ledger,QmT7ffwPzJ3isCMhN2qoj6NRyqinE2RkpSpUKNRFRXxpes fetchai/connections/local,QmUxLhmeE98S8BcuRDB7W7pRsJzpC3wVJV5ELLxVeEkoKC fetchai/connections/oef,QmaHQhxryQLaBk5TvC4iJFZtFvkPp4CoHxHg1iLnh2PAdm -fetchai/connections/p2p_libp2p,QmZLQXjJT2kFEDjrGHquBzAAWactzhRxPQjhZfocTXhf27 +fetchai/connections/p2p_libp2p,QmScRjUdhfd6KnYXErToUExaQEcJFfhRHDWzPSiN6eD69n fetchai/connections/p2p_libp2p_client,QmYq2U83xTTnv1WDy1quvEz47QaV2uw1ZmB6sFHoCL31GC fetchai/connections/p2p_stub,QmToCExj3ZpdxUu3vYSMo4jZRJiMTkXMbyhbh18Hq6Nc4b fetchai/connections/prometheus,QmeRmgrccxRh8UG4AfTMct6D5wSuK9BXYoLwnohut2LJaB From 182b96bdec241f1ff8d18d33b533eca5886a024b Mon Sep 17 00:00:00 2001 From: David Minarsch Date: Fri, 7 May 2021 13:19:22 +0100 Subject: [PATCH 013/147] feat: fix missing break statement --- libs/go/libp2p_node/dht/dhtpeer/dhtpeer.go | 2 ++ packages/fetchai/connections/p2p_libp2p/connection.yaml | 2 +- .../connections/p2p_libp2p/libp2p_node/dht/dhtpeer/dhtpeer.go | 2 ++ packages/hashes.csv | 2 +- 4 files changed, 6 insertions(+), 2 deletions(-) diff --git a/libs/go/libp2p_node/dht/dhtpeer/dhtpeer.go b/libs/go/libp2p_node/dht/dhtpeer/dhtpeer.go index 13b8bb4ca5..044b59beca 100644 --- a/libs/go/libp2p_node/dht/dhtpeer/dhtpeer.go +++ b/libs/go/libp2p_node/dht/dhtpeer/dhtpeer.go @@ -607,6 +607,7 @@ func (dhtPeer *DHTPeer) handleDelegateService(ready *sync.WaitGroup) { lerror, _, linfo, _ := dhtPeer.getLoggers() done := false +L: for { select { default: @@ -629,6 +630,7 @@ func (dhtPeer *DHTPeer) handleDelegateService(ready *sync.WaitGroup) { go dhtPeer.handleNewDelegationConnection(conn) } case <-dhtPeer.closing: + break L } } } diff --git a/packages/fetchai/connections/p2p_libp2p/connection.yaml b/packages/fetchai/connections/p2p_libp2p/connection.yaml index 426b25b37b..5ef391e165 100644 --- a/packages/fetchai/connections/p2p_libp2p/connection.yaml +++ b/packages/fetchai/connections/p2p_libp2p/connection.yaml @@ -26,7 +26,7 @@ fingerprint: libp2p_node/dht/dhtnode/streams.go: Qmc2JcyiU4wHsgDj6aUunMAp4c5yMzo2ixeqRZHSW5PVwo libp2p_node/dht/dhtnode/utils.go: QmcSEvmhU5TwL92qB1Nu3E28Lhs6UZ1GRnHwQ9knMdiyGx libp2p_node/dht/dhtpeer/benchmarks_test.go: QmX2KWsaFaVd9KGjvgYNgkLtgnu1CUhBPtTe9abKYndq4C - libp2p_node/dht/dhtpeer/dhtpeer.go: QmUS4bDXngfGL5X4CTPbb7qk2uN9rc2txerqdAuEi2QKQg + libp2p_node/dht/dhtpeer/dhtpeer.go: Qmd5nq6uhStQwsoYNrWwguY1xDY3Ako6gExjFsBC7E2nse libp2p_node/dht/dhtpeer/dhtpeer_test.go: QmbygeomT8dmpo6qbj1VyqMujgqfnmvN9kbQKYh77HcvdK libp2p_node/dht/dhtpeer/options.go: QmRbB1dnA5TEeJZQpKBJQoxFNHSPLRiEtwtkK6ZJDZdjAX libp2p_node/dht/dhttests/dhttests.go: QmWTwAqXy4xPBWx49dvg91pBfaeyh79bgbh4yYc3u6kGhg diff --git a/packages/fetchai/connections/p2p_libp2p/libp2p_node/dht/dhtpeer/dhtpeer.go b/packages/fetchai/connections/p2p_libp2p/libp2p_node/dht/dhtpeer/dhtpeer.go index 13b8bb4ca5..044b59beca 100644 --- a/packages/fetchai/connections/p2p_libp2p/libp2p_node/dht/dhtpeer/dhtpeer.go +++ b/packages/fetchai/connections/p2p_libp2p/libp2p_node/dht/dhtpeer/dhtpeer.go @@ -607,6 +607,7 @@ func (dhtPeer *DHTPeer) handleDelegateService(ready *sync.WaitGroup) { lerror, _, linfo, _ := dhtPeer.getLoggers() done := false +L: for { select { default: @@ -629,6 +630,7 @@ func (dhtPeer *DHTPeer) handleDelegateService(ready *sync.WaitGroup) { go dhtPeer.handleNewDelegationConnection(conn) } case <-dhtPeer.closing: + break L } } } diff --git a/packages/hashes.csv b/packages/hashes.csv index 800564ab3e..71404119ae 100644 --- a/packages/hashes.csv +++ b/packages/hashes.csv @@ -41,7 +41,7 @@ fetchai/connections/http_server,QmZqiszQJuG7XA6LFWsMqXYSmViTrUkDfpkHwgYtDMbyXy fetchai/connections/ledger,QmT7ffwPzJ3isCMhN2qoj6NRyqinE2RkpSpUKNRFRXxpes fetchai/connections/local,QmUxLhmeE98S8BcuRDB7W7pRsJzpC3wVJV5ELLxVeEkoKC fetchai/connections/oef,QmaHQhxryQLaBk5TvC4iJFZtFvkPp4CoHxHg1iLnh2PAdm -fetchai/connections/p2p_libp2p,QmScRjUdhfd6KnYXErToUExaQEcJFfhRHDWzPSiN6eD69n +fetchai/connections/p2p_libp2p,QmRip7XE3n4vpCSEShuGyQDNZz7AuJFbKvc3HSNEzEqV9a fetchai/connections/p2p_libp2p_client,QmYq2U83xTTnv1WDy1quvEz47QaV2uw1ZmB6sFHoCL31GC fetchai/connections/p2p_stub,QmToCExj3ZpdxUu3vYSMo4jZRJiMTkXMbyhbh18Hq6Nc4b fetchai/connections/prometheus,QmeRmgrccxRh8UG4AfTMct6D5wSuK9BXYoLwnohut2LJaB From f28661a97b44456b0d1194b890ca64e46a52f853 Mon Sep 17 00:00:00 2001 From: David Minarsch Date: Fri, 7 May 2021 14:30:33 +0100 Subject: [PATCH 014/147] feat: add resource spec for tac deploy fix: overlap in tac identifiers --- examples/tac_deploy/tac-deployment.yaml | 7 +++++++ .../test_packages/test_skills_integration/test_tac.py | 10 ++++++++++ 2 files changed, 17 insertions(+) diff --git a/examples/tac_deploy/tac-deployment.yaml b/examples/tac_deploy/tac-deployment.yaml index 8b82f70e63..4c309932db 100644 --- a/examples/tac_deploy/tac-deployment.yaml +++ b/examples/tac_deploy/tac-deployment.yaml @@ -19,6 +19,13 @@ spec: containers: - name: tac-deploy-container image: gcr.io/fetch-ai-sandbox/tac_deploy:0.0.13 + resources: + requests: + memory: "12000000Ki" + cpu: "3700m" + limits: + memory: "12000000Ki" + cpu: "3700m" env: - name: PARTICIPANTS_AMOUNT value: "70" diff --git a/tests/test_packages/test_skills_integration/test_tac.py b/tests/test_packages/test_skills_integration/test_tac.py index 41e2ad626f..92350a0eb5 100644 --- a/tests/test_packages/test_skills_integration/test_tac.py +++ b/tests/test_packages/test_skills_integration/test_tac.py @@ -82,6 +82,7 @@ def test_tac(self): # tac name tac_id = uuid.uuid4().hex + tac_service = f"tac_service_{tac_id}" # prepare tac controller for test self.set_agent_context(tac_controller_name) @@ -203,6 +204,10 @@ def test_tac(self): "vendor.fetchai.skills.tac_participation.models.game.args.search_query" ) self.nested_set_config(setting_path, data) + self.set_config( + "vendor.fetchai.skills.tac_negotiation.models.strategy.args.service_key", + tac_service, + ) self.run_cli_command("build", cwd=self._get_cwd()) self.run_cli_command("issue-certificates", cwd=self._get_cwd()) @@ -356,6 +361,7 @@ def test_tac(self): # tac name tac_id = uuid.uuid4().hex + tac_service = f"tac_service_{tac_id}" # prepare tac controller for test self.set_agent_context(tac_controller_name) @@ -538,6 +544,10 @@ def test_tac(self): ), "Difference between created and fetched project for files={}".format( diff ) + self.set_config( + "vendor.fetchai.skills.tac_negotiation.models.strategy.args.service_key", + tac_service, + ) # run tac controller self.set_agent_context(tac_controller_name) From e59b8da4b4a1f0951442c34c990fb79f784d1f50 Mon Sep 17 00:00:00 2001 From: David Minarsch Date: Fri, 7 May 2021 15:17:45 +0100 Subject: [PATCH 015/147] fix: missing await keyword --- packages/fetchai/connections/p2p_libp2p/connection.py | 2 +- packages/fetchai/connections/p2p_libp2p/connection.yaml | 2 +- packages/hashes.csv | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/fetchai/connections/p2p_libp2p/connection.py b/packages/fetchai/connections/p2p_libp2p/connection.py index 32ec7eb3b1..c240de23ae 100644 --- a/packages/fetchai/connections/p2p_libp2p/connection.py +++ b/packages/fetchai/connections/p2p_libp2p/connection.py @@ -803,7 +803,7 @@ async def _read_envelope_from_node(self) -> Optional[Envelope]: f"Failed to read. Exception: {e}. Try reconnect to node and read again." ) - self._restart_node() + await self._restart_node() return await self._node_client.read_envelope() async def _receive_from_node(self) -> None: diff --git a/packages/fetchai/connections/p2p_libp2p/connection.yaml b/packages/fetchai/connections/p2p_libp2p/connection.yaml index 5ef391e165..aae241665d 100644 --- a/packages/fetchai/connections/p2p_libp2p/connection.yaml +++ b/packages/fetchai/connections/p2p_libp2p/connection.yaml @@ -11,7 +11,7 @@ fingerprint: README.md: QmWcd2zHiRZLgXCSGw9gZ35WfcKsMeNSQouqNAaZnPBDDR __init__.py: QmYQuLNyQ8WTjgRYAoKAzoJEb7ocKXvM2hTyK4hsGch5D6 check_dependencies.py: QmP14nkQ8senwzdPdrZJLsA6EQ7zaKKEaLGDELhT42gp1P - connection.py: Qma7YF3frWSKL92FaScmGtR1QmQgvM9QPBdWZPPgHRcTzh + connection.py: QmXmCBy8AizGefC8JJo2nJZySY2kmevNgxxgMkJPvoNQRR libp2p_node/README.md: Qmak56XnWfarVxasiaGqYQWJaNVnEAh2hsLWstuFVND98w libp2p_node/aea/api.go: QmdFR5Rmkk2FGVDwzgHtjobjAKyLejqk2CAYBCvcF23AG7 libp2p_node/aea/envelope.pb.go: QmRfUNGpCeVJfsW3H1MzCN4pwDWgumfyWufVFp6xvUjjug diff --git a/packages/hashes.csv b/packages/hashes.csv index 71404119ae..84274a556f 100644 --- a/packages/hashes.csv +++ b/packages/hashes.csv @@ -41,7 +41,7 @@ fetchai/connections/http_server,QmZqiszQJuG7XA6LFWsMqXYSmViTrUkDfpkHwgYtDMbyXy fetchai/connections/ledger,QmT7ffwPzJ3isCMhN2qoj6NRyqinE2RkpSpUKNRFRXxpes fetchai/connections/local,QmUxLhmeE98S8BcuRDB7W7pRsJzpC3wVJV5ELLxVeEkoKC fetchai/connections/oef,QmaHQhxryQLaBk5TvC4iJFZtFvkPp4CoHxHg1iLnh2PAdm -fetchai/connections/p2p_libp2p,QmRip7XE3n4vpCSEShuGyQDNZz7AuJFbKvc3HSNEzEqV9a +fetchai/connections/p2p_libp2p,QmQ1FBNQezRPufMZDyVcW4KuADyKPQHb2Xo4qtLFcTgmAD fetchai/connections/p2p_libp2p_client,QmYq2U83xTTnv1WDy1quvEz47QaV2uw1ZmB6sFHoCL31GC fetchai/connections/p2p_stub,QmToCExj3ZpdxUu3vYSMo4jZRJiMTkXMbyhbh18Hq6Nc4b fetchai/connections/prometheus,QmeRmgrccxRh8UG4AfTMct6D5wSuK9BXYoLwnohut2LJaB From ea0c2b68a1c5713b03725a9de9a14088f74c36f9 Mon Sep 17 00:00:00 2001 From: "Yuri (solarw) Turchenkov" Date: Fri, 7 May 2021 17:56:53 +0300 Subject: [PATCH 016/147] USE_CLIENT fix --- examples/tac_deploy/entrypoint.sh | 2 +- examples/tac_deploy/tac-deployment.yaml | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/examples/tac_deploy/entrypoint.sh b/examples/tac_deploy/entrypoint.sh index fcd5460d8a..b04ed0248f 100755 --- a/examples/tac_deploy/entrypoint.sh +++ b/examples/tac_deploy/entrypoint.sh @@ -118,7 +118,7 @@ function set_agent(){ aea add-key fetchai $key_file_name key_file_name=$(generate_key $LEDGER $name $agent_data_dir 1) aea add-key fetchai $key_file_name --connection - if [ "$USE_CLIENT" == false ]; + if [ "$USE_CLIENT" == "false" ]; then json=$(printf '{"log_file": "%s", "delegate_uri": null, "entry_peers": ["%s"], "local_uri": "127.0.0.1:%s", "public_uri": null, "node_connection_timeout": '%i'}' "$agent_data_dir/libp2p_node.log" "$PEER" "$port" "$(($NODE_CONNECTION_TIMEOUT))") aea config set --type dict vendor.fetchai.connections.p2p_libp2p.config "$json" diff --git a/examples/tac_deploy/tac-deployment.yaml b/examples/tac_deploy/tac-deployment.yaml index 8b82f70e63..9004acfcef 100644 --- a/examples/tac_deploy/tac-deployment.yaml +++ b/examples/tac_deploy/tac-deployment.yaml @@ -42,6 +42,8 @@ spec: value: "true" - name: CLEAR_KEY_DATA_ON_LAUNCH value: "true" + - name: USE_CLIENT + value: "false" volumeMounts: - name: tac-deploy-data-vol mountPath: /data From 0b78f868a607614e9b86f590f19723be1616480d Mon Sep 17 00:00:00 2001 From: "Yuri (solarw) Turchenkov" Date: Fri, 7 May 2021 18:02:15 +0300 Subject: [PATCH 017/147] use_client default value --- examples/tac_deploy/entrypoint.sh | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/examples/tac_deploy/entrypoint.sh b/examples/tac_deploy/entrypoint.sh index b04ed0248f..8c554fc302 100755 --- a/examples/tac_deploy/entrypoint.sh +++ b/examples/tac_deploy/entrypoint.sh @@ -86,6 +86,10 @@ if [ "$CLEAR_KEY_DATA_ON_LAUNCH" == true ]; then find "$BASE_DIR" -name \*.txt -type f -delete fi +if [ -z "$USE_CLIENT" ]; then + USE_CLIENT=false +fi + function generate_key (){ ledger=$1 prefix=$2 From 2735bf8bd32e6f3ae0ead666f5889eafd0bf879d Mon Sep 17 00:00:00 2001 From: "Yuri (solarw) Turchenkov" Date: Mon, 10 May 2021 11:57:54 +0300 Subject: [PATCH 018/147] small fixes for str formatting --- plugins/aea-ledger-ethereum/tests/docker_image.py | 2 +- tests/common/docker_image.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/aea-ledger-ethereum/tests/docker_image.py b/plugins/aea-ledger-ethereum/tests/docker_image.py index ce8f63538d..5ee0ccb0c7 100644 --- a/plugins/aea-ledger-ethereum/tests/docker_image.py +++ b/plugins/aea-ledger-ethereum/tests/docker_image.py @@ -72,7 +72,7 @@ def _check_docker_binary_available(self): result.stdout.decode("utf-8"), ) if match is None: - pytest.skip(f"cannot read version from the output of 'docker --version'") + pytest.skip("cannot read version from the output of 'docker --version'") version = (int(match.group(1)), int(match.group(2)), int(match.group(3))) if version < self.MINIMUM_DOCKER_VERSION: pytest.skip( diff --git a/tests/common/docker_image.py b/tests/common/docker_image.py index 7e08bb5bd6..2c35b780de 100644 --- a/tests/common/docker_image.py +++ b/tests/common/docker_image.py @@ -79,7 +79,7 @@ def _check_docker_binary_available(self): result.stdout.decode("utf-8"), ) if match is None: - pytest.skip(f"cannot read version from the output of 'docker --version'") + pytest.skip("cannot read version from the output of 'docker --version'") version = (int(match.group(1)), int(match.group(2)), int(match.group(3))) if version < self.MINIMUM_DOCKER_VERSION: pytest.skip( From 0aa942f4ad4b165c8658d5435f4f756ce91cd101 Mon Sep 17 00:00:00 2001 From: "Yuri (solarw) Turchenkov" Date: Mon, 10 May 2021 12:01:16 +0300 Subject: [PATCH 019/147] p2p libp2p connection send envelope queue --- .../connections/p2p_libp2p/connection.py | 47 +++++++++++++++---- .../connections/p2p_libp2p/connection.yaml | 2 +- packages/hashes.csv | 2 +- .../test_p2p_libp2p/test_errors.py | 2 +- 4 files changed, 41 insertions(+), 12 deletions(-) diff --git a/packages/fetchai/connections/p2p_libp2p/connection.py b/packages/fetchai/connections/p2p_libp2p/connection.py index c240de23ae..af53df2914 100644 --- a/packages/fetchai/connections/p2p_libp2p/connection.py +++ b/packages/fetchai/connections/p2p_libp2p/connection.py @@ -681,6 +681,9 @@ def __init__(self, **kwargs: Any) -> None: self._receive_from_node_task = None # type: Optional[asyncio.Future] self._node_client: Optional[NodeClient] = None + self._send_queue: Optional[asyncio.Queue] = None + self._send_task: Optional[asyncio.Task] = None + def _check_node_built(self) -> str: """Check node built.""" if self.configuration.build_directory is None: @@ -710,9 +713,11 @@ async def connect(self) -> None: await self._start_node() # starting receiving msgs self._in_queue = asyncio.Queue() + self._send_queue = asyncio.Queue() self._receive_from_node_task = asyncio.ensure_future( self._receive_from_node(), loop=self.loop ) + self._send_task = self.loop.create_task(self._send_loop()) self.state = ConnectionStates.connected except (CancelledError, Exception) as e: self.state = ConnectionStates.disconnected @@ -736,10 +741,17 @@ async def disconnect(self) -> None: """ if self.is_disconnected: return # pragma: nocover + self.state = ConnectionStates.disconnecting + if self._receive_from_node_task is not None: self._receive_from_node_task.cancel() self._receive_from_node_task = None + + if self._send_task is not None: + self._send_task.cancel() + self._send_task = None + await self.node.stop() if self._in_queue is not None: self._in_queue.put_nowait(None) @@ -770,16 +782,10 @@ async def receive(self, *args: Any, **kwargs: Any) -> Optional["Envelope"]: self.logger.exception(e) return None - async def send(self, envelope: Envelope) -> None: - """ - Send messages. - - :return: None - """ - if not self._node_client: - raise ValueError("Node is not connected!") # pragma: nocover + async def _send_envelope_with_node_client(self, envelope: Envelope) -> None: + if not self._node_client: # pragma: nocover + raise ValueError(f"Node client not set! Can not send envelope: {envelope}") - self._ensure_valid_envelope_for_external_comms(envelope) try: await self._node_client.send_envelope(envelope) except asyncio.CancelledError: # pylint: disable=try-except-raise @@ -791,6 +797,29 @@ async def send(self, envelope: Envelope) -> None: await self._restart_node() await self._node_client.send_envelope(envelope) + async def _send_loop(self) -> None: + """Handle message in the send queue.""" + + if not self._send_queue or not self._node_client: # pragma: nocover + self.logger.error("Send loop not started cause not connected properly.") + return + + while self.is_connected: + envelope = await self._send_queue.get() + await self._send_envelope_with_node_client(envelope) + + async def send(self, envelope: Envelope) -> None: + """ + Send messages. + + :return: None + """ + if not self._node_client or not self._send_queue: + raise ValueError("Node is not connected!") # pragma: nocover + + self._ensure_valid_envelope_for_external_comms(envelope) + await self._send_queue.put(envelope) + async def _read_envelope_from_node(self) -> Optional[Envelope]: if not self._node_client: raise ValueError("Node is not connected!") # pragma: nocover diff --git a/packages/fetchai/connections/p2p_libp2p/connection.yaml b/packages/fetchai/connections/p2p_libp2p/connection.yaml index aae241665d..2c70c779fc 100644 --- a/packages/fetchai/connections/p2p_libp2p/connection.yaml +++ b/packages/fetchai/connections/p2p_libp2p/connection.yaml @@ -11,7 +11,7 @@ fingerprint: README.md: QmWcd2zHiRZLgXCSGw9gZ35WfcKsMeNSQouqNAaZnPBDDR __init__.py: QmYQuLNyQ8WTjgRYAoKAzoJEb7ocKXvM2hTyK4hsGch5D6 check_dependencies.py: QmP14nkQ8senwzdPdrZJLsA6EQ7zaKKEaLGDELhT42gp1P - connection.py: QmXmCBy8AizGefC8JJo2nJZySY2kmevNgxxgMkJPvoNQRR + connection.py: QmXpvN8paXXrgx1oAvV1XEHGvxkgENGEpAQ4nCYKLzUq6d libp2p_node/README.md: Qmak56XnWfarVxasiaGqYQWJaNVnEAh2hsLWstuFVND98w libp2p_node/aea/api.go: QmdFR5Rmkk2FGVDwzgHtjobjAKyLejqk2CAYBCvcF23AG7 libp2p_node/aea/envelope.pb.go: QmRfUNGpCeVJfsW3H1MzCN4pwDWgumfyWufVFp6xvUjjug diff --git a/packages/hashes.csv b/packages/hashes.csv index 84274a556f..ea7a1cd5a2 100644 --- a/packages/hashes.csv +++ b/packages/hashes.csv @@ -41,7 +41,7 @@ fetchai/connections/http_server,QmZqiszQJuG7XA6LFWsMqXYSmViTrUkDfpkHwgYtDMbyXy fetchai/connections/ledger,QmT7ffwPzJ3isCMhN2qoj6NRyqinE2RkpSpUKNRFRXxpes fetchai/connections/local,QmUxLhmeE98S8BcuRDB7W7pRsJzpC3wVJV5ELLxVeEkoKC fetchai/connections/oef,QmaHQhxryQLaBk5TvC4iJFZtFvkPp4CoHxHg1iLnh2PAdm -fetchai/connections/p2p_libp2p,QmQ1FBNQezRPufMZDyVcW4KuADyKPQHb2Xo4qtLFcTgmAD +fetchai/connections/p2p_libp2p,QmbL1bWiqvaTuHeQYLijBvgg4csksvtSEFCcF4s9r74SUo fetchai/connections/p2p_libp2p_client,QmYq2U83xTTnv1WDy1quvEz47QaV2uw1ZmB6sFHoCL31GC fetchai/connections/p2p_stub,QmToCExj3ZpdxUu3vYSMo4jZRJiMTkXMbyhbh18Hq6Nc4b fetchai/connections/prometheus,QmeRmgrccxRh8UG4AfTMct6D5wSuK9BXYoLwnohut2LJaB diff --git a/tests/test_packages/test_connections/test_p2p_libp2p/test_errors.py b/tests/test_packages/test_connections/test_p2p_libp2p/test_errors.py index f6c639015d..a90a17464b 100644 --- a/tests/test_packages/test_connections/test_p2p_libp2p/test_errors.py +++ b/tests/test_packages/test_connections/test_p2p_libp2p/test_errors.py @@ -259,7 +259,7 @@ async def test_reconnect_on_write_failed(): ), pytest.raises( Exception, match="expected" ): - await con.send(Mock()) + await con._send_envelope_with_node_client(Mock()) assert node.pipe.write.call_count == 2 restart_mock.assert_called_once() From 074d13087243f0c449bd27fa0195aeb04f498c41 Mon Sep 17 00:00:00 2001 From: ali Date: Mon, 10 May 2021 11:09:55 +0100 Subject: [PATCH 020/147] aligning locations across aw5 agents --- packages/fetchai/agents/simple_seller_aw5/aea-config.yaml | 4 ++-- packages/hashes.csv | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/fetchai/agents/simple_seller_aw5/aea-config.yaml b/packages/fetchai/agents/simple_seller_aw5/aea-config.yaml index 3ea31b20ed..533d8b0fa9 100644 --- a/packages/fetchai/agents/simple_seller_aw5/aea-config.yaml +++ b/packages/fetchai/agents/simple_seller_aw5/aea-config.yaml @@ -99,8 +99,8 @@ models: strategy: args: search_location: - latitude: 51.5194 - longitude: 0.127 + latitude: 51.5074 + longitude: -0.1278 search_query: constraint_type: == search_key: registration_service diff --git a/packages/hashes.csv b/packages/hashes.csv index 84274a556f..9183bf3d06 100644 --- a/packages/hashes.csv +++ b/packages/hashes.csv @@ -24,7 +24,7 @@ fetchai/agents/simple_aggregator,QmaBUEUoLtvh77LBjUNoDgZWdGFpJ84hovN4JEkbwT4Dg3 fetchai/agents/simple_buyer_aw2,QmeFEVetkqfiX5FHRPzYWA5MsT6Nr1hFpWSyqDijXf5PXp fetchai/agents/simple_buyer_aw5,QmP1GuNr3Vgyd95USSCB69hJszVgQn5DZPQwhPMY7Yp6H3 fetchai/agents/simple_seller_aw2,QmfFvf3BJkXLBC4BHA24n1qAc1yhZqvCHGK1mJJouhfxSL -fetchai/agents/simple_seller_aw5,QmbhmmfAQTachJs6sXceqc3xBiTgkqZGTRuP1vPcrMaUk4 +fetchai/agents/simple_seller_aw5,QmbsvZ3TGrwAP7hP5NdH3ekfnCRN4Lcobrzan73DBHKh7t fetchai/agents/simple_service_registration,QmdvXaondRwmtPG4jxnMkoufAUfePHg2fT4fKG7QnN7NKW fetchai/agents/simple_service_search,QmVWLNNnhNdStWhSREzQaMBpU5HxpmhLTR89WdXMXTpRMf fetchai/agents/tac_controller,QmeWpJcgY7C8k7woeJAr4QJqvW7X8U7AERReteYq1FhKNn From 56e8b45d2db18a598cfe5073cddb5147dd0ea2db Mon Sep 17 00:00:00 2001 From: David Minarsch Date: Mon, 10 May 2021 11:55:17 +0100 Subject: [PATCH 021/147] feat: connection check retry in soef --- examples/tac_deploy/README.md | 2 ++ .../fetchai/connections/soef/connection.py | 28 ++++++++++++++++--- .../fetchai/connections/soef/connection.yaml | 5 ++-- packages/hashes.csv | 2 +- tests/common/docker_image.py | 2 +- 5 files changed, 31 insertions(+), 8 deletions(-) diff --git a/examples/tac_deploy/README.md b/examples/tac_deploy/README.md index e0e6083689..7cc979ae95 100644 --- a/examples/tac_deploy/README.md +++ b/examples/tac_deploy/README.md @@ -95,8 +95,10 @@ grep -rl 'TAKE CARE! Circumventing controller identity check!' output_dir/ | sor grep -rl 'TAKE CARE! Circumventing controller identity check!' output_dir/ | wc -l grep -rnw 'SOEF Network Connection Error' output_dir/ | wc -l grep -rnw 'SOEF Server Bad Response Error' output_dir/ | wc -l +gret -rnw ' Connection reset by peer' output_dir/ | wc -l grep -rnw 'Failure during pipe closing.' output_dir/ | wc -l grep -rnw "Couldn't connect to libp2p process within timeout" output_dir/ | wc -l +grep -rnw 'Exception on connect:' output_dir/ | wc -l grep -rnw 'Exception' output_dir/ | wc -l grep -rnw 'connect to libp2p process within timeout' output_dir/ | wc -l grep -rnw 'handling valid transaction' output_dir/tac_controller/ | wc -l diff --git a/packages/fetchai/connections/soef/connection.py b/packages/fetchai/connections/soef/connection.py index c50cdaac6f..bc15e195eb 100644 --- a/packages/fetchai/connections/soef/connection.py +++ b/packages/fetchai/connections/soef/connection.py @@ -220,7 +220,8 @@ def __init__( chain_identifier: Optional[str] = None, token_storage_path: Optional[str] = None, logger: logging.Logger = _default_logger, - connection_check_timeout: float = 15, + connection_check_timeout: float = 15.0, + connection_check_max_retries: int = 3, ): """ Initialize. @@ -246,6 +247,7 @@ def __init__( self.base_url = "https://{}:{}".format(soef_addr, soef_port) self.oef_search_dialogues = OefSearchDialogues() self.connection_check_timeout = connection_check_timeout + self.connection_check_max_retries = connection_check_max_retries self._token_storage_path = token_storage_path if self._token_storage_path is not None: @@ -1071,14 +1073,23 @@ async def _check_server_reachable(self) -> None: ) except asyncio.TimeoutError: raise SOEFNetworkConnectionError( - f"Server can not be reached within timeout = {self.connection_check_timeout}!" + f"Server can not be reached within timeout={self.connection_check_timeout}!" ) async def connect(self) -> None: """Connect channel set queues and executor pool.""" self._loop = asyncio.get_event_loop() - await self._check_server_reachable() + reachable_check_count = 0 + while reachable_check_count < self.connection_check_max_retries: + reachable_check_count += 1 + try: + await self._check_server_reachable() + reachable_check_count = self.connection_check_max_retries + except Exception as e: # pragma: nocover + if reachable_check_count < self.connection_check_max_retries: + raise e + self.logger.debug(f"Exception during SOEF reachability check: {e}.") self.in_queue = asyncio.Queue() self._find_around_me_queue = asyncio.Queue() @@ -1249,7 +1260,8 @@ class SOEFConnection(Connection): """The SOEFConnection connects the Simple OEF to the mailbox.""" connection_id = PUBLIC_ID - DEFAULT_CONNECTION_CHECK_TIMEOUT: float = 15 + DEFAULT_CONNECTION_CHECK_TIMEOUT: float = 15.0 + DEFAULT_CONNECTION_CHECK_MAX_RETRIES: int = 3 def __init__(self, **kwargs: Any) -> None: """Initialize.""" @@ -1267,6 +1279,13 @@ def __init__(self, **kwargs: Any) -> None: "connection_check_timeout", self.DEFAULT_CONNECTION_CHECK_TIMEOUT ), ) + connection_check_max_retries = cast( + int, + self.configuration.config.get( + "connection_check_max_retries", + self.DEFAULT_CONNECTION_CHECK_MAX_RETRIES, + ), + ) soef_addr = cast(str, self.configuration.config.get("soef_addr")) soef_port = cast(int, self.configuration.config.get("soef_port")) chain_identifier = cast(str, self.configuration.config.get("chain_identifier")) @@ -1288,6 +1307,7 @@ def __init__(self, **kwargs: Any) -> None: chain_identifier=chain_identifier, token_storage_path=token_storage_path, connection_check_timeout=connection_check_timeout, + connection_check_max_retries=connection_check_max_retries, ) async def connect(self) -> None: diff --git a/packages/fetchai/connections/soef/connection.yaml b/packages/fetchai/connections/soef/connection.yaml index abfd94dc59..63ff14aa1d 100644 --- a/packages/fetchai/connections/soef/connection.yaml +++ b/packages/fetchai/connections/soef/connection.yaml @@ -8,7 +8,7 @@ aea_version: '>=1.0.0, <2.0.0' fingerprint: README.md: QmZk6CdnvtRHMRCiq9JjUsQ3jhyJyeGdnfn7VpgHvQEccK __init__.py: Qmd5VBGFJHXFe1H45XoUh5mMSYBwvLSViJuGFeMgbPdQts - connection.py: QmYn2wUxiTxT3hGoJeKvCUyP6NMYWeAzot7FdCkPBx6ydD + connection.py: QmcK1AAnJvgp8Md6wHjUBkfQyGqhzFeWtF4j9gCRfS4V2E fingerprint_ignore_patterns: [] connections: [] protocols: @@ -17,7 +17,8 @@ class_name: SOEFConnection config: api_key: TwiCIriSl0mLahw17pyqoA chain_identifier: fetchai_v2_testnet_stable - connection_check_timeout: 15 + connection_check_max_retries: 3 + connection_check_timeout: 15.0 soef_addr: s-oef.fetch.ai soef_port: 443 token_storage_path: soef_token.txt diff --git a/packages/hashes.csv b/packages/hashes.csv index 84274a556f..905f26774b 100644 --- a/packages/hashes.csv +++ b/packages/hashes.csv @@ -46,7 +46,7 @@ fetchai/connections/p2p_libp2p_client,QmYq2U83xTTnv1WDy1quvEz47QaV2uw1ZmB6sFHoCL fetchai/connections/p2p_stub,QmToCExj3ZpdxUu3vYSMo4jZRJiMTkXMbyhbh18Hq6Nc4b fetchai/connections/prometheus,QmeRmgrccxRh8UG4AfTMct6D5wSuK9BXYoLwnohut2LJaB fetchai/connections/scaffold,QmSrZ99ccW1VDxo6kR8TWENzNXcFWmV7aje6RNcSEwqRyd -fetchai/connections/soef,QmNSjceuUjK5DZkySRt2D789ty6MDSAGDkTvbirBEk2k7f +fetchai/connections/soef,QmcH951byBDs5fbjRF7CkLuUYitcRNHf9vuc8DP3hAceMF fetchai/connections/stub,QmQjSUgExNU6Wgks9rwBa1zYsjdPkzs7FZy3SS2Lo3bcqz fetchai/connections/tcp,QmceuewTDJ8eKeCkcHH1enwF7EEocajkmuHi7QptJB7r5j fetchai/connections/webhook,QmQn8vSouUJrjzH7SNj148jRRDK3snRDMHMkB5GDHWBbMP diff --git a/tests/common/docker_image.py b/tests/common/docker_image.py index 7e08bb5bd6..2c35b780de 100644 --- a/tests/common/docker_image.py +++ b/tests/common/docker_image.py @@ -79,7 +79,7 @@ def _check_docker_binary_available(self): result.stdout.decode("utf-8"), ) if match is None: - pytest.skip(f"cannot read version from the output of 'docker --version'") + pytest.skip("cannot read version from the output of 'docker --version'") version = (int(match.group(1)), int(match.group(2)), int(match.group(3))) if version < self.MINIMUM_DOCKER_VERSION: pytest.skip( From 37ae4a8560c136a82e83d39d6d89dc9d314ddbc1 Mon Sep 17 00:00:00 2001 From: David Minarsch Date: Mon, 10 May 2021 12:02:15 +0100 Subject: [PATCH 022/147] fix: minor bug fix on new soef feat --- packages/fetchai/connections/soef/connection.py | 2 +- packages/fetchai/connections/soef/connection.yaml | 2 +- packages/hashes.csv | 2 +- tests/test_packages/test_connections/test_soef/test_soef.py | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/fetchai/connections/soef/connection.py b/packages/fetchai/connections/soef/connection.py index bc15e195eb..e5ae90a33e 100644 --- a/packages/fetchai/connections/soef/connection.py +++ b/packages/fetchai/connections/soef/connection.py @@ -1087,7 +1087,7 @@ async def connect(self) -> None: await self._check_server_reachable() reachable_check_count = self.connection_check_max_retries except Exception as e: # pragma: nocover - if reachable_check_count < self.connection_check_max_retries: + if reachable_check_count == self.connection_check_max_retries: raise e self.logger.debug(f"Exception during SOEF reachability check: {e}.") diff --git a/packages/fetchai/connections/soef/connection.yaml b/packages/fetchai/connections/soef/connection.yaml index 63ff14aa1d..a5890f9041 100644 --- a/packages/fetchai/connections/soef/connection.yaml +++ b/packages/fetchai/connections/soef/connection.yaml @@ -8,7 +8,7 @@ aea_version: '>=1.0.0, <2.0.0' fingerprint: README.md: QmZk6CdnvtRHMRCiq9JjUsQ3jhyJyeGdnfn7VpgHvQEccK __init__.py: Qmd5VBGFJHXFe1H45XoUh5mMSYBwvLSViJuGFeMgbPdQts - connection.py: QmcK1AAnJvgp8Md6wHjUBkfQyGqhzFeWtF4j9gCRfS4V2E + connection.py: Qmaim6gumUXNsgQazNz2gjdSMh11b9pe9DcAYb6T919vpp fingerprint_ignore_patterns: [] connections: [] protocols: diff --git a/packages/hashes.csv b/packages/hashes.csv index 905f26774b..90f5befa43 100644 --- a/packages/hashes.csv +++ b/packages/hashes.csv @@ -46,7 +46,7 @@ fetchai/connections/p2p_libp2p_client,QmYq2U83xTTnv1WDy1quvEz47QaV2uw1ZmB6sFHoCL fetchai/connections/p2p_stub,QmToCExj3ZpdxUu3vYSMo4jZRJiMTkXMbyhbh18Hq6Nc4b fetchai/connections/prometheus,QmeRmgrccxRh8UG4AfTMct6D5wSuK9BXYoLwnohut2LJaB fetchai/connections/scaffold,QmSrZ99ccW1VDxo6kR8TWENzNXcFWmV7aje6RNcSEwqRyd -fetchai/connections/soef,QmcH951byBDs5fbjRF7CkLuUYitcRNHf9vuc8DP3hAceMF +fetchai/connections/soef,Qmd1mp7XZZAQp88wie486WYDYJyYFpZ5MHrSusU6MDpLzq fetchai/connections/stub,QmQjSUgExNU6Wgks9rwBa1zYsjdPkzs7FZy3SS2Lo3bcqz fetchai/connections/tcp,QmceuewTDJ8eKeCkcHH1enwF7EEocajkmuHi7QptJB7r5j fetchai/connections/webhook,QmQn8vSouUJrjzH7SNj148jRRDK3snRDMHMkB5GDHWBbMP diff --git a/tests/test_packages/test_connections/test_soef/test_soef.py b/tests/test_packages/test_connections/test_soef/test_soef.py index 651200de8a..bbdbbcd5a5 100644 --- a/tests/test_packages/test_connections/test_soef/test_soef.py +++ b/tests/test_packages/test_connections/test_soef/test_soef.py @@ -620,7 +620,7 @@ async def slow_request(*args, **kwargs): ), patch.object(self.connection.channel, "connection_check_timeout", 0.01): with pytest.raises( SOEFNetworkConnectionError, - match=" Date: Mon, 10 May 2021 13:25:21 +0100 Subject: [PATCH 024/147] fix: pylint error --- packages/fetchai/connections/soef/connection.py | 2 +- packages/fetchai/connections/soef/connection.yaml | 2 +- packages/hashes.csv | 2 +- plugins/aea-ledger-ethereum/tests/docker_image.py | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/fetchai/connections/soef/connection.py b/packages/fetchai/connections/soef/connection.py index e5ae90a33e..82583a0007 100644 --- a/packages/fetchai/connections/soef/connection.py +++ b/packages/fetchai/connections/soef/connection.py @@ -1086,7 +1086,7 @@ async def connect(self) -> None: try: await self._check_server_reachable() reachable_check_count = self.connection_check_max_retries - except Exception as e: # pragma: nocover + except Exception as e: # pylint: disable=broad-except # pragma: nocover if reachable_check_count == self.connection_check_max_retries: raise e self.logger.debug(f"Exception during SOEF reachability check: {e}.") diff --git a/packages/fetchai/connections/soef/connection.yaml b/packages/fetchai/connections/soef/connection.yaml index a5890f9041..4e6b33863e 100644 --- a/packages/fetchai/connections/soef/connection.yaml +++ b/packages/fetchai/connections/soef/connection.yaml @@ -8,7 +8,7 @@ aea_version: '>=1.0.0, <2.0.0' fingerprint: README.md: QmZk6CdnvtRHMRCiq9JjUsQ3jhyJyeGdnfn7VpgHvQEccK __init__.py: Qmd5VBGFJHXFe1H45XoUh5mMSYBwvLSViJuGFeMgbPdQts - connection.py: Qmaim6gumUXNsgQazNz2gjdSMh11b9pe9DcAYb6T919vpp + connection.py: QmQCvwPhyGZYwsUCekjuHvpBPpkjGmNGGPyphqbqwBaZwy fingerprint_ignore_patterns: [] connections: [] protocols: diff --git a/packages/hashes.csv b/packages/hashes.csv index 90f5befa43..10a73bf620 100644 --- a/packages/hashes.csv +++ b/packages/hashes.csv @@ -46,7 +46,7 @@ fetchai/connections/p2p_libp2p_client,QmYq2U83xTTnv1WDy1quvEz47QaV2uw1ZmB6sFHoCL fetchai/connections/p2p_stub,QmToCExj3ZpdxUu3vYSMo4jZRJiMTkXMbyhbh18Hq6Nc4b fetchai/connections/prometheus,QmeRmgrccxRh8UG4AfTMct6D5wSuK9BXYoLwnohut2LJaB fetchai/connections/scaffold,QmSrZ99ccW1VDxo6kR8TWENzNXcFWmV7aje6RNcSEwqRyd -fetchai/connections/soef,Qmd1mp7XZZAQp88wie486WYDYJyYFpZ5MHrSusU6MDpLzq +fetchai/connections/soef,QmXYKva5pyxunoDuw8EbMcRLLbZsLDvA22chGq4KMvoyys fetchai/connections/stub,QmQjSUgExNU6Wgks9rwBa1zYsjdPkzs7FZy3SS2Lo3bcqz fetchai/connections/tcp,QmceuewTDJ8eKeCkcHH1enwF7EEocajkmuHi7QptJB7r5j fetchai/connections/webhook,QmQn8vSouUJrjzH7SNj148jRRDK3snRDMHMkB5GDHWBbMP diff --git a/plugins/aea-ledger-ethereum/tests/docker_image.py b/plugins/aea-ledger-ethereum/tests/docker_image.py index ce8f63538d..10b36ed7d1 100644 --- a/plugins/aea-ledger-ethereum/tests/docker_image.py +++ b/plugins/aea-ledger-ethereum/tests/docker_image.py @@ -65,7 +65,7 @@ def _check_docker_binary_available(self): ["docker", "--version"], stdout=subprocess.PIPE, stderr=subprocess.PIPE ) if result.returncode != 0: - pytest.skip(f"'docker --version' failed with exit code {result.returncode}") + pytest.skip("'docker --version' failed with exit code {result.returncode}") match = re.search( r"Docker version ([0-9]+)\.([0-9]+)\.([0-9]+)", From 0e8a6b6cb9722e01d8e1fa783045da7eb55bf3ec Mon Sep 17 00:00:00 2001 From: MarcoFavorito Date: Mon, 10 May 2021 14:46:22 +0200 Subject: [PATCH 025/147] fix: minor refactoring to add bump of plugins --- scripts/bump_aea_version.py | 27 +++++++++++++++++---------- 1 file changed, 17 insertions(+), 10 deletions(-) diff --git a/scripts/bump_aea_version.py b/scripts/bump_aea_version.py index c5154138ca..ccf3f9df98 100644 --- a/scripts/bump_aea_version.py +++ b/scripts/bump_aea_version.py @@ -165,23 +165,30 @@ def parse_args() -> argparse.Namespace: return arguments_ -if __name__ == "__main__": - arguments = parse_args() - _new_version_str = arguments.new_version +def update_aea_version(new_version_string: str) -> bool: + """ + Update aea version. + :param new_version_string: the new version string. + :return: True if the update actually happened; False otherwise. + """ # validate new version - _new_version: Version = Version(_new_version_str) - _new_version_str = str(_new_version) - _current_version_str = update_version_for_aea(_new_version_str) + new_version: Version = Version(new_version_string) + new_version_string = str(new_version) + _current_version_str = update_version_for_aea(new_version_string) # validate current version _current_version: Version = Version(_current_version_str) _current_version_str = str(_current_version) - update_version_for_files(_current_version_str, _new_version_str) + update_version_for_files(_current_version_str, new_version_string) - have_updated_specifier_set = update_aea_version_specifiers( - _current_version, _new_version - ) + return update_aea_version_specifiers(_current_version, new_version) + + +if __name__ == "__main__": + arguments = parse_args() + new_version_str = arguments.new_version + have_updated_specifier_set = update_aea_version(new_version_str) print("OK") return_code = 0 From 85b585e5ec753a7b6d4b2ff8d9c308cae937bc79 Mon Sep 17 00:00:00 2001 From: "Yuri (solarw) Turchenkov" Date: Mon, 10 May 2021 16:04:13 +0300 Subject: [PATCH 026/147] libp2p connection pipe reconnect attempt --- .../connections/p2p_libp2p/connection.py | 52 +++++++++++++++++-- .../connections/p2p_libp2p/connection.yaml | 2 +- packages/hashes.csv | 2 +- .../test_p2p_libp2p/test_communication.py | 2 +- .../test_p2p_libp2p/test_errors.py | 32 ++++++++++++ 5 files changed, 82 insertions(+), 8 deletions(-) diff --git a/packages/fetchai/connections/p2p_libp2p/connection.py b/packages/fetchai/connections/p2p_libp2p/connection.py index af53df2914..1a108346e7 100644 --- a/packages/fetchai/connections/p2p_libp2p/connection.py +++ b/packages/fetchai/connections/p2p_libp2p/connection.py @@ -350,6 +350,14 @@ def _child_watcher_callback(self, *_) -> None: # type: ignore # pragma: nocover f"Node process with pid {self.proc.pid} was terminated with returncode {returncode}" ) + def is_proccess_running(self) -> bool: + """Check process is running.""" + if not self.proc: + return False + + self.proc.poll() + return self.proc.returncode is None + async def start(self) -> None: """ Start the node. @@ -786,16 +794,43 @@ async def _send_envelope_with_node_client(self, envelope: Envelope) -> None: if not self._node_client: # pragma: nocover raise ValueError(f"Node client not set! Can not send envelope: {envelope}") + if not self.node.pipe: # pragma: nocover + raise ValueError("Node is not connected") + try: await self._node_client.send_envelope(envelope) + return + except asyncio.CancelledError: # pylint: disable=try-except-raise + raise # pragma: nocover + except Exception as e: # pylint: disable=broad-except + self.logger.exception( + f"Failed to send. Exception: {e}. Try recover connection to node and send again." + ) + + try: + if self.node.is_proccess_running(): + await self.node.pipe.connect() + await self._node_client.send_envelope(envelope) + self.logger.info("Envelope sent after reconnect to node") + return except asyncio.CancelledError: # pylint: disable=try-except-raise raise # pragma: nocover except Exception as e: # pylint: disable=broad-except + self.logger.info("Envelope sending failed after reconnect to node") self.logger.exception( - f"Failed to send. Exception: {e}. Try reconnect to node and read again." + f"Failed to send after pipe reconnect. Exception: {e}. Try recover connection to node and send again." ) + + try: await self._restart_node() await self._node_client.send_envelope(envelope) + except asyncio.CancelledError: # pylint: disable=try-except-raise + raise # pragma: nocover + except Exception as e: # pylint: disable=broad-except + self.logger.exception( + f"Failed to send after node restart. Exception: {e}. Try recover connection to node and send again." + ) + raise async def _send_loop(self) -> None: """Handle message in the send queue.""" @@ -803,10 +838,17 @@ async def _send_loop(self) -> None: if not self._send_queue or not self._node_client: # pragma: nocover self.logger.error("Send loop not started cause not connected properly.") return - - while self.is_connected: - envelope = await self._send_queue.get() - await self._send_envelope_with_node_client(envelope) + try: + while self.is_connected: + envelope = await self._send_queue.get() + await self._send_envelope_with_node_client(envelope) + except asyncio.CancelledError: # pylint: disable=try-except-raise + raise # pragma: nocover + except Exception: # pylint: disable=broad-except # pragma: nocover + self.logger.exception( + f"Failed to send an aenvelope {envelope}. Stop connection." + ) + await self.disconnect() async def send(self, envelope: Envelope) -> None: """ diff --git a/packages/fetchai/connections/p2p_libp2p/connection.yaml b/packages/fetchai/connections/p2p_libp2p/connection.yaml index 2c70c779fc..8366369869 100644 --- a/packages/fetchai/connections/p2p_libp2p/connection.yaml +++ b/packages/fetchai/connections/p2p_libp2p/connection.yaml @@ -11,7 +11,7 @@ fingerprint: README.md: QmWcd2zHiRZLgXCSGw9gZ35WfcKsMeNSQouqNAaZnPBDDR __init__.py: QmYQuLNyQ8WTjgRYAoKAzoJEb7ocKXvM2hTyK4hsGch5D6 check_dependencies.py: QmP14nkQ8senwzdPdrZJLsA6EQ7zaKKEaLGDELhT42gp1P - connection.py: QmXpvN8paXXrgx1oAvV1XEHGvxkgENGEpAQ4nCYKLzUq6d + connection.py: QmSw9z9NhFDCrC8UFFCyZox8Tzx3XrWeEnGJxABEvm3KVL libp2p_node/README.md: Qmak56XnWfarVxasiaGqYQWJaNVnEAh2hsLWstuFVND98w libp2p_node/aea/api.go: QmdFR5Rmkk2FGVDwzgHtjobjAKyLejqk2CAYBCvcF23AG7 libp2p_node/aea/envelope.pb.go: QmRfUNGpCeVJfsW3H1MzCN4pwDWgumfyWufVFp6xvUjjug diff --git a/packages/hashes.csv b/packages/hashes.csv index ea7a1cd5a2..58268b6fef 100644 --- a/packages/hashes.csv +++ b/packages/hashes.csv @@ -41,7 +41,7 @@ fetchai/connections/http_server,QmZqiszQJuG7XA6LFWsMqXYSmViTrUkDfpkHwgYtDMbyXy fetchai/connections/ledger,QmT7ffwPzJ3isCMhN2qoj6NRyqinE2RkpSpUKNRFRXxpes fetchai/connections/local,QmUxLhmeE98S8BcuRDB7W7pRsJzpC3wVJV5ELLxVeEkoKC fetchai/connections/oef,QmaHQhxryQLaBk5TvC4iJFZtFvkPp4CoHxHg1iLnh2PAdm -fetchai/connections/p2p_libp2p,QmbL1bWiqvaTuHeQYLijBvgg4csksvtSEFCcF4s9r74SUo +fetchai/connections/p2p_libp2p,QmPDkW1HCtHdg3ovyrat6DYh1Hme76SsDFwzX7hPDPYtUW fetchai/connections/p2p_libp2p_client,QmYq2U83xTTnv1WDy1quvEz47QaV2uw1ZmB6sFHoCL31GC fetchai/connections/p2p_stub,QmToCExj3ZpdxUu3vYSMo4jZRJiMTkXMbyhbh18Hq6Nc4b fetchai/connections/prometheus,QmeRmgrccxRh8UG4AfTMct6D5wSuK9BXYoLwnohut2LJaB diff --git a/tests/test_packages/test_connections/test_p2p_libp2p/test_communication.py b/tests/test_packages/test_connections/test_p2p_libp2p/test_communication.py index 593647bc4e..8bc1c0c342 100644 --- a/tests/test_packages/test_connections/test_p2p_libp2p/test_communication.py +++ b/tests/test_packages/test_connections/test_p2p_libp2p/test_communication.py @@ -790,7 +790,7 @@ def test_envelope_routed(self): self.multiplexer2.put(envelope) delivered_envelope = self.multiplexer1.get(block=True, timeout=20) _mock_logger.assert_called_with( - "Failed to send. Exception: some error. Try reconnect to node and read again." + "Failed to send after pipe reconnect. Exception: some error. Try recover connection to node and send again." ) assert delivered_envelope is not None diff --git a/tests/test_packages/test_connections/test_p2p_libp2p/test_errors.py b/tests/test_packages/test_connections/test_p2p_libp2p/test_errors.py index a90a17464b..133eaf475f 100644 --- a/tests/test_packages/test_connections/test_p2p_libp2p/test_errors.py +++ b/tests/test_packages/test_connections/test_p2p_libp2p/test_errors.py @@ -265,6 +265,38 @@ async def test_reconnect_on_write_failed(): restart_mock.assert_called_once() +@pytest.mark.asyncio +async def test_reconnect_on_write_failed_reconnect_pipe(): + """Test node restart on write fail.""" + host = "localhost" + port = "10000" + with patch( + "packages.fetchai.connections.p2p_libp2p.connection.P2PLibp2pConnection._check_node_built", + return_value="./", + ), patch("tests.conftest.build_node"), tempfile.TemporaryDirectory() as data_dir: + con = _make_libp2p_connection( + port=port, host=host, data_dir=data_dir, build_directory=data_dir + ) + + node = Libp2pNode(Mock(), Mock(), "tmp", "tmp") + f = Future() + f.set_result(None) + con.node = node + node.pipe = Mock() + node.pipe.connect = Mock(return_value=f) + node.pipe.write = Mock(side_effect=[Exception("expected"), f]) + + con._node_client = node.get_client() + + with patch.object(con, "_ensure_valid_envelope_for_external_comms"), patch.object( + node, "is_proccess_running", return_value=True + ): + await con._send_envelope_with_node_client(Mock()) + + assert node.pipe.write.call_count == 2 + assert node.pipe.connect.call_count == 1 + + @pytest.mark.asyncio async def test_reconnect_on_read_failed(): """Test node restart on read fail.""" From bbb0e7c08750f7046a283f7589a7a2e3b911b693 Mon Sep 17 00:00:00 2001 From: "Yuri (solarw) Turchenkov" Date: Mon, 10 May 2021 16:18:00 +0300 Subject: [PATCH 027/147] log level adjust --- packages/fetchai/connections/p2p_libp2p/connection.py | 3 +-- packages/fetchai/connections/p2p_libp2p/connection.yaml | 2 +- packages/hashes.csv | 2 +- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/packages/fetchai/connections/p2p_libp2p/connection.py b/packages/fetchai/connections/p2p_libp2p/connection.py index 1a108346e7..7cf3e63a14 100644 --- a/packages/fetchai/connections/p2p_libp2p/connection.py +++ b/packages/fetchai/connections/p2p_libp2p/connection.py @@ -811,12 +811,11 @@ async def _send_envelope_with_node_client(self, envelope: Envelope) -> None: if self.node.is_proccess_running(): await self.node.pipe.connect() await self._node_client.send_envelope(envelope) - self.logger.info("Envelope sent after reconnect to node") + self.logger.debug("Envelope sent after reconnect to node") return except asyncio.CancelledError: # pylint: disable=try-except-raise raise # pragma: nocover except Exception as e: # pylint: disable=broad-except - self.logger.info("Envelope sending failed after reconnect to node") self.logger.exception( f"Failed to send after pipe reconnect. Exception: {e}. Try recover connection to node and send again." ) diff --git a/packages/fetchai/connections/p2p_libp2p/connection.yaml b/packages/fetchai/connections/p2p_libp2p/connection.yaml index 8366369869..385a5621c8 100644 --- a/packages/fetchai/connections/p2p_libp2p/connection.yaml +++ b/packages/fetchai/connections/p2p_libp2p/connection.yaml @@ -11,7 +11,7 @@ fingerprint: README.md: QmWcd2zHiRZLgXCSGw9gZ35WfcKsMeNSQouqNAaZnPBDDR __init__.py: QmYQuLNyQ8WTjgRYAoKAzoJEb7ocKXvM2hTyK4hsGch5D6 check_dependencies.py: QmP14nkQ8senwzdPdrZJLsA6EQ7zaKKEaLGDELhT42gp1P - connection.py: QmSw9z9NhFDCrC8UFFCyZox8Tzx3XrWeEnGJxABEvm3KVL + connection.py: Qmc5xhrj2GYijSyzMFH1SE3x4p5zqRjwfgkcYieVU9irdX libp2p_node/README.md: Qmak56XnWfarVxasiaGqYQWJaNVnEAh2hsLWstuFVND98w libp2p_node/aea/api.go: QmdFR5Rmkk2FGVDwzgHtjobjAKyLejqk2CAYBCvcF23AG7 libp2p_node/aea/envelope.pb.go: QmRfUNGpCeVJfsW3H1MzCN4pwDWgumfyWufVFp6xvUjjug diff --git a/packages/hashes.csv b/packages/hashes.csv index 58268b6fef..09bdee7849 100644 --- a/packages/hashes.csv +++ b/packages/hashes.csv @@ -41,7 +41,7 @@ fetchai/connections/http_server,QmZqiszQJuG7XA6LFWsMqXYSmViTrUkDfpkHwgYtDMbyXy fetchai/connections/ledger,QmT7ffwPzJ3isCMhN2qoj6NRyqinE2RkpSpUKNRFRXxpes fetchai/connections/local,QmUxLhmeE98S8BcuRDB7W7pRsJzpC3wVJV5ELLxVeEkoKC fetchai/connections/oef,QmaHQhxryQLaBk5TvC4iJFZtFvkPp4CoHxHg1iLnh2PAdm -fetchai/connections/p2p_libp2p,QmPDkW1HCtHdg3ovyrat6DYh1Hme76SsDFwzX7hPDPYtUW +fetchai/connections/p2p_libp2p,QmWZBVf4FYUFwhbMyEtTWYto7ZB38rvP9oPmH5Ypomy7CQ fetchai/connections/p2p_libp2p_client,QmYq2U83xTTnv1WDy1quvEz47QaV2uw1ZmB6sFHoCL31GC fetchai/connections/p2p_stub,QmToCExj3ZpdxUu3vYSMo4jZRJiMTkXMbyhbh18Hq6Nc4b fetchai/connections/prometheus,QmeRmgrccxRh8UG4AfTMct6D5wSuK9BXYoLwnohut2LJaB From 3bde071e10761b8a1eefd56c1cdeaddb25f967c5 Mon Sep 17 00:00:00 2001 From: MarcoFavorito Date: Mon, 10 May 2021 16:12:02 +0200 Subject: [PATCH 028/147] refactor 'bump_aea_version' script Use functor pattern; this will allow less code duplication for the incoming feature (#2504) --- scripts/bump_aea_version.py | 206 +++++++++++++++++++++++------------- 1 file changed, 130 insertions(+), 76 deletions(-) diff --git a/scripts/bump_aea_version.py b/scripts/bump_aea_version.py index ccf3f9df98..9f2a1d0cdd 100644 --- a/scripts/bump_aea_version.py +++ b/scripts/bump_aea_version.py @@ -21,9 +21,13 @@ """Bump the AEA version throughout the code base.""" import argparse +import inspect +import os import re import sys +from functools import wraps from pathlib import Path +from typing import Optional from packaging.version import Version @@ -44,6 +48,8 @@ PACKAGES_DIR = Path("packages") TESTS_DIR = Path("tests") AEA_DIR = Path("aea") +CUR_PATH = os.path.dirname(inspect.getfile(inspect.currentframe())) # type: ignore +ROOT_DIR = os.path.join(CUR_PATH, "..") CONFIGURATION_FILENAME_REGEX = re.compile( "|".join( [ @@ -59,62 +65,126 @@ IGNORE_DIRS = [Path(".git")] -def update_version_for_files(current_version: str, new_version: str) -> None: - """ - Update the version. +def check_executed(func): + """Check a functor has been already executed; if yes, raise error.""" - :param current_version: the current version - :param new_version: the new version - """ - files = [ - Path("benchmark", "run_from_branch.sh"), - Path("deploy-image", "Dockerfile"), - Path("develop-image", "docker-env.sh"), - Path("docs", "quickstart.md"), - Path("examples", "tac_deploy", "Dockerfile"), - Path("scripts", "install.ps1"), - Path("scripts", "install.sh"), - Path("tests", "test_docs", "test_bash_yaml", "md_files", "bash-quickstart.md"), - Path("user-image", "docker-env.sh"), - ] - for filepath in files: - update_version_for_file(filepath, current_version, new_version) - - -def update_version_for_aea(new_version: str) -> str: - """ - Update version for file. + @wraps(func) + def wrapper(self, *args, **kwargs): + if self.is_executed: + raise ValueError("already executed") + self._executed = True + self._result = func(self, *args, **kwargs) - :param new_version: the new version - :return: the current version - """ - current_version = "" - path = Path("aea", "__version__.py") - with open(path, "rt") as fin: - for line in fin: - if "__version__" not in line: - continue - match = re.search('__version__ = "(.*)"', line) - if match is None: - raise ValueError("Current version is not well formatted.") - current_version = match.group(1) - if current_version == "": - raise ValueError("No version found!") - update_version_for_file(path, current_version, new_version) - return current_version - - -def update_version_for_file(path: Path, current_version: str, new_version: str) -> None: - """ - Update version for file. + return wrapper - :param path: the file path - :param current_version: the current version - :param new_version: the new version - """ - content = path.read_text() - content = content.replace(current_version, new_version) - path.write_text(content) + +class PythonPackageVersionBumper: + """Utility class to bump Python package versions.""" + + def __init__(self, root_dir: Path, python_pkg_dir: Path, new_version: Version): + """ + Initialize the utility class. + + :param root_dir: the root directory from which to look for files. + :param python_pkg_dir: the path to the Python package to upgrade. + :param new_version: the new version. + """ + self.root_dir = root_dir + self.python_pkg_dir = python_pkg_dir + self.new_version = new_version + + self._current_version = None + + # functor pattern + self._executed: bool = False + self._result: Optional[bool] = None + + @property + def is_executed(self) -> bool: + """ + Return true if the functor has been executed; false otherwise. + + :return: True if it has been executed, False otherwise. + """ + return self._executed + + @property + def result(self) -> bool: + """Get the result.""" + if not self.is_executed: + raise ValueError("not executed yet") + return self._result + + @check_executed + def run(self) -> bool: + """Main entrypoint.""" + new_version_string = str(self.new_version) + current_version_str = self.update_version_for_aea(new_version_string) + + # validate current version + current_version: Version = Version(current_version_str) + current_version_str = str(current_version) + self._current_version = current_version_str + self.update_version_for_files() + + return update_aea_version_specifiers(current_version, self.new_version) + + def update_version_for_files(self) -> None: + """Update the version.""" + files = [ + Path("benchmark", "run_from_branch.sh"), + Path("deploy-image", "Dockerfile"), + Path("develop-image", "docker-env.sh"), + Path("docs", "quickstart.md"), + Path("examples", "tac_deploy", "Dockerfile"), + Path("scripts", "install.ps1"), + Path("scripts", "install.sh"), + Path( + "tests", "test_docs", "test_bash_yaml", "md_files", "bash-quickstart.md" + ), + Path("user-image", "docker-env.sh"), + ] + for filepath in files: + self.update_version_for_file( + filepath, self._current_version, str(self.new_version) + ) + + def update_version_for_aea(self, new_version: str) -> str: + """ + Update version for file. + + :param new_version: the new version + :return: the current version + """ + current_version = "" + path = Path("aea", "__version__.py") + with open(path, "rt") as fin: + for line in fin: + if "__version__" not in line: + continue + match = re.search('__version__ = "(.*)"', line) + if match is None: + raise ValueError("Current version is not well formatted.") + current_version = match.group(1) + if current_version == "": + raise ValueError("No version found!") + self.update_version_for_file(path, current_version, new_version) + return current_version + + @classmethod + def update_version_for_file( + cls, path: Path, current_version: str, new_version: str + ) -> None: + """ + Update version for file. + + :param path: the file path + :param current_version: the current version + :param new_version: the new version + """ + content = path.read_text() + content = content.replace(current_version, new_version) + path.write_text(content) def update_aea_version_specifiers(old_version: Version, new_version: Version) -> bool: @@ -158,37 +228,21 @@ def parse_args() -> argparse.Namespace: parser = argparse.ArgumentParser("bump_aea_version") parser.add_argument( - "--new-version", type=str, required=True, help="The new version." + "--new-version", type=str, required=True, help="The new AEA version." ) parser.add_argument("--no-fingerprint", action="store_true") arguments_ = parser.parse_args() return arguments_ -def update_aea_version(new_version_string: str) -> bool: - """ - Update aea version. - - :param new_version_string: the new version string. - :return: True if the update actually happened; False otherwise. - """ - # validate new version - new_version: Version = Version(new_version_string) - new_version_string = str(new_version) - _current_version_str = update_version_for_aea(new_version_string) - - # validate current version - _current_version: Version = Version(_current_version_str) - _current_version_str = str(_current_version) - update_version_for_files(_current_version_str, new_version_string) - - return update_aea_version_specifiers(_current_version, new_version) - - if __name__ == "__main__": arguments = parse_args() - new_version_str = arguments.new_version - have_updated_specifier_set = update_aea_version(new_version_str) + new_aea_version = Version(arguments.new_version) + + aea_version_bumper = PythonPackageVersionBumper( + AEA_DIR.parent, AEA_DIR, new_aea_version + ) + have_updated_specifier_set = aea_version_bumper.run() print("OK") return_code = 0 From bbd5794c6092ab6036d298c1108870c02fdc4808 Mon Sep 17 00:00:00 2001 From: "Yuri (solarw) Turchenkov" Date: Mon, 10 May 2021 17:12:40 +0300 Subject: [PATCH 029/147] libp2p utils tests --- libs/go/libp2p_node/Makefile | 12 + libs/go/libp2p_node/go.mod | 7 +- libs/go/libp2p_node/go.sum | 7 + libs/go/libp2p_node/mocks/mock_host.go | 224 +++++++++ libs/go/libp2p_node/mocks/mock_net.go | 150 ++++++ libs/go/libp2p_node/mocks/mock_network.go | 191 +++++++ libs/go/libp2p_node/mocks/mock_peerstore.go | 412 +++++++++++++++ libs/go/libp2p_node/utils/utils.go | 120 ++++- libs/go/libp2p_node/utils/utils_test.go | 475 ++++++++++++++++++ .../connections/p2p_libp2p/connection.yaml | 12 +- .../p2p_libp2p/libp2p_node/Makefile | 12 + .../connections/p2p_libp2p/libp2p_node/go.mod | 7 +- .../connections/p2p_libp2p/libp2p_node/go.sum | 7 + .../p2p_libp2p/libp2p_node/mocks/mock_host.go | 224 +++++++++ .../p2p_libp2p/libp2p_node/mocks/mock_net.go | 150 ++++++ .../libp2p_node/mocks/mock_network.go | 191 +++++++ .../libp2p_node/mocks/mock_peerstore.go | 412 +++++++++++++++ .../p2p_libp2p/libp2p_node/utils/utils.go | 120 ++++- .../libp2p_node/utils/utils_test.go | 475 ++++++++++++++++++ packages/hashes.csv | 2 +- 20 files changed, 3152 insertions(+), 58 deletions(-) create mode 100644 libs/go/libp2p_node/Makefile create mode 100644 libs/go/libp2p_node/mocks/mock_host.go create mode 100644 libs/go/libp2p_node/mocks/mock_net.go create mode 100644 libs/go/libp2p_node/mocks/mock_network.go create mode 100644 libs/go/libp2p_node/mocks/mock_peerstore.go create mode 100644 libs/go/libp2p_node/utils/utils_test.go create mode 100644 packages/fetchai/connections/p2p_libp2p/libp2p_node/Makefile create mode 100644 packages/fetchai/connections/p2p_libp2p/libp2p_node/mocks/mock_host.go create mode 100644 packages/fetchai/connections/p2p_libp2p/libp2p_node/mocks/mock_net.go create mode 100644 packages/fetchai/connections/p2p_libp2p/libp2p_node/mocks/mock_network.go create mode 100644 packages/fetchai/connections/p2p_libp2p/libp2p_node/mocks/mock_peerstore.go create mode 100644 packages/fetchai/connections/p2p_libp2p/libp2p_node/utils/utils_test.go diff --git a/libs/go/libp2p_node/Makefile b/libs/go/libp2p_node/Makefile new file mode 100644 index 0000000000..ae14b117be --- /dev/null +++ b/libs/go/libp2p_node/Makefile @@ -0,0 +1,12 @@ +test: + go test -gcflags=-l -p 1 -timeout 0 -count 1 -covermode=atomic -coverprofile=coverage.txt -v ./... + go tool cover -func=coverage.txt + +lint: + golines . -w + golangci-lint run + +build: + go build +install: + go get -v -t -d ./... \ No newline at end of file diff --git a/libs/go/libp2p_node/go.mod b/libs/go/libp2p_node/go.mod index a5d16c054d..18898cea10 100644 --- a/libs/go/libp2p_node/go.mod +++ b/libs/go/libp2p_node/go.mod @@ -3,11 +3,13 @@ module libp2p_node go 1.13 require ( + bou.ke/monkey v1.0.2 github.com/alecthomas/units v0.0.0-20201120081800-1786d5ef83d4 // indirect github.com/btcsuite/btcd v0.20.1-beta github.com/btcsuite/btcutil v0.0.0-20190425235716-9e5f4b9a998d github.com/dave/dst v0.26.2 // indirect github.com/ethereum/go-ethereum v1.9.25 + github.com/golang/mock v1.5.0 github.com/golang/protobuf v1.4.2 github.com/ipfs/go-cid v0.0.5 github.com/joho/godotenv v1.3.0 @@ -16,18 +18,21 @@ require ( github.com/libp2p/go-libp2p-circuit v0.2.2 github.com/libp2p/go-libp2p-core v0.5.3 github.com/libp2p/go-libp2p-kad-dht v0.7.11 + github.com/libp2p/go-libp2p-kbucket v0.4.1 github.com/mattn/go-colorable v0.1.8 // indirect github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect github.com/multiformats/go-multiaddr v0.2.1 github.com/multiformats/go-multihash v0.0.13 github.com/pkg/errors v0.9.1 github.com/prometheus/client_golang v1.7.1 - github.com/rs/zerolog v1.19.0 + github.com/rs/zerolog v1.21.0 github.com/segmentio/golines v0.0.0-20200824192126-7f30d3046793 // indirect github.com/sirupsen/logrus v1.7.0 // indirect + github.com/stretchr/testify v1.5.1 golang.org/x/crypto v0.0.0-20201208171446-5f87f3452ae9 golang.org/x/mod v0.4.0 // indirect golang.org/x/term v0.0.0-20201210144234-2321bbc49cbf // indirect google.golang.org/protobuf v1.25.0 honnef.co/go/tools v0.1.4 // indirect + ) diff --git a/libs/go/libp2p_node/go.sum b/libs/go/libp2p_node/go.sum index cb99823bb5..f108665f5e 100644 --- a/libs/go/libp2p_node/go.sum +++ b/libs/go/libp2p_node/go.sum @@ -1,3 +1,5 @@ +bou.ke/monkey v1.0.2 h1:kWcnsrCNUatbxncxR/ThdYqbytgOIArtYWqcQLQzKLI= +bou.ke/monkey v1.0.2/go.mod h1:OqickVX3tNx6t33n1xvtTtu85YN5s6cKwVug+oHMaIA= cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= github.com/AndreasBriese/bbloom v0.0.0-20180913140656-343706a395b7/go.mod h1:bOvUY6CB00SOBii9/FifXqc0awNKxLFCL/+pkDPuyl8= github.com/AndreasBriese/bbloom v0.0.0-20190306092124-e2d15f34fcf9/go.mod h1:bOvUY6CB00SOBii9/FifXqc0awNKxLFCL/+pkDPuyl8= @@ -129,6 +131,8 @@ github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfU github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6 h1:ZgQEtGgCBiWRM39fZuwSd1LwSqqSW0hOdXCYYDX0R3I= github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.5.0 h1:jlYHihg//f7RRwuPfptm04yp4s7O6Kw8EZiVYIGcH0g= +github.com/golang/mock v1.5.0/go.mod h1:CWnOUgYIOo4TcNZ0wHX3YZCqsaM1I1Jvs6v3mP3KVu8= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.0/go.mod h1:Qd/q+1AKNOZr9uGQzbzCmRO6sUih6GTPZv6a1/R87v0= github.com/golang/protobuf v1.3.1 h1:YF8+flBXS5eO826T4nzqPrxfhQThhXl0YzfuUPu4SBg= @@ -566,6 +570,8 @@ github.com/rs/xhandler v0.0.0-20160618193221-ed27b6fd6521/go.mod h1:RvLn4FgxWubr github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ= github.com/rs/zerolog v1.19.0 h1:hYz4ZVdUgjXTBUmrkrw55j1nHx68LfOKIQk5IYtyScg= github.com/rs/zerolog v1.19.0/go.mod h1:IzD0RJ65iWH0w97OQQebJEvTZYvsCUm9WVLWBQrJRjo= +github.com/rs/zerolog v1.21.0 h1:Q3vdXlfLNT+OftyBHsU0Y445MD+8m8axjKgf2si0QcM= +github.com/rs/zerolog v1.21.0/go.mod h1:ZPhntP/xmq1nnND05hhpAh2QMhSsA4UN3MGZ6O2J3hM= github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/segmentio/golines v0.0.0-20200824192126-7f30d3046793 h1:rhR7esJSmty+9ST6Gsp7mlQHkpISw2DiYjuFaz3dRDg= @@ -837,6 +843,7 @@ gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.4 h1:/eiJrUcujPVeJ3xlSWaiNi3uSVmDGBK1pDHUHAnao1I= gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU= gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= diff --git a/libs/go/libp2p_node/mocks/mock_host.go b/libs/go/libp2p_node/mocks/mock_host.go new file mode 100644 index 0000000000..fb890fa843 --- /dev/null +++ b/libs/go/libp2p_node/mocks/mock_host.go @@ -0,0 +1,224 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: github.com/libp2p/go-libp2p-core/host (interfaces: Host) + +// Package mock_host is a generated GoMock package. +package mocks + +import ( + context "context" + reflect "reflect" + + gomock "github.com/golang/mock/gomock" + connmgr "github.com/libp2p/go-libp2p-core/connmgr" + event "github.com/libp2p/go-libp2p-core/event" + network "github.com/libp2p/go-libp2p-core/network" + peer "github.com/libp2p/go-libp2p-core/peer" + peerstore "github.com/libp2p/go-libp2p-core/peerstore" + protocol "github.com/libp2p/go-libp2p-core/protocol" + multiaddr "github.com/multiformats/go-multiaddr" +) + +// MockHost is a mock of Host interface. +type MockHost struct { + ctrl *gomock.Controller + recorder *MockHostMockRecorder +} + +// MockHostMockRecorder is the mock recorder for MockHost. +type MockHostMockRecorder struct { + mock *MockHost +} + +// NewMockHost creates a new mock instance. +func NewMockHost(ctrl *gomock.Controller) *MockHost { + mock := &MockHost{ctrl: ctrl} + mock.recorder = &MockHostMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockHost) EXPECT() *MockHostMockRecorder { + return m.recorder +} + +// Addrs mocks base method. +func (m *MockHost) Addrs() []multiaddr.Multiaddr { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Addrs") + ret0, _ := ret[0].([]multiaddr.Multiaddr) + return ret0 +} + +// Addrs indicates an expected call of Addrs. +func (mr *MockHostMockRecorder) Addrs() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Addrs", reflect.TypeOf((*MockHost)(nil).Addrs)) +} + +// Close mocks base method. +func (m *MockHost) Close() error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Close") + ret0, _ := ret[0].(error) + return ret0 +} + +// Close indicates an expected call of Close. +func (mr *MockHostMockRecorder) Close() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Close", reflect.TypeOf((*MockHost)(nil).Close)) +} + +// ConnManager mocks base method. +func (m *MockHost) ConnManager() connmgr.ConnManager { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ConnManager") + ret0, _ := ret[0].(connmgr.ConnManager) + return ret0 +} + +// ConnManager indicates an expected call of ConnManager. +func (mr *MockHostMockRecorder) ConnManager() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ConnManager", reflect.TypeOf((*MockHost)(nil).ConnManager)) +} + +// Connect mocks base method. +func (m *MockHost) Connect(arg0 context.Context, arg1 peer.AddrInfo) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Connect", arg0, arg1) + ret0, _ := ret[0].(error) + return ret0 +} + +// Connect indicates an expected call of Connect. +func (mr *MockHostMockRecorder) Connect(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Connect", reflect.TypeOf((*MockHost)(nil).Connect), arg0, arg1) +} + +// EventBus mocks base method. +func (m *MockHost) EventBus() event.Bus { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "EventBus") + ret0, _ := ret[0].(event.Bus) + return ret0 +} + +// EventBus indicates an expected call of EventBus. +func (mr *MockHostMockRecorder) EventBus() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "EventBus", reflect.TypeOf((*MockHost)(nil).EventBus)) +} + +// ID mocks base method. +func (m *MockHost) ID() peer.ID { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ID") + ret0, _ := ret[0].(peer.ID) + return ret0 +} + +// ID indicates an expected call of ID. +func (mr *MockHostMockRecorder) ID() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ID", reflect.TypeOf((*MockHost)(nil).ID)) +} + +// Mux mocks base method. +func (m *MockHost) Mux() protocol.Switch { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Mux") + ret0, _ := ret[0].(protocol.Switch) + return ret0 +} + +// Mux indicates an expected call of Mux. +func (mr *MockHostMockRecorder) Mux() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Mux", reflect.TypeOf((*MockHost)(nil).Mux)) +} + +// Network mocks base method. +func (m *MockHost) Network() network.Network { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Network") + ret0, _ := ret[0].(network.Network) + return ret0 +} + +// Network indicates an expected call of Network. +func (mr *MockHostMockRecorder) Network() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Network", reflect.TypeOf((*MockHost)(nil).Network)) +} + +// NewStream mocks base method. +func (m *MockHost) NewStream(arg0 context.Context, arg1 peer.ID, arg2 ...protocol.ID) (network.Stream, error) { + m.ctrl.T.Helper() + varargs := []interface{}{arg0, arg1} + for _, a := range arg2 { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "NewStream", varargs...) + ret0, _ := ret[0].(network.Stream) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// NewStream indicates an expected call of NewStream. +func (mr *MockHostMockRecorder) NewStream(arg0, arg1 interface{}, arg2 ...interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]interface{}{arg0, arg1}, arg2...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "NewStream", reflect.TypeOf((*MockHost)(nil).NewStream), varargs...) +} + +// Peerstore mocks base method. +func (m *MockHost) Peerstore() peerstore.Peerstore { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Peerstore") + ret0, _ := ret[0].(peerstore.Peerstore) + return ret0 +} + +// Peerstore indicates an expected call of Peerstore. +func (mr *MockHostMockRecorder) Peerstore() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Peerstore", reflect.TypeOf((*MockHost)(nil).Peerstore)) +} + +// RemoveStreamHandler mocks base method. +func (m *MockHost) RemoveStreamHandler(arg0 protocol.ID) { + m.ctrl.T.Helper() + m.ctrl.Call(m, "RemoveStreamHandler", arg0) +} + +// RemoveStreamHandler indicates an expected call of RemoveStreamHandler. +func (mr *MockHostMockRecorder) RemoveStreamHandler(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RemoveStreamHandler", reflect.TypeOf((*MockHost)(nil).RemoveStreamHandler), arg0) +} + +// SetStreamHandler mocks base method. +func (m *MockHost) SetStreamHandler(arg0 protocol.ID, arg1 network.StreamHandler) { + m.ctrl.T.Helper() + m.ctrl.Call(m, "SetStreamHandler", arg0, arg1) +} + +// SetStreamHandler indicates an expected call of SetStreamHandler. +func (mr *MockHostMockRecorder) SetStreamHandler(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetStreamHandler", reflect.TypeOf((*MockHost)(nil).SetStreamHandler), arg0, arg1) +} + +// SetStreamHandlerMatch mocks base method. +func (m *MockHost) SetStreamHandlerMatch(arg0 protocol.ID, arg1 func(string) bool, arg2 network.StreamHandler) { + m.ctrl.T.Helper() + m.ctrl.Call(m, "SetStreamHandlerMatch", arg0, arg1, arg2) +} + +// SetStreamHandlerMatch indicates an expected call of SetStreamHandlerMatch. +func (mr *MockHostMockRecorder) SetStreamHandlerMatch(arg0, arg1, arg2 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetStreamHandlerMatch", reflect.TypeOf((*MockHost)(nil).SetStreamHandlerMatch), arg0, arg1, arg2) +} diff --git a/libs/go/libp2p_node/mocks/mock_net.go b/libs/go/libp2p_node/mocks/mock_net.go new file mode 100644 index 0000000000..2f6b440075 --- /dev/null +++ b/libs/go/libp2p_node/mocks/mock_net.go @@ -0,0 +1,150 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: net (interfaces: Conn) + +// Package mock_net is a generated GoMock package. +package mocks + +import ( + net "net" + reflect "reflect" + time "time" + + gomock "github.com/golang/mock/gomock" +) + +// MockConn is a mock of Conn interface. +type MockConn struct { + ctrl *gomock.Controller + recorder *MockConnMockRecorder +} + +// MockConnMockRecorder is the mock recorder for MockConn. +type MockConnMockRecorder struct { + mock *MockConn +} + +// NewMockConn creates a new mock instance. +func NewMockConn(ctrl *gomock.Controller) *MockConn { + mock := &MockConn{ctrl: ctrl} + mock.recorder = &MockConnMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockConn) EXPECT() *MockConnMockRecorder { + return m.recorder +} + +// Close mocks base method. +func (m *MockConn) Close() error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Close") + ret0, _ := ret[0].(error) + return ret0 +} + +// Close indicates an expected call of Close. +func (mr *MockConnMockRecorder) Close() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Close", reflect.TypeOf((*MockConn)(nil).Close)) +} + +// LocalAddr mocks base method. +func (m *MockConn) LocalAddr() net.Addr { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "LocalAddr") + ret0, _ := ret[0].(net.Addr) + return ret0 +} + +// LocalAddr indicates an expected call of LocalAddr. +func (mr *MockConnMockRecorder) LocalAddr() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "LocalAddr", reflect.TypeOf((*MockConn)(nil).LocalAddr)) +} + +// Read mocks base method. +func (m *MockConn) Read(arg0 []byte) (int, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Read", arg0) + ret0, _ := ret[0].(int) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Read indicates an expected call of Read. +func (mr *MockConnMockRecorder) Read(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Read", reflect.TypeOf((*MockConn)(nil).Read), arg0) +} + +// RemoteAddr mocks base method. +func (m *MockConn) RemoteAddr() net.Addr { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "RemoteAddr") + ret0, _ := ret[0].(net.Addr) + return ret0 +} + +// RemoteAddr indicates an expected call of RemoteAddr. +func (mr *MockConnMockRecorder) RemoteAddr() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RemoteAddr", reflect.TypeOf((*MockConn)(nil).RemoteAddr)) +} + +// SetDeadline mocks base method. +func (m *MockConn) SetDeadline(arg0 time.Time) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "SetDeadline", arg0) + ret0, _ := ret[0].(error) + return ret0 +} + +// SetDeadline indicates an expected call of SetDeadline. +func (mr *MockConnMockRecorder) SetDeadline(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetDeadline", reflect.TypeOf((*MockConn)(nil).SetDeadline), arg0) +} + +// SetReadDeadline mocks base method. +func (m *MockConn) SetReadDeadline(arg0 time.Time) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "SetReadDeadline", arg0) + ret0, _ := ret[0].(error) + return ret0 +} + +// SetReadDeadline indicates an expected call of SetReadDeadline. +func (mr *MockConnMockRecorder) SetReadDeadline(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetReadDeadline", reflect.TypeOf((*MockConn)(nil).SetReadDeadline), arg0) +} + +// SetWriteDeadline mocks base method. +func (m *MockConn) SetWriteDeadline(arg0 time.Time) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "SetWriteDeadline", arg0) + ret0, _ := ret[0].(error) + return ret0 +} + +// SetWriteDeadline indicates an expected call of SetWriteDeadline. +func (mr *MockConnMockRecorder) SetWriteDeadline(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetWriteDeadline", reflect.TypeOf((*MockConn)(nil).SetWriteDeadline), arg0) +} + +// Write mocks base method. +func (m *MockConn) Write(arg0 []byte) (int, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Write", arg0) + ret0, _ := ret[0].(int) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Write indicates an expected call of Write. +func (mr *MockConnMockRecorder) Write(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Write", reflect.TypeOf((*MockConn)(nil).Write), arg0) +} diff --git a/libs/go/libp2p_node/mocks/mock_network.go b/libs/go/libp2p_node/mocks/mock_network.go new file mode 100644 index 0000000000..ab38a029fc --- /dev/null +++ b/libs/go/libp2p_node/mocks/mock_network.go @@ -0,0 +1,191 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: github.com/libp2p/go-libp2p-core/network (interfaces: Stream) + +// Package mock_network is a generated GoMock package. +package mocks + +import ( + reflect "reflect" + time "time" + + gomock "github.com/golang/mock/gomock" + network "github.com/libp2p/go-libp2p-core/network" + protocol "github.com/libp2p/go-libp2p-core/protocol" +) + +// MockStream is a mock of Stream interface. +type MockStream struct { + ctrl *gomock.Controller + recorder *MockStreamMockRecorder +} + +// MockStreamMockRecorder is the mock recorder for MockStream. +type MockStreamMockRecorder struct { + mock *MockStream +} + +// NewMockStream creates a new mock instance. +func NewMockStream(ctrl *gomock.Controller) *MockStream { + mock := &MockStream{ctrl: ctrl} + mock.recorder = &MockStreamMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockStream) EXPECT() *MockStreamMockRecorder { + return m.recorder +} + +// Close mocks base method. +func (m *MockStream) Close() error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Close") + ret0, _ := ret[0].(error) + return ret0 +} + +// Close indicates an expected call of Close. +func (mr *MockStreamMockRecorder) Close() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Close", reflect.TypeOf((*MockStream)(nil).Close)) +} + +// Conn mocks base method. +func (m *MockStream) Conn() network.Conn { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Conn") + ret0, _ := ret[0].(network.Conn) + return ret0 +} + +// Conn indicates an expected call of Conn. +func (mr *MockStreamMockRecorder) Conn() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Conn", reflect.TypeOf((*MockStream)(nil).Conn)) +} + +// Protocol mocks base method. +func (m *MockStream) Protocol() protocol.ID { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Protocol") + ret0, _ := ret[0].(protocol.ID) + return ret0 +} + +// Protocol indicates an expected call of Protocol. +func (mr *MockStreamMockRecorder) Protocol() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Protocol", reflect.TypeOf((*MockStream)(nil).Protocol)) +} + +// Read mocks base method. +func (m *MockStream) Read(arg0 []byte) (int, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Read", arg0) + ret0, _ := ret[0].(int) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Read indicates an expected call of Read. +func (mr *MockStreamMockRecorder) Read(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Read", reflect.TypeOf((*MockStream)(nil).Read), arg0) +} + +// Reset mocks base method. +func (m *MockStream) Reset() error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Reset") + ret0, _ := ret[0].(error) + return ret0 +} + +// Reset indicates an expected call of Reset. +func (mr *MockStreamMockRecorder) Reset() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Reset", reflect.TypeOf((*MockStream)(nil).Reset)) +} + +// SetDeadline mocks base method. +func (m *MockStream) SetDeadline(arg0 time.Time) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "SetDeadline", arg0) + ret0, _ := ret[0].(error) + return ret0 +} + +// SetDeadline indicates an expected call of SetDeadline. +func (mr *MockStreamMockRecorder) SetDeadline(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetDeadline", reflect.TypeOf((*MockStream)(nil).SetDeadline), arg0) +} + +// SetProtocol mocks base method. +func (m *MockStream) SetProtocol(arg0 protocol.ID) { + m.ctrl.T.Helper() + m.ctrl.Call(m, "SetProtocol", arg0) +} + +// SetProtocol indicates an expected call of SetProtocol. +func (mr *MockStreamMockRecorder) SetProtocol(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetProtocol", reflect.TypeOf((*MockStream)(nil).SetProtocol), arg0) +} + +// SetReadDeadline mocks base method. +func (m *MockStream) SetReadDeadline(arg0 time.Time) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "SetReadDeadline", arg0) + ret0, _ := ret[0].(error) + return ret0 +} + +// SetReadDeadline indicates an expected call of SetReadDeadline. +func (mr *MockStreamMockRecorder) SetReadDeadline(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetReadDeadline", reflect.TypeOf((*MockStream)(nil).SetReadDeadline), arg0) +} + +// SetWriteDeadline mocks base method. +func (m *MockStream) SetWriteDeadline(arg0 time.Time) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "SetWriteDeadline", arg0) + ret0, _ := ret[0].(error) + return ret0 +} + +// SetWriteDeadline indicates an expected call of SetWriteDeadline. +func (mr *MockStreamMockRecorder) SetWriteDeadline(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetWriteDeadline", reflect.TypeOf((*MockStream)(nil).SetWriteDeadline), arg0) +} + +// Stat mocks base method. +func (m *MockStream) Stat() network.Stat { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Stat") + ret0, _ := ret[0].(network.Stat) + return ret0 +} + +// Stat indicates an expected call of Stat. +func (mr *MockStreamMockRecorder) Stat() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Stat", reflect.TypeOf((*MockStream)(nil).Stat)) +} + +// Write mocks base method. +func (m *MockStream) Write(arg0 []byte) (int, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Write", arg0) + ret0, _ := ret[0].(int) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Write indicates an expected call of Write. +func (mr *MockStreamMockRecorder) Write(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Write", reflect.TypeOf((*MockStream)(nil).Write), arg0) +} diff --git a/libs/go/libp2p_node/mocks/mock_peerstore.go b/libs/go/libp2p_node/mocks/mock_peerstore.go new file mode 100644 index 0000000000..c1e2568dcc --- /dev/null +++ b/libs/go/libp2p_node/mocks/mock_peerstore.go @@ -0,0 +1,412 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: github.com/libp2p/go-libp2p-core/peerstore (interfaces: Peerstore) + +// Package mock_peerstore is a generated GoMock package. +package mocks + +import ( + context "context" + reflect "reflect" + time "time" + + gomock "github.com/golang/mock/gomock" + crypto "github.com/libp2p/go-libp2p-core/crypto" + peer "github.com/libp2p/go-libp2p-core/peer" + multiaddr "github.com/multiformats/go-multiaddr" +) + +// MockPeerstore is a mock of Peerstore interface. +type MockPeerstore struct { + ctrl *gomock.Controller + recorder *MockPeerstoreMockRecorder +} + +// MockPeerstoreMockRecorder is the mock recorder for MockPeerstore. +type MockPeerstoreMockRecorder struct { + mock *MockPeerstore +} + +// NewMockPeerstore creates a new mock instance. +func NewMockPeerstore(ctrl *gomock.Controller) *MockPeerstore { + mock := &MockPeerstore{ctrl: ctrl} + mock.recorder = &MockPeerstoreMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockPeerstore) EXPECT() *MockPeerstoreMockRecorder { + return m.recorder +} + +// AddAddr mocks base method. +func (m *MockPeerstore) AddAddr(arg0 peer.ID, arg1 multiaddr.Multiaddr, arg2 time.Duration) { + m.ctrl.T.Helper() + m.ctrl.Call(m, "AddAddr", arg0, arg1, arg2) +} + +// AddAddr indicates an expected call of AddAddr. +func (mr *MockPeerstoreMockRecorder) AddAddr(arg0, arg1, arg2 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddAddr", reflect.TypeOf((*MockPeerstore)(nil).AddAddr), arg0, arg1, arg2) +} + +// AddAddrs mocks base method. +func (m *MockPeerstore) AddAddrs(arg0 peer.ID, arg1 []multiaddr.Multiaddr, arg2 time.Duration) { + m.ctrl.T.Helper() + m.ctrl.Call(m, "AddAddrs", arg0, arg1, arg2) +} + +// AddAddrs indicates an expected call of AddAddrs. +func (mr *MockPeerstoreMockRecorder) AddAddrs(arg0, arg1, arg2 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddAddrs", reflect.TypeOf((*MockPeerstore)(nil).AddAddrs), arg0, arg1, arg2) +} + +// AddPrivKey mocks base method. +func (m *MockPeerstore) AddPrivKey(arg0 peer.ID, arg1 crypto.PrivKey) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "AddPrivKey", arg0, arg1) + ret0, _ := ret[0].(error) + return ret0 +} + +// AddPrivKey indicates an expected call of AddPrivKey. +func (mr *MockPeerstoreMockRecorder) AddPrivKey(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddPrivKey", reflect.TypeOf((*MockPeerstore)(nil).AddPrivKey), arg0, arg1) +} + +// AddProtocols mocks base method. +func (m *MockPeerstore) AddProtocols(arg0 peer.ID, arg1 ...string) error { + m.ctrl.T.Helper() + varargs := []interface{}{arg0} + for _, a := range arg1 { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "AddProtocols", varargs...) + ret0, _ := ret[0].(error) + return ret0 +} + +// AddProtocols indicates an expected call of AddProtocols. +func (mr *MockPeerstoreMockRecorder) AddProtocols(arg0 interface{}, arg1 ...interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]interface{}{arg0}, arg1...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddProtocols", reflect.TypeOf((*MockPeerstore)(nil).AddProtocols), varargs...) +} + +// AddPubKey mocks base method. +func (m *MockPeerstore) AddPubKey(arg0 peer.ID, arg1 crypto.PubKey) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "AddPubKey", arg0, arg1) + ret0, _ := ret[0].(error) + return ret0 +} + +// AddPubKey indicates an expected call of AddPubKey. +func (mr *MockPeerstoreMockRecorder) AddPubKey(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddPubKey", reflect.TypeOf((*MockPeerstore)(nil).AddPubKey), arg0, arg1) +} + +// AddrStream mocks base method. +func (m *MockPeerstore) AddrStream(arg0 context.Context, arg1 peer.ID) <-chan multiaddr.Multiaddr { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "AddrStream", arg0, arg1) + ret0, _ := ret[0].(<-chan multiaddr.Multiaddr) + return ret0 +} + +// AddrStream indicates an expected call of AddrStream. +func (mr *MockPeerstoreMockRecorder) AddrStream(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddrStream", reflect.TypeOf((*MockPeerstore)(nil).AddrStream), arg0, arg1) +} + +// Addrs mocks base method. +func (m *MockPeerstore) Addrs(arg0 peer.ID) []multiaddr.Multiaddr { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Addrs", arg0) + ret0, _ := ret[0].([]multiaddr.Multiaddr) + return ret0 +} + +// Addrs indicates an expected call of Addrs. +func (mr *MockPeerstoreMockRecorder) Addrs(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Addrs", reflect.TypeOf((*MockPeerstore)(nil).Addrs), arg0) +} + +// ClearAddrs mocks base method. +func (m *MockPeerstore) ClearAddrs(arg0 peer.ID) { + m.ctrl.T.Helper() + m.ctrl.Call(m, "ClearAddrs", arg0) +} + +// ClearAddrs indicates an expected call of ClearAddrs. +func (mr *MockPeerstoreMockRecorder) ClearAddrs(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ClearAddrs", reflect.TypeOf((*MockPeerstore)(nil).ClearAddrs), arg0) +} + +// Close mocks base method. +func (m *MockPeerstore) Close() error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Close") + ret0, _ := ret[0].(error) + return ret0 +} + +// Close indicates an expected call of Close. +func (mr *MockPeerstoreMockRecorder) Close() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Close", reflect.TypeOf((*MockPeerstore)(nil).Close)) +} + +// Get mocks base method. +func (m *MockPeerstore) Get(arg0 peer.ID, arg1 string) (interface{}, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Get", arg0, arg1) + ret0, _ := ret[0].(interface{}) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Get indicates an expected call of Get. +func (mr *MockPeerstoreMockRecorder) Get(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Get", reflect.TypeOf((*MockPeerstore)(nil).Get), arg0, arg1) +} + +// GetProtocols mocks base method. +func (m *MockPeerstore) GetProtocols(arg0 peer.ID) ([]string, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetProtocols", arg0) + ret0, _ := ret[0].([]string) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetProtocols indicates an expected call of GetProtocols. +func (mr *MockPeerstoreMockRecorder) GetProtocols(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetProtocols", reflect.TypeOf((*MockPeerstore)(nil).GetProtocols), arg0) +} + +// LatencyEWMA mocks base method. +func (m *MockPeerstore) LatencyEWMA(arg0 peer.ID) time.Duration { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "LatencyEWMA", arg0) + ret0, _ := ret[0].(time.Duration) + return ret0 +} + +// LatencyEWMA indicates an expected call of LatencyEWMA. +func (mr *MockPeerstoreMockRecorder) LatencyEWMA(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "LatencyEWMA", reflect.TypeOf((*MockPeerstore)(nil).LatencyEWMA), arg0) +} + +// PeerInfo mocks base method. +func (m *MockPeerstore) PeerInfo(arg0 peer.ID) peer.AddrInfo { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "PeerInfo", arg0) + ret0, _ := ret[0].(peer.AddrInfo) + return ret0 +} + +// PeerInfo indicates an expected call of PeerInfo. +func (mr *MockPeerstoreMockRecorder) PeerInfo(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PeerInfo", reflect.TypeOf((*MockPeerstore)(nil).PeerInfo), arg0) +} + +// Peers mocks base method. +func (m *MockPeerstore) Peers() peer.IDSlice { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Peers") + ret0, _ := ret[0].(peer.IDSlice) + return ret0 +} + +// Peers indicates an expected call of Peers. +func (mr *MockPeerstoreMockRecorder) Peers() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Peers", reflect.TypeOf((*MockPeerstore)(nil).Peers)) +} + +// PeersWithAddrs mocks base method. +func (m *MockPeerstore) PeersWithAddrs() peer.IDSlice { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "PeersWithAddrs") + ret0, _ := ret[0].(peer.IDSlice) + return ret0 +} + +// PeersWithAddrs indicates an expected call of PeersWithAddrs. +func (mr *MockPeerstoreMockRecorder) PeersWithAddrs() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PeersWithAddrs", reflect.TypeOf((*MockPeerstore)(nil).PeersWithAddrs)) +} + +// PeersWithKeys mocks base method. +func (m *MockPeerstore) PeersWithKeys() peer.IDSlice { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "PeersWithKeys") + ret0, _ := ret[0].(peer.IDSlice) + return ret0 +} + +// PeersWithKeys indicates an expected call of PeersWithKeys. +func (mr *MockPeerstoreMockRecorder) PeersWithKeys() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PeersWithKeys", reflect.TypeOf((*MockPeerstore)(nil).PeersWithKeys)) +} + +// PrivKey mocks base method. +func (m *MockPeerstore) PrivKey(arg0 peer.ID) crypto.PrivKey { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "PrivKey", arg0) + ret0, _ := ret[0].(crypto.PrivKey) + return ret0 +} + +// PrivKey indicates an expected call of PrivKey. +func (mr *MockPeerstoreMockRecorder) PrivKey(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PrivKey", reflect.TypeOf((*MockPeerstore)(nil).PrivKey), arg0) +} + +// PubKey mocks base method. +func (m *MockPeerstore) PubKey(arg0 peer.ID) crypto.PubKey { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "PubKey", arg0) + ret0, _ := ret[0].(crypto.PubKey) + return ret0 +} + +// PubKey indicates an expected call of PubKey. +func (mr *MockPeerstoreMockRecorder) PubKey(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PubKey", reflect.TypeOf((*MockPeerstore)(nil).PubKey), arg0) +} + +// Put mocks base method. +func (m *MockPeerstore) Put(arg0 peer.ID, arg1 string, arg2 interface{}) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Put", arg0, arg1, arg2) + ret0, _ := ret[0].(error) + return ret0 +} + +// Put indicates an expected call of Put. +func (mr *MockPeerstoreMockRecorder) Put(arg0, arg1, arg2 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Put", reflect.TypeOf((*MockPeerstore)(nil).Put), arg0, arg1, arg2) +} + +// RecordLatency mocks base method. +func (m *MockPeerstore) RecordLatency(arg0 peer.ID, arg1 time.Duration) { + m.ctrl.T.Helper() + m.ctrl.Call(m, "RecordLatency", arg0, arg1) +} + +// RecordLatency indicates an expected call of RecordLatency. +func (mr *MockPeerstoreMockRecorder) RecordLatency(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RecordLatency", reflect.TypeOf((*MockPeerstore)(nil).RecordLatency), arg0, arg1) +} + +// RemoveProtocols mocks base method. +func (m *MockPeerstore) RemoveProtocols(arg0 peer.ID, arg1 ...string) error { + m.ctrl.T.Helper() + varargs := []interface{}{arg0} + for _, a := range arg1 { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "RemoveProtocols", varargs...) + ret0, _ := ret[0].(error) + return ret0 +} + +// RemoveProtocols indicates an expected call of RemoveProtocols. +func (mr *MockPeerstoreMockRecorder) RemoveProtocols(arg0 interface{}, arg1 ...interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]interface{}{arg0}, arg1...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RemoveProtocols", reflect.TypeOf((*MockPeerstore)(nil).RemoveProtocols), varargs...) +} + +// SetAddr mocks base method. +func (m *MockPeerstore) SetAddr(arg0 peer.ID, arg1 multiaddr.Multiaddr, arg2 time.Duration) { + m.ctrl.T.Helper() + m.ctrl.Call(m, "SetAddr", arg0, arg1, arg2) +} + +// SetAddr indicates an expected call of SetAddr. +func (mr *MockPeerstoreMockRecorder) SetAddr(arg0, arg1, arg2 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetAddr", reflect.TypeOf((*MockPeerstore)(nil).SetAddr), arg0, arg1, arg2) +} + +// SetAddrs mocks base method. +func (m *MockPeerstore) SetAddrs(arg0 peer.ID, arg1 []multiaddr.Multiaddr, arg2 time.Duration) { + m.ctrl.T.Helper() + m.ctrl.Call(m, "SetAddrs", arg0, arg1, arg2) +} + +// SetAddrs indicates an expected call of SetAddrs. +func (mr *MockPeerstoreMockRecorder) SetAddrs(arg0, arg1, arg2 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetAddrs", reflect.TypeOf((*MockPeerstore)(nil).SetAddrs), arg0, arg1, arg2) +} + +// SetProtocols mocks base method. +func (m *MockPeerstore) SetProtocols(arg0 peer.ID, arg1 ...string) error { + m.ctrl.T.Helper() + varargs := []interface{}{arg0} + for _, a := range arg1 { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "SetProtocols", varargs...) + ret0, _ := ret[0].(error) + return ret0 +} + +// SetProtocols indicates an expected call of SetProtocols. +func (mr *MockPeerstoreMockRecorder) SetProtocols(arg0 interface{}, arg1 ...interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]interface{}{arg0}, arg1...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetProtocols", reflect.TypeOf((*MockPeerstore)(nil).SetProtocols), varargs...) +} + +// SupportsProtocols mocks base method. +func (m *MockPeerstore) SupportsProtocols(arg0 peer.ID, arg1 ...string) ([]string, error) { + m.ctrl.T.Helper() + varargs := []interface{}{arg0} + for _, a := range arg1 { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "SupportsProtocols", varargs...) + ret0, _ := ret[0].([]string) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// SupportsProtocols indicates an expected call of SupportsProtocols. +func (mr *MockPeerstoreMockRecorder) SupportsProtocols(arg0 interface{}, arg1 ...interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]interface{}{arg0}, arg1...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SupportsProtocols", reflect.TypeOf((*MockPeerstore)(nil).SupportsProtocols), varargs...) +} + +// UpdateAddrs mocks base method. +func (m *MockPeerstore) UpdateAddrs(arg0 peer.ID, arg1, arg2 time.Duration) { + m.ctrl.T.Helper() + m.ctrl.Call(m, "UpdateAddrs", arg0, arg1, arg2) +} + +// UpdateAddrs indicates an expected call of UpdateAddrs. +func (mr *MockPeerstoreMockRecorder) UpdateAddrs(arg0, arg1, arg2 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateAddrs", reflect.TypeOf((*MockPeerstore)(nil).UpdateAddrs), arg0, arg1, arg2) +} diff --git a/libs/go/libp2p_node/utils/utils.go b/libs/go/libp2p_node/utils/utils.go index 573ad8f004..c8b533be0e 100644 --- a/libs/go/libp2p_node/utils/utils.go +++ b/libs/go/libp2p_node/utils/utils.go @@ -82,7 +82,7 @@ var logger zerolog.Logger = NewDefaultLogger() // SetLoggerLevel set utils logger level func SetLoggerLevel(lvl zerolog.Level) { - logger.Level(lvl) + logger = logger.Level(lvl) } func ignore(err error) { @@ -180,7 +180,6 @@ func BootstrapConnect( if count == len(peers) { return errors.New("failed to bootstrap: " + err.Error()) } - // workaround: to avoid getting `failed to find any peer in table` // when calling dht.Provide (happens occasionally) logger.Debug().Msg("waiting for bootstrap peers to be added to dht routing table...") @@ -252,7 +251,7 @@ func FetchAIPublicKeyFromPubKey(publicKey crypto.PubKey) (string, error) { return hex.EncodeToString(raw), nil } -// BTCPubKeyFromFetchAIPublicKey +// BTCPubKeyFromFetchAIPublicKey from public key string func BTCPubKeyFromFetchAIPublicKey(publicKey string) (*btcec.PublicKey, error) { pbkBytes, err := hex.DecodeString(publicKey) if err != nil { @@ -268,7 +267,7 @@ func BTCPubKeyFromEthereumPublicKey(publicKey string) (*btcec.PublicKey, error) return BTCPubKeyFromUncompressedHex(publicKey[2:]) } -// ConvertStrEncodedSignatureToDER +// ConvertStrEncodedSignatureToDER to convert signature to DER format // References: // - https://github.com/fetchai/agents-aea/blob/main/aea/crypto/cosmos.py#L258 // - https://github.com/btcsuite/btcd/blob/master/btcec/signature.go#L47 @@ -288,7 +287,7 @@ func ConvertStrEncodedSignatureToDER(signature []byte) []byte { return sigDER } -// ConvertDEREncodedSignatureToStr +// ConvertDEREncodedSignatureToStr Convert signatue from der format to string // References: // - https://github.com/fetchai/agents-aea/blob/main/aea/crypto/cosmos.py#L258 // - https://github.com/btcsuite/btcd/blob/master/btcec/signature.go#L47 @@ -316,14 +315,14 @@ func ParseFetchAISignature(signature string) (*btcec.Signature, error) { // VerifyLedgerSignature verify signature of message using public key for supported ledgers func VerifyLedgerSignature( - ledgerId string, + ledgerID string, message []byte, signature string, - pubkey string, + pubKey string, ) (bool, error) { - verifySignature, found := verifyLedgerSignatureTable[ledgerId] + verifySignature, found := verifyLedgerSignatureTable[ledgerID] if found { - return verifySignature(message, signature, pubkey) + return verifySignature(message, signature, pubKey) } return false, errors.New("unsupported ledger") } @@ -371,6 +370,7 @@ func VerifyFetchAISignatureLibp2p(message []byte, signature string, pubkey strin return verifyKey.Verify(message, sigDER) } +// SignFetchAI signs message with private key func SignFetchAI(message []byte, privKey string) (string, error) { signingKey, _, err := KeyPairFromFetchAIKey(privKey) if err != nil { @@ -420,8 +420,8 @@ func RecoverAddressFromEthereumSignature(message []byte, signature string) (stri // VerifyEthereumSignatureETH verify ethereum signature using ethereum public key func VerifyEthereumSignatureETH(message []byte, signature string, pubkey string) (bool, error) { - // get expected signer address - expectedAddress, err := EthereumAddressFromPublicKey(pubkey) + // get ted signer address + tedAddress, err := EthereumAddressFromPublicKey(pubkey) if err != nil { return false, err } @@ -432,8 +432,8 @@ func VerifyEthereumSignatureETH(message []byte, signature string, pubkey string) return false, err } - if recoveredAddress != expectedAddress { - return false, errors.New("recovered and expected addresses don't match") + if recoveredAddress != tedAddress { + return false, errors.New("recovered and ted addresses don't match") } return true, nil @@ -441,13 +441,13 @@ func VerifyEthereumSignatureETH(message []byte, signature string, pubkey string) // KeyPairFromFetchAIKey key pair from hex encoded secp256k1 private key func KeyPairFromFetchAIKey(key string) (crypto.PrivKey, crypto.PubKey, error) { - pk_bytes, err := hex.DecodeString(key) + pkBytes, err := hex.DecodeString(key) if err != nil { return nil, nil, err } - btc_private_key, _ := btcec.PrivKeyFromBytes(btcec.S256(), pk_bytes) - prvKey, pubKey, err := crypto.KeyPairFromStdKey(btc_private_key) + btcPrivateKey, _ := btcec.PrivKeyFromBytes(btcec.S256(), pkBytes) + prvKey, pubKey, err := crypto.KeyPairFromStdKey(btcPrivateKey) if err != nil { return nil, nil, err } @@ -457,11 +457,11 @@ func KeyPairFromFetchAIKey(key string) (crypto.PrivKey, crypto.PubKey, error) { // AgentAddressFromPublicKey get wallet address from public key associated with ledgerId // format from: https://github.com/fetchai/agents-aea/blob/main/aea/crypto/cosmos.py#L120 -func AgentAddressFromPublicKey(ledgerId string, publicKey string) (string, error) { - if addressFromPublicKey, found := addressFromPublicKeyTable[ledgerId]; found { +func AgentAddressFromPublicKey(ledgerID string, publicKey string) (string, error) { + if addressFromPublicKey, found := addressFromPublicKeyTable[ledgerID]; found { return addressFromPublicKey(publicKey) } - return "", errors.New("unsupported ledger " + ledgerId) + return "", errors.New("Unsupported ledger " + ledgerID) } // FetchAIAddressFromPublicKey get wallet address from hex encoded secp256k1 public key @@ -549,18 +549,18 @@ func encodeChecksumEIP55(address []byte) string { } // IDFromFetchAIPublicKey Get PeeID (multihash) from fetchai public key -func IDFromFetchAIPublicKey(public_key string) (peer.ID, error) { - b, err := hex.DecodeString(public_key) +func IDFromFetchAIPublicKey(publicKey string) (peer.ID, error) { + b, err := hex.DecodeString(publicKey) if err != nil { return "", err } - pub_key, err := btcec.ParsePubKey(b, btcec.S256()) + pubKey, err := btcec.ParsePubKey(b, btcec.S256()) if err != nil { return "", err } - multihash, err := peer.IDFromPublicKey((*crypto.Secp256k1PublicKey)(pub_key)) + multihash, err := peer.IDFromPublicKey((*crypto.Secp256k1PublicKey)(pubKey)) if err != nil { return "", err } @@ -597,6 +597,7 @@ func IDFromFetchAIPublicKeyUncompressed(publicKey string) (peer.ID, error) { return multihash, nil } +// FetchAIPublicKeyFromFetchAIPrivateKey get fetchai public key from fetchai private key func FetchAIPublicKeyFromFetchAIPrivateKey(privateKey string) (string, error) { pkBytes, err := hex.DecodeString(privateKey) if err != nil { @@ -613,6 +614,7 @@ func FetchAIPublicKeyFromFetchAIPrivateKey(privateKey string) (string, error) { // WriteBytesConn send bytes to `conn` func WriteBytesConn(conn net.Conn, data []byte) error { + if len(data) > math.MaxInt32 { logger.Error().Msg("data size too large") return errors.New("data size too large") @@ -621,6 +623,7 @@ func WriteBytesConn(conn net.Conn, data []byte) error { logger.Error().Msg("No data to write") return nil } + size := uint32(len(data)) buf := make([]byte, 4, 4+size) binary.BigEndian.PutUint32(buf, size) @@ -672,6 +675,7 @@ func ReadBytes(s network.Stream) ([]byte, error) { if s == nil { panic("CRITICAL can not write to nil stream") } + rstream := bufio.NewReader(s) buf := make([]byte, 4) @@ -687,6 +691,7 @@ func ReadBytes(s network.Stream) ([]byte, error) { if size > maxMessageSizeDelegateConnection { return nil, errors.New("expected message size larger than maximum allowed") } + //logger.Debug().Msgf("expecting %d", size) buf = make([]byte, size) @@ -709,6 +714,7 @@ func WriteBytes(s network.Stream, data []byte) error { if s == nil { panic("CRITICAL, can not write to nil stream") } + wstream := bufio.NewWriter(s) size := uint32(len(data)) @@ -723,6 +729,7 @@ func WriteBytes(s network.Stream, data []byte) error { return err } + //logger.Debug().Msgf("writing %d", len(data)) _, err = wstream.Write(data) if err != nil { logger.Error(). @@ -733,12 +740,73 @@ func WriteBytes(s network.Stream, data []byte) error { if s == nil { panic("CRITICAL, can not flush nil stream") } + err = wstream.Flush() + return err +} + +// ReadString from a network stream +func ReadString(s network.Stream) (string, error) { + data, err := ReadBytes(s) + return string(data), err +} + +// WriteEnvelope to a network stream +func WriteEnvelope(envel *aea.Envelope, s network.Stream) error { + wstream := bufio.NewWriter(s) + data, err := proto.Marshal(envel) + if err != nil { + return err + } + size := uint32(len(data)) + + buf := make([]byte, 4) + binary.BigEndian.PutUint32(buf, size) + //log.Println("DEBUG writing size:", size, buf) + _, err = wstream.Write(buf) + if err != nil { + return err + } + + //log.Println("DEBUG writing data:", data) + _, err = wstream.Write(data) + if err != nil { + return err + } + + wstream.Flush() + return nil +} + +// ReadEnvelope from a network stream +func ReadEnvelope(s network.Stream) (*aea.Envelope, error) { + envel := &aea.Envelope{} + rstream := bufio.NewReader(s) + + buf := make([]byte, 4) + _, err := io.ReadFull(rstream, buf) + if err != nil { logger.Error(). Str("err", err.Error()). - Msg("Error on stream flush") - return err + Msg("while reading size") + return envel, err } - return err + + size := binary.BigEndian.Uint32(buf) + if size > maxMessageSizeDelegateConnection { + return nil, errors.New("ted message size larger than maximum allowed") + } + //logger.Debug().Msgf("received size: %d %x", size, buf) + buf = make([]byte, size) + _, err = io.ReadFull(rstream, buf) + if err != nil { + logger.Error(). + Str("err", err.Error()). + Msg("while reading data") + return envel, err + } + + err = proto.Unmarshal(buf, envel) + return envel, err } diff --git a/libs/go/libp2p_node/utils/utils_test.go b/libs/go/libp2p_node/utils/utils_test.go new file mode 100644 index 0000000000..f959b0cabd --- /dev/null +++ b/libs/go/libp2p_node/utils/utils_test.go @@ -0,0 +1,475 @@ +/* -*- coding: utf-8 -*- +* ------------------------------------------------------------------------------ +* +* Copyright 2018-2021 Fetch.AI Limited +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +* +* ------------------------------------------------------------------------------ + */ + +package utils + +import ( + "bufio" + "bytes" + "context" + "encoding/json" + "errors" + "io" + "libp2p_node/aea" + mocks "libp2p_node/mocks" + "net" + "reflect" + "testing" + + "bou.ke/monkey" + gomock "github.com/golang/mock/gomock" + "github.com/libp2p/go-libp2p-core/peer" + dht "github.com/libp2p/go-libp2p-kad-dht" + kb "github.com/libp2p/go-libp2p-kbucket" + ma "github.com/multiformats/go-multiaddr" + "github.com/rs/zerolog" + "github.com/stretchr/testify/assert" +) + +// Crypto operations + +func TestEthereumCrypto(t *testing.T) { + //privateKey := "0xb60fe8027fb82f1a1bd6b8e66d4400f858989a2c67428a4e7f589441700339b0" + publicKey := "0xf753e5a9e2368e97f4db869a0d956d3ffb64672d6392670572906c786b5712ada13b6bff882951b3ba3dd65bdacc915c2b532efc3f183aa44657205c6c337225" + address := "0xb8d8c62d4a1999b7aea0aebBD5020244a4a9bAD8" + publicKeySignature := "0x304c2ba4ae7fa71295bfc2920b9c1268d574d65531f1f4d2117fc1439a45310c37ab75085a9df2a4169a4d47982b330a4387b1ded0c8881b030629db30bbaf3a1c" + + addFromPublicKey, err := EthereumAddressFromPublicKey(publicKey) + if err != nil || addFromPublicKey != address { + t.Error( + "Error when computing address from public key or address and public key don't match", + ) + } + + _, err = BTCPubKeyFromEthereumPublicKey(publicKey) + if err != nil { + t.Errorf("While building BTC public key from string: %s", err.Error()) + } + + /* + ethSig, err := secp256k1.Sign(hashedPublicKey, hexutil.MustDecode(privateKey)) + if err != nil { + t.Error(err.Error()) + } + println(hexutil.Encode(ethSig)) + hash := sha3.NewLegacyKeccak256() + _, err = hash.Write([]byte(publicKey)) + if err != nil { + t.Error(err.Error()) + } + sha3KeccakHash := hash.Sum(nil) + */ + + valid, err := VerifyEthereumSignatureETH([]byte(publicKey), publicKeySignature, publicKey) + if err != nil { + t.Error(err.Error()) + } + + if !valid { + t.Errorf("Signer address don't match %s", addFromPublicKey) + } +} + +func TestFetchAICrypto(t *testing.T) { + publicKey := "02358e3e42a6ba15cf6b2ba6eb05f02b8893acf82b316d7dd9cda702b0892b8c71" + address := "fetch19dq2mkcpp6x0aypxt9c9gz6n4fqvax0x9a7t5r" + peerPublicKey := "027af21aff853b9d9589867ea142b0a60a9611fc8e1fae04c2f7144113fa4e938e" + pySigStrCanonize := "N/GOa7/m3HU8/gpLJ88VCQ6vXsdrfiiYcqnNtF+c2N9VG9ZIiycykN4hdbpbOCGrChMYZQA3G1GpozsShrUBgg==" + + addressFromPublicKey, _ := FetchAIAddressFromPublicKey(publicKey) + if address != addressFromPublicKey { + t.Error("[ERR] Addresses don't match") + } else { + t.Log("[OK] Agent address matches its public key") + } + + valid, err := VerifyFetchAISignatureBTC( + []byte(peerPublicKey), + pySigStrCanonize, + publicKey, + ) + if !valid { + t.Errorf("Signature using BTC don't match %s", err.Error()) + } + valid, err = VerifyFetchAISignatureLibp2p( + []byte(peerPublicKey), + pySigStrCanonize, + publicKey, + ) + if !valid { + t.Errorf("Signature using LPP don't match %s", err.Error()) + } +} + +func TestSetLoggerLevel(t *testing.T) { + assert.Equal(t, logger.GetLevel(), zerolog.Level(0), "Initial log level is not 0") + + lvl := zerolog.InfoLevel + SetLoggerLevel(lvl) + + assert.Equal( + t, + logger.GetLevel(), + lvl, + "Waited for logger level %d but got %d", + lvl, + logger.GetLevel(), + ) +} + +func Example_ignore() { + ignore(errors.New("Test")) + // Output: IGNORED: Test +} + +func TestNewDefaultLoggerWithFields(t *testing.T) { + fields := map[string]string{ + "test_field": "test_value", + } + var logBuffer bytes.Buffer + logger := NewDefaultLoggerWithFields(fields).Output(&logBuffer) + logger.Info().Msg("test") + var jsonResult map[string]interface{} + err := json.Unmarshal(logBuffer.Bytes(), &jsonResult) + assert.Equal(t, nil, err) + assert.Equal(t, jsonResult["test_field"], "test_value") +} + +func TestComputeCID(t *testing.T) { + address := "fetch19dq2mkcpp6x0aypxt9c9gz6n4fqvax0x9a7t5r" + cid, err := ComputeCID(address) + assert.Equal(t, nil, err) + assert.Equal(t, "QmZ6ryKyS9rSnesX8YnFLAmFwFuRMdHpE7pQ2V6SjXTbqM", cid.String()) +} + +func TestWriteBytes(t *testing.T) { + mockCtrl := gomock.NewController(t) + defer mockCtrl.Finish() + mockStream := mocks.NewMockStream(mockCtrl) + mockStream.EXPECT().Write([]byte{0, 0, 0, 5, 104, 101, 108, 108, 111}).Return(9, nil).Times(1) + err := WriteBytes(mockStream, []byte("hello")) + assert.Equal(t, nil, err) + + mockStream.EXPECT(). + Write([]byte{0, 0, 0, 4, 104, 101, 108, 108}). + Return(8, errors.New("oops")). + Times(1) + err = WriteBytes(mockStream, []byte("hell")) + assert.NotEqual(t, err, nil) +} + +func TestReadBytesConn(t *testing.T) { + mockCtrl := gomock.NewController(t) + defer mockCtrl.Finish() + mockConn := mocks.NewMockConn(mockCtrl) + mockConn.EXPECT().Read(gomock.Any()).Return(4, nil).Times(2) + buf, err := ReadBytesConn(mockConn) + assert.Equal(t, nil, err) + assert.Equal(t, "", string(buf)) +} + +func TestWriteBytesConn(t *testing.T) { + mockCtrl := gomock.NewController(t) + defer mockCtrl.Finish() + mockConn := mocks.NewMockConn(mockCtrl) + mockConn.EXPECT().Write(gomock.Any()).Return(0, nil).Times(1) + err := WriteBytesConn(mockConn, []byte("ABC")) + assert.Equal(t, nil, err) +} + +func TestReadString(t *testing.T) { + // test ReadString and ReadBytes + mockCtrl := gomock.NewController(t) + defer mockCtrl.Finish() + mockStream := mocks.NewMockStream(mockCtrl) + + defer monkey.UnpatchAll() + + t.Run("TestReadString", func(t *testing.T) { + monkey.Patch(bufio.NewReader, func(reader io.Reader) *bufio.Reader { + return bufio.NewReaderSize( + bytes.NewReader([]byte{0, 0, 0, 5, 104, 101, 108, 108, 111}), + 100, + ) + }) + buf, err := ReadString(mockStream) + assert.Equal(t, nil, err) + assert.Equal(t, "hello", buf) + }) +} + +func TestReadWriteEnvelope(t *testing.T) { + mockCtrl := gomock.NewController(t) + defer mockCtrl.Finish() + mockStream := mocks.NewMockStream(mockCtrl) + defer monkey.UnpatchAll() + address := "0xb8d8c62d4a1999b7aea0aebBD5020244a4a9bAD8" + buffer := bytes.NewBuffer([]byte{}) + + t.Run("TestWriteEnvelope", func(t *testing.T) { + monkey.Patch(bufio.NewWriter, func(writer io.Writer) *bufio.Writer { + return bufio.NewWriterSize(buffer, 100) + }) + err := WriteEnvelope(&aea.Envelope{ + To: address, + Sender: address, + }, mockStream) + assert.Equal(t, nil, err) + }) + + t.Run("TestReadEnvelope", func(t *testing.T) { + monkey.Patch(bufio.NewReader, func(reader io.Reader) *bufio.Reader { + return bufio.NewReaderSize(bytes.NewReader(buffer.Bytes()), 100) + }) + env, err := ReadEnvelope(mockStream) + assert.Equal(t, nil, err) + assert.Equal(t, address, env.To) + }) +} + +func TestReadWriteEnvelopeFromConnection(t *testing.T) { + mockCtrl := gomock.NewController(t) + defer mockCtrl.Finish() + defer monkey.UnpatchAll() + address := "0xb8d8c62d4a1999b7aea0aebBD5020244a4a9bAD8" + buffer := bytes.NewBuffer([]byte{}) + mockConn := mocks.NewMockConn(mockCtrl) + + t.Run("TestWriteEnvelope", func(t *testing.T) { + monkey.PatchInstanceMethod( + reflect.TypeOf(mockConn), + "Write", + func(_ *mocks.MockConn, b []byte) (int, error) { + buffer.Write(b) + return 0, nil + }, + ) + + err := WriteEnvelopeConn(mockConn, &aea.Envelope{ + To: address, + Sender: address, + }) + assert.Equal(t, nil, err) + assert.NotEqual(t, 0, buffer) + }) + + t.Run("TestReadEnvelope", func(t *testing.T) { + monkey.Patch(ReadBytesConn, func(conn net.Conn) ([]byte, error) { + return buffer.Bytes()[4:], nil + }) + env, err := ReadEnvelopeConn(mockConn) + assert.Equal(t, nil, err) + assert.Equal(t, address, env.To) + }) +} + +func TestGetPeersAddrInfo(t *testing.T) { + addrs, err := GetPeersAddrInfo( + []string{ + "/dns4/acn.fetch.ai/tcp/9001/p2p/16Uiu2HAmVWnopQAqq4pniYLw44VRvYxBUoRHqjz1Hh2SoCyjbyRW", + }, + ) + assert.Equal(t, nil, err) + assert.Equal(t, 1, len(addrs)) +} + +func TestFetchAIPublicKeyFromPubKey(t *testing.T) { + //(publicKey crypto.PubKey) (string, error) { + _, pubKey, err := KeyPairFromFetchAIKey( + "3e7a1f43b2d8a4b9f63a2ffeb1d597f971a8db7ffd95453173268b453106cadc", + ) + assert.Equal(t, nil, err) + key, err := FetchAIPublicKeyFromPubKey(pubKey) + assert.Equal(t, nil, err) + assert.Equal(t, "03b7e977f498dce004e2614764ff576e17cc6691135497e7bcb5d3441e816ba9e1", key) +} + +func TestIDFromFetchAIPublicKey(t *testing.T) { + _, pubKey, err := KeyPairFromFetchAIKey( + "3e7a1f43b2d8a4b9f63a2ffeb1d597f971a8db7ffd95453173268b453106cadc", + ) + assert.Equal(t, nil, err) + key, err := FetchAIPublicKeyFromPubKey(pubKey) + assert.Equal(t, nil, err) + peerID, err := IDFromFetchAIPublicKey(key) + assert.Equal(t, nil, err) + assert.NotEqual(t, 0, len(peerID)) +} + +func TestAgentAddressFromPublicKey(t *testing.T) { + address, err := AgentAddressFromPublicKey( + "fetchai", + "3e7a1f43b2d8a4b9f63a2ffeb1d597f971a8db7ffd95453173268b453106cadc", + ) + assert.Equal(t, nil, err) + assert.NotEqual(t, 0, len(address)) +} + +func TestCosmosAddressFromPublicKey(t *testing.T) { + address, err := CosmosAddressFromPublicKey( + "3e7a1f43b2d8a4b9f63a2ffeb1d597f971a8db7ffd95453173268b453106cadc", + ) + assert.Equal(t, nil, err) + assert.NotEqual(t, 0, len(address)) +} + +func TestFetchAIPublicKeyFromFetchAIPrivateKey(t *testing.T) { + key, err := FetchAIPublicKeyFromFetchAIPrivateKey( + "3e7a1f43b2d8a4b9f63a2ffeb1d597f971a8db7ffd95453173268b453106cadc", + ) + assert.Equal(t, nil, err) + assert.Equal(t, "03b7e977f498dce004e2614764ff576e17cc6691135497e7bcb5d3441e816ba9e1", key) +} + +func TestIDFromFetchAIPublicKeyUncompressed(t *testing.T) { + //bad pub key + _, err := IDFromFetchAIPublicKeyUncompressed("some") + assert.NotEqual(t, nil, err) + // good pub key + id, err := IDFromFetchAIPublicKeyUncompressed( + "50863AD64A87AE8A2FE83C1AF1A8403CB53F53E486D8511DAD8A04887E5B23522CD470243453A299FA9E77237716103ABC11A1DF38855ED6F2EE187E9C582BA6", + ) + assert.Equal(t, nil, err) + assert.Equal( + t, + peer.ID( + "\x00%\b\x02\x12!\x02P\x86:\xd6J\x87\xae\x8a/\xe8<\x1a\xf1\xa8@<\xb5?S\xe4\x86\xd8Q\x1d\xad\x8a\x04\x88~[#R", + ), + id, + ) +} + +func TestSignFetchAI(t *testing.T) { + privKey := "3e7a1f43b2d8a4b9f63a2ffeb1d597f971a8db7ffd95453173268b453106cadc" + message := []byte("somebytes") + + _, pubKey, err := KeyPairFromFetchAIKey(privKey) + assert.Equal(t, nil, err) + fetchPubKey, err := FetchAIPublicKeyFromPubKey(pubKey) + assert.Equal(t, nil, err) + + signature, err := SignFetchAI(message, privKey) + assert.Equal(t, nil, err) + assert.NotEqual(t, 0, len(signature)) + + isValid, err := VerifyLedgerSignature("fetchai", message, signature, fetchPubKey) + assert.Equal(t, nil, err) + assert.Equal(t, true, isValid) + +} + +func TestBootstrapConnect(t *testing.T) { + ctx := context.Background() + mockCtrl := gomock.NewController(t) + defer mockCtrl.Finish() + defer monkey.UnpatchAll() + var ipfsdht *dht.IpfsDHT + var routingTable *kb.RoutingTable + + mockPeerstore := mocks.NewMockPeerstore(mockCtrl) + peers := make([]peer.AddrInfo, 2) + var addrs []ma.Multiaddr + peers[0] = peer.AddrInfo{ID: peer.ID("peer1"), Addrs: addrs} + peers[1] = peer.AddrInfo{ID: peer.ID("peer2"), Addrs: addrs} + + mockHost := mocks.NewMockHost(mockCtrl) + + mockHost.EXPECT().ID().Return(peer.ID("host_id")).Times(2) + mockHost.EXPECT().Peerstore().Return(mockPeerstore).Times(2) + mockHost.EXPECT().Connect(gomock.Any(), gomock.Any()).Return(nil).Times(2) + mockPeerstore.EXPECT().AddAddrs(gomock.Any(), gomock.Any(), gomock.Any()).Return().Times(2) + + t.Run("TestOk", func(t *testing.T) { + monkey.PatchInstanceMethod( + reflect.TypeOf(routingTable), + "Find", + func(_ *kb.RoutingTable, _ peer.ID) peer.ID { + return peer.ID("som peer") + }, + ) + monkey.PatchInstanceMethod( + reflect.TypeOf(ipfsdht), + "RoutingTable", + func(_ *dht.IpfsDHT) *kb.RoutingTable { + return routingTable + }, + ) + + err := BootstrapConnect(ctx, mockHost, ipfsdht, peers) + assert.Equal(t, nil, err) + }) + + mockHost = mocks.NewMockHost(mockCtrl) + + mockHost.EXPECT().ID().Return(peer.ID("host_id")).Times(2) + mockHost.EXPECT().Peerstore().Return(mockPeerstore).Times(2) + mockHost.EXPECT().Connect(gomock.Any(), gomock.Any()).Return(nil).Times(2) + mockPeerstore.EXPECT().AddAddrs(gomock.Any(), gomock.Any(), gomock.Any()).Return().Times(2) + + t.Run("Test_PeersNotAdded", func(t *testing.T) { + monkey.PatchInstanceMethod( + reflect.TypeOf(routingTable), + "Find", + func(_ *kb.RoutingTable, _ peer.ID) peer.ID { + return peer.ID("") + }, + ) + monkey.PatchInstanceMethod( + reflect.TypeOf(ipfsdht), + "RoutingTable", + func(_ *dht.IpfsDHT) *kb.RoutingTable { + return routingTable + }, + ) + + err := BootstrapConnect(ctx, mockHost, ipfsdht, peers) + assert.NotEqual(t, nil, err) + assert.Contains(t, err.Error(), "timeout: entry peer haven't been added to DHT") + }) + + mockHost = mocks.NewMockHost(mockCtrl) + + mockHost.EXPECT().ID().Return(peer.ID("host_id")).Times(2) + mockHost.EXPECT().Peerstore().Return(mockPeerstore).Times(2) + mockHost.EXPECT().Connect(gomock.Any(), gomock.Any()).Return(errors.New("some error")).Times(2) + mockPeerstore.EXPECT().AddAddrs(gomock.Any(), gomock.Any(), gomock.Any()).Return().Times(2) + + t.Run("Test_PeersNotConnected", func(t *testing.T) { + monkey.PatchInstanceMethod( + reflect.TypeOf(routingTable), + "Find", + func(_ *kb.RoutingTable, _ peer.ID) peer.ID { + return peer.ID("") + }, + ) + monkey.PatchInstanceMethod( + reflect.TypeOf(ipfsdht), + "RoutingTable", + func(_ *dht.IpfsDHT) *kb.RoutingTable { + return routingTable + }, + ) + + err := BootstrapConnect(ctx, mockHost, ipfsdht, peers) + assert.NotEqual(t, nil, err) + assert.Equal(t, "failed to bootstrap: some error", err.Error()) + }) + +} diff --git a/packages/fetchai/connections/p2p_libp2p/connection.yaml b/packages/fetchai/connections/p2p_libp2p/connection.yaml index aae241665d..6b630ad621 100644 --- a/packages/fetchai/connections/p2p_libp2p/connection.yaml +++ b/packages/fetchai/connections/p2p_libp2p/connection.yaml @@ -12,6 +12,7 @@ fingerprint: __init__.py: QmYQuLNyQ8WTjgRYAoKAzoJEb7ocKXvM2hTyK4hsGch5D6 check_dependencies.py: QmP14nkQ8senwzdPdrZJLsA6EQ7zaKKEaLGDELhT42gp1P connection.py: QmXmCBy8AizGefC8JJo2nJZySY2kmevNgxxgMkJPvoNQRR + libp2p_node/Makefile: QmQ7bjtiHcW5m5xfuvA6rzrZYptGkTcrcX74wCndjuX5ZA libp2p_node/README.md: Qmak56XnWfarVxasiaGqYQWJaNVnEAh2hsLWstuFVND98w libp2p_node/aea/api.go: QmdFR5Rmkk2FGVDwzgHtjobjAKyLejqk2CAYBCvcF23AG7 libp2p_node/aea/envelope.pb.go: QmRfUNGpCeVJfsW3H1MzCN4pwDWgumfyWufVFp6xvUjjug @@ -33,10 +34,15 @@ fingerprint: libp2p_node/dht/monitoring/file.go: Qmc4QpKtjXaEFqGPeunV6TR4qR5RcMzoy8atzJH4ouBkfH libp2p_node/dht/monitoring/prometheus.go: QmQvXjEozVPMvRjda5WGRAU5b7cfUcRZUACQkTESG7Aewu libp2p_node/dht/monitoring/service.go: QmT47y2LHZECYcoE2uJ9QCGh3Kq8ePhYedo8dQE7X7v6YV - libp2p_node/go.mod: QmU2MJhhvwTCfyztCei5R9LKStadFuFVUDb16s3tZwrZP8 - libp2p_node/go.sum: QmVpr925Kp8ZS7Z1iLfbjDB6NxG5BA7JczhhckzhmbYqzT + libp2p_node/go.mod: QmZAefiBvioX9DfJvKWibSbDGnAEo1w9LuoEVzjRoWJGWT + libp2p_node/go.sum: QmXELVbhPqVqSN9osMh1zpnsihPZmCC3A8tCt9maF6sjFS libp2p_node/libp2p_node.go: QmPgMQ3g93Jqu4GAv8e7fTWbrGK8hjSp7BDrKj1EuR1WcS - libp2p_node/utils/utils.go: QmUSw5SD3i7WhkPCVHc5ha6BhJbzk4Cf8HbyysEwUVz5zt + libp2p_node/mocks/mock_host.go: QmSJ7g6S2PGhC5j8xBQyrjs56hXJGEewkgFgiivyZDthbW + libp2p_node/mocks/mock_net.go: QmVKnkDdH8XCJ9jriEkZui4NoB36GBYF2DtfX8uCqAthMw + libp2p_node/mocks/mock_network.go: QmbVVvd3wrY6PnVs1rn9T9m6FD5kbmSVJLwhSxUgSLAiM5 + libp2p_node/mocks/mock_peerstore.go: QmaPCBrwsTeWCHZoAKDzaxN6uhY3bez1karzeGeovWYwkB + libp2p_node/utils/utils.go: QmXxQCCsEFZH1wkzze1Z1MfcBi6rQ7ZEzACGKptQsgL8Zg + libp2p_node/utils/utils_test.go: QmX5bBqCwFVky6ix7pu9QNvScntr3GMDvon9cNC8dKhJiS fingerprint_ignore_patterns: [] build_entrypoint: check_dependencies.py connections: [] diff --git a/packages/fetchai/connections/p2p_libp2p/libp2p_node/Makefile b/packages/fetchai/connections/p2p_libp2p/libp2p_node/Makefile new file mode 100644 index 0000000000..ae14b117be --- /dev/null +++ b/packages/fetchai/connections/p2p_libp2p/libp2p_node/Makefile @@ -0,0 +1,12 @@ +test: + go test -gcflags=-l -p 1 -timeout 0 -count 1 -covermode=atomic -coverprofile=coverage.txt -v ./... + go tool cover -func=coverage.txt + +lint: + golines . -w + golangci-lint run + +build: + go build +install: + go get -v -t -d ./... \ No newline at end of file diff --git a/packages/fetchai/connections/p2p_libp2p/libp2p_node/go.mod b/packages/fetchai/connections/p2p_libp2p/libp2p_node/go.mod index a5d16c054d..18898cea10 100644 --- a/packages/fetchai/connections/p2p_libp2p/libp2p_node/go.mod +++ b/packages/fetchai/connections/p2p_libp2p/libp2p_node/go.mod @@ -3,11 +3,13 @@ module libp2p_node go 1.13 require ( + bou.ke/monkey v1.0.2 github.com/alecthomas/units v0.0.0-20201120081800-1786d5ef83d4 // indirect github.com/btcsuite/btcd v0.20.1-beta github.com/btcsuite/btcutil v0.0.0-20190425235716-9e5f4b9a998d github.com/dave/dst v0.26.2 // indirect github.com/ethereum/go-ethereum v1.9.25 + github.com/golang/mock v1.5.0 github.com/golang/protobuf v1.4.2 github.com/ipfs/go-cid v0.0.5 github.com/joho/godotenv v1.3.0 @@ -16,18 +18,21 @@ require ( github.com/libp2p/go-libp2p-circuit v0.2.2 github.com/libp2p/go-libp2p-core v0.5.3 github.com/libp2p/go-libp2p-kad-dht v0.7.11 + github.com/libp2p/go-libp2p-kbucket v0.4.1 github.com/mattn/go-colorable v0.1.8 // indirect github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect github.com/multiformats/go-multiaddr v0.2.1 github.com/multiformats/go-multihash v0.0.13 github.com/pkg/errors v0.9.1 github.com/prometheus/client_golang v1.7.1 - github.com/rs/zerolog v1.19.0 + github.com/rs/zerolog v1.21.0 github.com/segmentio/golines v0.0.0-20200824192126-7f30d3046793 // indirect github.com/sirupsen/logrus v1.7.0 // indirect + github.com/stretchr/testify v1.5.1 golang.org/x/crypto v0.0.0-20201208171446-5f87f3452ae9 golang.org/x/mod v0.4.0 // indirect golang.org/x/term v0.0.0-20201210144234-2321bbc49cbf // indirect google.golang.org/protobuf v1.25.0 honnef.co/go/tools v0.1.4 // indirect + ) diff --git a/packages/fetchai/connections/p2p_libp2p/libp2p_node/go.sum b/packages/fetchai/connections/p2p_libp2p/libp2p_node/go.sum index cb99823bb5..f108665f5e 100644 --- a/packages/fetchai/connections/p2p_libp2p/libp2p_node/go.sum +++ b/packages/fetchai/connections/p2p_libp2p/libp2p_node/go.sum @@ -1,3 +1,5 @@ +bou.ke/monkey v1.0.2 h1:kWcnsrCNUatbxncxR/ThdYqbytgOIArtYWqcQLQzKLI= +bou.ke/monkey v1.0.2/go.mod h1:OqickVX3tNx6t33n1xvtTtu85YN5s6cKwVug+oHMaIA= cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= github.com/AndreasBriese/bbloom v0.0.0-20180913140656-343706a395b7/go.mod h1:bOvUY6CB00SOBii9/FifXqc0awNKxLFCL/+pkDPuyl8= github.com/AndreasBriese/bbloom v0.0.0-20190306092124-e2d15f34fcf9/go.mod h1:bOvUY6CB00SOBii9/FifXqc0awNKxLFCL/+pkDPuyl8= @@ -129,6 +131,8 @@ github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfU github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6 h1:ZgQEtGgCBiWRM39fZuwSd1LwSqqSW0hOdXCYYDX0R3I= github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.5.0 h1:jlYHihg//f7RRwuPfptm04yp4s7O6Kw8EZiVYIGcH0g= +github.com/golang/mock v1.5.0/go.mod h1:CWnOUgYIOo4TcNZ0wHX3YZCqsaM1I1Jvs6v3mP3KVu8= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.0/go.mod h1:Qd/q+1AKNOZr9uGQzbzCmRO6sUih6GTPZv6a1/R87v0= github.com/golang/protobuf v1.3.1 h1:YF8+flBXS5eO826T4nzqPrxfhQThhXl0YzfuUPu4SBg= @@ -566,6 +570,8 @@ github.com/rs/xhandler v0.0.0-20160618193221-ed27b6fd6521/go.mod h1:RvLn4FgxWubr github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ= github.com/rs/zerolog v1.19.0 h1:hYz4ZVdUgjXTBUmrkrw55j1nHx68LfOKIQk5IYtyScg= github.com/rs/zerolog v1.19.0/go.mod h1:IzD0RJ65iWH0w97OQQebJEvTZYvsCUm9WVLWBQrJRjo= +github.com/rs/zerolog v1.21.0 h1:Q3vdXlfLNT+OftyBHsU0Y445MD+8m8axjKgf2si0QcM= +github.com/rs/zerolog v1.21.0/go.mod h1:ZPhntP/xmq1nnND05hhpAh2QMhSsA4UN3MGZ6O2J3hM= github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/segmentio/golines v0.0.0-20200824192126-7f30d3046793 h1:rhR7esJSmty+9ST6Gsp7mlQHkpISw2DiYjuFaz3dRDg= @@ -837,6 +843,7 @@ gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.4 h1:/eiJrUcujPVeJ3xlSWaiNi3uSVmDGBK1pDHUHAnao1I= gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU= gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= diff --git a/packages/fetchai/connections/p2p_libp2p/libp2p_node/mocks/mock_host.go b/packages/fetchai/connections/p2p_libp2p/libp2p_node/mocks/mock_host.go new file mode 100644 index 0000000000..fb890fa843 --- /dev/null +++ b/packages/fetchai/connections/p2p_libp2p/libp2p_node/mocks/mock_host.go @@ -0,0 +1,224 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: github.com/libp2p/go-libp2p-core/host (interfaces: Host) + +// Package mock_host is a generated GoMock package. +package mocks + +import ( + context "context" + reflect "reflect" + + gomock "github.com/golang/mock/gomock" + connmgr "github.com/libp2p/go-libp2p-core/connmgr" + event "github.com/libp2p/go-libp2p-core/event" + network "github.com/libp2p/go-libp2p-core/network" + peer "github.com/libp2p/go-libp2p-core/peer" + peerstore "github.com/libp2p/go-libp2p-core/peerstore" + protocol "github.com/libp2p/go-libp2p-core/protocol" + multiaddr "github.com/multiformats/go-multiaddr" +) + +// MockHost is a mock of Host interface. +type MockHost struct { + ctrl *gomock.Controller + recorder *MockHostMockRecorder +} + +// MockHostMockRecorder is the mock recorder for MockHost. +type MockHostMockRecorder struct { + mock *MockHost +} + +// NewMockHost creates a new mock instance. +func NewMockHost(ctrl *gomock.Controller) *MockHost { + mock := &MockHost{ctrl: ctrl} + mock.recorder = &MockHostMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockHost) EXPECT() *MockHostMockRecorder { + return m.recorder +} + +// Addrs mocks base method. +func (m *MockHost) Addrs() []multiaddr.Multiaddr { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Addrs") + ret0, _ := ret[0].([]multiaddr.Multiaddr) + return ret0 +} + +// Addrs indicates an expected call of Addrs. +func (mr *MockHostMockRecorder) Addrs() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Addrs", reflect.TypeOf((*MockHost)(nil).Addrs)) +} + +// Close mocks base method. +func (m *MockHost) Close() error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Close") + ret0, _ := ret[0].(error) + return ret0 +} + +// Close indicates an expected call of Close. +func (mr *MockHostMockRecorder) Close() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Close", reflect.TypeOf((*MockHost)(nil).Close)) +} + +// ConnManager mocks base method. +func (m *MockHost) ConnManager() connmgr.ConnManager { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ConnManager") + ret0, _ := ret[0].(connmgr.ConnManager) + return ret0 +} + +// ConnManager indicates an expected call of ConnManager. +func (mr *MockHostMockRecorder) ConnManager() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ConnManager", reflect.TypeOf((*MockHost)(nil).ConnManager)) +} + +// Connect mocks base method. +func (m *MockHost) Connect(arg0 context.Context, arg1 peer.AddrInfo) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Connect", arg0, arg1) + ret0, _ := ret[0].(error) + return ret0 +} + +// Connect indicates an expected call of Connect. +func (mr *MockHostMockRecorder) Connect(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Connect", reflect.TypeOf((*MockHost)(nil).Connect), arg0, arg1) +} + +// EventBus mocks base method. +func (m *MockHost) EventBus() event.Bus { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "EventBus") + ret0, _ := ret[0].(event.Bus) + return ret0 +} + +// EventBus indicates an expected call of EventBus. +func (mr *MockHostMockRecorder) EventBus() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "EventBus", reflect.TypeOf((*MockHost)(nil).EventBus)) +} + +// ID mocks base method. +func (m *MockHost) ID() peer.ID { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ID") + ret0, _ := ret[0].(peer.ID) + return ret0 +} + +// ID indicates an expected call of ID. +func (mr *MockHostMockRecorder) ID() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ID", reflect.TypeOf((*MockHost)(nil).ID)) +} + +// Mux mocks base method. +func (m *MockHost) Mux() protocol.Switch { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Mux") + ret0, _ := ret[0].(protocol.Switch) + return ret0 +} + +// Mux indicates an expected call of Mux. +func (mr *MockHostMockRecorder) Mux() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Mux", reflect.TypeOf((*MockHost)(nil).Mux)) +} + +// Network mocks base method. +func (m *MockHost) Network() network.Network { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Network") + ret0, _ := ret[0].(network.Network) + return ret0 +} + +// Network indicates an expected call of Network. +func (mr *MockHostMockRecorder) Network() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Network", reflect.TypeOf((*MockHost)(nil).Network)) +} + +// NewStream mocks base method. +func (m *MockHost) NewStream(arg0 context.Context, arg1 peer.ID, arg2 ...protocol.ID) (network.Stream, error) { + m.ctrl.T.Helper() + varargs := []interface{}{arg0, arg1} + for _, a := range arg2 { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "NewStream", varargs...) + ret0, _ := ret[0].(network.Stream) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// NewStream indicates an expected call of NewStream. +func (mr *MockHostMockRecorder) NewStream(arg0, arg1 interface{}, arg2 ...interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]interface{}{arg0, arg1}, arg2...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "NewStream", reflect.TypeOf((*MockHost)(nil).NewStream), varargs...) +} + +// Peerstore mocks base method. +func (m *MockHost) Peerstore() peerstore.Peerstore { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Peerstore") + ret0, _ := ret[0].(peerstore.Peerstore) + return ret0 +} + +// Peerstore indicates an expected call of Peerstore. +func (mr *MockHostMockRecorder) Peerstore() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Peerstore", reflect.TypeOf((*MockHost)(nil).Peerstore)) +} + +// RemoveStreamHandler mocks base method. +func (m *MockHost) RemoveStreamHandler(arg0 protocol.ID) { + m.ctrl.T.Helper() + m.ctrl.Call(m, "RemoveStreamHandler", arg0) +} + +// RemoveStreamHandler indicates an expected call of RemoveStreamHandler. +func (mr *MockHostMockRecorder) RemoveStreamHandler(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RemoveStreamHandler", reflect.TypeOf((*MockHost)(nil).RemoveStreamHandler), arg0) +} + +// SetStreamHandler mocks base method. +func (m *MockHost) SetStreamHandler(arg0 protocol.ID, arg1 network.StreamHandler) { + m.ctrl.T.Helper() + m.ctrl.Call(m, "SetStreamHandler", arg0, arg1) +} + +// SetStreamHandler indicates an expected call of SetStreamHandler. +func (mr *MockHostMockRecorder) SetStreamHandler(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetStreamHandler", reflect.TypeOf((*MockHost)(nil).SetStreamHandler), arg0, arg1) +} + +// SetStreamHandlerMatch mocks base method. +func (m *MockHost) SetStreamHandlerMatch(arg0 protocol.ID, arg1 func(string) bool, arg2 network.StreamHandler) { + m.ctrl.T.Helper() + m.ctrl.Call(m, "SetStreamHandlerMatch", arg0, arg1, arg2) +} + +// SetStreamHandlerMatch indicates an expected call of SetStreamHandlerMatch. +func (mr *MockHostMockRecorder) SetStreamHandlerMatch(arg0, arg1, arg2 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetStreamHandlerMatch", reflect.TypeOf((*MockHost)(nil).SetStreamHandlerMatch), arg0, arg1, arg2) +} diff --git a/packages/fetchai/connections/p2p_libp2p/libp2p_node/mocks/mock_net.go b/packages/fetchai/connections/p2p_libp2p/libp2p_node/mocks/mock_net.go new file mode 100644 index 0000000000..2f6b440075 --- /dev/null +++ b/packages/fetchai/connections/p2p_libp2p/libp2p_node/mocks/mock_net.go @@ -0,0 +1,150 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: net (interfaces: Conn) + +// Package mock_net is a generated GoMock package. +package mocks + +import ( + net "net" + reflect "reflect" + time "time" + + gomock "github.com/golang/mock/gomock" +) + +// MockConn is a mock of Conn interface. +type MockConn struct { + ctrl *gomock.Controller + recorder *MockConnMockRecorder +} + +// MockConnMockRecorder is the mock recorder for MockConn. +type MockConnMockRecorder struct { + mock *MockConn +} + +// NewMockConn creates a new mock instance. +func NewMockConn(ctrl *gomock.Controller) *MockConn { + mock := &MockConn{ctrl: ctrl} + mock.recorder = &MockConnMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockConn) EXPECT() *MockConnMockRecorder { + return m.recorder +} + +// Close mocks base method. +func (m *MockConn) Close() error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Close") + ret0, _ := ret[0].(error) + return ret0 +} + +// Close indicates an expected call of Close. +func (mr *MockConnMockRecorder) Close() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Close", reflect.TypeOf((*MockConn)(nil).Close)) +} + +// LocalAddr mocks base method. +func (m *MockConn) LocalAddr() net.Addr { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "LocalAddr") + ret0, _ := ret[0].(net.Addr) + return ret0 +} + +// LocalAddr indicates an expected call of LocalAddr. +func (mr *MockConnMockRecorder) LocalAddr() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "LocalAddr", reflect.TypeOf((*MockConn)(nil).LocalAddr)) +} + +// Read mocks base method. +func (m *MockConn) Read(arg0 []byte) (int, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Read", arg0) + ret0, _ := ret[0].(int) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Read indicates an expected call of Read. +func (mr *MockConnMockRecorder) Read(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Read", reflect.TypeOf((*MockConn)(nil).Read), arg0) +} + +// RemoteAddr mocks base method. +func (m *MockConn) RemoteAddr() net.Addr { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "RemoteAddr") + ret0, _ := ret[0].(net.Addr) + return ret0 +} + +// RemoteAddr indicates an expected call of RemoteAddr. +func (mr *MockConnMockRecorder) RemoteAddr() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RemoteAddr", reflect.TypeOf((*MockConn)(nil).RemoteAddr)) +} + +// SetDeadline mocks base method. +func (m *MockConn) SetDeadline(arg0 time.Time) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "SetDeadline", arg0) + ret0, _ := ret[0].(error) + return ret0 +} + +// SetDeadline indicates an expected call of SetDeadline. +func (mr *MockConnMockRecorder) SetDeadline(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetDeadline", reflect.TypeOf((*MockConn)(nil).SetDeadline), arg0) +} + +// SetReadDeadline mocks base method. +func (m *MockConn) SetReadDeadline(arg0 time.Time) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "SetReadDeadline", arg0) + ret0, _ := ret[0].(error) + return ret0 +} + +// SetReadDeadline indicates an expected call of SetReadDeadline. +func (mr *MockConnMockRecorder) SetReadDeadline(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetReadDeadline", reflect.TypeOf((*MockConn)(nil).SetReadDeadline), arg0) +} + +// SetWriteDeadline mocks base method. +func (m *MockConn) SetWriteDeadline(arg0 time.Time) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "SetWriteDeadline", arg0) + ret0, _ := ret[0].(error) + return ret0 +} + +// SetWriteDeadline indicates an expected call of SetWriteDeadline. +func (mr *MockConnMockRecorder) SetWriteDeadline(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetWriteDeadline", reflect.TypeOf((*MockConn)(nil).SetWriteDeadline), arg0) +} + +// Write mocks base method. +func (m *MockConn) Write(arg0 []byte) (int, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Write", arg0) + ret0, _ := ret[0].(int) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Write indicates an expected call of Write. +func (mr *MockConnMockRecorder) Write(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Write", reflect.TypeOf((*MockConn)(nil).Write), arg0) +} diff --git a/packages/fetchai/connections/p2p_libp2p/libp2p_node/mocks/mock_network.go b/packages/fetchai/connections/p2p_libp2p/libp2p_node/mocks/mock_network.go new file mode 100644 index 0000000000..ab38a029fc --- /dev/null +++ b/packages/fetchai/connections/p2p_libp2p/libp2p_node/mocks/mock_network.go @@ -0,0 +1,191 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: github.com/libp2p/go-libp2p-core/network (interfaces: Stream) + +// Package mock_network is a generated GoMock package. +package mocks + +import ( + reflect "reflect" + time "time" + + gomock "github.com/golang/mock/gomock" + network "github.com/libp2p/go-libp2p-core/network" + protocol "github.com/libp2p/go-libp2p-core/protocol" +) + +// MockStream is a mock of Stream interface. +type MockStream struct { + ctrl *gomock.Controller + recorder *MockStreamMockRecorder +} + +// MockStreamMockRecorder is the mock recorder for MockStream. +type MockStreamMockRecorder struct { + mock *MockStream +} + +// NewMockStream creates a new mock instance. +func NewMockStream(ctrl *gomock.Controller) *MockStream { + mock := &MockStream{ctrl: ctrl} + mock.recorder = &MockStreamMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockStream) EXPECT() *MockStreamMockRecorder { + return m.recorder +} + +// Close mocks base method. +func (m *MockStream) Close() error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Close") + ret0, _ := ret[0].(error) + return ret0 +} + +// Close indicates an expected call of Close. +func (mr *MockStreamMockRecorder) Close() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Close", reflect.TypeOf((*MockStream)(nil).Close)) +} + +// Conn mocks base method. +func (m *MockStream) Conn() network.Conn { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Conn") + ret0, _ := ret[0].(network.Conn) + return ret0 +} + +// Conn indicates an expected call of Conn. +func (mr *MockStreamMockRecorder) Conn() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Conn", reflect.TypeOf((*MockStream)(nil).Conn)) +} + +// Protocol mocks base method. +func (m *MockStream) Protocol() protocol.ID { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Protocol") + ret0, _ := ret[0].(protocol.ID) + return ret0 +} + +// Protocol indicates an expected call of Protocol. +func (mr *MockStreamMockRecorder) Protocol() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Protocol", reflect.TypeOf((*MockStream)(nil).Protocol)) +} + +// Read mocks base method. +func (m *MockStream) Read(arg0 []byte) (int, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Read", arg0) + ret0, _ := ret[0].(int) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Read indicates an expected call of Read. +func (mr *MockStreamMockRecorder) Read(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Read", reflect.TypeOf((*MockStream)(nil).Read), arg0) +} + +// Reset mocks base method. +func (m *MockStream) Reset() error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Reset") + ret0, _ := ret[0].(error) + return ret0 +} + +// Reset indicates an expected call of Reset. +func (mr *MockStreamMockRecorder) Reset() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Reset", reflect.TypeOf((*MockStream)(nil).Reset)) +} + +// SetDeadline mocks base method. +func (m *MockStream) SetDeadline(arg0 time.Time) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "SetDeadline", arg0) + ret0, _ := ret[0].(error) + return ret0 +} + +// SetDeadline indicates an expected call of SetDeadline. +func (mr *MockStreamMockRecorder) SetDeadline(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetDeadline", reflect.TypeOf((*MockStream)(nil).SetDeadline), arg0) +} + +// SetProtocol mocks base method. +func (m *MockStream) SetProtocol(arg0 protocol.ID) { + m.ctrl.T.Helper() + m.ctrl.Call(m, "SetProtocol", arg0) +} + +// SetProtocol indicates an expected call of SetProtocol. +func (mr *MockStreamMockRecorder) SetProtocol(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetProtocol", reflect.TypeOf((*MockStream)(nil).SetProtocol), arg0) +} + +// SetReadDeadline mocks base method. +func (m *MockStream) SetReadDeadline(arg0 time.Time) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "SetReadDeadline", arg0) + ret0, _ := ret[0].(error) + return ret0 +} + +// SetReadDeadline indicates an expected call of SetReadDeadline. +func (mr *MockStreamMockRecorder) SetReadDeadline(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetReadDeadline", reflect.TypeOf((*MockStream)(nil).SetReadDeadline), arg0) +} + +// SetWriteDeadline mocks base method. +func (m *MockStream) SetWriteDeadline(arg0 time.Time) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "SetWriteDeadline", arg0) + ret0, _ := ret[0].(error) + return ret0 +} + +// SetWriteDeadline indicates an expected call of SetWriteDeadline. +func (mr *MockStreamMockRecorder) SetWriteDeadline(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetWriteDeadline", reflect.TypeOf((*MockStream)(nil).SetWriteDeadline), arg0) +} + +// Stat mocks base method. +func (m *MockStream) Stat() network.Stat { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Stat") + ret0, _ := ret[0].(network.Stat) + return ret0 +} + +// Stat indicates an expected call of Stat. +func (mr *MockStreamMockRecorder) Stat() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Stat", reflect.TypeOf((*MockStream)(nil).Stat)) +} + +// Write mocks base method. +func (m *MockStream) Write(arg0 []byte) (int, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Write", arg0) + ret0, _ := ret[0].(int) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Write indicates an expected call of Write. +func (mr *MockStreamMockRecorder) Write(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Write", reflect.TypeOf((*MockStream)(nil).Write), arg0) +} diff --git a/packages/fetchai/connections/p2p_libp2p/libp2p_node/mocks/mock_peerstore.go b/packages/fetchai/connections/p2p_libp2p/libp2p_node/mocks/mock_peerstore.go new file mode 100644 index 0000000000..c1e2568dcc --- /dev/null +++ b/packages/fetchai/connections/p2p_libp2p/libp2p_node/mocks/mock_peerstore.go @@ -0,0 +1,412 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: github.com/libp2p/go-libp2p-core/peerstore (interfaces: Peerstore) + +// Package mock_peerstore is a generated GoMock package. +package mocks + +import ( + context "context" + reflect "reflect" + time "time" + + gomock "github.com/golang/mock/gomock" + crypto "github.com/libp2p/go-libp2p-core/crypto" + peer "github.com/libp2p/go-libp2p-core/peer" + multiaddr "github.com/multiformats/go-multiaddr" +) + +// MockPeerstore is a mock of Peerstore interface. +type MockPeerstore struct { + ctrl *gomock.Controller + recorder *MockPeerstoreMockRecorder +} + +// MockPeerstoreMockRecorder is the mock recorder for MockPeerstore. +type MockPeerstoreMockRecorder struct { + mock *MockPeerstore +} + +// NewMockPeerstore creates a new mock instance. +func NewMockPeerstore(ctrl *gomock.Controller) *MockPeerstore { + mock := &MockPeerstore{ctrl: ctrl} + mock.recorder = &MockPeerstoreMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockPeerstore) EXPECT() *MockPeerstoreMockRecorder { + return m.recorder +} + +// AddAddr mocks base method. +func (m *MockPeerstore) AddAddr(arg0 peer.ID, arg1 multiaddr.Multiaddr, arg2 time.Duration) { + m.ctrl.T.Helper() + m.ctrl.Call(m, "AddAddr", arg0, arg1, arg2) +} + +// AddAddr indicates an expected call of AddAddr. +func (mr *MockPeerstoreMockRecorder) AddAddr(arg0, arg1, arg2 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddAddr", reflect.TypeOf((*MockPeerstore)(nil).AddAddr), arg0, arg1, arg2) +} + +// AddAddrs mocks base method. +func (m *MockPeerstore) AddAddrs(arg0 peer.ID, arg1 []multiaddr.Multiaddr, arg2 time.Duration) { + m.ctrl.T.Helper() + m.ctrl.Call(m, "AddAddrs", arg0, arg1, arg2) +} + +// AddAddrs indicates an expected call of AddAddrs. +func (mr *MockPeerstoreMockRecorder) AddAddrs(arg0, arg1, arg2 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddAddrs", reflect.TypeOf((*MockPeerstore)(nil).AddAddrs), arg0, arg1, arg2) +} + +// AddPrivKey mocks base method. +func (m *MockPeerstore) AddPrivKey(arg0 peer.ID, arg1 crypto.PrivKey) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "AddPrivKey", arg0, arg1) + ret0, _ := ret[0].(error) + return ret0 +} + +// AddPrivKey indicates an expected call of AddPrivKey. +func (mr *MockPeerstoreMockRecorder) AddPrivKey(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddPrivKey", reflect.TypeOf((*MockPeerstore)(nil).AddPrivKey), arg0, arg1) +} + +// AddProtocols mocks base method. +func (m *MockPeerstore) AddProtocols(arg0 peer.ID, arg1 ...string) error { + m.ctrl.T.Helper() + varargs := []interface{}{arg0} + for _, a := range arg1 { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "AddProtocols", varargs...) + ret0, _ := ret[0].(error) + return ret0 +} + +// AddProtocols indicates an expected call of AddProtocols. +func (mr *MockPeerstoreMockRecorder) AddProtocols(arg0 interface{}, arg1 ...interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]interface{}{arg0}, arg1...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddProtocols", reflect.TypeOf((*MockPeerstore)(nil).AddProtocols), varargs...) +} + +// AddPubKey mocks base method. +func (m *MockPeerstore) AddPubKey(arg0 peer.ID, arg1 crypto.PubKey) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "AddPubKey", arg0, arg1) + ret0, _ := ret[0].(error) + return ret0 +} + +// AddPubKey indicates an expected call of AddPubKey. +func (mr *MockPeerstoreMockRecorder) AddPubKey(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddPubKey", reflect.TypeOf((*MockPeerstore)(nil).AddPubKey), arg0, arg1) +} + +// AddrStream mocks base method. +func (m *MockPeerstore) AddrStream(arg0 context.Context, arg1 peer.ID) <-chan multiaddr.Multiaddr { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "AddrStream", arg0, arg1) + ret0, _ := ret[0].(<-chan multiaddr.Multiaddr) + return ret0 +} + +// AddrStream indicates an expected call of AddrStream. +func (mr *MockPeerstoreMockRecorder) AddrStream(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddrStream", reflect.TypeOf((*MockPeerstore)(nil).AddrStream), arg0, arg1) +} + +// Addrs mocks base method. +func (m *MockPeerstore) Addrs(arg0 peer.ID) []multiaddr.Multiaddr { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Addrs", arg0) + ret0, _ := ret[0].([]multiaddr.Multiaddr) + return ret0 +} + +// Addrs indicates an expected call of Addrs. +func (mr *MockPeerstoreMockRecorder) Addrs(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Addrs", reflect.TypeOf((*MockPeerstore)(nil).Addrs), arg0) +} + +// ClearAddrs mocks base method. +func (m *MockPeerstore) ClearAddrs(arg0 peer.ID) { + m.ctrl.T.Helper() + m.ctrl.Call(m, "ClearAddrs", arg0) +} + +// ClearAddrs indicates an expected call of ClearAddrs. +func (mr *MockPeerstoreMockRecorder) ClearAddrs(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ClearAddrs", reflect.TypeOf((*MockPeerstore)(nil).ClearAddrs), arg0) +} + +// Close mocks base method. +func (m *MockPeerstore) Close() error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Close") + ret0, _ := ret[0].(error) + return ret0 +} + +// Close indicates an expected call of Close. +func (mr *MockPeerstoreMockRecorder) Close() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Close", reflect.TypeOf((*MockPeerstore)(nil).Close)) +} + +// Get mocks base method. +func (m *MockPeerstore) Get(arg0 peer.ID, arg1 string) (interface{}, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Get", arg0, arg1) + ret0, _ := ret[0].(interface{}) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Get indicates an expected call of Get. +func (mr *MockPeerstoreMockRecorder) Get(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Get", reflect.TypeOf((*MockPeerstore)(nil).Get), arg0, arg1) +} + +// GetProtocols mocks base method. +func (m *MockPeerstore) GetProtocols(arg0 peer.ID) ([]string, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetProtocols", arg0) + ret0, _ := ret[0].([]string) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetProtocols indicates an expected call of GetProtocols. +func (mr *MockPeerstoreMockRecorder) GetProtocols(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetProtocols", reflect.TypeOf((*MockPeerstore)(nil).GetProtocols), arg0) +} + +// LatencyEWMA mocks base method. +func (m *MockPeerstore) LatencyEWMA(arg0 peer.ID) time.Duration { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "LatencyEWMA", arg0) + ret0, _ := ret[0].(time.Duration) + return ret0 +} + +// LatencyEWMA indicates an expected call of LatencyEWMA. +func (mr *MockPeerstoreMockRecorder) LatencyEWMA(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "LatencyEWMA", reflect.TypeOf((*MockPeerstore)(nil).LatencyEWMA), arg0) +} + +// PeerInfo mocks base method. +func (m *MockPeerstore) PeerInfo(arg0 peer.ID) peer.AddrInfo { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "PeerInfo", arg0) + ret0, _ := ret[0].(peer.AddrInfo) + return ret0 +} + +// PeerInfo indicates an expected call of PeerInfo. +func (mr *MockPeerstoreMockRecorder) PeerInfo(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PeerInfo", reflect.TypeOf((*MockPeerstore)(nil).PeerInfo), arg0) +} + +// Peers mocks base method. +func (m *MockPeerstore) Peers() peer.IDSlice { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Peers") + ret0, _ := ret[0].(peer.IDSlice) + return ret0 +} + +// Peers indicates an expected call of Peers. +func (mr *MockPeerstoreMockRecorder) Peers() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Peers", reflect.TypeOf((*MockPeerstore)(nil).Peers)) +} + +// PeersWithAddrs mocks base method. +func (m *MockPeerstore) PeersWithAddrs() peer.IDSlice { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "PeersWithAddrs") + ret0, _ := ret[0].(peer.IDSlice) + return ret0 +} + +// PeersWithAddrs indicates an expected call of PeersWithAddrs. +func (mr *MockPeerstoreMockRecorder) PeersWithAddrs() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PeersWithAddrs", reflect.TypeOf((*MockPeerstore)(nil).PeersWithAddrs)) +} + +// PeersWithKeys mocks base method. +func (m *MockPeerstore) PeersWithKeys() peer.IDSlice { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "PeersWithKeys") + ret0, _ := ret[0].(peer.IDSlice) + return ret0 +} + +// PeersWithKeys indicates an expected call of PeersWithKeys. +func (mr *MockPeerstoreMockRecorder) PeersWithKeys() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PeersWithKeys", reflect.TypeOf((*MockPeerstore)(nil).PeersWithKeys)) +} + +// PrivKey mocks base method. +func (m *MockPeerstore) PrivKey(arg0 peer.ID) crypto.PrivKey { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "PrivKey", arg0) + ret0, _ := ret[0].(crypto.PrivKey) + return ret0 +} + +// PrivKey indicates an expected call of PrivKey. +func (mr *MockPeerstoreMockRecorder) PrivKey(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PrivKey", reflect.TypeOf((*MockPeerstore)(nil).PrivKey), arg0) +} + +// PubKey mocks base method. +func (m *MockPeerstore) PubKey(arg0 peer.ID) crypto.PubKey { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "PubKey", arg0) + ret0, _ := ret[0].(crypto.PubKey) + return ret0 +} + +// PubKey indicates an expected call of PubKey. +func (mr *MockPeerstoreMockRecorder) PubKey(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PubKey", reflect.TypeOf((*MockPeerstore)(nil).PubKey), arg0) +} + +// Put mocks base method. +func (m *MockPeerstore) Put(arg0 peer.ID, arg1 string, arg2 interface{}) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Put", arg0, arg1, arg2) + ret0, _ := ret[0].(error) + return ret0 +} + +// Put indicates an expected call of Put. +func (mr *MockPeerstoreMockRecorder) Put(arg0, arg1, arg2 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Put", reflect.TypeOf((*MockPeerstore)(nil).Put), arg0, arg1, arg2) +} + +// RecordLatency mocks base method. +func (m *MockPeerstore) RecordLatency(arg0 peer.ID, arg1 time.Duration) { + m.ctrl.T.Helper() + m.ctrl.Call(m, "RecordLatency", arg0, arg1) +} + +// RecordLatency indicates an expected call of RecordLatency. +func (mr *MockPeerstoreMockRecorder) RecordLatency(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RecordLatency", reflect.TypeOf((*MockPeerstore)(nil).RecordLatency), arg0, arg1) +} + +// RemoveProtocols mocks base method. +func (m *MockPeerstore) RemoveProtocols(arg0 peer.ID, arg1 ...string) error { + m.ctrl.T.Helper() + varargs := []interface{}{arg0} + for _, a := range arg1 { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "RemoveProtocols", varargs...) + ret0, _ := ret[0].(error) + return ret0 +} + +// RemoveProtocols indicates an expected call of RemoveProtocols. +func (mr *MockPeerstoreMockRecorder) RemoveProtocols(arg0 interface{}, arg1 ...interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]interface{}{arg0}, arg1...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RemoveProtocols", reflect.TypeOf((*MockPeerstore)(nil).RemoveProtocols), varargs...) +} + +// SetAddr mocks base method. +func (m *MockPeerstore) SetAddr(arg0 peer.ID, arg1 multiaddr.Multiaddr, arg2 time.Duration) { + m.ctrl.T.Helper() + m.ctrl.Call(m, "SetAddr", arg0, arg1, arg2) +} + +// SetAddr indicates an expected call of SetAddr. +func (mr *MockPeerstoreMockRecorder) SetAddr(arg0, arg1, arg2 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetAddr", reflect.TypeOf((*MockPeerstore)(nil).SetAddr), arg0, arg1, arg2) +} + +// SetAddrs mocks base method. +func (m *MockPeerstore) SetAddrs(arg0 peer.ID, arg1 []multiaddr.Multiaddr, arg2 time.Duration) { + m.ctrl.T.Helper() + m.ctrl.Call(m, "SetAddrs", arg0, arg1, arg2) +} + +// SetAddrs indicates an expected call of SetAddrs. +func (mr *MockPeerstoreMockRecorder) SetAddrs(arg0, arg1, arg2 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetAddrs", reflect.TypeOf((*MockPeerstore)(nil).SetAddrs), arg0, arg1, arg2) +} + +// SetProtocols mocks base method. +func (m *MockPeerstore) SetProtocols(arg0 peer.ID, arg1 ...string) error { + m.ctrl.T.Helper() + varargs := []interface{}{arg0} + for _, a := range arg1 { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "SetProtocols", varargs...) + ret0, _ := ret[0].(error) + return ret0 +} + +// SetProtocols indicates an expected call of SetProtocols. +func (mr *MockPeerstoreMockRecorder) SetProtocols(arg0 interface{}, arg1 ...interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]interface{}{arg0}, arg1...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetProtocols", reflect.TypeOf((*MockPeerstore)(nil).SetProtocols), varargs...) +} + +// SupportsProtocols mocks base method. +func (m *MockPeerstore) SupportsProtocols(arg0 peer.ID, arg1 ...string) ([]string, error) { + m.ctrl.T.Helper() + varargs := []interface{}{arg0} + for _, a := range arg1 { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "SupportsProtocols", varargs...) + ret0, _ := ret[0].([]string) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// SupportsProtocols indicates an expected call of SupportsProtocols. +func (mr *MockPeerstoreMockRecorder) SupportsProtocols(arg0 interface{}, arg1 ...interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]interface{}{arg0}, arg1...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SupportsProtocols", reflect.TypeOf((*MockPeerstore)(nil).SupportsProtocols), varargs...) +} + +// UpdateAddrs mocks base method. +func (m *MockPeerstore) UpdateAddrs(arg0 peer.ID, arg1, arg2 time.Duration) { + m.ctrl.T.Helper() + m.ctrl.Call(m, "UpdateAddrs", arg0, arg1, arg2) +} + +// UpdateAddrs indicates an expected call of UpdateAddrs. +func (mr *MockPeerstoreMockRecorder) UpdateAddrs(arg0, arg1, arg2 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateAddrs", reflect.TypeOf((*MockPeerstore)(nil).UpdateAddrs), arg0, arg1, arg2) +} diff --git a/packages/fetchai/connections/p2p_libp2p/libp2p_node/utils/utils.go b/packages/fetchai/connections/p2p_libp2p/libp2p_node/utils/utils.go index 573ad8f004..c8b533be0e 100644 --- a/packages/fetchai/connections/p2p_libp2p/libp2p_node/utils/utils.go +++ b/packages/fetchai/connections/p2p_libp2p/libp2p_node/utils/utils.go @@ -82,7 +82,7 @@ var logger zerolog.Logger = NewDefaultLogger() // SetLoggerLevel set utils logger level func SetLoggerLevel(lvl zerolog.Level) { - logger.Level(lvl) + logger = logger.Level(lvl) } func ignore(err error) { @@ -180,7 +180,6 @@ func BootstrapConnect( if count == len(peers) { return errors.New("failed to bootstrap: " + err.Error()) } - // workaround: to avoid getting `failed to find any peer in table` // when calling dht.Provide (happens occasionally) logger.Debug().Msg("waiting for bootstrap peers to be added to dht routing table...") @@ -252,7 +251,7 @@ func FetchAIPublicKeyFromPubKey(publicKey crypto.PubKey) (string, error) { return hex.EncodeToString(raw), nil } -// BTCPubKeyFromFetchAIPublicKey +// BTCPubKeyFromFetchAIPublicKey from public key string func BTCPubKeyFromFetchAIPublicKey(publicKey string) (*btcec.PublicKey, error) { pbkBytes, err := hex.DecodeString(publicKey) if err != nil { @@ -268,7 +267,7 @@ func BTCPubKeyFromEthereumPublicKey(publicKey string) (*btcec.PublicKey, error) return BTCPubKeyFromUncompressedHex(publicKey[2:]) } -// ConvertStrEncodedSignatureToDER +// ConvertStrEncodedSignatureToDER to convert signature to DER format // References: // - https://github.com/fetchai/agents-aea/blob/main/aea/crypto/cosmos.py#L258 // - https://github.com/btcsuite/btcd/blob/master/btcec/signature.go#L47 @@ -288,7 +287,7 @@ func ConvertStrEncodedSignatureToDER(signature []byte) []byte { return sigDER } -// ConvertDEREncodedSignatureToStr +// ConvertDEREncodedSignatureToStr Convert signatue from der format to string // References: // - https://github.com/fetchai/agents-aea/blob/main/aea/crypto/cosmos.py#L258 // - https://github.com/btcsuite/btcd/blob/master/btcec/signature.go#L47 @@ -316,14 +315,14 @@ func ParseFetchAISignature(signature string) (*btcec.Signature, error) { // VerifyLedgerSignature verify signature of message using public key for supported ledgers func VerifyLedgerSignature( - ledgerId string, + ledgerID string, message []byte, signature string, - pubkey string, + pubKey string, ) (bool, error) { - verifySignature, found := verifyLedgerSignatureTable[ledgerId] + verifySignature, found := verifyLedgerSignatureTable[ledgerID] if found { - return verifySignature(message, signature, pubkey) + return verifySignature(message, signature, pubKey) } return false, errors.New("unsupported ledger") } @@ -371,6 +370,7 @@ func VerifyFetchAISignatureLibp2p(message []byte, signature string, pubkey strin return verifyKey.Verify(message, sigDER) } +// SignFetchAI signs message with private key func SignFetchAI(message []byte, privKey string) (string, error) { signingKey, _, err := KeyPairFromFetchAIKey(privKey) if err != nil { @@ -420,8 +420,8 @@ func RecoverAddressFromEthereumSignature(message []byte, signature string) (stri // VerifyEthereumSignatureETH verify ethereum signature using ethereum public key func VerifyEthereumSignatureETH(message []byte, signature string, pubkey string) (bool, error) { - // get expected signer address - expectedAddress, err := EthereumAddressFromPublicKey(pubkey) + // get ted signer address + tedAddress, err := EthereumAddressFromPublicKey(pubkey) if err != nil { return false, err } @@ -432,8 +432,8 @@ func VerifyEthereumSignatureETH(message []byte, signature string, pubkey string) return false, err } - if recoveredAddress != expectedAddress { - return false, errors.New("recovered and expected addresses don't match") + if recoveredAddress != tedAddress { + return false, errors.New("recovered and ted addresses don't match") } return true, nil @@ -441,13 +441,13 @@ func VerifyEthereumSignatureETH(message []byte, signature string, pubkey string) // KeyPairFromFetchAIKey key pair from hex encoded secp256k1 private key func KeyPairFromFetchAIKey(key string) (crypto.PrivKey, crypto.PubKey, error) { - pk_bytes, err := hex.DecodeString(key) + pkBytes, err := hex.DecodeString(key) if err != nil { return nil, nil, err } - btc_private_key, _ := btcec.PrivKeyFromBytes(btcec.S256(), pk_bytes) - prvKey, pubKey, err := crypto.KeyPairFromStdKey(btc_private_key) + btcPrivateKey, _ := btcec.PrivKeyFromBytes(btcec.S256(), pkBytes) + prvKey, pubKey, err := crypto.KeyPairFromStdKey(btcPrivateKey) if err != nil { return nil, nil, err } @@ -457,11 +457,11 @@ func KeyPairFromFetchAIKey(key string) (crypto.PrivKey, crypto.PubKey, error) { // AgentAddressFromPublicKey get wallet address from public key associated with ledgerId // format from: https://github.com/fetchai/agents-aea/blob/main/aea/crypto/cosmos.py#L120 -func AgentAddressFromPublicKey(ledgerId string, publicKey string) (string, error) { - if addressFromPublicKey, found := addressFromPublicKeyTable[ledgerId]; found { +func AgentAddressFromPublicKey(ledgerID string, publicKey string) (string, error) { + if addressFromPublicKey, found := addressFromPublicKeyTable[ledgerID]; found { return addressFromPublicKey(publicKey) } - return "", errors.New("unsupported ledger " + ledgerId) + return "", errors.New("Unsupported ledger " + ledgerID) } // FetchAIAddressFromPublicKey get wallet address from hex encoded secp256k1 public key @@ -549,18 +549,18 @@ func encodeChecksumEIP55(address []byte) string { } // IDFromFetchAIPublicKey Get PeeID (multihash) from fetchai public key -func IDFromFetchAIPublicKey(public_key string) (peer.ID, error) { - b, err := hex.DecodeString(public_key) +func IDFromFetchAIPublicKey(publicKey string) (peer.ID, error) { + b, err := hex.DecodeString(publicKey) if err != nil { return "", err } - pub_key, err := btcec.ParsePubKey(b, btcec.S256()) + pubKey, err := btcec.ParsePubKey(b, btcec.S256()) if err != nil { return "", err } - multihash, err := peer.IDFromPublicKey((*crypto.Secp256k1PublicKey)(pub_key)) + multihash, err := peer.IDFromPublicKey((*crypto.Secp256k1PublicKey)(pubKey)) if err != nil { return "", err } @@ -597,6 +597,7 @@ func IDFromFetchAIPublicKeyUncompressed(publicKey string) (peer.ID, error) { return multihash, nil } +// FetchAIPublicKeyFromFetchAIPrivateKey get fetchai public key from fetchai private key func FetchAIPublicKeyFromFetchAIPrivateKey(privateKey string) (string, error) { pkBytes, err := hex.DecodeString(privateKey) if err != nil { @@ -613,6 +614,7 @@ func FetchAIPublicKeyFromFetchAIPrivateKey(privateKey string) (string, error) { // WriteBytesConn send bytes to `conn` func WriteBytesConn(conn net.Conn, data []byte) error { + if len(data) > math.MaxInt32 { logger.Error().Msg("data size too large") return errors.New("data size too large") @@ -621,6 +623,7 @@ func WriteBytesConn(conn net.Conn, data []byte) error { logger.Error().Msg("No data to write") return nil } + size := uint32(len(data)) buf := make([]byte, 4, 4+size) binary.BigEndian.PutUint32(buf, size) @@ -672,6 +675,7 @@ func ReadBytes(s network.Stream) ([]byte, error) { if s == nil { panic("CRITICAL can not write to nil stream") } + rstream := bufio.NewReader(s) buf := make([]byte, 4) @@ -687,6 +691,7 @@ func ReadBytes(s network.Stream) ([]byte, error) { if size > maxMessageSizeDelegateConnection { return nil, errors.New("expected message size larger than maximum allowed") } + //logger.Debug().Msgf("expecting %d", size) buf = make([]byte, size) @@ -709,6 +714,7 @@ func WriteBytes(s network.Stream, data []byte) error { if s == nil { panic("CRITICAL, can not write to nil stream") } + wstream := bufio.NewWriter(s) size := uint32(len(data)) @@ -723,6 +729,7 @@ func WriteBytes(s network.Stream, data []byte) error { return err } + //logger.Debug().Msgf("writing %d", len(data)) _, err = wstream.Write(data) if err != nil { logger.Error(). @@ -733,12 +740,73 @@ func WriteBytes(s network.Stream, data []byte) error { if s == nil { panic("CRITICAL, can not flush nil stream") } + err = wstream.Flush() + return err +} + +// ReadString from a network stream +func ReadString(s network.Stream) (string, error) { + data, err := ReadBytes(s) + return string(data), err +} + +// WriteEnvelope to a network stream +func WriteEnvelope(envel *aea.Envelope, s network.Stream) error { + wstream := bufio.NewWriter(s) + data, err := proto.Marshal(envel) + if err != nil { + return err + } + size := uint32(len(data)) + + buf := make([]byte, 4) + binary.BigEndian.PutUint32(buf, size) + //log.Println("DEBUG writing size:", size, buf) + _, err = wstream.Write(buf) + if err != nil { + return err + } + + //log.Println("DEBUG writing data:", data) + _, err = wstream.Write(data) + if err != nil { + return err + } + + wstream.Flush() + return nil +} + +// ReadEnvelope from a network stream +func ReadEnvelope(s network.Stream) (*aea.Envelope, error) { + envel := &aea.Envelope{} + rstream := bufio.NewReader(s) + + buf := make([]byte, 4) + _, err := io.ReadFull(rstream, buf) + if err != nil { logger.Error(). Str("err", err.Error()). - Msg("Error on stream flush") - return err + Msg("while reading size") + return envel, err } - return err + + size := binary.BigEndian.Uint32(buf) + if size > maxMessageSizeDelegateConnection { + return nil, errors.New("ted message size larger than maximum allowed") + } + //logger.Debug().Msgf("received size: %d %x", size, buf) + buf = make([]byte, size) + _, err = io.ReadFull(rstream, buf) + if err != nil { + logger.Error(). + Str("err", err.Error()). + Msg("while reading data") + return envel, err + } + + err = proto.Unmarshal(buf, envel) + return envel, err } diff --git a/packages/fetchai/connections/p2p_libp2p/libp2p_node/utils/utils_test.go b/packages/fetchai/connections/p2p_libp2p/libp2p_node/utils/utils_test.go new file mode 100644 index 0000000000..f959b0cabd --- /dev/null +++ b/packages/fetchai/connections/p2p_libp2p/libp2p_node/utils/utils_test.go @@ -0,0 +1,475 @@ +/* -*- coding: utf-8 -*- +* ------------------------------------------------------------------------------ +* +* Copyright 2018-2021 Fetch.AI Limited +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +* +* ------------------------------------------------------------------------------ + */ + +package utils + +import ( + "bufio" + "bytes" + "context" + "encoding/json" + "errors" + "io" + "libp2p_node/aea" + mocks "libp2p_node/mocks" + "net" + "reflect" + "testing" + + "bou.ke/monkey" + gomock "github.com/golang/mock/gomock" + "github.com/libp2p/go-libp2p-core/peer" + dht "github.com/libp2p/go-libp2p-kad-dht" + kb "github.com/libp2p/go-libp2p-kbucket" + ma "github.com/multiformats/go-multiaddr" + "github.com/rs/zerolog" + "github.com/stretchr/testify/assert" +) + +// Crypto operations + +func TestEthereumCrypto(t *testing.T) { + //privateKey := "0xb60fe8027fb82f1a1bd6b8e66d4400f858989a2c67428a4e7f589441700339b0" + publicKey := "0xf753e5a9e2368e97f4db869a0d956d3ffb64672d6392670572906c786b5712ada13b6bff882951b3ba3dd65bdacc915c2b532efc3f183aa44657205c6c337225" + address := "0xb8d8c62d4a1999b7aea0aebBD5020244a4a9bAD8" + publicKeySignature := "0x304c2ba4ae7fa71295bfc2920b9c1268d574d65531f1f4d2117fc1439a45310c37ab75085a9df2a4169a4d47982b330a4387b1ded0c8881b030629db30bbaf3a1c" + + addFromPublicKey, err := EthereumAddressFromPublicKey(publicKey) + if err != nil || addFromPublicKey != address { + t.Error( + "Error when computing address from public key or address and public key don't match", + ) + } + + _, err = BTCPubKeyFromEthereumPublicKey(publicKey) + if err != nil { + t.Errorf("While building BTC public key from string: %s", err.Error()) + } + + /* + ethSig, err := secp256k1.Sign(hashedPublicKey, hexutil.MustDecode(privateKey)) + if err != nil { + t.Error(err.Error()) + } + println(hexutil.Encode(ethSig)) + hash := sha3.NewLegacyKeccak256() + _, err = hash.Write([]byte(publicKey)) + if err != nil { + t.Error(err.Error()) + } + sha3KeccakHash := hash.Sum(nil) + */ + + valid, err := VerifyEthereumSignatureETH([]byte(publicKey), publicKeySignature, publicKey) + if err != nil { + t.Error(err.Error()) + } + + if !valid { + t.Errorf("Signer address don't match %s", addFromPublicKey) + } +} + +func TestFetchAICrypto(t *testing.T) { + publicKey := "02358e3e42a6ba15cf6b2ba6eb05f02b8893acf82b316d7dd9cda702b0892b8c71" + address := "fetch19dq2mkcpp6x0aypxt9c9gz6n4fqvax0x9a7t5r" + peerPublicKey := "027af21aff853b9d9589867ea142b0a60a9611fc8e1fae04c2f7144113fa4e938e" + pySigStrCanonize := "N/GOa7/m3HU8/gpLJ88VCQ6vXsdrfiiYcqnNtF+c2N9VG9ZIiycykN4hdbpbOCGrChMYZQA3G1GpozsShrUBgg==" + + addressFromPublicKey, _ := FetchAIAddressFromPublicKey(publicKey) + if address != addressFromPublicKey { + t.Error("[ERR] Addresses don't match") + } else { + t.Log("[OK] Agent address matches its public key") + } + + valid, err := VerifyFetchAISignatureBTC( + []byte(peerPublicKey), + pySigStrCanonize, + publicKey, + ) + if !valid { + t.Errorf("Signature using BTC don't match %s", err.Error()) + } + valid, err = VerifyFetchAISignatureLibp2p( + []byte(peerPublicKey), + pySigStrCanonize, + publicKey, + ) + if !valid { + t.Errorf("Signature using LPP don't match %s", err.Error()) + } +} + +func TestSetLoggerLevel(t *testing.T) { + assert.Equal(t, logger.GetLevel(), zerolog.Level(0), "Initial log level is not 0") + + lvl := zerolog.InfoLevel + SetLoggerLevel(lvl) + + assert.Equal( + t, + logger.GetLevel(), + lvl, + "Waited for logger level %d but got %d", + lvl, + logger.GetLevel(), + ) +} + +func Example_ignore() { + ignore(errors.New("Test")) + // Output: IGNORED: Test +} + +func TestNewDefaultLoggerWithFields(t *testing.T) { + fields := map[string]string{ + "test_field": "test_value", + } + var logBuffer bytes.Buffer + logger := NewDefaultLoggerWithFields(fields).Output(&logBuffer) + logger.Info().Msg("test") + var jsonResult map[string]interface{} + err := json.Unmarshal(logBuffer.Bytes(), &jsonResult) + assert.Equal(t, nil, err) + assert.Equal(t, jsonResult["test_field"], "test_value") +} + +func TestComputeCID(t *testing.T) { + address := "fetch19dq2mkcpp6x0aypxt9c9gz6n4fqvax0x9a7t5r" + cid, err := ComputeCID(address) + assert.Equal(t, nil, err) + assert.Equal(t, "QmZ6ryKyS9rSnesX8YnFLAmFwFuRMdHpE7pQ2V6SjXTbqM", cid.String()) +} + +func TestWriteBytes(t *testing.T) { + mockCtrl := gomock.NewController(t) + defer mockCtrl.Finish() + mockStream := mocks.NewMockStream(mockCtrl) + mockStream.EXPECT().Write([]byte{0, 0, 0, 5, 104, 101, 108, 108, 111}).Return(9, nil).Times(1) + err := WriteBytes(mockStream, []byte("hello")) + assert.Equal(t, nil, err) + + mockStream.EXPECT(). + Write([]byte{0, 0, 0, 4, 104, 101, 108, 108}). + Return(8, errors.New("oops")). + Times(1) + err = WriteBytes(mockStream, []byte("hell")) + assert.NotEqual(t, err, nil) +} + +func TestReadBytesConn(t *testing.T) { + mockCtrl := gomock.NewController(t) + defer mockCtrl.Finish() + mockConn := mocks.NewMockConn(mockCtrl) + mockConn.EXPECT().Read(gomock.Any()).Return(4, nil).Times(2) + buf, err := ReadBytesConn(mockConn) + assert.Equal(t, nil, err) + assert.Equal(t, "", string(buf)) +} + +func TestWriteBytesConn(t *testing.T) { + mockCtrl := gomock.NewController(t) + defer mockCtrl.Finish() + mockConn := mocks.NewMockConn(mockCtrl) + mockConn.EXPECT().Write(gomock.Any()).Return(0, nil).Times(1) + err := WriteBytesConn(mockConn, []byte("ABC")) + assert.Equal(t, nil, err) +} + +func TestReadString(t *testing.T) { + // test ReadString and ReadBytes + mockCtrl := gomock.NewController(t) + defer mockCtrl.Finish() + mockStream := mocks.NewMockStream(mockCtrl) + + defer monkey.UnpatchAll() + + t.Run("TestReadString", func(t *testing.T) { + monkey.Patch(bufio.NewReader, func(reader io.Reader) *bufio.Reader { + return bufio.NewReaderSize( + bytes.NewReader([]byte{0, 0, 0, 5, 104, 101, 108, 108, 111}), + 100, + ) + }) + buf, err := ReadString(mockStream) + assert.Equal(t, nil, err) + assert.Equal(t, "hello", buf) + }) +} + +func TestReadWriteEnvelope(t *testing.T) { + mockCtrl := gomock.NewController(t) + defer mockCtrl.Finish() + mockStream := mocks.NewMockStream(mockCtrl) + defer monkey.UnpatchAll() + address := "0xb8d8c62d4a1999b7aea0aebBD5020244a4a9bAD8" + buffer := bytes.NewBuffer([]byte{}) + + t.Run("TestWriteEnvelope", func(t *testing.T) { + monkey.Patch(bufio.NewWriter, func(writer io.Writer) *bufio.Writer { + return bufio.NewWriterSize(buffer, 100) + }) + err := WriteEnvelope(&aea.Envelope{ + To: address, + Sender: address, + }, mockStream) + assert.Equal(t, nil, err) + }) + + t.Run("TestReadEnvelope", func(t *testing.T) { + monkey.Patch(bufio.NewReader, func(reader io.Reader) *bufio.Reader { + return bufio.NewReaderSize(bytes.NewReader(buffer.Bytes()), 100) + }) + env, err := ReadEnvelope(mockStream) + assert.Equal(t, nil, err) + assert.Equal(t, address, env.To) + }) +} + +func TestReadWriteEnvelopeFromConnection(t *testing.T) { + mockCtrl := gomock.NewController(t) + defer mockCtrl.Finish() + defer monkey.UnpatchAll() + address := "0xb8d8c62d4a1999b7aea0aebBD5020244a4a9bAD8" + buffer := bytes.NewBuffer([]byte{}) + mockConn := mocks.NewMockConn(mockCtrl) + + t.Run("TestWriteEnvelope", func(t *testing.T) { + monkey.PatchInstanceMethod( + reflect.TypeOf(mockConn), + "Write", + func(_ *mocks.MockConn, b []byte) (int, error) { + buffer.Write(b) + return 0, nil + }, + ) + + err := WriteEnvelopeConn(mockConn, &aea.Envelope{ + To: address, + Sender: address, + }) + assert.Equal(t, nil, err) + assert.NotEqual(t, 0, buffer) + }) + + t.Run("TestReadEnvelope", func(t *testing.T) { + monkey.Patch(ReadBytesConn, func(conn net.Conn) ([]byte, error) { + return buffer.Bytes()[4:], nil + }) + env, err := ReadEnvelopeConn(mockConn) + assert.Equal(t, nil, err) + assert.Equal(t, address, env.To) + }) +} + +func TestGetPeersAddrInfo(t *testing.T) { + addrs, err := GetPeersAddrInfo( + []string{ + "/dns4/acn.fetch.ai/tcp/9001/p2p/16Uiu2HAmVWnopQAqq4pniYLw44VRvYxBUoRHqjz1Hh2SoCyjbyRW", + }, + ) + assert.Equal(t, nil, err) + assert.Equal(t, 1, len(addrs)) +} + +func TestFetchAIPublicKeyFromPubKey(t *testing.T) { + //(publicKey crypto.PubKey) (string, error) { + _, pubKey, err := KeyPairFromFetchAIKey( + "3e7a1f43b2d8a4b9f63a2ffeb1d597f971a8db7ffd95453173268b453106cadc", + ) + assert.Equal(t, nil, err) + key, err := FetchAIPublicKeyFromPubKey(pubKey) + assert.Equal(t, nil, err) + assert.Equal(t, "03b7e977f498dce004e2614764ff576e17cc6691135497e7bcb5d3441e816ba9e1", key) +} + +func TestIDFromFetchAIPublicKey(t *testing.T) { + _, pubKey, err := KeyPairFromFetchAIKey( + "3e7a1f43b2d8a4b9f63a2ffeb1d597f971a8db7ffd95453173268b453106cadc", + ) + assert.Equal(t, nil, err) + key, err := FetchAIPublicKeyFromPubKey(pubKey) + assert.Equal(t, nil, err) + peerID, err := IDFromFetchAIPublicKey(key) + assert.Equal(t, nil, err) + assert.NotEqual(t, 0, len(peerID)) +} + +func TestAgentAddressFromPublicKey(t *testing.T) { + address, err := AgentAddressFromPublicKey( + "fetchai", + "3e7a1f43b2d8a4b9f63a2ffeb1d597f971a8db7ffd95453173268b453106cadc", + ) + assert.Equal(t, nil, err) + assert.NotEqual(t, 0, len(address)) +} + +func TestCosmosAddressFromPublicKey(t *testing.T) { + address, err := CosmosAddressFromPublicKey( + "3e7a1f43b2d8a4b9f63a2ffeb1d597f971a8db7ffd95453173268b453106cadc", + ) + assert.Equal(t, nil, err) + assert.NotEqual(t, 0, len(address)) +} + +func TestFetchAIPublicKeyFromFetchAIPrivateKey(t *testing.T) { + key, err := FetchAIPublicKeyFromFetchAIPrivateKey( + "3e7a1f43b2d8a4b9f63a2ffeb1d597f971a8db7ffd95453173268b453106cadc", + ) + assert.Equal(t, nil, err) + assert.Equal(t, "03b7e977f498dce004e2614764ff576e17cc6691135497e7bcb5d3441e816ba9e1", key) +} + +func TestIDFromFetchAIPublicKeyUncompressed(t *testing.T) { + //bad pub key + _, err := IDFromFetchAIPublicKeyUncompressed("some") + assert.NotEqual(t, nil, err) + // good pub key + id, err := IDFromFetchAIPublicKeyUncompressed( + "50863AD64A87AE8A2FE83C1AF1A8403CB53F53E486D8511DAD8A04887E5B23522CD470243453A299FA9E77237716103ABC11A1DF38855ED6F2EE187E9C582BA6", + ) + assert.Equal(t, nil, err) + assert.Equal( + t, + peer.ID( + "\x00%\b\x02\x12!\x02P\x86:\xd6J\x87\xae\x8a/\xe8<\x1a\xf1\xa8@<\xb5?S\xe4\x86\xd8Q\x1d\xad\x8a\x04\x88~[#R", + ), + id, + ) +} + +func TestSignFetchAI(t *testing.T) { + privKey := "3e7a1f43b2d8a4b9f63a2ffeb1d597f971a8db7ffd95453173268b453106cadc" + message := []byte("somebytes") + + _, pubKey, err := KeyPairFromFetchAIKey(privKey) + assert.Equal(t, nil, err) + fetchPubKey, err := FetchAIPublicKeyFromPubKey(pubKey) + assert.Equal(t, nil, err) + + signature, err := SignFetchAI(message, privKey) + assert.Equal(t, nil, err) + assert.NotEqual(t, 0, len(signature)) + + isValid, err := VerifyLedgerSignature("fetchai", message, signature, fetchPubKey) + assert.Equal(t, nil, err) + assert.Equal(t, true, isValid) + +} + +func TestBootstrapConnect(t *testing.T) { + ctx := context.Background() + mockCtrl := gomock.NewController(t) + defer mockCtrl.Finish() + defer monkey.UnpatchAll() + var ipfsdht *dht.IpfsDHT + var routingTable *kb.RoutingTable + + mockPeerstore := mocks.NewMockPeerstore(mockCtrl) + peers := make([]peer.AddrInfo, 2) + var addrs []ma.Multiaddr + peers[0] = peer.AddrInfo{ID: peer.ID("peer1"), Addrs: addrs} + peers[1] = peer.AddrInfo{ID: peer.ID("peer2"), Addrs: addrs} + + mockHost := mocks.NewMockHost(mockCtrl) + + mockHost.EXPECT().ID().Return(peer.ID("host_id")).Times(2) + mockHost.EXPECT().Peerstore().Return(mockPeerstore).Times(2) + mockHost.EXPECT().Connect(gomock.Any(), gomock.Any()).Return(nil).Times(2) + mockPeerstore.EXPECT().AddAddrs(gomock.Any(), gomock.Any(), gomock.Any()).Return().Times(2) + + t.Run("TestOk", func(t *testing.T) { + monkey.PatchInstanceMethod( + reflect.TypeOf(routingTable), + "Find", + func(_ *kb.RoutingTable, _ peer.ID) peer.ID { + return peer.ID("som peer") + }, + ) + monkey.PatchInstanceMethod( + reflect.TypeOf(ipfsdht), + "RoutingTable", + func(_ *dht.IpfsDHT) *kb.RoutingTable { + return routingTable + }, + ) + + err := BootstrapConnect(ctx, mockHost, ipfsdht, peers) + assert.Equal(t, nil, err) + }) + + mockHost = mocks.NewMockHost(mockCtrl) + + mockHost.EXPECT().ID().Return(peer.ID("host_id")).Times(2) + mockHost.EXPECT().Peerstore().Return(mockPeerstore).Times(2) + mockHost.EXPECT().Connect(gomock.Any(), gomock.Any()).Return(nil).Times(2) + mockPeerstore.EXPECT().AddAddrs(gomock.Any(), gomock.Any(), gomock.Any()).Return().Times(2) + + t.Run("Test_PeersNotAdded", func(t *testing.T) { + monkey.PatchInstanceMethod( + reflect.TypeOf(routingTable), + "Find", + func(_ *kb.RoutingTable, _ peer.ID) peer.ID { + return peer.ID("") + }, + ) + monkey.PatchInstanceMethod( + reflect.TypeOf(ipfsdht), + "RoutingTable", + func(_ *dht.IpfsDHT) *kb.RoutingTable { + return routingTable + }, + ) + + err := BootstrapConnect(ctx, mockHost, ipfsdht, peers) + assert.NotEqual(t, nil, err) + assert.Contains(t, err.Error(), "timeout: entry peer haven't been added to DHT") + }) + + mockHost = mocks.NewMockHost(mockCtrl) + + mockHost.EXPECT().ID().Return(peer.ID("host_id")).Times(2) + mockHost.EXPECT().Peerstore().Return(mockPeerstore).Times(2) + mockHost.EXPECT().Connect(gomock.Any(), gomock.Any()).Return(errors.New("some error")).Times(2) + mockPeerstore.EXPECT().AddAddrs(gomock.Any(), gomock.Any(), gomock.Any()).Return().Times(2) + + t.Run("Test_PeersNotConnected", func(t *testing.T) { + monkey.PatchInstanceMethod( + reflect.TypeOf(routingTable), + "Find", + func(_ *kb.RoutingTable, _ peer.ID) peer.ID { + return peer.ID("") + }, + ) + monkey.PatchInstanceMethod( + reflect.TypeOf(ipfsdht), + "RoutingTable", + func(_ *dht.IpfsDHT) *kb.RoutingTable { + return routingTable + }, + ) + + err := BootstrapConnect(ctx, mockHost, ipfsdht, peers) + assert.NotEqual(t, nil, err) + assert.Equal(t, "failed to bootstrap: some error", err.Error()) + }) + +} diff --git a/packages/hashes.csv b/packages/hashes.csv index 84274a556f..0c64dda7ef 100644 --- a/packages/hashes.csv +++ b/packages/hashes.csv @@ -41,7 +41,7 @@ fetchai/connections/http_server,QmZqiszQJuG7XA6LFWsMqXYSmViTrUkDfpkHwgYtDMbyXy fetchai/connections/ledger,QmT7ffwPzJ3isCMhN2qoj6NRyqinE2RkpSpUKNRFRXxpes fetchai/connections/local,QmUxLhmeE98S8BcuRDB7W7pRsJzpC3wVJV5ELLxVeEkoKC fetchai/connections/oef,QmaHQhxryQLaBk5TvC4iJFZtFvkPp4CoHxHg1iLnh2PAdm -fetchai/connections/p2p_libp2p,QmQ1FBNQezRPufMZDyVcW4KuADyKPQHb2Xo4qtLFcTgmAD +fetchai/connections/p2p_libp2p,QmTw3hmsvSggmWkEnh74VbGMn4CZKRES1c2FEE6jroGyfD fetchai/connections/p2p_libp2p_client,QmYq2U83xTTnv1WDy1quvEz47QaV2uw1ZmB6sFHoCL31GC fetchai/connections/p2p_stub,QmToCExj3ZpdxUu3vYSMo4jZRJiMTkXMbyhbh18Hq6Nc4b fetchai/connections/prometheus,QmeRmgrccxRh8UG4AfTMct6D5wSuK9BXYoLwnohut2LJaB From 1e81944743a6f7593a72f9369028553d18e09348 Mon Sep 17 00:00:00 2001 From: MarcoFavorito Date: Mon, 10 May 2021 22:04:06 +0200 Subject: [PATCH 030/147] parametrize bumper version helper class --- scripts/bump_aea_version.py | 191 +++++++++++++++++++++++------------- 1 file changed, 124 insertions(+), 67 deletions(-) diff --git a/scripts/bump_aea_version.py b/scripts/bump_aea_version.py index 9f2a1d0cdd..0d6660bdf0 100644 --- a/scripts/bump_aea_version.py +++ b/scripts/bump_aea_version.py @@ -27,8 +27,9 @@ import sys from functools import wraps from pathlib import Path -from typing import Optional +from typing import Any, Callable, Collection, Optional, Pattern, Tuple, cast +from packaging.specifiers import SpecifierSet from packaging.version import Version from aea.configurations.constants import ( @@ -62,14 +63,27 @@ ) ) -IGNORE_DIRS = [Path(".git")] - - -def check_executed(func): +_AEA_ALL_PATTERN = "(?<={package_name}\[all\]==){version}" +AEA_PATHS = [ + (Path("deploy-image", "Dockerfile"), _AEA_ALL_PATTERN), + (Path("develop-image", "docker-env.sh"), "(?<=aea-develop:){version}"), + (Path("docs", "quickstart.md"), "(?<=v){version}"), + (Path("examples", "tac_deploy", "Dockerfile"), _AEA_ALL_PATTERN), + (Path("scripts", "install.ps1"), _AEA_ALL_PATTERN), + (Path("scripts", "install.sh"), _AEA_ALL_PATTERN), + ( + Path("tests", "test_docs", "test_bash_yaml", "md_files", "bash-quickstart.md"), + "(?<=v){version}", + ), + (Path("user-image", "docker-env.sh"), "(?<=aea-user:){version}"), +] + + +def check_executed(func: Callable) -> Callable: """Check a functor has been already executed; if yes, raise error.""" @wraps(func) - def wrapper(self, *args, **kwargs): + def wrapper(self: Any, *args: Any, **kwargs: Any) -> None: if self.is_executed: raise ValueError("already executed") self._executed = True @@ -81,19 +95,35 @@ def wrapper(self, *args, **kwargs): class PythonPackageVersionBumper: """Utility class to bump Python package versions.""" - def __init__(self, root_dir: Path, python_pkg_dir: Path, new_version: Version): + IGNORE_DIRS = (Path(".git"),) + + def __init__( + self, + root_dir: Path, + python_pkg_dir: Path, + new_version: Version, + package_name: Optional[str] = None, + files_to_pattern: Collection[Tuple[Path, str]] = (), + ignore_dirs: Collection[Path] = (), + ): """ Initialize the utility class. :param root_dir: the root directory from which to look for files. :param python_pkg_dir: the path to the Python package to upgrade. :param new_version: the new version. + :param package_name: the Python package name aliases (defaults to + dirname of python_pkg_dir). + :param ignore_dirs: a list of paths to ignore during the substitution. """ self.root_dir = root_dir self.python_pkg_dir = python_pkg_dir self.new_version = new_version + self.package_name = package_name or self.python_pkg_dir.name + self.ignore_dirs = ignore_dirs or self.IGNORE_DIRS + self.files_to_pattern = files_to_pattern - self._current_version = None + self._current_version: Optional[str] = None # functor pattern self._executed: bool = False @@ -113,13 +143,13 @@ def result(self) -> bool: """Get the result.""" if not self.is_executed: raise ValueError("not executed yet") - return self._result + return cast(bool, self._result) @check_executed def run(self) -> bool: """Main entrypoint.""" new_version_string = str(self.new_version) - current_version_str = self.update_version_for_aea(new_version_string) + current_version_str = self.update_version_for_package(new_version_string) # validate current version current_version: Version = Version(current_version_str) @@ -127,29 +157,19 @@ def run(self) -> bool: self._current_version = current_version_str self.update_version_for_files() - return update_aea_version_specifiers(current_version, self.new_version) + return self.update_version_specifiers(current_version, self.new_version) def update_version_for_files(self) -> None: """Update the version.""" - files = [ - Path("benchmark", "run_from_branch.sh"), - Path("deploy-image", "Dockerfile"), - Path("develop-image", "docker-env.sh"), - Path("docs", "quickstart.md"), - Path("examples", "tac_deploy", "Dockerfile"), - Path("scripts", "install.ps1"), - Path("scripts", "install.sh"), - Path( - "tests", "test_docs", "test_bash_yaml", "md_files", "bash-quickstart.md" - ), - Path("user-image", "docker-env.sh"), - ] - for filepath in files: + for filepath, regex_template in self.files_to_pattern: self.update_version_for_file( - filepath, self._current_version, str(self.new_version) + filepath, + cast(str, self._current_version), + str(self.new_version), + version_regex_template=regex_template, ) - def update_version_for_aea(self, new_version: str) -> str: + def update_version_for_package(self, new_version: str) -> str: """ Update version for file. @@ -157,7 +177,7 @@ def update_version_for_aea(self, new_version: str) -> str: :return: the current version """ current_version = "" - path = Path("aea", "__version__.py") + path = self.python_pkg_dir / Path("__version__.py") with open(path, "rt") as fin: for line in fin: if "__version__" not in line: @@ -171,56 +191,93 @@ def update_version_for_aea(self, new_version: str) -> str: self.update_version_for_file(path, current_version, new_version) return current_version - @classmethod def update_version_for_file( - cls, path: Path, current_version: str, new_version: str + self, + path: Path, + current_version: str, + new_version: str, + version_regex_template: Optional[str] = None, ) -> None: """ Update version for file. :param path: the file path - :param current_version: the current version + :param current_version: the regex for the current version :param new_version: the new version + :param version_regex_template: the regex template + to replace with the current version. Defaults to exactly + the current version. """ + if version_regex_template is not None: + regex_str = version_regex_template.format(package_name=self.package_name, version=current_version) + else: + regex_str = current_version + pattern = re.compile(regex_str) content = path.read_text() - content = content.replace(current_version, new_version) + content = pattern.sub(new_version, content) path.write_text(content) + def update_version_specifiers( + self, old_version: Version, new_version: Version + ) -> bool: + """ + Update specifier set. -def update_aea_version_specifiers(old_version: Version, new_version: Version) -> bool: - """ - Update aea_version specifier set in docs. - - :param old_version: the old version. - :param new_version: the new version. - :return: True if the update has been done, False otherwise. - """ - old_specifier_set = compute_specifier_from_version(old_version) - new_specifier_set = compute_specifier_from_version(new_version) - print(f"Old version specifier: {old_specifier_set}") - print(f"New version specifier: {new_specifier_set}") - old_specifier_set_regex = re.compile(str(old_specifier_set).replace(" ", " *")) - if old_specifier_set == new_specifier_set: - print("Not updating version specifier - they haven't changed.") - return False - for file in filter(lambda p: not p.is_dir(), Path(".").rglob("*")): - dir_root = Path(file.parts[0]) - if dir_root in IGNORE_DIRS: - print(f"Skipping '{file}'...") - continue - print( - f"Replacing '{old_specifier_set}' with '{new_specifier_set}' in '{file}'... ", - end="", - ) - try: - content = file.read_text() - except UnicodeDecodeError as e: - print(f"Cannot read {file}: {str(e)}. Continue...") - else: - if old_specifier_set_regex.search(content) is not None: - content = old_specifier_set_regex.sub(new_specifier_set, content) - file.write_text(content) - return True + :param old_version: the old version. + :param new_version: the new version. + :return: True if the update has been done, False otherwise. + """ + old_specifier_set = compute_specifier_from_version(old_version) + new_specifier_set = compute_specifier_from_version(new_version) + print(f"Old version specifier: {old_specifier_set}") + print(f"New version specifier: {new_specifier_set}") + old_specifier_set_regex = self.get_regex_from_specifier_set(old_specifier_set) + if old_specifier_set == new_specifier_set: + print("Not updating version specifier - they haven't changed.") + return False + for file in filter(lambda p: not p.is_dir(), self.root_dir.rglob("*")): + dir_root = Path(file.parts[0]) + if dir_root in self.ignore_dirs: + print(f"Skipping '{file}'...") + continue + print( + f"Replacing '{old_specifier_set}' with '{new_specifier_set}' in '{file}'... ", + end="", + ) + try: + content = file.read_text() + except UnicodeDecodeError as e: + print(f"Cannot read {file}: {str(e)}. Continue...") + else: + if old_specifier_set_regex.search(content) is not None: + content = old_specifier_set_regex.sub(new_specifier_set, content) + file.write_text(content) + return True + + def get_regex_from_specifier_set(self, specifier_set: str) -> Pattern: + """ + Get the regex for specifier sets. + + This function accepts input of the form: + + ">={lower_bound_version}, <{upper_bound_version}" + + And computes a regex pattern: + + ">={lower_bound_version}, *<{upper_bound_version}|<{upper_bound_version}, *>={lower_bound_version}" + + i.e. not considering the order of the specifiers. + + :param specifier_set: The string representation of the specifier set + :return: a regex pattern + """ + # TODO add different type of matches + specifiers = SpecifierSet(specifier_set) + upper, lower = sorted(specifiers, key=lambda x: str(x)) + alternatives = list() + alternatives.append(f"{upper} *{lower}") + alternatives.append(f"{lower} *{upper}") + return re.compile("|".join(alternatives)) def parse_args() -> argparse.Namespace: @@ -240,7 +297,7 @@ def parse_args() -> argparse.Namespace: new_aea_version = Version(arguments.new_version) aea_version_bumper = PythonPackageVersionBumper( - AEA_DIR.parent, AEA_DIR, new_aea_version + AEA_DIR.parent, AEA_DIR, new_aea_version, files_to_pattern=AEA_PATHS ) have_updated_specifier_set = aea_version_bumper.run() From e1c7ec3805093c1c5d3db8da47d64642cefaa379 Mon Sep 17 00:00:00 2001 From: MarcoFavorito Date: Tue, 11 May 2021 00:18:34 +0200 Subject: [PATCH 031/147] update bump script so to add flexible specifier update --- scripts/bump_aea_version.py | 84 ++++++++++++++++++++++++------------- 1 file changed, 56 insertions(+), 28 deletions(-) diff --git a/scripts/bump_aea_version.py b/scripts/bump_aea_version.py index 0d6660bdf0..773d8ea8ae 100644 --- a/scripts/bump_aea_version.py +++ b/scripts/bump_aea_version.py @@ -27,7 +27,7 @@ import sys from functools import wraps from pathlib import Path -from typing import Any, Callable, Collection, Optional, Pattern, Tuple, cast +from typing import Any, Callable, Collection, Dict, Optional, cast from packaging.specifiers import SpecifierSet from packaging.version import Version @@ -63,20 +63,19 @@ ) ) -_AEA_ALL_PATTERN = "(?<={package_name}\[all\]==){version}" -AEA_PATHS = [ - (Path("deploy-image", "Dockerfile"), _AEA_ALL_PATTERN), - (Path("develop-image", "docker-env.sh"), "(?<=aea-develop:){version}"), - (Path("docs", "quickstart.md"), "(?<=v){version}"), - (Path("examples", "tac_deploy", "Dockerfile"), _AEA_ALL_PATTERN), - (Path("scripts", "install.ps1"), _AEA_ALL_PATTERN), - (Path("scripts", "install.sh"), _AEA_ALL_PATTERN), - ( - Path("tests", "test_docs", "test_bash_yaml", "md_files", "bash-quickstart.md"), - "(?<=v){version}", - ), - (Path("user-image", "docker-env.sh"), "(?<=aea-user:){version}"), -] +_AEA_ALL_PATTERN = r"(?<={package_name}\[all\]==){version}" +AEA_PATHS = { + Path("deploy-image", "Dockerfile"): _AEA_ALL_PATTERN, + Path("develop-image", "docker-env.sh"): "(?<=aea-develop:){version}", + Path("docs", "quickstart.md"): "(?<=v){version}", + Path("examples", "tac_deploy", "Dockerfile"): _AEA_ALL_PATTERN, + Path("scripts", "install.ps1"): _AEA_ALL_PATTERN, + Path("scripts", "install.sh"): _AEA_ALL_PATTERN, + Path( + "tests", "test_docs", "test_bash_yaml", "md_files", "bash-quickstart.md" + ): "(?<=v){version}", + Path("user-image", "docker-env.sh"): "(?<=aea-user:){version}", +} def check_executed(func: Callable) -> Callable: @@ -95,15 +94,18 @@ def wrapper(self: Any, *args: Any, **kwargs: Any) -> None: class PythonPackageVersionBumper: """Utility class to bump Python package versions.""" - IGNORE_DIRS = (Path(".git"),) + IGNORE_DIRS = ( + Path(".git"), + ) def __init__( self, root_dir: Path, python_pkg_dir: Path, new_version: Version, + files_to_pattern: Dict[Path, str], + specifier_set_patterns: Collection[str], package_name: Optional[str] = None, - files_to_pattern: Collection[Tuple[Path, str]] = (), ignore_dirs: Collection[Path] = (), ): """ @@ -114,14 +116,17 @@ def __init__( :param new_version: the new version. :param package_name: the Python package name aliases (defaults to dirname of python_pkg_dir). + :param files_to_pattern: a list of pairs. + :param specifier_set_patterns: a list of patterns for specifier sets. :param ignore_dirs: a list of paths to ignore during the substitution. """ self.root_dir = root_dir self.python_pkg_dir = python_pkg_dir self.new_version = new_version + self.files_to_pattern = files_to_pattern + self.specifier_set_patterns = specifier_set_patterns self.package_name = package_name or self.python_pkg_dir.name self.ignore_dirs = ignore_dirs or self.IGNORE_DIRS - self.files_to_pattern = files_to_pattern self._current_version: Optional[str] = None @@ -161,7 +166,7 @@ def run(self) -> bool: def update_version_for_files(self) -> None: """Update the version.""" - for filepath, regex_template in self.files_to_pattern: + for filepath, regex_template in self.files_to_pattern.items(): self.update_version_for_file( filepath, cast(str, self._current_version), @@ -209,7 +214,9 @@ def update_version_for_file( the current version. """ if version_regex_template is not None: - regex_str = version_regex_template.format(package_name=self.package_name, version=current_version) + regex_str = version_regex_template.format( + package_name=self.package_name, version=current_version + ) else: regex_str = current_version pattern = re.compile(regex_str) @@ -231,7 +238,6 @@ def update_version_specifiers( new_specifier_set = compute_specifier_from_version(new_version) print(f"Old version specifier: {old_specifier_set}") print(f"New version specifier: {new_specifier_set}") - old_specifier_set_regex = self.get_regex_from_specifier_set(old_specifier_set) if old_specifier_set == new_specifier_set: print("Not updating version specifier - they haven't changed.") return False @@ -249,12 +255,28 @@ def update_version_specifiers( except UnicodeDecodeError as e: print(f"Cannot read {file}: {str(e)}. Continue...") else: - if old_specifier_set_regex.search(content) is not None: - content = old_specifier_set_regex.sub(new_specifier_set, content) - file.write_text(content) + content = self._replace_specifier_sets( + old_specifier_set, new_specifier_set, content + ) + file.write_text(content) return True - def get_regex_from_specifier_set(self, specifier_set: str) -> Pattern: + def _replace_specifier_sets( + self, old_specifier_set: str, new_specifier_set: str, content: str + ) -> str: + old_specifier_set_regex = self.get_regex_from_specifier_set(old_specifier_set) + for pattern_template in self.specifier_set_patterns: + pattern = re.compile( + pattern_template.format( + package_name=self.package_name, + specifier_set=old_specifier_set_regex, + ) + ) + if pattern.search(content) is not None: + content = pattern.sub(new_specifier_set, content) + return content + + def get_regex_from_specifier_set(self, specifier_set: str) -> str: """ Get the regex for specifier sets. @@ -271,13 +293,12 @@ def get_regex_from_specifier_set(self, specifier_set: str) -> Pattern: :param specifier_set: The string representation of the specifier set :return: a regex pattern """ - # TODO add different type of matches specifiers = SpecifierSet(specifier_set) upper, lower = sorted(specifiers, key=lambda x: str(x)) alternatives = list() alternatives.append(f"{upper} *{lower}") alternatives.append(f"{lower} *{upper}") - return re.compile("|".join(alternatives)) + return "|".join(alternatives) def parse_args() -> argparse.Namespace: @@ -297,7 +318,14 @@ def parse_args() -> argparse.Namespace: new_aea_version = Version(arguments.new_version) aea_version_bumper = PythonPackageVersionBumper( - AEA_DIR.parent, AEA_DIR, new_aea_version, files_to_pattern=AEA_PATHS + AEA_DIR.parent, + AEA_DIR, + new_aea_version, + specifier_set_patterns=[ + "(?<=aea_version:) *({specifier_set})", + "(?<={package_name})({specifier_set})", + ], + files_to_pattern=AEA_PATHS, ) have_updated_specifier_set = aea_version_bumper.run() From e903e544ea7025e666af2522a59b05c0216fca85 Mon Sep 17 00:00:00 2001 From: "Yuri (solarw) Turchenkov" Date: Tue, 11 May 2021 11:36:06 +0300 Subject: [PATCH 032/147] libp2p utils fixes/typos/cleanup --- libs/go/libp2p_node/utils/utils.go | 76 ++----------------- libs/go/libp2p_node/utils/utils_test.go | 52 ------------- .../connections/p2p_libp2p/connection.yaml | 4 +- .../p2p_libp2p/libp2p_node/utils/utils.go | 76 ++----------------- .../libp2p_node/utils/utils_test.go | 52 ------------- packages/hashes.csv | 2 +- 6 files changed, 13 insertions(+), 249 deletions(-) diff --git a/libs/go/libp2p_node/utils/utils.go b/libs/go/libp2p_node/utils/utils.go index c8b533be0e..728d9a123e 100644 --- a/libs/go/libp2p_node/utils/utils.go +++ b/libs/go/libp2p_node/utils/utils.go @@ -420,8 +420,8 @@ func RecoverAddressFromEthereumSignature(message []byte, signature string) (stri // VerifyEthereumSignatureETH verify ethereum signature using ethereum public key func VerifyEthereumSignatureETH(message []byte, signature string, pubkey string) (bool, error) { - // get ted signer address - tedAddress, err := EthereumAddressFromPublicKey(pubkey) + // get expected signer address + expectedAddress, err := EthereumAddressFromPublicKey(pubkey) if err != nil { return false, err } @@ -432,8 +432,8 @@ func VerifyEthereumSignatureETH(message []byte, signature string, pubkey string) return false, err } - if recoveredAddress != tedAddress { - return false, errors.New("recovered and ted addresses don't match") + if recoveredAddress != expectedAddress { + return false, errors.New("recovered and expected addresses don't match") } return true, nil @@ -743,70 +743,4 @@ func WriteBytes(s network.Stream, data []byte) error { err = wstream.Flush() return err -} - -// ReadString from a network stream -func ReadString(s network.Stream) (string, error) { - data, err := ReadBytes(s) - return string(data), err -} - -// WriteEnvelope to a network stream -func WriteEnvelope(envel *aea.Envelope, s network.Stream) error { - wstream := bufio.NewWriter(s) - data, err := proto.Marshal(envel) - if err != nil { - return err - } - size := uint32(len(data)) - - buf := make([]byte, 4) - binary.BigEndian.PutUint32(buf, size) - //log.Println("DEBUG writing size:", size, buf) - _, err = wstream.Write(buf) - if err != nil { - return err - } - - //log.Println("DEBUG writing data:", data) - _, err = wstream.Write(data) - if err != nil { - return err - } - - wstream.Flush() - return nil -} - -// ReadEnvelope from a network stream -func ReadEnvelope(s network.Stream) (*aea.Envelope, error) { - envel := &aea.Envelope{} - rstream := bufio.NewReader(s) - - buf := make([]byte, 4) - _, err := io.ReadFull(rstream, buf) - - if err != nil { - logger.Error(). - Str("err", err.Error()). - Msg("while reading size") - return envel, err - } - - size := binary.BigEndian.Uint32(buf) - if size > maxMessageSizeDelegateConnection { - return nil, errors.New("ted message size larger than maximum allowed") - } - //logger.Debug().Msgf("received size: %d %x", size, buf) - buf = make([]byte, size) - _, err = io.ReadFull(rstream, buf) - if err != nil { - logger.Error(). - Str("err", err.Error()). - Msg("while reading data") - return envel, err - } - - err = proto.Unmarshal(buf, envel) - return envel, err -} +} \ No newline at end of file diff --git a/libs/go/libp2p_node/utils/utils_test.go b/libs/go/libp2p_node/utils/utils_test.go index f959b0cabd..237a787054 100644 --- a/libs/go/libp2p_node/utils/utils_test.go +++ b/libs/go/libp2p_node/utils/utils_test.go @@ -21,12 +21,10 @@ package utils import ( - "bufio" "bytes" "context" "encoding/json" "errors" - "io" "libp2p_node/aea" mocks "libp2p_node/mocks" "net" @@ -194,56 +192,6 @@ func TestWriteBytesConn(t *testing.T) { assert.Equal(t, nil, err) } -func TestReadString(t *testing.T) { - // test ReadString and ReadBytes - mockCtrl := gomock.NewController(t) - defer mockCtrl.Finish() - mockStream := mocks.NewMockStream(mockCtrl) - - defer monkey.UnpatchAll() - - t.Run("TestReadString", func(t *testing.T) { - monkey.Patch(bufio.NewReader, func(reader io.Reader) *bufio.Reader { - return bufio.NewReaderSize( - bytes.NewReader([]byte{0, 0, 0, 5, 104, 101, 108, 108, 111}), - 100, - ) - }) - buf, err := ReadString(mockStream) - assert.Equal(t, nil, err) - assert.Equal(t, "hello", buf) - }) -} - -func TestReadWriteEnvelope(t *testing.T) { - mockCtrl := gomock.NewController(t) - defer mockCtrl.Finish() - mockStream := mocks.NewMockStream(mockCtrl) - defer monkey.UnpatchAll() - address := "0xb8d8c62d4a1999b7aea0aebBD5020244a4a9bAD8" - buffer := bytes.NewBuffer([]byte{}) - - t.Run("TestWriteEnvelope", func(t *testing.T) { - monkey.Patch(bufio.NewWriter, func(writer io.Writer) *bufio.Writer { - return bufio.NewWriterSize(buffer, 100) - }) - err := WriteEnvelope(&aea.Envelope{ - To: address, - Sender: address, - }, mockStream) - assert.Equal(t, nil, err) - }) - - t.Run("TestReadEnvelope", func(t *testing.T) { - monkey.Patch(bufio.NewReader, func(reader io.Reader) *bufio.Reader { - return bufio.NewReaderSize(bytes.NewReader(buffer.Bytes()), 100) - }) - env, err := ReadEnvelope(mockStream) - assert.Equal(t, nil, err) - assert.Equal(t, address, env.To) - }) -} - func TestReadWriteEnvelopeFromConnection(t *testing.T) { mockCtrl := gomock.NewController(t) defer mockCtrl.Finish() diff --git a/packages/fetchai/connections/p2p_libp2p/connection.yaml b/packages/fetchai/connections/p2p_libp2p/connection.yaml index 448470fcab..950b1237ca 100644 --- a/packages/fetchai/connections/p2p_libp2p/connection.yaml +++ b/packages/fetchai/connections/p2p_libp2p/connection.yaml @@ -41,8 +41,8 @@ fingerprint: libp2p_node/mocks/mock_net.go: QmVKnkDdH8XCJ9jriEkZui4NoB36GBYF2DtfX8uCqAthMw libp2p_node/mocks/mock_network.go: QmbVVvd3wrY6PnVs1rn9T9m6FD5kbmSVJLwhSxUgSLAiM5 libp2p_node/mocks/mock_peerstore.go: QmaPCBrwsTeWCHZoAKDzaxN6uhY3bez1karzeGeovWYwkB - libp2p_node/utils/utils.go: QmXxQCCsEFZH1wkzze1Z1MfcBi6rQ7ZEzACGKptQsgL8Zg - libp2p_node/utils/utils_test.go: QmX5bBqCwFVky6ix7pu9QNvScntr3GMDvon9cNC8dKhJiS + libp2p_node/utils/utils.go: QmW8ri6vw6CwyJXYHUMhYWoH8RwuMBnbryf5fb6YzE8Fik + libp2p_node/utils/utils_test.go: QmaUTrtqhPYcCLTHnJic4EN7R1pZY8ra7QbeRrPuedgAuc fingerprint_ignore_patterns: [] build_entrypoint: check_dependencies.py connections: [] diff --git a/packages/fetchai/connections/p2p_libp2p/libp2p_node/utils/utils.go b/packages/fetchai/connections/p2p_libp2p/libp2p_node/utils/utils.go index c8b533be0e..728d9a123e 100644 --- a/packages/fetchai/connections/p2p_libp2p/libp2p_node/utils/utils.go +++ b/packages/fetchai/connections/p2p_libp2p/libp2p_node/utils/utils.go @@ -420,8 +420,8 @@ func RecoverAddressFromEthereumSignature(message []byte, signature string) (stri // VerifyEthereumSignatureETH verify ethereum signature using ethereum public key func VerifyEthereumSignatureETH(message []byte, signature string, pubkey string) (bool, error) { - // get ted signer address - tedAddress, err := EthereumAddressFromPublicKey(pubkey) + // get expected signer address + expectedAddress, err := EthereumAddressFromPublicKey(pubkey) if err != nil { return false, err } @@ -432,8 +432,8 @@ func VerifyEthereumSignatureETH(message []byte, signature string, pubkey string) return false, err } - if recoveredAddress != tedAddress { - return false, errors.New("recovered and ted addresses don't match") + if recoveredAddress != expectedAddress { + return false, errors.New("recovered and expected addresses don't match") } return true, nil @@ -743,70 +743,4 @@ func WriteBytes(s network.Stream, data []byte) error { err = wstream.Flush() return err -} - -// ReadString from a network stream -func ReadString(s network.Stream) (string, error) { - data, err := ReadBytes(s) - return string(data), err -} - -// WriteEnvelope to a network stream -func WriteEnvelope(envel *aea.Envelope, s network.Stream) error { - wstream := bufio.NewWriter(s) - data, err := proto.Marshal(envel) - if err != nil { - return err - } - size := uint32(len(data)) - - buf := make([]byte, 4) - binary.BigEndian.PutUint32(buf, size) - //log.Println("DEBUG writing size:", size, buf) - _, err = wstream.Write(buf) - if err != nil { - return err - } - - //log.Println("DEBUG writing data:", data) - _, err = wstream.Write(data) - if err != nil { - return err - } - - wstream.Flush() - return nil -} - -// ReadEnvelope from a network stream -func ReadEnvelope(s network.Stream) (*aea.Envelope, error) { - envel := &aea.Envelope{} - rstream := bufio.NewReader(s) - - buf := make([]byte, 4) - _, err := io.ReadFull(rstream, buf) - - if err != nil { - logger.Error(). - Str("err", err.Error()). - Msg("while reading size") - return envel, err - } - - size := binary.BigEndian.Uint32(buf) - if size > maxMessageSizeDelegateConnection { - return nil, errors.New("ted message size larger than maximum allowed") - } - //logger.Debug().Msgf("received size: %d %x", size, buf) - buf = make([]byte, size) - _, err = io.ReadFull(rstream, buf) - if err != nil { - logger.Error(). - Str("err", err.Error()). - Msg("while reading data") - return envel, err - } - - err = proto.Unmarshal(buf, envel) - return envel, err -} +} \ No newline at end of file diff --git a/packages/fetchai/connections/p2p_libp2p/libp2p_node/utils/utils_test.go b/packages/fetchai/connections/p2p_libp2p/libp2p_node/utils/utils_test.go index f959b0cabd..237a787054 100644 --- a/packages/fetchai/connections/p2p_libp2p/libp2p_node/utils/utils_test.go +++ b/packages/fetchai/connections/p2p_libp2p/libp2p_node/utils/utils_test.go @@ -21,12 +21,10 @@ package utils import ( - "bufio" "bytes" "context" "encoding/json" "errors" - "io" "libp2p_node/aea" mocks "libp2p_node/mocks" "net" @@ -194,56 +192,6 @@ func TestWriteBytesConn(t *testing.T) { assert.Equal(t, nil, err) } -func TestReadString(t *testing.T) { - // test ReadString and ReadBytes - mockCtrl := gomock.NewController(t) - defer mockCtrl.Finish() - mockStream := mocks.NewMockStream(mockCtrl) - - defer monkey.UnpatchAll() - - t.Run("TestReadString", func(t *testing.T) { - monkey.Patch(bufio.NewReader, func(reader io.Reader) *bufio.Reader { - return bufio.NewReaderSize( - bytes.NewReader([]byte{0, 0, 0, 5, 104, 101, 108, 108, 111}), - 100, - ) - }) - buf, err := ReadString(mockStream) - assert.Equal(t, nil, err) - assert.Equal(t, "hello", buf) - }) -} - -func TestReadWriteEnvelope(t *testing.T) { - mockCtrl := gomock.NewController(t) - defer mockCtrl.Finish() - mockStream := mocks.NewMockStream(mockCtrl) - defer monkey.UnpatchAll() - address := "0xb8d8c62d4a1999b7aea0aebBD5020244a4a9bAD8" - buffer := bytes.NewBuffer([]byte{}) - - t.Run("TestWriteEnvelope", func(t *testing.T) { - monkey.Patch(bufio.NewWriter, func(writer io.Writer) *bufio.Writer { - return bufio.NewWriterSize(buffer, 100) - }) - err := WriteEnvelope(&aea.Envelope{ - To: address, - Sender: address, - }, mockStream) - assert.Equal(t, nil, err) - }) - - t.Run("TestReadEnvelope", func(t *testing.T) { - monkey.Patch(bufio.NewReader, func(reader io.Reader) *bufio.Reader { - return bufio.NewReaderSize(bytes.NewReader(buffer.Bytes()), 100) - }) - env, err := ReadEnvelope(mockStream) - assert.Equal(t, nil, err) - assert.Equal(t, address, env.To) - }) -} - func TestReadWriteEnvelopeFromConnection(t *testing.T) { mockCtrl := gomock.NewController(t) defer mockCtrl.Finish() diff --git a/packages/hashes.csv b/packages/hashes.csv index 82c783765a..69b39f8097 100644 --- a/packages/hashes.csv +++ b/packages/hashes.csv @@ -41,7 +41,7 @@ fetchai/connections/http_server,QmZqiszQJuG7XA6LFWsMqXYSmViTrUkDfpkHwgYtDMbyXy fetchai/connections/ledger,QmT7ffwPzJ3isCMhN2qoj6NRyqinE2RkpSpUKNRFRXxpes fetchai/connections/local,QmUxLhmeE98S8BcuRDB7W7pRsJzpC3wVJV5ELLxVeEkoKC fetchai/connections/oef,QmaHQhxryQLaBk5TvC4iJFZtFvkPp4CoHxHg1iLnh2PAdm -fetchai/connections/p2p_libp2p,QmRjseBG7KVLZKwSnofHByPBJZqwofqXkLV6tF1iyySNzv +fetchai/connections/p2p_libp2p,Qmdwgr56UaAsaESRT7pMq6EbkpdqUBanuBvmnu9rdwiY4t fetchai/connections/p2p_libp2p_client,QmYq2U83xTTnv1WDy1quvEz47QaV2uw1ZmB6sFHoCL31GC fetchai/connections/p2p_stub,QmToCExj3ZpdxUu3vYSMo4jZRJiMTkXMbyhbh18Hq6Nc4b fetchai/connections/prometheus,QmeRmgrccxRh8UG4AfTMct6D5wSuK9BXYoLwnohut2LJaB From edab6a3185104b471995eebba07345ea1b790e72 Mon Sep 17 00:00:00 2001 From: "Yuri (solarw) Turchenkov" Date: Tue, 11 May 2021 11:49:18 +0300 Subject: [PATCH 033/147] github ci golang test command updated --- .github/workflows/workflow.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/workflow.yml b/.github/workflows/workflow.yml index db1d24562e..10377fe518 100644 --- a/.github/workflows/workflow.yml +++ b/.github/workflows/workflow.yml @@ -482,7 +482,7 @@ jobs: - if: matrix.python-version == '3.6' name: Golang unit tests (libp2p_node) working-directory: ./libs/go/libp2p_node - run: go test -p 1 -timeout 0 -count 1 -v ./... + run: go test -gcflags=-l -p 1 -timeout 0 -count 1 -covermode=atomic -coverprofile=coverage.txt -v ./... - if: matrix.python-version == '3.6' name: Golang unit tests (aealite) working-directory: ./libs/go/aealite From 9c73865f88f4915bcb9d51449179ae5c4dcda790 Mon Sep 17 00:00:00 2001 From: "Yuri (solarw) Turchenkov" Date: Tue, 11 May 2021 12:02:19 +0300 Subject: [PATCH 034/147] github ci golang test command updated --- .github/workflows/workflow.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/workflow.yml b/.github/workflows/workflow.yml index 10377fe518..db1d24562e 100644 --- a/.github/workflows/workflow.yml +++ b/.github/workflows/workflow.yml @@ -482,7 +482,7 @@ jobs: - if: matrix.python-version == '3.6' name: Golang unit tests (libp2p_node) working-directory: ./libs/go/libp2p_node - run: go test -gcflags=-l -p 1 -timeout 0 -count 1 -covermode=atomic -coverprofile=coverage.txt -v ./... + run: go test -p 1 -timeout 0 -count 1 -v ./... - if: matrix.python-version == '3.6' name: Golang unit tests (aealite) working-directory: ./libs/go/aealite From 965423b042c612894a3ef9a453ab674c0e55198e Mon Sep 17 00:00:00 2001 From: "Yuri (solarw) Turchenkov" Date: Tue, 11 May 2021 12:23:54 +0300 Subject: [PATCH 035/147] github ci golang test command updated --- .github/workflows/workflow.yml | 33 ++++++++++++++++++++++++++++++--- 1 file changed, 30 insertions(+), 3 deletions(-) diff --git a/.github/workflows/workflow.yml b/.github/workflows/workflow.yml index db1d24562e..feed402041 100644 --- a/.github/workflows/workflow.yml +++ b/.github/workflows/workflow.yml @@ -457,7 +457,6 @@ jobs: - name: Unit tests with sync agent loop run: | tox -e py3.8 -- --aea-loop sync -m 'not integration and not unstable' - golang_checks: continue-on-error: True needs: @@ -482,12 +481,40 @@ jobs: - if: matrix.python-version == '3.6' name: Golang unit tests (libp2p_node) working-directory: ./libs/go/libp2p_node - run: go test -p 1 -timeout 0 -count 1 -v ./... + run: make test - if: matrix.python-version == '3.6' name: Golang unit tests (aealite) working-directory: ./libs/go/aealite run: go test -p 1 -timeout 0 -count 1 -v ./... - + libp2p_coverage: + name: libp2p_coverage + runs-on: ubuntu-latest + steps: + - name: Set up Go 1.14 + uses: actions/setup-go@v1 + with: + go-version: 1.14 + id: go + - name: Check out code into the Go module directory + uses: actions/checkout@v1 + - name: Install dependencies (ubuntu-latest) + run: | + sudo apt-get update --fix-missing + sudo apt-get autoremove + sudo apt-get autoclean + sudo apt-get install make -y + - name: Get dependencies + working-directory: ./libs/go/libp2p_node/ + run: | + make install + - name: Generate coverage report + working-directory: ./libs/go/libp2p_node/ + run: | + make test + - name: Print coverage report + working-directory: ./libs/go/libp2p_node/ + run: | + go tool cover -func=coverage.txt coverage_checks: continue-on-error: True needs: From 78701e82984eeadf552583cd410494fb88a121e2 Mon Sep 17 00:00:00 2001 From: MarcoFavorito Date: Tue, 11 May 2021 09:28:06 +0200 Subject: [PATCH 036/147] use git diff to detect need of bumping --- scripts/bump_aea_version.py | 85 ++++++++++++++++++++++++++++--------- 1 file changed, 66 insertions(+), 19 deletions(-) diff --git a/scripts/bump_aea_version.py b/scripts/bump_aea_version.py index 773d8ea8ae..c354723242 100644 --- a/scripts/bump_aea_version.py +++ b/scripts/bump_aea_version.py @@ -29,6 +29,7 @@ from pathlib import Path from typing import Any, Callable, Collection, Dict, Optional, cast +from git import Repo from packaging.specifiers import SpecifierSet from packaging.version import Version @@ -43,6 +44,10 @@ from scripts.generate_ipfs_hashes import update_hashes +# if the key is a file, just process it +# if the key is a directory, process all files below it +PatternByPath = Dict[Path, str] + VERSION_NUMBER_PART_REGEX = r"(0|[1-9]\d*)" VERSION_REGEX = fr"(any|latest|({VERSION_NUMBER_PART_REGEX})\.({VERSION_NUMBER_PART_REGEX})\.({VERSION_NUMBER_PART_REGEX})(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?)" @@ -64,7 +69,7 @@ ) _AEA_ALL_PATTERN = r"(?<={package_name}\[all\]==){version}" -AEA_PATHS = { +AEA_PATHS: PatternByPath = { Path("deploy-image", "Dockerfile"): _AEA_ALL_PATTERN, Path("develop-image", "docker-env.sh"): "(?<=aea-develop:){version}", Path("docs", "quickstart.md"): "(?<=v){version}", @@ -94,16 +99,14 @@ def wrapper(self: Any, *args: Any, **kwargs: Any) -> None: class PythonPackageVersionBumper: """Utility class to bump Python package versions.""" - IGNORE_DIRS = ( - Path(".git"), - ) + IGNORE_DIRS = (Path(".git"),) def __init__( self, root_dir: Path, python_pkg_dir: Path, new_version: Version, - files_to_pattern: Dict[Path, str], + files_to_pattern: PatternByPath, specifier_set_patterns: Collection[str], package_name: Optional[str] = None, ignore_dirs: Collection[Path] = (), @@ -128,6 +131,7 @@ def __init__( self.package_name = package_name or self.python_pkg_dir.name self.ignore_dirs = ignore_dirs or self.IGNORE_DIRS + self.repo = Repo(self.root_dir) self._current_version: Optional[str] = None # functor pattern @@ -153,6 +157,9 @@ def result(self) -> bool: @check_executed def run(self) -> bool: """Main entrypoint.""" + if not self.is_different_from_latest_tag(): + print(f"The package {self.python_pkg_dir} has no changes since last tag.") + return False new_version_string = str(self.new_version) current_version_str = self.update_version_for_package(new_version_string) @@ -178,21 +185,32 @@ def update_version_for_package(self, new_version: str) -> str: """ Update version for file. + If __version__.py is available, parse it and check for __version__ variable. + Otherwise, try to parse setup.py. + Otherwise, raise error. + :param new_version: the new version :return: the current version """ - current_version = "" - path = self.python_pkg_dir / Path("__version__.py") - with open(path, "rt") as fin: - for line in fin: - if "__version__" not in line: - continue - match = re.search('__version__ = "(.*)"', line) - if match is None: - raise ValueError("Current version is not well formatted.") - current_version = match.group(1) - if current_version == "": - raise ValueError("No version found!") + version_path = self.python_pkg_dir / Path("__version__.py") + setup_path = self.python_pkg_dir.parent / "setup.py" + if version_path.exists(): + regex = re.compile('(?<=__version__ = [\'"])(.*)(?=")') + path = version_path + elif setup_path.exists(): + regex = re.compile(r'(?<=version=[\'"])(.*)(?=[\'"],)') + path = setup_path + else: + raise ValueError("cannot fine neither '__version__.py' nor 'setup.py'") + + content = path.read_text() + current_version_candidates = regex.findall(content) + more_than_one_match = len(current_version_candidates) > 0 + if more_than_one_match: + raise ValueError( + f"find more than one match for current version in {path}: {current_version_candidates}" + ) + current_version = current_version_candidates[0][0] self.update_version_for_file(path, current_version, new_version) return current_version @@ -300,6 +318,15 @@ def get_regex_from_specifier_set(self, specifier_set: str) -> str: alternatives.append(f"{lower} *{upper}") return "|".join(alternatives) + def is_different_from_latest_tag(self) -> bool: + """Check whether the package has changes since the latest tag.""" + assert len(self.repo.tags) > 0, "no git tags found" + latest_tag_str = str(self.repo.tags[-1]) + args = latest_tag_str, "--", str(self.python_pkg_dir) + print(f"Running 'git diff with args: {args}'") + diff = self.repo.git.diff() + return diff != "" + def parse_args() -> argparse.Namespace: """Parse arguments.""" @@ -313,7 +340,26 @@ def parse_args() -> argparse.Namespace: return arguments_ +class RepositoryBumper: + """A functor-class to manage AEA repository version bump.""" + + def __init__(self, root_dir: str = ROOT_DIR): + """ + Initialize the helper class. + + :param root_dir: the root directory. + """ + self.root_dir = root_dir + + self.repo = Repo(self.root_dir) + + if __name__ == "__main__": + repo = Repo(ROOT_DIR) + if repo.is_dirty(): + print("Repository is dirty. Please clean it up before running this script.") + exit(1) + arguments = parse_args() new_aea_version = Version(arguments.new_version) @@ -327,13 +373,14 @@ def parse_args() -> argparse.Namespace: ], files_to_pattern=AEA_PATHS, ) - have_updated_specifier_set = aea_version_bumper.run() + aea_version_bumper.run() + have_updated_specifier_set = aea_version_bumper.result print("OK") return_code = 0 if arguments.no_fingerprint: print("Not updating fingerprints, since --no-fingerprint was specified.") - elif not have_updated_specifier_set: + elif have_updated_specifier_set is False: print("Not updating fingerprints, since no specifier set has been updated.") else: print("Updating hashes and fingerprints.") From c74130c2ff0e241cecc3859b1d276680a8e9a5c8 Mon Sep 17 00:00:00 2001 From: MarcoFavorito Date: Tue, 11 May 2021 12:52:31 +0200 Subject: [PATCH 037/147] support new plugin versions --- scripts/bump_aea_version.py | 213 ++++++++++++++++++++++++------------ 1 file changed, 144 insertions(+), 69 deletions(-) diff --git a/scripts/bump_aea_version.py b/scripts/bump_aea_version.py index c354723242..0d15244c4a 100644 --- a/scripts/bump_aea_version.py +++ b/scripts/bump_aea_version.py @@ -22,52 +22,61 @@ import argparse import inspect +import logging +import operator import os import re import sys from functools import wraps from pathlib import Path -from typing import Any, Callable, Collection, Dict, Optional, cast +from typing import Any, Callable, Collection, Dict, List, Optional, cast from git import Repo from packaging.specifiers import SpecifierSet from packaging.version import Version -from aea.configurations.constants import ( - DEFAULT_AEA_CONFIG_FILE, - DEFAULT_CONNECTION_CONFIG_FILE, - DEFAULT_CONTRACT_CONFIG_FILE, - DEFAULT_PROTOCOL_CONFIG_FILE, - DEFAULT_SKILL_CONFIG_FILE, -) from aea.helpers.base import compute_specifier_from_version from scripts.generate_ipfs_hashes import update_hashes +logging.basicConfig( + level=logging.INFO, format="[%(asctime)s][%(name)s][%(levelname)s] %(message)s" +) + # if the key is a file, just process it # if the key is a directory, process all files below it PatternByPath = Dict[Path, str] -VERSION_NUMBER_PART_REGEX = r"(0|[1-9]\d*)" -VERSION_REGEX = fr"(any|latest|({VERSION_NUMBER_PART_REGEX})\.({VERSION_NUMBER_PART_REGEX})\.({VERSION_NUMBER_PART_REGEX})(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?)" - -PACKAGES_DIR = Path("packages") -TESTS_DIR = Path("tests") AEA_DIR = Path("aea") CUR_PATH = os.path.dirname(inspect.getfile(inspect.currentframe())) # type: ignore -ROOT_DIR = os.path.join(CUR_PATH, "..") -CONFIGURATION_FILENAME_REGEX = re.compile( - "|".join( - [ - DEFAULT_AEA_CONFIG_FILE, - DEFAULT_SKILL_CONFIG_FILE, - DEFAULT_CONNECTION_CONFIG_FILE, - DEFAULT_CONTRACT_CONFIG_FILE, - DEFAULT_PROTOCOL_CONFIG_FILE, - ] - ) +ROOT_DIR = Path(os.path.join(CUR_PATH, "..")) + +PLUGINS_DIR = Path("plugins") +ALL_PLUGINS = tuple(PLUGINS_DIR.iterdir()) + +""" +This pattern captures a specifier set in the dependencies section +of an AEA package configuration file, e.g.: + +dependencies: + ... + aea-ledger-fetchai: + version: >=1.0.0,<2.0.0 +""" +YAML_DEPENDENCY_SPECIFIER_SET_PATTERN = "(?<={package_name}:\n version: )({specifier_set})" + +""" +This pattern captures a specifier set for PyPI dependencies +in JSON format. + +e.g.: +"aea-ledger-fetchai": {"version": ">=2.0.0, <3.0.0"} +""" +JSON_DEPENDENCY_SPECIFIER_SET_PATTERN = ( + '(?<="{package_name}": ."version": ")({specifier_set})(?=".)' ) + _AEA_ALL_PATTERN = r"(?<={package_name}\[all\]==){version}" AEA_PATHS: PatternByPath = { Path("deploy-image", "Dockerfile"): _AEA_ALL_PATTERN, @@ -96,6 +105,23 @@ def wrapper(self: Any, *args: Any, **kwargs: Any) -> None: return wrapper +def compute_specifier_from_version_custom(version: Version) -> str: + """ + Post-process aea.helpers.compute_specifier_from_version + + The output is post-process in the following way: + - remove spaces between specifier sets + - put upper bound before lower bound + + :param version: the version + :return: the specifier set according to the version and semantic versioning. + """ + specifier_set_str = compute_specifier_from_version(version) + specifiers = SpecifierSet(specifier_set_str) + upper, lower = sorted(specifiers, key=lambda x: str(x)) + return f"{upper},{lower}" + + class PythonPackageVersionBumper: """Utility class to bump Python package versions.""" @@ -158,7 +184,9 @@ def result(self) -> bool: def run(self) -> bool: """Main entrypoint.""" if not self.is_different_from_latest_tag(): - print(f"The package {self.python_pkg_dir} has no changes since last tag.") + logging.info( + f"The package {self.python_pkg_dir} has no changes since last tag." + ) return False new_version_string = str(self.new_version) current_version_str = self.update_version_for_package(new_version_string) @@ -195,23 +223,24 @@ def update_version_for_package(self, new_version: str) -> str: version_path = self.python_pkg_dir / Path("__version__.py") setup_path = self.python_pkg_dir.parent / "setup.py" if version_path.exists(): - regex = re.compile('(?<=__version__ = [\'"])(.*)(?=")') + regex_template = '(?<=__version__ = [\'"])({version})(?=")' path = version_path elif setup_path.exists(): - regex = re.compile(r'(?<=version=[\'"])(.*)(?=[\'"],)') + regex_template = r'(?<=version=[\'"])({version})(?=[\'"],)' path = setup_path else: raise ValueError("cannot fine neither '__version__.py' nor 'setup.py'") content = path.read_text() - current_version_candidates = regex.findall(content) - more_than_one_match = len(current_version_candidates) > 0 + pattern = regex_template.format(version=".*") + current_version_candidates = re.findall(pattern, content) + more_than_one_match = len(current_version_candidates) > 1 if more_than_one_match: raise ValueError( f"find more than one match for current version in {path}: {current_version_candidates}" ) - current_version = current_version_candidates[0][0] - self.update_version_for_file(path, current_version, new_version) + current_version = current_version_candidates[0] + self.update_version_for_file(path, current_version, new_version, version_regex_template=regex_template) return current_version def update_version_for_file( @@ -252,27 +281,28 @@ def update_version_specifiers( :param new_version: the new version. :return: True if the update has been done, False otherwise. """ - old_specifier_set = compute_specifier_from_version(old_version) - new_specifier_set = compute_specifier_from_version(new_version) - print(f"Old version specifier: {old_specifier_set}") - print(f"New version specifier: {new_specifier_set}") + old_specifier_set = compute_specifier_from_version_custom(old_version) + new_specifier_set = compute_specifier_from_version_custom(new_version) + logging.info(f"Old version specifier: {old_specifier_set}") + logging.info(f"New version specifier: {new_specifier_set}") if old_specifier_set == new_specifier_set: - print("Not updating version specifier - they haven't changed.") + logging.info("Not updating version specifier - they haven't changed.") return False for file in filter(lambda p: not p.is_dir(), self.root_dir.rglob("*")): dir_root = Path(file.parts[0]) if dir_root in self.ignore_dirs: - print(f"Skipping '{file}'...") + logging.info(f"Skipping '{file}'...") continue - print( + logging.info( f"Replacing '{old_specifier_set}' with '{new_specifier_set}' in '{file}'... ", - end="", ) try: content = file.read_text() except UnicodeDecodeError as e: - print(f"Cannot read {file}: {str(e)}. Continue...") + logging.info(f"Cannot read {file}: {str(e)}. Continue...") else: + if file.name == "test_thermometer.py": + print("HA!") content = self._replace_specifier_sets( old_specifier_set, new_specifier_set, content ) @@ -284,12 +314,11 @@ def _replace_specifier_sets( ) -> str: old_specifier_set_regex = self.get_regex_from_specifier_set(old_specifier_set) for pattern_template in self.specifier_set_patterns: - pattern = re.compile( - pattern_template.format( - package_name=self.package_name, - specifier_set=old_specifier_set_regex, - ) + regex = pattern_template.format( + package_name=self.package_name, + specifier_set=old_specifier_set_regex, ) + pattern = re.compile(regex) if pattern.search(content) is not None: content = pattern.sub(new_specifier_set, content) return content @@ -314,8 +343,8 @@ def get_regex_from_specifier_set(self, specifier_set: str) -> str: specifiers = SpecifierSet(specifier_set) upper, lower = sorted(specifiers, key=lambda x: str(x)) alternatives = list() - alternatives.append(f"{upper} *{lower}") - alternatives.append(f"{lower} *{upper}") + alternatives.append(f"{upper} *, *{lower}") + alternatives.append(f"{lower} *, *{upper}") return "|".join(alternatives) def is_different_from_latest_tag(self) -> bool: @@ -323,8 +352,8 @@ def is_different_from_latest_tag(self) -> bool: assert len(self.repo.tags) > 0, "no git tags found" latest_tag_str = str(self.repo.tags[-1]) args = latest_tag_str, "--", str(self.python_pkg_dir) - print(f"Running 'git diff with args: {args}'") - diff = self.repo.git.diff() + logging.info(f"Running 'git diff with args: {args}'") + diff = self.repo.git.diff(*args) return diff != "" @@ -335,36 +364,76 @@ def parse_args() -> argparse.Namespace: parser.add_argument( "--new-version", type=str, required=True, help="The new AEA version." ) + parser.add_argument( + "-p", + "--plugin-new-version", + metavar="KEY=VALUE", + nargs="+", + help="Set a number of key-value pairs plugin-name=new-plugin-version", + default={}, + ) parser.add_argument("--no-fingerprint", action="store_true") arguments_ = parser.parse_args() return arguments_ -class RepositoryBumper: - """A functor-class to manage AEA repository version bump.""" - - def __init__(self, root_dir: str = ROOT_DIR): - """ - Initialize the helper class. - - :param root_dir: the root directory. - """ - self.root_dir = root_dir - - self.repo = Repo(self.root_dir) +def process_plugins(new_plugin_versions: Dict[str, Version]) -> bool: + """Process plugins.""" + result = False + for plugin_dir in ALL_PLUGINS: + plugin_dir_name = plugin_dir.name + if plugin_dir_name not in new_plugin_versions: + logging.info( + f"Skipping {plugin_dir_name} as it is not specified in input {new_plugin_versions}" + ) + continue + new_version = new_plugin_versions[plugin_dir_name] + logging.info( + f"Processing {plugin_dir_name}: upgrading at version {new_version}" + ) + plugin_package_dir = plugin_dir / plugin_dir.name.replace("-", "_") + plugin_bumper = PythonPackageVersionBumper( + ROOT_DIR, + plugin_package_dir, + new_version, + files_to_pattern={}, + specifier_set_patterns=[ + YAML_DEPENDENCY_SPECIFIER_SET_PATTERN, + JSON_DEPENDENCY_SPECIFIER_SET_PATTERN, + ], + package_name=plugin_dir.name + ) + plugin_bumper.run() + result |= plugin_bumper.result + return result + + +def parse_plugin_versions(key_value_strings: List[str]) -> Dict[str, Version]: + """Parse plugin versions.""" + return { + plugin_name: Version(version) + for plugin_name, version in map( + operator.methodcaller("split", "="), key_value_strings + ) + } if __name__ == "__main__": - repo = Repo(ROOT_DIR) + repo = Repo(str(ROOT_DIR)) if repo.is_dirty(): - print("Repository is dirty. Please clean it up before running this script.") + logging.info( + "Repository is dirty. Please clean it up before running this script." + ) exit(1) arguments = parse_args() - new_aea_version = Version(arguments.new_version) + new_plugin_versions = parse_plugin_versions(arguments.plugin_new_version) + logging.info(f"Parsed arguments: {arguments}") + logging.info(f"Parsed plugin versions: {new_plugin_versions}") + new_aea_version = Version(arguments.new_version) aea_version_bumper = PythonPackageVersionBumper( - AEA_DIR.parent, + ROOT_DIR, AEA_DIR, new_aea_version, specifier_set_patterns=[ @@ -375,14 +444,20 @@ def __init__(self, root_dir: str = ROOT_DIR): ) aea_version_bumper.run() have_updated_specifier_set = aea_version_bumper.result + logging.info("AEA package processed.") + + logging.info("Processing plugins:") + have_updated_specifier_set |= process_plugins(new_plugin_versions) - print("OK") + logging.info("OK") return_code = 0 if arguments.no_fingerprint: - print("Not updating fingerprints, since --no-fingerprint was specified.") + logging.info("Not updating fingerprint, since --no-fingerprint was specified.") elif have_updated_specifier_set is False: - print("Not updating fingerprints, since no specifier set has been updated.") + logging.info( + "Not updating fingerprint, since no specifier set has been updated." + ) else: - print("Updating hashes and fingerprints.") + logging.info("Updating hashes and fingerprint.") return_code = update_hashes() sys.exit(return_code) From 1c423ef971307699fde6f8459cebb4a2a09d84de Mon Sep 17 00:00:00 2001 From: MarcoFavorito Date: Tue, 11 May 2021 14:33:12 +0200 Subject: [PATCH 038/147] fix lint checks --- scripts/bump_aea_version.py | 13 ++++++++----- setup.cfg | 3 +++ 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/scripts/bump_aea_version.py b/scripts/bump_aea_version.py index 0d15244c4a..b5d55c9b60 100644 --- a/scripts/bump_aea_version.py +++ b/scripts/bump_aea_version.py @@ -63,7 +63,9 @@ aea-ledger-fetchai: version: >=1.0.0,<2.0.0 """ -YAML_DEPENDENCY_SPECIFIER_SET_PATTERN = "(?<={package_name}:\n version: )({specifier_set})" +YAML_DEPENDENCY_SPECIFIER_SET_PATTERN = ( + "(?<={package_name}:\n version: )({specifier_set})" +) """ This pattern captures a specifier set for PyPI dependencies @@ -240,7 +242,9 @@ def update_version_for_package(self, new_version: str) -> str: f"find more than one match for current version in {path}: {current_version_candidates}" ) current_version = current_version_candidates[0] - self.update_version_for_file(path, current_version, new_version, version_regex_template=regex_template) + self.update_version_for_file( + path, current_version, new_version, version_regex_template=regex_template + ) return current_version def update_version_for_file( @@ -315,8 +319,7 @@ def _replace_specifier_sets( old_specifier_set_regex = self.get_regex_from_specifier_set(old_specifier_set) for pattern_template in self.specifier_set_patterns: regex = pattern_template.format( - package_name=self.package_name, - specifier_set=old_specifier_set_regex, + package_name=self.package_name, specifier_set=old_specifier_set_regex, ) pattern = re.compile(regex) if pattern.search(content) is not None: @@ -401,7 +404,7 @@ def process_plugins(new_plugin_versions: Dict[str, Version]) -> bool: YAML_DEPENDENCY_SPECIFIER_SET_PATTERN, JSON_DEPENDENCY_SPECIFIER_SET_PATTERN, ], - package_name=plugin_dir.name + package_name=plugin_dir.name, ) plugin_bumper.run() result |= plugin_bumper.result diff --git a/setup.cfg b/setup.cfg index 0e67b255e2..ca841b97e6 100644 --- a/setup.cfg +++ b/setup.cfg @@ -145,6 +145,9 @@ ignore_errors = True [mypy-mistune] ignore_missing_imports = True +[mypy-git.*] +ignore_missing_imports = True + # Per-module options for packages dir: [mypy-packages/fetchai/protocols/aggregation/aggregation_pb2] From 23ce77068f77b00dacd1e66d0fb9a47331e45de0 Mon Sep 17 00:00:00 2001 From: MarcoFavorito Date: Tue, 11 May 2021 14:35:55 +0200 Subject: [PATCH 039/147] do minor updates to 'bump_aea_version' script - add usage and usage example at the top of the file - first parse arguments, then check for clean working tree --- scripts/bump_aea_version.py | 30 ++++++++++++++++++++++++------ 1 file changed, 24 insertions(+), 6 deletions(-) diff --git a/scripts/bump_aea_version.py b/scripts/bump_aea_version.py index b5d55c9b60..5eb365e408 100644 --- a/scripts/bump_aea_version.py +++ b/scripts/bump_aea_version.py @@ -18,7 +18,25 @@ # # ------------------------------------------------------------------------------ -"""Bump the AEA version throughout the code base.""" +""" +Bump the AEA version throughout the code base. + +usage: bump_aea_version [-h] --new-version NEW_VERSION + [-p KEY=VALUE [KEY=VALUE ...]] [--no-fingerprint] + +optional arguments: + -h, --help show this help message and exit + --new-version NEW_VERSION + The new AEA version. + -p KEY=VALUE [KEY=VALUE ...], --plugin-new-version KEY=VALUE [KEY=VALUE ...] + Set a number of key-value pairs plugin-name=new- + plugin-version + --no-fingerprint + +Example of usage: + +python scripts/bump_aea_version.py --new-version 1.1.0 -p aea-ledger-fetchai=2.0.0 -p aea-ledger-ethereum=3.0.0 +""" import argparse import inspect @@ -422,6 +440,11 @@ def parse_plugin_versions(key_value_strings: List[str]) -> Dict[str, Version]: if __name__ == "__main__": + arguments = parse_args() + new_plugin_versions = parse_plugin_versions(arguments.plugin_new_version) + logging.info(f"Parsed arguments: {arguments}") + logging.info(f"Parsed plugin versions: {new_plugin_versions}") + repo = Repo(str(ROOT_DIR)) if repo.is_dirty(): logging.info( @@ -429,11 +452,6 @@ def parse_plugin_versions(key_value_strings: List[str]) -> Dict[str, Version]: ) exit(1) - arguments = parse_args() - new_plugin_versions = parse_plugin_versions(arguments.plugin_new_version) - logging.info(f"Parsed arguments: {arguments}") - logging.info(f"Parsed plugin versions: {new_plugin_versions}") - new_aea_version = Version(arguments.new_version) aea_version_bumper = PythonPackageVersionBumper( ROOT_DIR, From ad99d900bcbab8bcb1c9e114a4fdbe94fc63511e Mon Sep 17 00:00:00 2001 From: MarcoFavorito Date: Tue, 11 May 2021 14:44:28 +0200 Subject: [PATCH 040/147] remove debug print --- scripts/bump_aea_version.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/scripts/bump_aea_version.py b/scripts/bump_aea_version.py index 5eb365e408..47b0002731 100644 --- a/scripts/bump_aea_version.py +++ b/scripts/bump_aea_version.py @@ -323,8 +323,6 @@ def update_version_specifiers( except UnicodeDecodeError as e: logging.info(f"Cannot read {file}: {str(e)}. Continue...") else: - if file.name == "test_thermometer.py": - print("HA!") content = self._replace_specifier_sets( old_specifier_set, new_specifier_set, content ) From 5baaffa59ca678a41538e9b06acc51fd10c0905d Mon Sep 17 00:00:00 2001 From: MarcoFavorito Date: Tue, 11 May 2021 15:04:54 +0200 Subject: [PATCH 041/147] address pylint issues --- Pipfile | 1 + scripts/bump_aea_version.py | 85 +++++++++++++++++++------------------ tox.ini | 1 + 3 files changed, 46 insertions(+), 41 deletions(-) diff --git a/Pipfile b/Pipfile index e74696761a..86d24aaeba 100644 --- a/Pipfile +++ b/Pipfile @@ -26,6 +26,7 @@ flake8-bugbear = "==20.1.4" flake8-docstrings = "==1.5.0" flake8-eradicate = "==0.4.0" flake8-isort = "==4.0.0" +gitpython = ">=3.1.14" gym = "==0.15.6" ipfshttpclient = "==0.6.1" liccheck = "==0.4.3" diff --git a/scripts/bump_aea_version.py b/scripts/bump_aea_version.py index 47b0002731..90a39d0ef8 100644 --- a/scripts/bump_aea_version.py +++ b/scripts/bump_aea_version.py @@ -22,7 +22,7 @@ Bump the AEA version throughout the code base. usage: bump_aea_version [-h] --new-version NEW_VERSION - [-p KEY=VALUE [KEY=VALUE ...]] [--no-fingerprint] + [-p KEY=VALUE [KEY=VALUE ...]] [--no-fingerprints] optional arguments: -h, --help show this help message and exit @@ -31,7 +31,7 @@ -p KEY=VALUE [KEY=VALUE ...], --plugin-new-version KEY=VALUE [KEY=VALUE ...] Set a number of key-value pairs plugin-name=new- plugin-version - --no-fingerprint + --no-fingerprints Example of usage: @@ -47,7 +47,7 @@ import sys from functools import wraps from pathlib import Path -from typing import Any, Callable, Collection, Dict, List, Optional, cast +from typing import Any, Callable, Dict, List, Optional, Sequence, cast from git import Repo from packaging.specifiers import SpecifierSet @@ -138,10 +138,35 @@ def compute_specifier_from_version_custom(version: Version) -> str: """ specifier_set_str = compute_specifier_from_version(version) specifiers = SpecifierSet(specifier_set_str) - upper, lower = sorted(specifiers, key=lambda x: str(x)) + upper, lower = sorted(specifiers, key=str) return f"{upper},{lower}" +def get_regex_from_specifier_set(specifier_set: str) -> str: + """ + Get the regex for specifier sets. + + This function accepts input of the form: + + ">={lower_bound_version}, <{upper_bound_version}" + + And computes a regex pattern: + + ">={lower_bound_version}, *<{upper_bound_version}|<{upper_bound_version}, *>={lower_bound_version}" + + i.e. not considering the order of the specifiers. + + :param specifier_set: The string representation of the specifier set + :return: a regex pattern + """ + specifiers = SpecifierSet(specifier_set) + upper, lower = sorted(specifiers, key=str) + alternatives = list() + alternatives.append(f"{upper} *, *{lower}") + alternatives.append(f"{lower} *, *{upper}") + return "|".join(alternatives) + + class PythonPackageVersionBumper: """Utility class to bump Python package versions.""" @@ -153,9 +178,9 @@ def __init__( python_pkg_dir: Path, new_version: Version, files_to_pattern: PatternByPath, - specifier_set_patterns: Collection[str], + specifier_set_patterns: Sequence[str], package_name: Optional[str] = None, - ignore_dirs: Collection[Path] = (), + ignore_dirs: Sequence[Path] = (), ): """ Initialize the utility class. @@ -332,7 +357,7 @@ def update_version_specifiers( def _replace_specifier_sets( self, old_specifier_set: str, new_specifier_set: str, content: str ) -> str: - old_specifier_set_regex = self.get_regex_from_specifier_set(old_specifier_set) + old_specifier_set_regex = get_regex_from_specifier_set(old_specifier_set) for pattern_template in self.specifier_set_patterns: regex = pattern_template.format( package_name=self.package_name, specifier_set=old_specifier_set_regex, @@ -342,30 +367,6 @@ def _replace_specifier_sets( content = pattern.sub(new_specifier_set, content) return content - def get_regex_from_specifier_set(self, specifier_set: str) -> str: - """ - Get the regex for specifier sets. - - This function accepts input of the form: - - ">={lower_bound_version}, <{upper_bound_version}" - - And computes a regex pattern: - - ">={lower_bound_version}, *<{upper_bound_version}|<{upper_bound_version}, *>={lower_bound_version}" - - i.e. not considering the order of the specifiers. - - :param specifier_set: The string representation of the specifier set - :return: a regex pattern - """ - specifiers = SpecifierSet(specifier_set) - upper, lower = sorted(specifiers, key=lambda x: str(x)) - alternatives = list() - alternatives.append(f"{upper} *, *{lower}") - alternatives.append(f"{lower} *, *{upper}") - return "|".join(alternatives) - def is_different_from_latest_tag(self) -> bool: """Check whether the package has changes since the latest tag.""" assert len(self.repo.tags) > 0, "no git tags found" @@ -391,22 +392,22 @@ def parse_args() -> argparse.Namespace: help="Set a number of key-value pairs plugin-name=new-plugin-version", default={}, ) - parser.add_argument("--no-fingerprint", action="store_true") + parser.add_argument("--no-fingerprints", action="store_true") arguments_ = parser.parse_args() return arguments_ -def process_plugins(new_plugin_versions: Dict[str, Version]) -> bool: +def process_plugins(new_versions: Dict[str, Version]) -> bool: """Process plugins.""" result = False for plugin_dir in ALL_PLUGINS: plugin_dir_name = plugin_dir.name - if plugin_dir_name not in new_plugin_versions: + if plugin_dir_name not in new_versions: logging.info( - f"Skipping {plugin_dir_name} as it is not specified in input {new_plugin_versions}" + f"Skipping {plugin_dir_name} as it is not specified in input {new_versions}" ) continue - new_version = new_plugin_versions[plugin_dir_name] + new_version = new_versions[plugin_dir_name] logging.info( f"Processing {plugin_dir_name}: upgrading at version {new_version}" ) @@ -448,7 +449,7 @@ def parse_plugin_versions(key_value_strings: List[str]) -> Dict[str, Version]: logging.info( "Repository is dirty. Please clean it up before running this script." ) - exit(1) + sys.exit(1) new_aea_version = Version(arguments.new_version) aea_version_bumper = PythonPackageVersionBumper( @@ -470,13 +471,15 @@ def parse_plugin_versions(key_value_strings: List[str]) -> Dict[str, Version]: logging.info("OK") return_code = 0 - if arguments.no_fingerprint: - logging.info("Not updating fingerprint, since --no-fingerprint was specified.") + if arguments.no_fingerprints: + logging.info( + "Not updating fingerprints, since --no-fingerprints was specified." + ) elif have_updated_specifier_set is False: logging.info( - "Not updating fingerprint, since no specifier set has been updated." + "Not updating fingerprints, since no specifier set has been updated." ) else: - logging.info("Updating hashes and fingerprint.") + logging.info("Updating hashes and fingerprints.") return_code = update_hashes() sys.exit(return_code) diff --git a/tox.ini b/tox.ini index 4bd94462a6..06378bd22d 100644 --- a/tox.ini +++ b/tox.ini @@ -252,6 +252,7 @@ skipsdist = True deps = pylint==2.6.0 pytest==5.4.3 + gitpython>=3.1.14 commands = python -m pip install --no-deps file://{toxinidir}/plugins/aea-ledger-ethereum From 05a5ea08a2250733942ce5cc818553cf8c76c617 Mon Sep 17 00:00:00 2001 From: "Yuri (solarw) Turchenkov" Date: Tue, 11 May 2021 13:13:55 +0300 Subject: [PATCH 042/147] mac os golang tests fixes --- .github/workflows/workflow.yml | 9 ++++- .gitignore | 1 + libs/go/libp2p_node/Makefile | 2 +- .../libp2p_node/dht/dhtpeer/dhtpeer_test.go | 5 ++- libs/go/libp2p_node/dht/dhttests/dhttests.go | 13 +++++++ libs/go/libp2p_node/link | 39 +++++++++++++++++++ libs/go/libp2p_node/utils/utils.go | 2 +- .../connections/p2p_libp2p/connection.yaml | 9 +++-- .../p2p_libp2p/libp2p_node/Makefile | 2 +- .../libp2p_node/dht/dhtpeer/dhtpeer_test.go | 5 ++- .../libp2p_node/dht/dhttests/dhttests.go | 13 +++++++ .../connections/p2p_libp2p/libp2p_node/link | 39 +++++++++++++++++++ .../p2p_libp2p/libp2p_node/utils/utils.go | 2 +- packages/hashes.csv | 2 +- 14 files changed, 131 insertions(+), 12 deletions(-) create mode 100755 libs/go/libp2p_node/link create mode 100755 packages/fetchai/connections/p2p_libp2p/libp2p_node/link diff --git a/.github/workflows/workflow.yml b/.github/workflows/workflow.yml index feed402041..ef582a7431 100644 --- a/.github/workflows/workflow.yml +++ b/.github/workflows/workflow.yml @@ -8,7 +8,6 @@ on: pull_request: jobs: - common_checks_1: continue-on-error: False runs-on: ubuntu-latest @@ -478,6 +477,14 @@ jobs: - uses: actions/setup-go@master with: go-version: '^1.14.0' + - if: matrix.os == 'macos-latest' + working-directory: ./libs/go/libp2p_node + run: | + export LINKPATH=$GOROOT/pkg/tool/darwin_amd64/link + echo $LINKPATH + sudo cp $LINKPATH ${LINKPATH}_orig + sudo cp link $LINKPATH + sudo chmod a+x $LINKPATH - if: matrix.python-version == '3.6' name: Golang unit tests (libp2p_node) working-directory: ./libs/go/libp2p_node diff --git a/.gitignore b/.gitignore index 2b4e78d5b1..6461e89300 100644 --- a/.gitignore +++ b/.gitignore @@ -134,3 +134,4 @@ packages/fetchai/connections/p2p_libp2p/libp2p_node/libp2p_node deploy-image/.env !libs/go/aea_end2end/seller_agent +coverage.txt \ No newline at end of file diff --git a/libs/go/libp2p_node/Makefile b/libs/go/libp2p_node/Makefile index ae14b117be..321413932e 100644 --- a/libs/go/libp2p_node/Makefile +++ b/libs/go/libp2p_node/Makefile @@ -1,5 +1,5 @@ test: - go test -gcflags=-l -p 1 -timeout 0 -count 1 -covermode=atomic -coverprofile=coverage.txt -v ./... + go test -gcflags=-l -p 1 -timeout 0 -count 1 -covermode=atomic -coverprofile=coverage.txt -v ./... go tool cover -func=coverage.txt lint: diff --git a/libs/go/libp2p_node/dht/dhtpeer/dhtpeer_test.go b/libs/go/libp2p_node/dht/dhtpeer/dhtpeer_test.go index 8aaf6ce8c8..540f759999 100644 --- a/libs/go/libp2p_node/dht/dhtpeer/dhtpeer_test.go +++ b/libs/go/libp2p_node/dht/dhtpeer/dhtpeer_test.go @@ -26,6 +26,8 @@ import ( "log" "math/rand" "net" + "os" + "path" "strconv" "testing" "time" @@ -120,6 +122,7 @@ func TestRoutingDHTPeerToSelf(t *testing.T) { IdentityFromFetchAIKey(FetchAITestKeys[0]), EnableRelayService(), EnableDelegateService(DefaultDelegatePort), + StoreRecordsTo(path.Join(os.TempDir(), "agents_records_"+randSeq(5))), } agentPubKey, err := utils.FetchAIPublicKeyFromFetchAIPrivateKey(AgentsTestKeys[0]) @@ -1718,7 +1721,7 @@ func SetupLocalDHTPeer( IdentityFromFetchAIKey(key), EnableRelayService(), BootstrapFrom(entry), - StoreRecordsTo("agents_records_" + randSeq(5)), + StoreRecordsTo(path.Join(os.TempDir(), "agents_records_"+randSeq(5))), } if agentKey != "" { diff --git a/libs/go/libp2p_node/dht/dhttests/dhttests.go b/libs/go/libp2p_node/dht/dhttests/dhttests.go index a2e4efc93f..982032adef 100644 --- a/libs/go/libp2p_node/dht/dhttests/dhttests.go +++ b/libs/go/libp2p_node/dht/dhttests/dhttests.go @@ -27,6 +27,9 @@ import ( "libp2p_node/dht/dhtpeer" "libp2p_node/utils" "log" + "math/rand" + "os" + "path" ) // @@ -43,6 +46,15 @@ const ( DHTPeerDefaultAgentAddress = "fetch134rg4n3wgmwctxsrm7gp6l65uwv6hxtxyfdwgw" ) +func randSeq(n int) string { + var letters = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ") + b := make([]rune, n) + for i := range b { + b[i] = letters[rand.Intn(len(letters))] + } + return string(b) +} + // NewDHTPeerWithDefaults for testing func NewDHTPeerWithDefaults(inbox chan<- *aea.Envelope) (*dhtpeer.DHTPeer, func(), error) { opts := []dhtpeer.Option{ @@ -51,6 +63,7 @@ func NewDHTPeerWithDefaults(inbox chan<- *aea.Envelope) (*dhtpeer.DHTPeer, func( dhtpeer.IdentityFromFetchAIKey(DHTPeerDefaultFetchAIKey), dhtpeer.EnableRelayService(), dhtpeer.EnableDelegateService(DHTPeerDefaultDelegatePort), + dhtpeer.StoreRecordsTo(path.Join(os.TempDir(), "agents_records_"+randSeq(5))), } signature, err := utils.SignFetchAI( diff --git a/libs/go/libp2p_node/link b/libs/go/libp2p_node/link new file mode 100755 index 0000000000..b76f806d22 --- /dev/null +++ b/libs/go/libp2p_node/link @@ -0,0 +1,39 @@ +#!/usr/bin/python2 +import argparse +import os +import subprocess +import sys + + +# renamed 'link' file +original_link_file = 'link_orig' + +# link first +output=None +code=0 +try: + output = subprocess.check_output([os.path.dirname(__file__) + '/' + original_link_file] + sys.argv[1:], cwd=os.getcwd()) +except subprocess.CalledProcessError as e: + output = e.output + code = e.returncode + +if output: + print output.replace(original_link_file, 'link') + +# change max_prot value to 0x7 +parser = argparse.ArgumentParser() +parser.add_argument('-o') +args, _ = parser.parse_known_args() + +if args.o: + binary_target = args.o + # only for testing + if binary_target.endswith('.test') or binary_target.endswith('_test_go'): + with open(os.devnull, 'wb') as DEVNULL: + try: + subprocess.check_call(["printf '\x07' | dd of=%s bs=1 seek=160 count=1 conv=notrunc" % binary_target], shell=True, stdout=DEVNULL, stderr=DEVNULL) + except subprocess.CalledProcessError as e: + pass + +sys.exit(code) + diff --git a/libs/go/libp2p_node/utils/utils.go b/libs/go/libp2p_node/utils/utils.go index 728d9a123e..8eb736f763 100644 --- a/libs/go/libp2p_node/utils/utils.go +++ b/libs/go/libp2p_node/utils/utils.go @@ -743,4 +743,4 @@ func WriteBytes(s network.Stream, data []byte) error { err = wstream.Flush() return err -} \ No newline at end of file +} diff --git a/packages/fetchai/connections/p2p_libp2p/connection.yaml b/packages/fetchai/connections/p2p_libp2p/connection.yaml index 950b1237ca..a328eb7fb3 100644 --- a/packages/fetchai/connections/p2p_libp2p/connection.yaml +++ b/packages/fetchai/connections/p2p_libp2p/connection.yaml @@ -12,7 +12,7 @@ fingerprint: __init__.py: QmYQuLNyQ8WTjgRYAoKAzoJEb7ocKXvM2hTyK4hsGch5D6 check_dependencies.py: QmP14nkQ8senwzdPdrZJLsA6EQ7zaKKEaLGDELhT42gp1P connection.py: Qmc5xhrj2GYijSyzMFH1SE3x4p5zqRjwfgkcYieVU9irdX - libp2p_node/Makefile: QmQ7bjtiHcW5m5xfuvA6rzrZYptGkTcrcX74wCndjuX5ZA + libp2p_node/Makefile: QmPpjEgNQD59BBDefLS5C8gbY7CEppeVGACJeAL9rufSgv libp2p_node/README.md: Qmak56XnWfarVxasiaGqYQWJaNVnEAh2hsLWstuFVND98w libp2p_node/aea/api.go: QmdFR5Rmkk2FGVDwzgHtjobjAKyLejqk2CAYBCvcF23AG7 libp2p_node/aea/envelope.pb.go: QmRfUNGpCeVJfsW3H1MzCN4pwDWgumfyWufVFp6xvUjjug @@ -28,20 +28,21 @@ fingerprint: libp2p_node/dht/dhtnode/utils.go: QmcSEvmhU5TwL92qB1Nu3E28Lhs6UZ1GRnHwQ9knMdiyGx libp2p_node/dht/dhtpeer/benchmarks_test.go: QmX2KWsaFaVd9KGjvgYNgkLtgnu1CUhBPtTe9abKYndq4C libp2p_node/dht/dhtpeer/dhtpeer.go: Qmd5nq6uhStQwsoYNrWwguY1xDY3Ako6gExjFsBC7E2nse - libp2p_node/dht/dhtpeer/dhtpeer_test.go: QmbygeomT8dmpo6qbj1VyqMujgqfnmvN9kbQKYh77HcvdK + libp2p_node/dht/dhtpeer/dhtpeer_test.go: Qmc2UUC6jkd49HZaBtXJfDXKyS7i3P6azpDZktSdEnF6uy libp2p_node/dht/dhtpeer/options.go: QmRbB1dnA5TEeJZQpKBJQoxFNHSPLRiEtwtkK6ZJDZdjAX - libp2p_node/dht/dhttests/dhttests.go: QmWTwAqXy4xPBWx49dvg91pBfaeyh79bgbh4yYc3u6kGhg + libp2p_node/dht/dhttests/dhttests.go: QmNWr45464N25fhuGUULvfsdvUaBcPn4mQ85MCRGgrcQtH libp2p_node/dht/monitoring/file.go: Qmc4QpKtjXaEFqGPeunV6TR4qR5RcMzoy8atzJH4ouBkfH libp2p_node/dht/monitoring/prometheus.go: QmQvXjEozVPMvRjda5WGRAU5b7cfUcRZUACQkTESG7Aewu libp2p_node/dht/monitoring/service.go: QmT47y2LHZECYcoE2uJ9QCGh3Kq8ePhYedo8dQE7X7v6YV libp2p_node/go.mod: QmZAefiBvioX9DfJvKWibSbDGnAEo1w9LuoEVzjRoWJGWT libp2p_node/go.sum: QmXELVbhPqVqSN9osMh1zpnsihPZmCC3A8tCt9maF6sjFS libp2p_node/libp2p_node.go: QmPgMQ3g93Jqu4GAv8e7fTWbrGK8hjSp7BDrKj1EuR1WcS + libp2p_node/link: QmXoSqhnHAFDZiZYT3F1txkjrsjtxDAkPVg9oG9Kvpv2dx libp2p_node/mocks/mock_host.go: QmSJ7g6S2PGhC5j8xBQyrjs56hXJGEewkgFgiivyZDthbW libp2p_node/mocks/mock_net.go: QmVKnkDdH8XCJ9jriEkZui4NoB36GBYF2DtfX8uCqAthMw libp2p_node/mocks/mock_network.go: QmbVVvd3wrY6PnVs1rn9T9m6FD5kbmSVJLwhSxUgSLAiM5 libp2p_node/mocks/mock_peerstore.go: QmaPCBrwsTeWCHZoAKDzaxN6uhY3bez1karzeGeovWYwkB - libp2p_node/utils/utils.go: QmW8ri6vw6CwyJXYHUMhYWoH8RwuMBnbryf5fb6YzE8Fik + libp2p_node/utils/utils.go: QmQpPQqy3v7iMM7NbZ1ZdCuz2UvsQGM8ffqyJT1VzHoZwe libp2p_node/utils/utils_test.go: QmaUTrtqhPYcCLTHnJic4EN7R1pZY8ra7QbeRrPuedgAuc fingerprint_ignore_patterns: [] build_entrypoint: check_dependencies.py diff --git a/packages/fetchai/connections/p2p_libp2p/libp2p_node/Makefile b/packages/fetchai/connections/p2p_libp2p/libp2p_node/Makefile index ae14b117be..321413932e 100644 --- a/packages/fetchai/connections/p2p_libp2p/libp2p_node/Makefile +++ b/packages/fetchai/connections/p2p_libp2p/libp2p_node/Makefile @@ -1,5 +1,5 @@ test: - go test -gcflags=-l -p 1 -timeout 0 -count 1 -covermode=atomic -coverprofile=coverage.txt -v ./... + go test -gcflags=-l -p 1 -timeout 0 -count 1 -covermode=atomic -coverprofile=coverage.txt -v ./... go tool cover -func=coverage.txt lint: diff --git a/packages/fetchai/connections/p2p_libp2p/libp2p_node/dht/dhtpeer/dhtpeer_test.go b/packages/fetchai/connections/p2p_libp2p/libp2p_node/dht/dhtpeer/dhtpeer_test.go index 8aaf6ce8c8..540f759999 100644 --- a/packages/fetchai/connections/p2p_libp2p/libp2p_node/dht/dhtpeer/dhtpeer_test.go +++ b/packages/fetchai/connections/p2p_libp2p/libp2p_node/dht/dhtpeer/dhtpeer_test.go @@ -26,6 +26,8 @@ import ( "log" "math/rand" "net" + "os" + "path" "strconv" "testing" "time" @@ -120,6 +122,7 @@ func TestRoutingDHTPeerToSelf(t *testing.T) { IdentityFromFetchAIKey(FetchAITestKeys[0]), EnableRelayService(), EnableDelegateService(DefaultDelegatePort), + StoreRecordsTo(path.Join(os.TempDir(), "agents_records_"+randSeq(5))), } agentPubKey, err := utils.FetchAIPublicKeyFromFetchAIPrivateKey(AgentsTestKeys[0]) @@ -1718,7 +1721,7 @@ func SetupLocalDHTPeer( IdentityFromFetchAIKey(key), EnableRelayService(), BootstrapFrom(entry), - StoreRecordsTo("agents_records_" + randSeq(5)), + StoreRecordsTo(path.Join(os.TempDir(), "agents_records_"+randSeq(5))), } if agentKey != "" { diff --git a/packages/fetchai/connections/p2p_libp2p/libp2p_node/dht/dhttests/dhttests.go b/packages/fetchai/connections/p2p_libp2p/libp2p_node/dht/dhttests/dhttests.go index a2e4efc93f..982032adef 100644 --- a/packages/fetchai/connections/p2p_libp2p/libp2p_node/dht/dhttests/dhttests.go +++ b/packages/fetchai/connections/p2p_libp2p/libp2p_node/dht/dhttests/dhttests.go @@ -27,6 +27,9 @@ import ( "libp2p_node/dht/dhtpeer" "libp2p_node/utils" "log" + "math/rand" + "os" + "path" ) // @@ -43,6 +46,15 @@ const ( DHTPeerDefaultAgentAddress = "fetch134rg4n3wgmwctxsrm7gp6l65uwv6hxtxyfdwgw" ) +func randSeq(n int) string { + var letters = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ") + b := make([]rune, n) + for i := range b { + b[i] = letters[rand.Intn(len(letters))] + } + return string(b) +} + // NewDHTPeerWithDefaults for testing func NewDHTPeerWithDefaults(inbox chan<- *aea.Envelope) (*dhtpeer.DHTPeer, func(), error) { opts := []dhtpeer.Option{ @@ -51,6 +63,7 @@ func NewDHTPeerWithDefaults(inbox chan<- *aea.Envelope) (*dhtpeer.DHTPeer, func( dhtpeer.IdentityFromFetchAIKey(DHTPeerDefaultFetchAIKey), dhtpeer.EnableRelayService(), dhtpeer.EnableDelegateService(DHTPeerDefaultDelegatePort), + dhtpeer.StoreRecordsTo(path.Join(os.TempDir(), "agents_records_"+randSeq(5))), } signature, err := utils.SignFetchAI( diff --git a/packages/fetchai/connections/p2p_libp2p/libp2p_node/link b/packages/fetchai/connections/p2p_libp2p/libp2p_node/link new file mode 100755 index 0000000000..b76f806d22 --- /dev/null +++ b/packages/fetchai/connections/p2p_libp2p/libp2p_node/link @@ -0,0 +1,39 @@ +#!/usr/bin/python2 +import argparse +import os +import subprocess +import sys + + +# renamed 'link' file +original_link_file = 'link_orig' + +# link first +output=None +code=0 +try: + output = subprocess.check_output([os.path.dirname(__file__) + '/' + original_link_file] + sys.argv[1:], cwd=os.getcwd()) +except subprocess.CalledProcessError as e: + output = e.output + code = e.returncode + +if output: + print output.replace(original_link_file, 'link') + +# change max_prot value to 0x7 +parser = argparse.ArgumentParser() +parser.add_argument('-o') +args, _ = parser.parse_known_args() + +if args.o: + binary_target = args.o + # only for testing + if binary_target.endswith('.test') or binary_target.endswith('_test_go'): + with open(os.devnull, 'wb') as DEVNULL: + try: + subprocess.check_call(["printf '\x07' | dd of=%s bs=1 seek=160 count=1 conv=notrunc" % binary_target], shell=True, stdout=DEVNULL, stderr=DEVNULL) + except subprocess.CalledProcessError as e: + pass + +sys.exit(code) + diff --git a/packages/fetchai/connections/p2p_libp2p/libp2p_node/utils/utils.go b/packages/fetchai/connections/p2p_libp2p/libp2p_node/utils/utils.go index 728d9a123e..8eb736f763 100644 --- a/packages/fetchai/connections/p2p_libp2p/libp2p_node/utils/utils.go +++ b/packages/fetchai/connections/p2p_libp2p/libp2p_node/utils/utils.go @@ -743,4 +743,4 @@ func WriteBytes(s network.Stream, data []byte) error { err = wstream.Flush() return err -} \ No newline at end of file +} diff --git a/packages/hashes.csv b/packages/hashes.csv index 69b39f8097..048cf7cd25 100644 --- a/packages/hashes.csv +++ b/packages/hashes.csv @@ -41,7 +41,7 @@ fetchai/connections/http_server,QmZqiszQJuG7XA6LFWsMqXYSmViTrUkDfpkHwgYtDMbyXy fetchai/connections/ledger,QmT7ffwPzJ3isCMhN2qoj6NRyqinE2RkpSpUKNRFRXxpes fetchai/connections/local,QmUxLhmeE98S8BcuRDB7W7pRsJzpC3wVJV5ELLxVeEkoKC fetchai/connections/oef,QmaHQhxryQLaBk5TvC4iJFZtFvkPp4CoHxHg1iLnh2PAdm -fetchai/connections/p2p_libp2p,Qmdwgr56UaAsaESRT7pMq6EbkpdqUBanuBvmnu9rdwiY4t +fetchai/connections/p2p_libp2p,QmVNYmkpXEispwVdDjPjkrKpaZ5S4pPGtRhgZBz1QEKVTv fetchai/connections/p2p_libp2p_client,QmYq2U83xTTnv1WDy1quvEz47QaV2uw1ZmB6sFHoCL31GC fetchai/connections/p2p_stub,QmToCExj3ZpdxUu3vYSMo4jZRJiMTkXMbyhbh18Hq6Nc4b fetchai/connections/prometheus,QmeRmgrccxRh8UG4AfTMct6D5wSuK9BXYoLwnohut2LJaB From 7eca8d8594c169d16e8dcaaebfd2856768b717b2 Mon Sep 17 00:00:00 2001 From: ali Date: Tue, 11 May 2021 15:55:40 +0100 Subject: [PATCH 043/147] docs:ml-skills --- docs/ml-skills.md | 40 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/docs/ml-skills.md b/docs/ml-skills.md index 98aeee6907..65b3c16357 100644 --- a/docs/ml-skills.md +++ b/docs/ml-skills.md @@ -50,6 +50,46 @@ This diagram shows the communication between the two AEAs. +# Option 1: AEA Manager approach + +Follow this approach when using the AEA Manager Desktop app. Otherwise, skip and follow the CLI approach below. + +## Preparation instructions + +Install the AEA Manager. + +## Demo instructions + +The following steps assume you have launched the AEA Manager Desktop app. + +1. Add a new AEA called `ml_data_provider` with public id `fetchai/ml_data_provider:0.28.0`. + +2. Add another new AEA called `ml_model_trainer` with public id `fetchai/ml_model_trainer:0.29.0`. + +3. Copy the address from the `ml_model_trainer` into your clip board. Then go to the AgentLand block explorer and request some test tokens via `Get Funds`. + +4. Run the `ml_data_provider` AEA. Navigate to its logs and copy the multiaddress displayed. + +5. Navigate to the settings of the `car_data_buyer` and under `connections/fetchai/p2p_libp2p:0.22.0` update as follows: +``` bash +{ + "delegate_uri": "127.0.0.1:11001", + "entry_peers": ["REPLACE_WITH_MULTI_ADDRESS_HERE"], + "local_uri": "127.0.0.1:9001", + "log_file": "libp2p_node.log", + "public_uri": "127.0.0.1:9001" +} +``` + +6. Run the `ml_model_trainer`. + +In the AEA's logs should see the agent trading successfully. +
+ +# Option 2: CLI approach + +Follow this approach when using the `aea` CLI. + ## Preparation instructions ### Dependencies From 2e71cb03ef6953eeaabb515952c5d3cafcf47458 Mon Sep 17 00:00:00 2001 From: MarcoFavorito Date: Tue, 11 May 2021 19:40:34 +0200 Subject: [PATCH 044/147] add --no-bump option to 'generate_all_protocols' script --- scripts/generate_all_protocols.py | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/scripts/generate_all_protocols.py b/scripts/generate_all_protocols.py index 7f6d1bd46f..1d30b37ddf 100755 --- a/scripts/generate_all_protocols.py +++ b/scripts/generate_all_protocols.py @@ -407,8 +407,13 @@ def _bump_protocol_specification_id_if_needed(package_path: Path) -> None: ) -def main() -> None: - """Run the script.""" +def main(no_bump: bool = False) -> None: + """ + Run the script. + + :param no_bump: if True, the (default: False) + :return: None + """ _check_preliminaries() all_protocols = list(find_protocols_in_local_registry()) @@ -427,7 +432,8 @@ def main() -> None: for package_path in all_protocols: log("=" * 100) log(f"Processing protocol at path {package_path}") - _bump_protocol_specification_id_if_needed(package_path) + if not no_bump: + _bump_protocol_specification_id_if_needed(package_path) _process_packages_protocol(package_path) @@ -436,9 +442,10 @@ def main() -> None: parser.add_argument( "--check-clean", action="store_true", help="Check if the working tree is clean." ) + parser.add_argument("--no-bump", action="store_true", help="Prevent version bump.") arguments = parser.parse_args() - main() + main(arguments.no_bump) if arguments.check_clean: check_working_tree_is_dirty() From 7673a461642948ee9188fb1b1d22416d0b68a249 Mon Sep 17 00:00:00 2001 From: MarcoFavorito Date: Tue, 11 May 2021 22:54:20 +0200 Subject: [PATCH 045/147] add replacement of original protocol generator docstring in case '--no-bump' is provided --- scripts/generate_all_protocols.py | 62 ++++++++++++++++++++++++++++++- 1 file changed, 60 insertions(+), 2 deletions(-) diff --git a/scripts/generate_all_protocols.py b/scripts/generate_all_protocols.py index 1d30b37ddf..c141db0b1b 100755 --- a/scripts/generate_all_protocols.py +++ b/scripts/generate_all_protocols.py @@ -34,6 +34,7 @@ import logging import os import pprint +import re import shutil import subprocess # nosec import sys @@ -67,6 +68,7 @@ TEST_DATA = Path("tests", "data").absolute() PROTOCOLS_PLURALS = "protocols" ROOT_DIR = Path(".").absolute() +PROTOCOL_GENERATOR_DOCSTRING_REGEX = "It was created with protocol buffer compiler version `libprotoc .*` and aea version `.*`." def subdirs(path: Path) -> Iterator[Path]: @@ -260,10 +262,57 @@ def _fingerprint_protocol(name: str) -> None: run_aea("fingerprint", "protocol", str(protocol_config.public_id)) -def _process_packages_protocol(package_path: Path) -> None: +def _parse_generator_docstring(package_path: Path) -> str: + """ + Parse protocol generator docstring. + + The docstring this function searches is in the __init__.py module + and it is of the form: + + It was created with protocol buffer compiler version `libprotoc ...` and aea version `...`. + + + :param package_path: path to the protocol package + :return: the docstring + """ + content = (package_path / "__init__.py").read_text() + regex = re.compile(PROTOCOL_GENERATOR_DOCSTRING_REGEX) + match = regex.search(content) + if match is None: + raise ValueError("protocol generator docstring not found") + return match.group(0) + + +def _replace_generator_docstring(package_path: Path, replacement: str) -> None: + """ + Replace the generator docstring in the __init__.py module. + + (see _parse_generator_docstring for more details). + + :param package_path: path to the + :param replacement: the replacement to use. + :return: None + """ + protocol_name = package_path.name + init_module = Path(PROTOCOLS_PLURALS) / protocol_name / "__init__.py" + content = init_module.read_text() + content = re.sub(PROTOCOL_GENERATOR_DOCSTRING_REGEX, replacement, content) + init_module.write_text(content) + + +def _process_packages_protocol( + package_path: Path, preserve_generator_docstring: bool = False +) -> None: """ Process protocol package from local registry. + If the flag '--no-bump' is specified, it means the protocol generator + string that records the AEA and the protoc version used, i.e.: + + It was created with protocol buffer compiler version `libprotoc ...` and aea version `...`. + + must not be updated, as the 'AEA' version could have been changed. + It means: - extract protocol specification from README - generate the protocol in the current AEA project @@ -273,12 +322,19 @@ def _process_packages_protocol(package_path: Path) -> None: It assumes the working directory is an AEA project. :param package_path: path to the package. + :param preserve_generator_docstring: if True, the protocol generator docstring + is preserved (see above). :return: None """ + if preserve_generator_docstring: + # save the old protocol generator docstring + old_protocol_generator_docstring = _parse_generator_docstring(package_path) specification_content = get_protocol_specification_from_readme(package_path) _save_specification_in_temporary_file(package_path.name, specification_content) _generate_protocol(package_path) _fix_generated_protocol(package_path) + if preserve_generator_docstring: + _replace_generator_docstring(package_path, old_protocol_generator_docstring) run_isort_and_black(Path(PROTOCOLS_PLURALS, package_path.name), cwd=str(ROOT_DIR)) _fingerprint_protocol(package_path.name) _update_original_protocol(package_path) @@ -434,7 +490,9 @@ def main(no_bump: bool = False) -> None: log(f"Processing protocol at path {package_path}") if not no_bump: _bump_protocol_specification_id_if_needed(package_path) - _process_packages_protocol(package_path) + # no_bump implies to ignore the docstring: + # 'It was created with protocol buffer compiler ... and aea version ...' + _process_packages_protocol(package_path, no_bump) if __name__ == "__main__": From 6194438352f57b92738b6cb808d56b689126476c Mon Sep 17 00:00:00 2001 From: MarcoFavorito Date: Tue, 11 May 2021 23:05:23 +0200 Subject: [PATCH 046/147] update test protocols --- tests/data/generator/t_protocol/__init__.py | 2 +- tests/data/generator/t_protocol/protocol.yaml | 2 +- tests/data/generator/t_protocol_no_ct/__init__.py | 2 +- tests/data/generator/t_protocol_no_ct/protocol.yaml | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/data/generator/t_protocol/__init__.py b/tests/data/generator/t_protocol/__init__.py index 5133682355..e72adaf165 100644 --- a/tests/data/generator/t_protocol/__init__.py +++ b/tests/data/generator/t_protocol/__init__.py @@ -20,7 +20,7 @@ """ This module contains the support resources for the t_protocol protocol. -It was created with protocol buffer compiler version `libprotoc 3.11.4` and aea version `1.0.0`. +It was created with protocol buffer compiler version `libprotoc 3.11.4` and aea version `1.0.1`. """ from tests.data.generator.t_protocol.message import TProtocolMessage diff --git a/tests/data/generator/t_protocol/protocol.yaml b/tests/data/generator/t_protocol/protocol.yaml index bba77a073d..fa8e8b32f2 100644 --- a/tests/data/generator/t_protocol/protocol.yaml +++ b/tests/data/generator/t_protocol/protocol.yaml @@ -7,7 +7,7 @@ description: A protocol for testing purposes. license: Apache-2.0 aea_version: '>=1.0.0, <2.0.0' fingerprint: - __init__.py: QmXXHNCXajz8QcHirDJbeFB1sz9ncWrTsbsQydvKV51EUB + __init__.py: QmRFM25tWb2wzG4KsJjMQUiWBVq2Sv4x1U9bMtz7j14Mqh custom_types.py: QmWg8HFav8w9tfZfMrTG5Uo7QpexvYKKkhpGPD18233pLw dialogues.py: QmWAdikDRJWTG7HUXsCsZRg4Wxnf8cMr5KujpyC4M75gnB message.py: QmTjeJythfhy4XyzXRCA4fY2eULHciHBRCowWpZkEH77z9 diff --git a/tests/data/generator/t_protocol_no_ct/__init__.py b/tests/data/generator/t_protocol_no_ct/__init__.py index 856225bc44..b47c9a013d 100644 --- a/tests/data/generator/t_protocol_no_ct/__init__.py +++ b/tests/data/generator/t_protocol_no_ct/__init__.py @@ -20,7 +20,7 @@ """ This module contains the support resources for the t_protocol_no_ct protocol. -It was created with protocol buffer compiler version `libprotoc 3.11.4` and aea version `1.0.0`. +It was created with protocol buffer compiler version `libprotoc 3.11.4` and aea version `1.0.1`. """ from tests.data.generator.t_protocol_no_ct.message import TProtocolNoCtMessage diff --git a/tests/data/generator/t_protocol_no_ct/protocol.yaml b/tests/data/generator/t_protocol_no_ct/protocol.yaml index bd03b9cc8d..cb4d6420c9 100644 --- a/tests/data/generator/t_protocol_no_ct/protocol.yaml +++ b/tests/data/generator/t_protocol_no_ct/protocol.yaml @@ -7,7 +7,7 @@ description: A protocol for testing purposes. license: Apache-2.0 aea_version: '>=1.0.0, <2.0.0' fingerprint: - __init__.py: QmYqGhtZRqrk3d6Sjvu3GGq1JqURkhcEdS8jwqhBTDkdQv + __init__.py: QmP56cVMkxKriXjZYRsjrN5NY1qPo9xdCQjhm5oBvXePN3 dialogues.py: Qmeq7m8vf1LW5WeehNG8qnoGoRstQrABw2vdQh5tmB3KxX message.py: QmWBJxk69r34YiRTpadyd6JbKxMEBGj8zwnpZsj6r2Wg5d serialization.py: QmSGoA2WjKU7F6oYfLwxG4uJVFuVhHNRSo7TNJ1EuYadEg From 3e3be447ae8cc50acd6999cc0a99c3f7781ed4c7 Mon Sep 17 00:00:00 2001 From: MarcoFavorito Date: Tue, 11 May 2021 23:06:29 +0200 Subject: [PATCH 047/147] reintroduce check_all_protocols in CI --- .github/workflows/workflow.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/workflow.yml b/.github/workflows/workflow.yml index db1d24562e..dd76f4ad34 100644 --- a/.github/workflows/workflow.yml +++ b/.github/workflows/workflow.yml @@ -149,8 +149,8 @@ jobs: run: tox -e package_version_checks - name: Check package dependencies run: tox -e package_dependencies_checks - # - name: Check generate protocols - # run: tox -e check_generate_all_protocols + - name: Check generate protocols + run: tox -e check_generate_all_protocols - name: Generate Documentation run: tox -e docs From 7626d9ee3deb171fbea4fdf82589e80c7cae3ae5 Mon Sep 17 00:00:00 2001 From: MarcoFavorito Date: Tue, 11 May 2021 23:23:17 +0200 Subject: [PATCH 048/147] add --no-bump to check_generate_all_protocols in tox.ini --- .github/workflows/workflow.yml | 4 ++-- tox.ini | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/workflow.yml b/.github/workflows/workflow.yml index dd76f4ad34..f51aeb8aef 100644 --- a/.github/workflows/workflow.yml +++ b/.github/workflows/workflow.yml @@ -149,8 +149,8 @@ jobs: run: tox -e package_version_checks - name: Check package dependencies run: tox -e package_dependencies_checks - - name: Check generate protocols - run: tox -e check_generate_all_protocols + - name: Check generate protocols + run: tox -e check_generate_all_protocols - name: Generate Documentation run: tox -e docs diff --git a/tox.ini b/tox.ini index 4bd94462a6..3483ed172c 100644 --- a/tox.ini +++ b/tox.ini @@ -294,7 +294,7 @@ deps = ipfshttpclient==0.6.1 black==19.10b0 isort==5.7.0 -commands = {toxinidir}/scripts/generate_all_protocols.py --check-clean +commands = {toxinidir}/scripts/generate_all_protocols.py --no-bump --check-clean [testenv:spell_check] skipsdist = True From 7433548a505e9322ff9253cbda95b91e5b9cf3d8 Mon Sep 17 00:00:00 2001 From: MarcoFavorito Date: Tue, 11 May 2021 23:40:15 +0200 Subject: [PATCH 049/147] put upperbound to 'click' <8.0.0 --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 509f8bcd83..17347d6402 100644 --- a/setup.py +++ b/setup.py @@ -30,7 +30,7 @@ def get_all_extras() -> Dict: cli_deps = [ - "click", + "click<8.0.0", "pyyaml>=4.2b1", "jsonschema>=3.0.0", "packaging>=20.3", From 449db3d32dd07173d57939601c74b06b2f7ba6af Mon Sep 17 00:00:00 2001 From: MarcoFavorito Date: Tue, 11 May 2021 23:55:11 +0200 Subject: [PATCH 050/147] update test aea package hashes --- tests/data/hashes.csv | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/data/hashes.csv b/tests/data/hashes.csv index a4eaac9895..4c642dd06a 100644 --- a/tests/data/hashes.csv +++ b/tests/data/hashes.csv @@ -2,7 +2,7 @@ dummy_author/agents/dummy_aea,QmVmz9HDijBdNmTmkCqCBBYkEviwcLj7ib37qSguvP25EN dummy_author/skills/dummy_skill,QmXHU1KwFNtJWrXv9TUqE1dLWwumky2HFuGKohGiD4HKiP fetchai/connections/dummy_connection,QmTLkQHXmZd8xF46Ds47pyTUuLQuosC4PNwh9waxtzQv1b fetchai/contracts/dummy_contract,QmP67brp7EU1kg6n2ckQP6A6jfxLJDeCBD5J6EzpDGb5Kb -fetchai/protocols/t_protocol,QmQPdqsTrRxwXEPDBd9whn49aEWLKeyxg68pH31xFR7ME9 -fetchai/protocols/t_protocol_no_ct,QmU16WJ38M9rorGR9M4inHh5iVpy1btv38cy8cbbPwP33J +fetchai/protocols/t_protocol,QmNqV6QvRuBrfXbNgqKhU9BWZ83Tz1CegbhXT8anf8RiQE +fetchai/protocols/t_protocol_no_ct,QmfNEK4uVktCWNmBzsb9xzsd256NdKADSH3hnJNqeKPJE9 fetchai/skills/dependencies_skill,QmaxnwbY9u3JPYfc2gnmiCjFd9mCfgXV8Yv5B9VbsDkg7K fetchai/skills/exception_skill,Qmcch6VUH2YELniNiaJxLNa19BRD8PAzb5HTzd7SQhEBgf From 1f27ebafbea75c6dd2fe92e4aadc4cf835103740 Mon Sep 17 00:00:00 2001 From: "Yuri (solarw) Turchenkov" Date: Wed, 12 May 2021 13:16:36 +0300 Subject: [PATCH 051/147] fix click package version --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 509f8bcd83..378147e257 100644 --- a/setup.py +++ b/setup.py @@ -30,7 +30,7 @@ def get_all_extras() -> Dict: cli_deps = [ - "click", + "click>=7.1.2,<8.0.0", "pyyaml>=4.2b1", "jsonschema>=3.0.0", "packaging>=20.3", From 513f8fc49457e499f7fc5cba05240b208e0e0b81 Mon Sep 17 00:00:00 2001 From: David Minarsch Date: Wed, 12 May 2021 12:45:14 +0100 Subject: [PATCH 052/147] feat: bound version of dependencies by next major --- plugins/aea-cli-ipfs/setup.py | 2 +- plugins/aea-ledger-cosmos/setup.py | 4 ++-- plugins/aea-ledger-fetchai/setup.py | 4 ++-- setup.py | 24 ++++++++++++------------ 4 files changed, 17 insertions(+), 17 deletions(-) diff --git a/plugins/aea-cli-ipfs/setup.py b/plugins/aea-cli-ipfs/setup.py index e54cb86146..7f9877ffeb 100755 --- a/plugins/aea-cli-ipfs/setup.py +++ b/plugins/aea-cli-ipfs/setup.py @@ -33,7 +33,7 @@ description="CLI extension for AEA framework wrapping IPFS functionality.", packages=["aea_cli_ipfs"], entry_points={"aea.cli": ["ipfs_cli_command = aea_cli_ipfs.core:ipfs"]}, - install_requires=["aea>=1.0.0, <2.0.0", "ipfshttpclient>=0.6.1"], + install_requires=["aea>=1.0.0, <2.0.0", "ipfshttpclient>=0.6.1,<0.8.0"], classifiers=[ "Environment :: Console", "Environment :: Web Environment", diff --git a/plugins/aea-ledger-cosmos/setup.py b/plugins/aea-ledger-cosmos/setup.py index 6ce87d0fce..937c179a06 100644 --- a/plugins/aea-ledger-cosmos/setup.py +++ b/plugins/aea-ledger-cosmos/setup.py @@ -32,9 +32,9 @@ packages=find_packages(include=["aea_ledger_cosmos*"]), install_requires=[ "aea>=1.0.0, <2.0.0", - "ecdsa>=0.15", + "ecdsa>=0.15,<0.17.0", "bech32==1.2.0", - "pycryptodome>=3.10.1", + "pycryptodome>=3.10.1,<4.0.0", ], tests_require=["pytest"], entry_points={ diff --git a/plugins/aea-ledger-fetchai/setup.py b/plugins/aea-ledger-fetchai/setup.py index fdb7a83d9c..a4c69fd6a8 100644 --- a/plugins/aea-ledger-fetchai/setup.py +++ b/plugins/aea-ledger-fetchai/setup.py @@ -37,9 +37,9 @@ packages=find_packages(include=["aea_ledger_fetchai*"]), install_requires=[ "aea>=1.0.0, <2.0.0", - "ecdsa>=0.15", + "ecdsa>=0.15,<0.17.0", "bech32==1.2.0", - "pycryptodome>=3.10.1", + "pycryptodome>=3.10.1,<4.0.0", ], tests_require=["pytest"], entry_points={ diff --git a/setup.py b/setup.py index 17347d6402..45b39f4de2 100644 --- a/setup.py +++ b/setup.py @@ -30,10 +30,10 @@ def get_all_extras() -> Dict: cli_deps = [ - "click<8.0.0", - "pyyaml>=4.2b1", - "jsonschema>=3.0.0", - "packaging>=20.3", + "click>=7.0.0,<8.0.0", + "pyyaml>=4.2b1,<6.0", + "jsonschema>=3.0.0,<4.0.0", + "packaging>=20.3,<21.0", ] extras = { @@ -49,16 +49,16 @@ def get_all_extras() -> Dict: all_extras = get_all_extras() base_deps = [ - "base58>=1.0.3", - "jsonschema>=3.0.0", - "packaging>=20.3", - "semver>=2.9.1", + "base58>=1.0.3,<3.0.0", + "jsonschema>=3.0.0,<4.0.0", + "packaging>=20.3,<21.0", + "semver>=2.9.1,<3.0.0", "protobuf==3.13.0", "pymultihash==0.8.2", - "pyyaml>=4.2b1", - "requests>=2.22.0", - "python-dotenv>=0.14.0", - "ecdsa>=0.15" + "pyyaml>=4.2b1,<6.0", + "requests>=2.22.0,<3.0.0", + "python-dotenv>=0.14.0,<0.18.0", + "ecdsa>=0.15,<0.17.0" ] if os.name == "nt" or os.getenv("WIN_BUILD_WHEEL", None) == "1": From adbaa81881125f76719c6ef42990b04db161770a Mon Sep 17 00:00:00 2001 From: ali Date: Wed, 12 May 2021 17:59:54 +0100 Subject: [PATCH 053/147] docs: update with aea manager instructions carpark;ml;thermometer;weather --- docs/car-park-skills.md | 28 ++++++++-------- docs/ml-skills.md | 43 ++++++++++++------------ docs/thermometer-skills.md | 68 ++++++++++++++++++++++++++++++-------- docs/weather-skills.md | 63 +++++++++++++++++++++++++++++------ 4 files changed, 144 insertions(+), 58 deletions(-) diff --git a/docs/car-park-skills.md b/docs/car-park-skills.md index 52410c5d79..618711b99c 100644 --- a/docs/car-park-skills.md +++ b/docs/car-park-skills.md @@ -45,15 +45,15 @@ This diagram shows the communication between the various entities as data is suc
-# Option 1: AEA Manager approach +## Option 1: AEA Manager approach Follow this approach when using the AEA Manager Desktop app. Otherwise, skip and follow the CLI approach below. -## Preparation instructions +### Preparation instructions Install the AEA Manager. -## Demo instructions +### Demo instructions The following steps assume you have launched the AEA Manager Desktop app. @@ -65,7 +65,7 @@ The following steps assume you have launched the AEA Manager Desktop app. 4. Run the `car_detector` AEA. Navigate to its logs and copy the multiaddress displayed. -5. Navigate to the settings of the `car_data_buyer` and update as follows: +5. Navigate to the settings of the `car_data_buyer` and under `connections/fetchai/p2p_libp2p:0.22.0` update as follows: ``` bash { "delegate_uri": "127.0.0.1:11001", @@ -81,19 +81,19 @@ The following steps assume you have launched the AEA Manager Desktop app. In the AEA's logs should see the agent trading successfully.
-# Option 2: CLI approach +## Option 2: CLI approach Follow this approach when using the `aea` CLI. -## Preparation instructions +### Preparation instructions -### Dependencies +#### Dependencies Follow the Preliminaries and Installation sections from the AEA quick start. -## Demo instructions +### Demo instructions -### Create car detector AEA +#### Create car detector AEA First, fetch the car detector AEA: ``` bash @@ -131,7 +131,7 @@ aea build

-### Create car data buyer AEA +#### Create car data buyer AEA Then, fetch the car data client AEA: ``` bash @@ -170,7 +170,7 @@ aea build

-### Add keys for the car data seller AEA +#### Add keys for the car data seller AEA First, create the private key for the car data seller AEA based on the network you want to transact. To generate and add a private-public key pair for Fetch.ai `AgentLand` use: ``` bash @@ -189,7 +189,7 @@ Finally, certify the key for use by the connections that request that: aea issue-certificates ``` -### Add keys and generate wealth for the car data buyer AEA +#### Add keys and generate wealth for the car data buyer AEA The buyer needs to have some wealth to purchase the service from the seller. @@ -215,7 +215,7 @@ Finally, certify the key for use by the connections that request that: aea issue-certificates ``` -## Run the AEAs +### Run the AEAs Run both AEAs from their respective terminals. @@ -248,7 +248,7 @@ aea run You will see that the AEAs negotiate and then transact using the Fetch.ai testnet. -### Cleaning up +#### Cleaning up When you're finished, delete your AEAs: ``` bash diff --git a/docs/ml-skills.md b/docs/ml-skills.md index 65b3c16357..dc06c4fdc3 100644 --- a/docs/ml-skills.md +++ b/docs/ml-skills.md @@ -1,18 +1,18 @@ -The AEA ML (machine learning) skills demonstrate an interaction between two AEAs trading data. +The AEA ML (machine learning) skills demonstrate an interaction between two AEAs, one purchasing data from the other and training a machine learning model with it. There are two types of AEAs: * The `ml_data_provider` which sells training data. -* The `ml_model_trainer` which trains a model +* The `ml_model_trainer` which purchases data and trains a model ## Discussion -The scope of the specific demo is to demonstrate how to create a simple AEA with integration of machine learning, and the usage of the AEA framework. The `ml_data_provider` AEA -will provide some sample data and will deliver to the client upon payment. Once the client gets the data, it will train the model. The process can be found in the `tasks.py` file. -This demo does not utilize a smart contract. As a result, we interact with a ledger only to complete a transaction. +This demo aims to demonstrate the integration of a simple AEA with machine learning using the AEA framework. The `ml_data_provider` AEA provides some sample data and delivers to the client upon payment. +Once the client receives the data, it trains a model. This process can be found in `tasks.py`. +This demo does not utilize a smart contract. As a result, the ledger interaction is only for completing a transaction. -Since the AEA framework enables us to use third-party libraries hosted on PyPI we can directly reference the external dependencies. -The `aea install` command will install each dependency that the specific AEA needs and is listed in the skill's YAML file. +Since the AEA framework enables using third-party libraries from PyPI, we can directly reference any external dependencies. +The `aea install` command installs all dependencies an AEA needs that is listed in one of its skills' YAML file. ## Communication @@ -48,17 +48,18 @@ This diagram shows the communication between the two AEAs. deactivate Search deactivate Ledger - + +
-# Option 1: AEA Manager approach +## Option 1: AEA Manager approach Follow this approach when using the AEA Manager Desktop app. Otherwise, skip and follow the CLI approach below. -## Preparation instructions +### Preparation instructions Install the AEA Manager. -## Demo instructions +### Demo instructions The following steps assume you have launched the AEA Manager Desktop app. @@ -86,19 +87,19 @@ The following steps assume you have launched the AEA Manager Desktop app. In the AEA's logs should see the agent trading successfully.
-# Option 2: CLI approach +## Option 2: CLI approach Follow this approach when using the `aea` CLI. -## Preparation instructions +### Preparation instructions -### Dependencies +#### Dependencies Follow the Preliminaries and Installation sections from the AEA quick start. -## Demo instructions +### Demo instructions -### Create data provider AEA +#### Create data provider AEA First, fetch the data provider AEA: ``` bash @@ -136,7 +137,7 @@ aea build

-### Create model trainer AEA +#### Create model trainer AEA Then, fetch the model trainer AEA: ``` bash @@ -175,7 +176,7 @@ aea build

-### Add keys for the data provider AEA +#### Add keys for the data provider AEA First, create the private key for the data provider AEA based on the network you want to transact. To generate and add a private-public key pair for Fetch.ai `AgentLand` use: ``` bash @@ -194,7 +195,7 @@ Finally, certify the key for use by the connections that request that: aea issue-certificates ``` -### Add keys and generate wealth for the model trainer AEA +#### Add keys and generate wealth for the model trainer AEA The model trainer needs to have some wealth to purchase the data from the data provider. @@ -220,7 +221,7 @@ Finally, certify the key for use by the connections that request that: aea issue-certificates ``` -### Run both AEAs +#### Run both AEAs Run both AEAs from their respective terminals. @@ -272,7 +273,7 @@ aea run You can see that the AEAs find each other, negotiate and eventually trade. -### Cleaning up +#### Cleaning up When you're finished, delete your AEAs: ``` bash diff --git a/docs/thermometer-skills.md b/docs/thermometer-skills.md index 5e556e1087..0ed2fa9987 100644 --- a/docs/thermometer-skills.md +++ b/docs/thermometer-skills.md @@ -1,15 +1,15 @@ -The AEA thermometer skills demonstrate an interaction between two AEAs. +The AEA thermometer skills demonstrate an interaction between two AEAs, one purchasing temperature data from the other. * The provider of thermometer data (the `thermometer`). * The buyer of thermometer data (the `thermometer_client`). ## Discussion -The scope of the specific demo is to demonstrate how to create a very simple AEA with the usage of the AEA framework and a thermometer sensor. The thermometer AEA will read data from the sensor each time a client requests and will deliver to the client upon payment. To keep the demo simple we avoided the usage of a database since this would increase the complexity. As a result, the AEA can provide only one reading from the sensor. This demo does not utilise a smart contract. As a result, we interact with a ledger only to complete a transaction. +This demo aims to demonstrate how to create a very simple AEA with the usage of the AEA framework and a thermometer sensor. The thermometer AEA will read data from the sensor each time a client requests the data and will deliver it to the client upon payment. To keep the demo simple, we avoided the usage of a database since this would increase the complexity. As a result, the AEA can provide only one reading from the sensor. This demo does not utilise a smart contract. As a result, the ledger interaction is only for completing a transaction. ## Communication -This diagram shows the communication between the various entities as data is successfully sold by the thermometer AEA to the client. +This diagram shows the communication between the various entities as data is successfully sold by the thermometer AEA to the client AEA.
sequenceDiagram @@ -41,18 +41,59 @@ This diagram shows the communication between the various entities as data is suc deactivate Blockchain
+
-## Preparation instructions +## Option 1: AEA Manager approach + +Follow this approach when using the AEA Manager Desktop app. Otherwise, skip and follow the CLI approach below. + +### Preparation instructions + +Install the AEA Manager. + +### Demo instructions + +The following steps assume you have launched the AEA Manager Desktop app. + +1. Add a new AEA called `my_thermometer_aea` with public id `fetchai/thermometer_aea:0.27.0`. + +2. Add another new AEA called `my_thermometer_client` with public id `fetchai/thermometer_client:0.28.0`. + +3. Copy the address from the `my_thermometer_client` into your clip board. Then go to the AgentLand block explorer and request some test tokens via `Get Funds`. + +4. Run the `my_thermometer_aea` AEA. Navigate to its logs and copy the multiaddress displayed. + +5. Navigate to the settings of the `my_thermometer_client` and under `connections/fetchai/p2p_libp2p:0.22.0` update as follows: +``` bash +{ + "delegate_uri": "127.0.0.1:11001", + "entry_peers": ["REPLACE_WITH_MULTI_ADDRESS_HERE"], + "local_uri": "127.0.0.1:9001", + "log_file": "libp2p_node.log", + "public_uri": "127.0.0.1:9001" +} +``` + +6. Run the `my_thermometer_client`. + +In the AEA's logs should see the agent trading successfully. +
+ +## Option 2: CLI approach + +Follow this approach when using the `aea` CLI. + +### Preparation instructions -### Dependencies +#### Dependencies Follow the Preliminaries and Installation sections from the AEA quick start. -## Demo instructions +### Demo instructions A demo to run the thermometer scenario with a true ledger transaction This demo assumes the buyer trusts the seller AEA to send the data upon successful payment. -### Create thermometer AEA +#### Create thermometer AEA First, fetch the thermometer AEA: ``` bash @@ -86,7 +127,7 @@ aea config set --type dict agent.default_routing \

-### Create thermometer client +#### Create thermometer client Then, fetch the thermometer client AEA: ``` bash @@ -120,7 +161,7 @@ aea config set --type dict agent.default_routing \

-### Add keys for the thermometer AEA +#### Add keys for the thermometer AEA First, create the private key for the thermometer AEA based on the network you want to transact. To generate and add a private-public key pair for Fetch.ai `AgentLand` use: ``` bash @@ -139,7 +180,7 @@ Finally, certify the key for use by the connections that request that: aea issue-certificates ``` -### Add keys and generate wealth for the thermometer client AEA +#### Add keys and generate wealth for the thermometer client AEA The thermometer client needs to have some wealth to purchase the thermometer information. @@ -165,7 +206,7 @@ Finally, certify the key for use by the connections that request that: aea issue-certificates ``` -### Run both AEAs +#### Run both AEAs Run both AEAs from their respective terminals. @@ -197,9 +238,10 @@ aea run You can see that the AEAs find each other, negotiate and eventually trade. -### Cleaning up +#### Cleaning up + +When you're done, go up a level and delete the AEAs. -When you're finished, delete your AEAs: ``` bash cd .. aea delete my_thermometer_aea diff --git a/docs/weather-skills.md b/docs/weather-skills.md index 9efb190c21..f9dae23430 100644 --- a/docs/weather-skills.md +++ b/docs/weather-skills.md @@ -45,19 +45,60 @@ This diagram shows the communication between the various entities as data is suc deactivate Blockchain +
-## Preparation instructions +## Option 1: AEA Manager approach -### Dependencies +Follow this approach when using the AEA Manager Desktop app. Otherwise, skip and follow the CLI approach below. + +### Preparation instructions + +Install the AEA Manager. + +### Demo instructions + +The following steps assume you have launched the AEA Manager Desktop app. + +1. Add a new AEA called `my_weather_station` with public id `fetchai/weather_station:0.29.0`. + +2. Add another new AEA called `my_weather_client` with public id `fetchai/weather_client:0.30.0`. + +3. Copy the address from the `my_weather_client` into your clip board. Then go to the AgentLand block explorer and request some test tokens via `Get Funds`. + +4. Run the `my_weather_station` AEA. Navigate to its logs and copy the multiaddress displayed. + +5. Navigate to the settings of the `my_weather_client` and under `connections/fetchai/p2p_libp2p:0.22.0` update as follows: +``` bash +{ + "delegate_uri": "127.0.0.1:11001", + "entry_peers": ["REPLACE_WITH_MULTI_ADDRESS_HERE"], + "local_uri": "127.0.0.1:9001", + "log_file": "libp2p_node.log", + "public_uri": "127.0.0.1:9001" +} +``` + +6. Run the `my_weather_client`. + +In the AEA's logs should see the agent trading successfully. +
+ +## Option 2: CLI approach + +Follow this approach when using the `aea` CLI. + +### Preparation instructions + +#### Dependencies Follow the Preliminaries and Installation sections from the AEA quick start. -## Demo instructions: +### Demo instructions: A demo to run the same scenario but with a true ledger transaction on Fetch.ai `testnet` or Ethereum `ropsten` network. This demo assumes the buyer trusts the seller AEA to send the data upon successful payment. -### Create the weather station +#### Create the weather station First, fetch the AEA that will provide weather measurements: ``` bash @@ -96,7 +137,7 @@ aea build -### Create the weather client +#### Create the weather client In another terminal, fetch the AEA that will query the weather station: ``` bash @@ -135,7 +176,7 @@ aea build -### Add keys for the weather station AEA +#### Add keys for the weather station AEA First, create the private key for the weather station AEA based on the network you want to transact. To generate and add a private-public key pair for Fetch.ai `AgentLand` use: ``` bash @@ -154,7 +195,7 @@ Finally, certify the key for use by the connections that request that: aea issue-certificates ``` -### Add keys and generate wealth for the weather client AEA +#### Add keys and generate wealth for the weather client AEA The weather client needs to have some wealth to purchase the service from the weather station. @@ -180,7 +221,7 @@ Finally, certify the key for use by the connections that request that: aea issue-certificates ``` -### Run the AEAs +#### Run the AEAs Run both AEAs from their respective terminals. @@ -212,7 +253,7 @@ aea run You will see that the AEAs negotiate and then transact using the selected ledger. -### Delete the AEAs +#### Cleaning up When you're done, go up a level and delete the AEAs. @@ -220,4 +261,6 @@ When you're done, go up a level and delete the AEAs. cd .. aea delete my_weather_station aea delete my_weather_client -``` \ No newline at end of file +``` + +
\ No newline at end of file From f65d7a19553b1d1a0089d8ff1c670dc12268c055 Mon Sep 17 00:00:00 2001 From: MarcoFavorito Date: Sat, 15 May 2021 11:59:09 +0200 Subject: [PATCH 054/147] feat: update bump_aea_version script. - add '--only-check' flag to only check the need for bumping (but no bumping) - slighly refactor code to switch between the two modes --- scripts/bump_aea_version.py | 150 +++++++++++++++++++++++++++--------- 1 file changed, 113 insertions(+), 37 deletions(-) diff --git a/scripts/bump_aea_version.py b/scripts/bump_aea_version.py index 90a39d0ef8..c95654fc14 100644 --- a/scripts/bump_aea_version.py +++ b/scripts/bump_aea_version.py @@ -382,7 +382,7 @@ def parse_args() -> argparse.Namespace: parser = argparse.ArgumentParser("bump_aea_version") parser.add_argument( - "--new-version", type=str, required=True, help="The new AEA version." + "--new-version", type=str, required=False, help="The new AEA version." ) parser.add_argument( "-p", @@ -392,11 +392,52 @@ def parse_args() -> argparse.Namespace: help="Set a number of key-value pairs plugin-name=new-plugin-version", default={}, ) - parser.add_argument("--no-fingerprints", action="store_true") + parser.add_argument( + "--no-fingerprints", + action="store_true", + help="Skip the recomputation of fingerprints.", + ) + parser.add_argument( + "--only-check", action="store_true", help="Only check the need of upgrade." + ) arguments_ = parser.parse_args() return arguments_ +def make_aea_bumper(new_aea_version: Version) -> PythonPackageVersionBumper: + """Build the AEA Python package version bumper.""" + aea_version_bumper = PythonPackageVersionBumper( + ROOT_DIR, + AEA_DIR, + new_aea_version, + specifier_set_patterns=[ + "(?<=aea_version:) *({specifier_set})", + "(?<={package_name})({specifier_set})", + ], + files_to_pattern=AEA_PATHS, + ) + return aea_version_bumper + + +def make_plugin_bumper( + plugin_dir: Path, new_version: Version +) -> PythonPackageVersionBumper: + """Build the plugin Python package version bumper.""" + plugin_package_dir = plugin_dir / plugin_dir.name.replace("-", "_") + plugin_version_bumper = PythonPackageVersionBumper( + ROOT_DIR, + plugin_package_dir, + new_version, + files_to_pattern={}, + specifier_set_patterns=[ + YAML_DEPENDENCY_SPECIFIER_SET_PATTERN, + JSON_DEPENDENCY_SPECIFIER_SET_PATTERN, + ], + package_name=plugin_dir.name, + ) + return plugin_version_bumper + + def process_plugins(new_versions: Dict[str, Version]) -> bool: """Process plugins.""" result = False @@ -411,18 +452,7 @@ def process_plugins(new_versions: Dict[str, Version]) -> bool: logging.info( f"Processing {plugin_dir_name}: upgrading at version {new_version}" ) - plugin_package_dir = plugin_dir / plugin_dir.name.replace("-", "_") - plugin_bumper = PythonPackageVersionBumper( - ROOT_DIR, - plugin_package_dir, - new_version, - files_to_pattern={}, - specifier_set_patterns=[ - YAML_DEPENDENCY_SPECIFIER_SET_PATTERN, - JSON_DEPENDENCY_SPECIFIER_SET_PATTERN, - ], - package_name=plugin_dir.name, - ) + plugin_bumper = make_plugin_bumper(plugin_dir, new_version) plugin_bumper.run() result |= plugin_bumper.result return result @@ -438,33 +468,58 @@ def parse_plugin_versions(key_value_strings: List[str]) -> Dict[str, Version]: } -if __name__ == "__main__": - arguments = parse_args() +def only_check_bump_needed() -> int: + """ + Check whether a version bump is needed for AEA and plugins. + + :return: the return code + """ + bumpers: List[PythonPackageVersionBumper] = list() + to_upgrade: List[Path] = list() + bumpers.append(make_aea_bumper(None)) # type: ignore + for plugin_dir in ALL_PLUGINS: + bumpers.append(make_plugin_bumper(plugin_dir, None)) # type: ignore + + latest_tag = str(bumpers[0].repo.tags[-1]) + logging.info( + f"Checking packages that have changes from tag {latest_tag} and that require a new release..." + ) + for bumper in bumpers: + if bumper.is_different_from_latest_tag(): + logging.info( + f"Package {bumper.python_pkg_dir} is different from latest tag {latest_tag}." + ) + to_upgrade.append(bumper.python_pkg_dir) + + if len(to_upgrade) > 0: + logging.info("Packages to upgrade:") + for path in to_upgrade: + logging.info(path) + else: + logging.info("No packages to upgrade.") + return 0 + + +def bump(arguments: argparse.Namespace) -> int: + """ + Bump versions. + + :param arguments: arguments from argparse + :return: the return code + """ new_plugin_versions = parse_plugin_versions(arguments.plugin_new_version) logging.info(f"Parsed arguments: {arguments}") logging.info(f"Parsed plugin versions: {new_plugin_versions}") - repo = Repo(str(ROOT_DIR)) - if repo.is_dirty(): - logging.info( - "Repository is dirty. Please clean it up before running this script." - ) - sys.exit(1) - - new_aea_version = Version(arguments.new_version) - aea_version_bumper = PythonPackageVersionBumper( - ROOT_DIR, - AEA_DIR, - new_aea_version, - specifier_set_patterns=[ - "(?<=aea_version:) *({specifier_set})", - "(?<={package_name})({specifier_set})", - ], - files_to_pattern=AEA_PATHS, - ) - aea_version_bumper.run() - have_updated_specifier_set = aea_version_bumper.result - logging.info("AEA package processed.") + have_updated_specifier_set = False + if arguments.new_version is not None: + new_aea_version = Version(arguments.new_version) + aea_version_bumper = make_aea_bumper(new_aea_version) + aea_version_bumper.run() + have_updated_specifier_set = aea_version_bumper.result + logging.info("AEA package processed.") + else: + logging.info("AEA package not processed - no version provided.") logging.info("Processing plugins:") have_updated_specifier_set |= process_plugins(new_plugin_versions) @@ -482,4 +537,25 @@ def parse_plugin_versions(key_value_strings: List[str]) -> Dict[str, Version]: else: logging.info("Updating hashes and fingerprints.") return_code = update_hashes() + return return_code + + +def main() -> None: + """Run the script.""" + repo = Repo(str(ROOT_DIR)) + if repo.is_dirty(): + logging.info( + "Repository is dirty. Please clean it up before running this script." + ) + sys.exit(1) + + arguments = parse_args() + if arguments.only_check: + sys.exit(only_check_bump_needed()) + + return_code = bump(arguments) sys.exit(return_code) + + +if __name__ == "__main__": + main() From 81104fdd8be70e56ad787d581b8c375fe84405f7 Mon Sep 17 00:00:00 2001 From: MarcoFavorito Date: Sat, 15 May 2021 12:12:10 +0200 Subject: [PATCH 055/147] improve 'git diff' info log message --- scripts/bump_aea_version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/bump_aea_version.py b/scripts/bump_aea_version.py index c95654fc14..44ed1e8d9c 100644 --- a/scripts/bump_aea_version.py +++ b/scripts/bump_aea_version.py @@ -372,7 +372,7 @@ def is_different_from_latest_tag(self) -> bool: assert len(self.repo.tags) > 0, "no git tags found" latest_tag_str = str(self.repo.tags[-1]) args = latest_tag_str, "--", str(self.python_pkg_dir) - logging.info(f"Running 'git diff with args: {args}'") + logging.info(f"Running 'git diff {' '.join(args)}'") diff = self.repo.git.diff(*args) return diff != "" From 538c1ab22c44c9562f38fd666a65f10cd5ec6814 Mon Sep 17 00:00:00 2001 From: MarcoFavorito Date: Sat, 15 May 2021 14:20:57 +0200 Subject: [PATCH 056/147] update docstring of 'scripts/bump_aea_version.py' --- scripts/bump_aea_version.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/scripts/bump_aea_version.py b/scripts/bump_aea_version.py index 44ed1e8d9c..29e9d6b43c 100644 --- a/scripts/bump_aea_version.py +++ b/scripts/bump_aea_version.py @@ -21,8 +21,9 @@ """ Bump the AEA version throughout the code base. -usage: bump_aea_version [-h] --new-version NEW_VERSION +usage: bump_aea_version [-h] [--new-version NEW_VERSION] [-p KEY=VALUE [KEY=VALUE ...]] [--no-fingerprints] + [--only-check] optional arguments: -h, --help show this help message and exit @@ -31,11 +32,14 @@ -p KEY=VALUE [KEY=VALUE ...], --plugin-new-version KEY=VALUE [KEY=VALUE ...] Set a number of key-value pairs plugin-name=new- plugin-version - --no-fingerprints + --no-fingerprints Skip the computation of fingerprints. + --only-check Only check the need of upgrade. + Example of usage: python scripts/bump_aea_version.py --new-version 1.1.0 -p aea-ledger-fetchai=2.0.0 -p aea-ledger-ethereum=3.0.0 +python scripts/bump_aea_version.py --only-check """ import argparse @@ -395,7 +399,7 @@ def parse_args() -> argparse.Namespace: parser.add_argument( "--no-fingerprints", action="store_true", - help="Skip the recomputation of fingerprints.", + help="Skip the computation of fingerprints.", ) parser.add_argument( "--only-check", action="store_true", help="Only check the need of upgrade." From 189db196a4a77f27ca2de93f43c27a3ad8ee88ba Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 17 May 2021 06:46:00 +0000 Subject: [PATCH 057/147] chore(deps-dev): bump pydoc-markdown from 3.3.0 to 3.10.3 Bumps [pydoc-markdown](https://github.com/NiklasRosenstein/pydoc-markdown) from 3.3.0 to 3.10.3. - [Release notes](https://github.com/NiklasRosenstein/pydoc-markdown/releases) - [Commits](https://github.com/NiklasRosenstein/pydoc-markdown/compare/v3.3.0...v3.10.3) Signed-off-by: dependabot[bot] --- Pipfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Pipfile b/Pipfile index e74696761a..a9bf947d1b 100644 --- a/Pipfile +++ b/Pipfile @@ -45,7 +45,7 @@ packaging = "==20.4" pexpect = "==4.8.0" psutil = "==5.7.0" pycryptodome = ">=3.10.1" -pydoc-markdown = "==3.3.0" +pydoc-markdown = "==3.10.3" pydocstyle = "==3.0.0" pygments = "==2.7.4" pylint = "==2.6.0" From f81d7546da4a220593b5674ecef41166902a8c1e Mon Sep 17 00:00:00 2001 From: David Minarsch Date: Mon, 17 May 2021 10:31:23 +0100 Subject: [PATCH 058/147] chore: update tox.ini deps for pydoc too --- tox.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index 3483ed172c..d874576c00 100644 --- a/tox.ini +++ b/tox.ini @@ -284,7 +284,7 @@ commands = {toxinidir}/scripts/check_doc_links.py skipsdist = True usedevelop = True deps = - pydoc-markdown==3.3.0 + pydoc-markdown==3.10.3 commands = {toxinidir}/scripts/generate_api_docs.py --check-clean [testenv:check_generate_all_protocols] From 1aadb4f6d8dbc79f9ba2f2e2b6c5f23f7c6a23a6 Mon Sep 17 00:00:00 2001 From: ali Date: Mon, 17 May 2021 12:35:33 +0100 Subject: [PATCH 059/147] docs: add manager to tac demo + cleanup --- docs/car-park-skills.md | 2 +- docs/ml-skills.md | 2 +- docs/tac-skills.md | 54 ++++++++++++++++++++++++++++++++++++++ docs/thermometer-skills.md | 2 +- docs/weather-skills.md | 2 +- 5 files changed, 58 insertions(+), 4 deletions(-) diff --git a/docs/car-park-skills.md b/docs/car-park-skills.md index 618711b99c..1b70bea8ec 100644 --- a/docs/car-park-skills.md +++ b/docs/car-park-skills.md @@ -65,7 +65,7 @@ The following steps assume you have launched the AEA Manager Desktop app. 4. Run the `car_detector` AEA. Navigate to its logs and copy the multiaddress displayed. -5. Navigate to the settings of the `car_data_buyer` and under `connections/fetchai/p2p_libp2p:0.22.0` update as follows: +5. Navigate to the settings of the `car_data_buyer` and under `components > connections/fetchai/p2p_libp2p:0.22.0` update as follows: ``` bash { "delegate_uri": "127.0.0.1:11001", diff --git a/docs/ml-skills.md b/docs/ml-skills.md index dc06c4fdc3..f58d1314cc 100644 --- a/docs/ml-skills.md +++ b/docs/ml-skills.md @@ -71,7 +71,7 @@ The following steps assume you have launched the AEA Manager Desktop app. 4. Run the `ml_data_provider` AEA. Navigate to its logs and copy the multiaddress displayed. -5. Navigate to the settings of the `car_data_buyer` and under `connections/fetchai/p2p_libp2p:0.22.0` update as follows: +5. Navigate to the settings of the `ml_model_trainer` and under `components > connections/fetchai/p2p_libp2p:0.22.0` update as follows: ``` bash { "delegate_uri": "127.0.0.1:11001", diff --git a/docs/tac-skills.md b/docs/tac-skills.md index f4310a52c5..b4816037f9 100644 --- a/docs/tac-skills.md +++ b/docs/tac-skills.md @@ -87,6 +87,60 @@ In the above case, the proposal received contains a set of good which the seller There is an equivalent diagram for seller AEAs set up to search for buyers and their interaction with AEAs which are registered as buyers. In that scenario, the proposal will instead, be a list of goods that the buyer wishes to buy and the price it is willing to pay for them. +## Option 1: AEA Manager approach + +Follow this approach when using the AEA Manager Desktop app. Otherwise, skip and follow the CLI approach below. + +### Preparation instructions + +Install the AEA Manager. + +### Demo instructions + +The following steps assume you have launched the AEA Manager Desktop app. + +1. Add a new AEA called `controller` with public id `fetchai/tac_controller:0.26.0`. + +2. Add another new AEA called `participant_1` with public id `fetchai/tac_participant:0.28.0`. + +3. Add another new AEA called `participant_2` with public id `fetchai/tac_participant:0.28.0`. + +4. Navigate to the settings of `controller` and under `components > skills/fetchai/tac_controller:0.22.0 > models > parameters > args` update `registration_start_time` to the time you want TAC to begin (e.g. 2 minutes in the future) + +5. Run the `controller` AEA. Navigate to its logs and copy the multiaddress displayed. Stop the `controller`. + +5. Navigate to the settings of `participant_1` and under `components > connections/fetchai/p2p_libp2p:0.22.0` update as follows: +``` bash +{ + "delegate_uri": "127.0.0.1:11001", + "entry_peers": ["REPLACE_WITH_MULTI_ADDRESS_HERE"], + "local_uri": "127.0.0.1:9001", + "log_file": "libp2p_node.log", + "public_uri": "127.0.0.1:9001" +} +``` + +6. Navigate to the settings of `participant_2` and under `components > connections/fetchai/p2p_libp2p:0.22.0` update as follows: +``` bash +{ + "delegate_uri": "127.0.0.1:11002", + "entry_peers": ["REPLACE_WITH_MULTI_ADDRESS_HERE"], + "local_uri": "127.0.0.1:9002", + "log_file": "libp2p_node.log", + "public_uri": "127.0.0.1:9002" +} +``` + +7. You may add more participants by repeating steps 3 (with an updated name) and 6 (bumping the port numbers. See the difference between steps 5 and 6). + +8. Run the `controller`, then `participant_1` and `participant_2` (and any other participants you added). + +In the `controller`'s log, you should see the details of the transactions participants submit as well as changes in their scores and holdings. In participants' logs, you should see the agents trading. +
+ +## Option 2: CLI approach + +Follow this approach when using the `aea` CLI. ## Preparation instructions diff --git a/docs/thermometer-skills.md b/docs/thermometer-skills.md index 0ed2fa9987..4a3e908e80 100644 --- a/docs/thermometer-skills.md +++ b/docs/thermometer-skills.md @@ -63,7 +63,7 @@ The following steps assume you have launched the AEA Manager Desktop app. 4. Run the `my_thermometer_aea` AEA. Navigate to its logs and copy the multiaddress displayed. -5. Navigate to the settings of the `my_thermometer_client` and under `connections/fetchai/p2p_libp2p:0.22.0` update as follows: +5. Navigate to the settings of the `my_thermometer_client` and under `components > connections/fetchai/p2p_libp2p:0.22.0` update as follows: ``` bash { "delegate_uri": "127.0.0.1:11001", diff --git a/docs/weather-skills.md b/docs/weather-skills.md index f9dae23430..fa4c5cf793 100644 --- a/docs/weather-skills.md +++ b/docs/weather-skills.md @@ -67,7 +67,7 @@ The following steps assume you have launched the AEA Manager Desktop app. 4. Run the `my_weather_station` AEA. Navigate to its logs and copy the multiaddress displayed. -5. Navigate to the settings of the `my_weather_client` and under `connections/fetchai/p2p_libp2p:0.22.0` update as follows: +5. Navigate to the settings of the `my_weather_client` and under `components > connections/fetchai/p2p_libp2p:0.22.0` update as follows: ``` bash { "delegate_uri": "127.0.0.1:11001", From 5b387d5c2a2bd88eda7bfce81fefa6457078e035 Mon Sep 17 00:00:00 2001 From: ali Date: Mon, 17 May 2021 12:40:40 +0100 Subject: [PATCH 060/147] docs: pr comments --- docs/car-park-skills.md | 4 ++-- docs/ml-skills.md | 4 ++-- docs/tac-skills.md | 4 ++-- docs/thermometer-skills.md | 4 ++-- docs/weather-skills.md | 4 ++-- 5 files changed, 10 insertions(+), 10 deletions(-) diff --git a/docs/car-park-skills.md b/docs/car-park-skills.md index 1b70bea8ec..e7b248e4ed 100644 --- a/docs/car-park-skills.md +++ b/docs/car-park-skills.md @@ -65,7 +65,7 @@ The following steps assume you have launched the AEA Manager Desktop app. 4. Run the `car_detector` AEA. Navigate to its logs and copy the multiaddress displayed. -5. Navigate to the settings of the `car_data_buyer` and under `components > connections/fetchai/p2p_libp2p:0.22.0` update as follows: +5. Navigate to the settings of the `car_data_buyer` and under `components > connections/fetchai/p2p_libp2p:0.22.0` update as follows (make sure to replace the placeholder with the multiaddress): ``` bash { "delegate_uri": "127.0.0.1:11001", @@ -78,7 +78,7 @@ The following steps assume you have launched the AEA Manager Desktop app. 6. Run the `car_data_buyer`. -In the AEA's logs should see the agent trading successfully. +In the AEA's logs, you should see the agent trading successfully.
## Option 2: CLI approach diff --git a/docs/ml-skills.md b/docs/ml-skills.md index f58d1314cc..933dfde84a 100644 --- a/docs/ml-skills.md +++ b/docs/ml-skills.md @@ -71,7 +71,7 @@ The following steps assume you have launched the AEA Manager Desktop app. 4. Run the `ml_data_provider` AEA. Navigate to its logs and copy the multiaddress displayed. -5. Navigate to the settings of the `ml_model_trainer` and under `components > connections/fetchai/p2p_libp2p:0.22.0` update as follows: +5. Navigate to the settings of the `ml_model_trainer` and under `components > connections/fetchai/p2p_libp2p:0.22.0` update as follows (make sure to replace the placeholder with the multiaddress): ``` bash { "delegate_uri": "127.0.0.1:11001", @@ -84,7 +84,7 @@ The following steps assume you have launched the AEA Manager Desktop app. 6. Run the `ml_model_trainer`. -In the AEA's logs should see the agent trading successfully. +In the AEA's logs, you should see the agent trading successfully.
## Option 2: CLI approach diff --git a/docs/tac-skills.md b/docs/tac-skills.md index b4816037f9..c58d8f5415 100644 --- a/docs/tac-skills.md +++ b/docs/tac-skills.md @@ -109,7 +109,7 @@ The following steps assume you have launched the AEA Manager Desktop app. 5. Run the `controller` AEA. Navigate to its logs and copy the multiaddress displayed. Stop the `controller`. -5. Navigate to the settings of `participant_1` and under `components > connections/fetchai/p2p_libp2p:0.22.0` update as follows: +5. Navigate to the settings of `participant_1` and under `components > connections/fetchai/p2p_libp2p:0.22.0` update as follows (make sure to replace the placeholder with the multiaddress): ``` bash { "delegate_uri": "127.0.0.1:11001", @@ -120,7 +120,7 @@ The following steps assume you have launched the AEA Manager Desktop app. } ``` -6. Navigate to the settings of `participant_2` and under `components > connections/fetchai/p2p_libp2p:0.22.0` update as follows: +6. Navigate to the settings of `participant_2` and under `components > connections/fetchai/p2p_libp2p:0.22.0` update as follows (make sure to replace the placeholder with the multiaddress): ``` bash { "delegate_uri": "127.0.0.1:11002", diff --git a/docs/thermometer-skills.md b/docs/thermometer-skills.md index 4a3e908e80..5db274e192 100644 --- a/docs/thermometer-skills.md +++ b/docs/thermometer-skills.md @@ -63,7 +63,7 @@ The following steps assume you have launched the AEA Manager Desktop app. 4. Run the `my_thermometer_aea` AEA. Navigate to its logs and copy the multiaddress displayed. -5. Navigate to the settings of the `my_thermometer_client` and under `components > connections/fetchai/p2p_libp2p:0.22.0` update as follows: +5. Navigate to the settings of the `my_thermometer_client` and under `components > connections/fetchai/p2p_libp2p:0.22.0` update as follows (make sure to replace the placeholder with the multiaddress): ``` bash { "delegate_uri": "127.0.0.1:11001", @@ -76,7 +76,7 @@ The following steps assume you have launched the AEA Manager Desktop app. 6. Run the `my_thermometer_client`. -In the AEA's logs should see the agent trading successfully. +In the AEA's logs, you should see the agent trading successfully.
## Option 2: CLI approach diff --git a/docs/weather-skills.md b/docs/weather-skills.md index fa4c5cf793..8336259a12 100644 --- a/docs/weather-skills.md +++ b/docs/weather-skills.md @@ -67,7 +67,7 @@ The following steps assume you have launched the AEA Manager Desktop app. 4. Run the `my_weather_station` AEA. Navigate to its logs and copy the multiaddress displayed. -5. Navigate to the settings of the `my_weather_client` and under `components > connections/fetchai/p2p_libp2p:0.22.0` update as follows: +5. Navigate to the settings of the `my_weather_client` and under `components > connections/fetchai/p2p_libp2p:0.22.0` update as follows (make sure to replace the placeholder with the multiaddress): ``` bash { "delegate_uri": "127.0.0.1:11001", @@ -80,7 +80,7 @@ The following steps assume you have launched the AEA Manager Desktop app. 6. Run the `my_weather_client`. -In the AEA's logs should see the agent trading successfully. +In the AEA's logs, you should see the agent trading successfully.
## Option 2: CLI approach From 2cd993eae0fab3a59ac75fcbab4d1963f92868d2 Mon Sep 17 00:00:00 2001 From: MarcoFavorito Date: Mon, 17 May 2021 14:15:05 +0200 Subject: [PATCH 061/147] update aea.skills.base.py on skill loading Fix 'not declared in configuration file' warning for 'tac_controller_contract' agent --- aea/skills/base.py | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/aea/skills/base.py b/aea/skills/base.py index d7edf469af..7ff9bd6fc4 100644 --- a/aea/skills/base.py +++ b/aea/skills/base.py @@ -900,15 +900,19 @@ def _filter_classes( - the class must be a subclass of "SkillComponent"; - its __module__ attribute must not start with 'aea.' (we exclude classes provided by the framework) - its __module__ attribute starts with the expected dotted path of this skill. + In particular, it should not be imported from another skill. :param classes: a list of pairs (class name, class object) :return: a list of the same kind, but filtered with only skill component classes. """ filtered_classes = filter( lambda name_and_class: issubclass(name_and_class[1], SkillComponent) + # the following condition filters out classes imported from 'aea' and not str.startswith(name_and_class[1].__module__, "aea.") + # the following condition filters out classes imported + # from other skills and not str.startswith( - name_and_class[1].__module__, self.skill_dotted_path + name_and_class[1].__module__, self.skill_dotted_path + "." ), classes, ) @@ -1142,6 +1146,14 @@ def _print_warning_message_for_unused_classes( set_of_unused_classes = set( filter(lambda x: x not in used_classes, set_of_classes) ) + # filter out classes that are from other packages + set_of_unused_classes = set( + filter( + lambda x: not str.startswith(x.__module__, "packages."), + set_of_unused_classes, + ) + ) + if len(set_of_unused_classes) == 0: # all classes in the module are used! continue From 897896d3fba0d6e7bb70cb37c5c38df2748dc422 Mon Sep 17 00:00:00 2001 From: ali Date: Mon, 17 May 2021 15:25:21 +0100 Subject: [PATCH 062/147] tests: resolve failed tests --- .../test_bash_yaml/md_files/bash-ml-skills.md | 9 +++++++++ .../test_bash_yaml/md_files/bash-tac-skills.md | 18 ++++++++++++++++++ .../md_files/bash-thermometer-skills.md | 9 +++++++++ .../md_files/bash-weather-skills.md | 9 +++++++++ 4 files changed, 45 insertions(+) diff --git a/tests/test_docs/test_bash_yaml/md_files/bash-ml-skills.md b/tests/test_docs/test_bash_yaml/md_files/bash-ml-skills.md index 34de4e0e17..93fde92b98 100644 --- a/tests/test_docs/test_bash_yaml/md_files/bash-ml-skills.md +++ b/tests/test_docs/test_bash_yaml/md_files/bash-ml-skills.md @@ -1,4 +1,13 @@ ``` bash +{ + "delegate_uri": "127.0.0.1:11001", + "entry_peers": ["REPLACE_WITH_MULTI_ADDRESS_HERE"], + "local_uri": "127.0.0.1:9001", + "log_file": "libp2p_node.log", + "public_uri": "127.0.0.1:9001" +} +``` +``` bash aea fetch fetchai/ml_data_provider:0.29.0 cd ml_data_provider aea install diff --git a/tests/test_docs/test_bash_yaml/md_files/bash-tac-skills.md b/tests/test_docs/test_bash_yaml/md_files/bash-tac-skills.md index 71cdbe2606..9a6003ddad 100644 --- a/tests/test_docs/test_bash_yaml/md_files/bash-tac-skills.md +++ b/tests/test_docs/test_bash_yaml/md_files/bash-tac-skills.md @@ -1,4 +1,22 @@ ``` bash +{ + "delegate_uri": "127.0.0.1:11001", + "entry_peers": ["REPLACE_WITH_MULTI_ADDRESS_HERE"], + "local_uri": "127.0.0.1:9001", + "log_file": "libp2p_node.log", + "public_uri": "127.0.0.1:9001" +} +``` +``` bash +{ + "delegate_uri": "127.0.0.1:11002", + "entry_peers": ["REPLACE_WITH_MULTI_ADDRESS_HERE"], + "local_uri": "127.0.0.1:9002", + "log_file": "libp2p_node.log", + "public_uri": "127.0.0.1:9002" +} +``` +``` bash aea fetch fetchai/tac_controller:0.27.0 cd tac_controller aea install diff --git a/tests/test_docs/test_bash_yaml/md_files/bash-thermometer-skills.md b/tests/test_docs/test_bash_yaml/md_files/bash-thermometer-skills.md index f22b86234a..58b0f3a5ad 100644 --- a/tests/test_docs/test_bash_yaml/md_files/bash-thermometer-skills.md +++ b/tests/test_docs/test_bash_yaml/md_files/bash-thermometer-skills.md @@ -1,3 +1,12 @@ +``` bash +{ + "delegate_uri": "127.0.0.1:11001", + "entry_peers": ["REPLACE_WITH_MULTI_ADDRESS_HERE"], + "local_uri": "127.0.0.1:9001", + "log_file": "libp2p_node.log", + "public_uri": "127.0.0.1:9001" +} +``` ``` bash aea fetch fetchai/thermometer_aea:0.27.0 --alias my_thermometer_aea cd my_thermometer_aea diff --git a/tests/test_docs/test_bash_yaml/md_files/bash-weather-skills.md b/tests/test_docs/test_bash_yaml/md_files/bash-weather-skills.md index 5e56fc6f0d..1837df33d0 100644 --- a/tests/test_docs/test_bash_yaml/md_files/bash-weather-skills.md +++ b/tests/test_docs/test_bash_yaml/md_files/bash-weather-skills.md @@ -1,4 +1,13 @@ ``` bash +{ + "delegate_uri": "127.0.0.1:11001", + "entry_peers": ["REPLACE_WITH_MULTI_ADDRESS_HERE"], + "local_uri": "127.0.0.1:9001", + "log_file": "libp2p_node.log", + "public_uri": "127.0.0.1:9001" +} +``` +``` bash aea fetch fetchai/weather_station:0.29.0 --alias my_weather_station cd my_weather_station aea install From 25b44e9a3ebece1acff14c340cd6449d05116030 Mon Sep 17 00:00:00 2001 From: David Minarsch Date: Mon, 17 May 2021 16:59:09 +0100 Subject: [PATCH 063/147] feat: update deployment guide re kubernetes feat: new tac deployment and config improvements --- docs/deployment.md | 6 +----- examples/tac_deploy/.env | 1 + examples/tac_deploy/README.md | 14 ++++++++------ examples/tac_deploy/build.sh | 14 ++++++++++---- examples/tac_deploy/tac-deployment.yaml | 2 +- 5 files changed, 21 insertions(+), 16 deletions(-) diff --git a/docs/deployment.md b/docs/deployment.md index 7eac2b9964..2b179a1c18 100644 --- a/docs/deployment.md +++ b/docs/deployment.md @@ -17,8 +17,4 @@ Then follow the `README.md` contained in the folder. ## Deployment using Kubernetes -
-

Note

-

This section is incomplete and will soon be updated. -

-
+For an example of how to use Kubernets navigate to our TAC deployment example. diff --git a/examples/tac_deploy/.env b/examples/tac_deploy/.env index 0fd448f2f1..c451b7199e 100644 --- a/examples/tac_deploy/.env +++ b/examples/tac_deploy/.env @@ -1,4 +1,5 @@ PARTICIPANTS_AMOUNT=10 LOG_LEVEL=INFO USE_CLIENT=false +PACKAGES_SOURCE=--remote CLEAR_KEY_DATA_ON_LAUNCH=true \ No newline at end of file diff --git a/examples/tac_deploy/README.md b/examples/tac_deploy/README.md index b1b917ff7e..de314736a9 100644 --- a/examples/tac_deploy/README.md +++ b/examples/tac_deploy/README.md @@ -4,14 +4,16 @@ The TAC deployment deploys one controller and `n` tac participants. ### Build the image -To build the image: +First, ensure the specifications in `.env` match your requirements. + +Then, to build the image run: ``` bash docker build -t tac-deploy -f Dockerfile . --no-cache ``` ## Run locally -Add preferred amount of tac participants agents to `.env` file: +Specify preferred amount of tac participants agents in `.env` file, e.g.: ``` PARTICIPANTS_AMOUNT=5 ``` @@ -21,7 +23,7 @@ Run: docker run --env-file .env -v "$(pwd)/data:/data" -ti tac-deploy ``` -## Run in the cloud +## Run in the cloud (here using GCloud) GCloud should be configured first! @@ -29,12 +31,12 @@ GCloud should be configured first! Tag the image first with the latest tag: ``` bash -docker image tag tac-deploy gcr.io/fetch-ai-sandbox/tac_deploy:0.0.13 +docker image tag tac-deploy gcr.io/fetch-ai-sandbox/tac_deploy:0.0.14 ``` Push it to remote repo: ``` bash -docker push gcr.io/fetch-ai-sandbox/tac_deploy:0.0.13 +docker push gcr.io/fetch-ai-sandbox/tac_deploy:0.0.14 ``` ### Run it manually @@ -67,7 +69,7 @@ find . -name \*.txt -type f -delete First, push the latest image, as per above. -Second, update the `tac-deployment.yaml` file and then run: +Second, update the `tac-deployment.yaml` file with the correct image tag and configurations and then run: ``` bash kubectl apply -f ./tac-deployment.yaml ``` diff --git a/examples/tac_deploy/build.sh b/examples/tac_deploy/build.sh index 21e74af0eb..f75641281d 100644 --- a/examples/tac_deploy/build.sh +++ b/examples/tac_deploy/build.sh @@ -7,27 +7,33 @@ then fi echo USE_CLIENT $USE_CLIENT +if [ -z "$PACKAGES_SOURCE" ]; +then + PACKAGES_SOURCE="--remote" +fi +echo PACKAGES_SOURCE $PACKAGES_SOURCE + mkdir /data # setup the agent -aea fetch fetchai/tac_controller:latest +aea fetch $PACKAGES_SOURCE fetchai/tac_controller:latest cd tac_controller if [[ "$USE_CLIENT" == "true" ]] then aea remove connection fetchai/p2p_libp2p - aea add connection fetchai/p2p_libp2p_client + aea add $PACKAGES_SOURCE connection fetchai/p2p_libp2p_client aea config set agent.default_connection fetchai/p2p_libp2p_client:0.18.0 fi aea install aea build cd .. -aea fetch fetchai/tac_participant:latest --alias tac_participant_template +aea fetch $PACKAGES_SOURCE fetchai/tac_participant:latest --alias tac_participant_template cd tac_participant_template if [[ "$USE_CLIENT" == "true" ]] then aea remove connection fetchai/p2p_libp2p - aea add connection fetchai/p2p_libp2p_client + aea add $PACKAGES_SOURCE connection fetchai/p2p_libp2p_client aea config set agent.default_connection fetchai/p2p_libp2p_client:0.18.0 fi aea install diff --git a/examples/tac_deploy/tac-deployment.yaml b/examples/tac_deploy/tac-deployment.yaml index 8715709e7d..565669f699 100644 --- a/examples/tac_deploy/tac-deployment.yaml +++ b/examples/tac_deploy/tac-deployment.yaml @@ -18,7 +18,7 @@ spec: kubernetes.io/os: linux containers: - name: tac-deploy-container - image: gcr.io/fetch-ai-sandbox/tac_deploy:0.0.13 + image: gcr.io/fetch-ai-sandbox/tac_deploy:0.0.14 resources: requests: memory: "12000000Ki" From 6347123e03881dc8c9b1e5f575826a9fd4d7107a Mon Sep 17 00:00:00 2001 From: MarcoFavorito Date: Mon, 17 May 2021 18:14:53 +0200 Subject: [PATCH 064/147] improve error message for local fetch (#2482) --- aea/cli/utils/package_utils.py | 2 +- tests/test_cli/test_utils/test_utils.py | 8 ++++++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/aea/cli/utils/package_utils.py b/aea/cli/utils/package_utils.py index 4c8d19370b..901c32dc84 100644 --- a/aea/cli/utils/package_utils.py +++ b/aea/cli/utils/package_utils.py @@ -162,7 +162,7 @@ def try_get_item_source_path( source_path = os.path.join(path, author_name, item_type_plural, item_name) if not os.path.exists(source_path): raise click.ClickException( - 'Item "{}" not found in source folder.'.format(item_name) + f'Item "{author_name}/{item_name}" not found in source folder "{source_path}".' ) return source_path diff --git a/tests/test_cli/test_utils/test_utils.py b/tests/test_cli/test_utils/test_utils.py index 31db550767..ff3b8fa71a 100644 --- a/tests/test_cli/test_utils/test_utils.py +++ b/tests/test_cli/test_utils/test_utils.py @@ -133,8 +133,12 @@ def test_get_item_source_path_positive(self, exists_mock, join_mock): @mock.patch("aea.cli.utils.package_utils.os.path.exists", return_value=False) def test_get_item_source_path_not_exists(self, exists_mock, join_mock): """Test for get_item_source_path item already exists.""" - with self.assertRaises(ClickException): - try_get_item_source_path("cwd", AUTHOR, "skills", "skill_name") + item_name = "skill_name" + with pytest.raises( + ClickException, + match=f'Item "{AUTHOR}/{item_name}" not found in source folder "some-path"', + ): + try_get_item_source_path("cwd", AUTHOR, "skills", item_name) @mock.patch("aea.cli.utils.package_utils.os.path.join", return_value="some-path") From c499d21da7e949d3054702210bb93144511fe9b2 Mon Sep 17 00:00:00 2001 From: David Minarsch Date: Mon, 17 May 2021 17:27:53 +0100 Subject: [PATCH 065/147] feat: add link to gcloud init --- examples/tac_deploy/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/tac_deploy/README.md b/examples/tac_deploy/README.md index de314736a9..ef71ade2fe 100644 --- a/examples/tac_deploy/README.md +++ b/examples/tac_deploy/README.md @@ -25,7 +25,7 @@ docker run --env-file .env -v "$(pwd)/data:/data" -ti tac-deploy ## Run in the cloud (here using GCloud) -GCloud should be configured first! +GCloud should be [configured](https://cloud.google.com/sdk/docs/initializing) first! ### Push image From 90a69021c6720be9bfa5914d825ca7aad41f8ec4 Mon Sep 17 00:00:00 2001 From: MarcoFavorito Date: Mon, 17 May 2021 19:10:31 +0200 Subject: [PATCH 066/147] fix test in test_push after 6347123e0 --- tests/test_cli/test_push.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tests/test_cli/test_push.py b/tests/test_cli/test_push.py index 4f041fd7b1..efe1a6aa50 100644 --- a/tests/test_cli/test_push.py +++ b/tests/test_cli/test_push.py @@ -90,9 +90,10 @@ def test_vendor_ok( def test_fail_no_item( self, *mocks, ): - """Test fail, item_noit_exists .""" + """Test fail, item_not_exists .""" with pytest.raises( - ClickException, match='Item "not_exists" not found in source folder.' + ClickException, + match='Item "fetchai/not_exists" not found in source folder "./vendor/fetchai/skills/not_exists".', ): self.invoke("push", "--local", "skill", "fetchai/not_exists") From 9f33c195ceef1feff4bf121bf917add1cc71caa2 Mon Sep 17 00:00:00 2001 From: ali Date: Mon, 17 May 2021 18:14:36 +0100 Subject: [PATCH 067/147] docs: isolating publi_id strings --- docs/car-park-skills.md | 2 +- docs/ml-skills.md | 2 +- docs/tac-skills.md | 6 +++--- docs/thermometer-skills.md | 2 +- docs/weather-skills.md | 2 +- 5 files changed, 7 insertions(+), 7 deletions(-) diff --git a/docs/car-park-skills.md b/docs/car-park-skills.md index e7b248e4ed..5b2530b78e 100644 --- a/docs/car-park-skills.md +++ b/docs/car-park-skills.md @@ -65,7 +65,7 @@ The following steps assume you have launched the AEA Manager Desktop app. 4. Run the `car_detector` AEA. Navigate to its logs and copy the multiaddress displayed. -5. Navigate to the settings of the `car_data_buyer` and under `components > connections/fetchai/p2p_libp2p:0.22.0` update as follows (make sure to replace the placeholder with the multiaddress): +5. Navigate to the settings of the `car_data_buyer` and under `components > connection >` `fetchai/p2p_libp2p:0.22.0` update as follows (make sure to replace the placeholder with the multiaddress): ``` bash { "delegate_uri": "127.0.0.1:11001", diff --git a/docs/ml-skills.md b/docs/ml-skills.md index 933dfde84a..04a8bf2d5e 100644 --- a/docs/ml-skills.md +++ b/docs/ml-skills.md @@ -71,7 +71,7 @@ The following steps assume you have launched the AEA Manager Desktop app. 4. Run the `ml_data_provider` AEA. Navigate to its logs and copy the multiaddress displayed. -5. Navigate to the settings of the `ml_model_trainer` and under `components > connections/fetchai/p2p_libp2p:0.22.0` update as follows (make sure to replace the placeholder with the multiaddress): +5. Navigate to the settings of the `ml_model_trainer` and under `components > connection >` `fetchai/p2p_libp2p:0.22.0` update as follows (make sure to replace the placeholder with the multiaddress): ``` bash { "delegate_uri": "127.0.0.1:11001", diff --git a/docs/tac-skills.md b/docs/tac-skills.md index c58d8f5415..46e21d1737 100644 --- a/docs/tac-skills.md +++ b/docs/tac-skills.md @@ -105,11 +105,11 @@ The following steps assume you have launched the AEA Manager Desktop app. 3. Add another new AEA called `participant_2` with public id `fetchai/tac_participant:0.28.0`. -4. Navigate to the settings of `controller` and under `components > skills/fetchai/tac_controller:0.22.0 > models > parameters > args` update `registration_start_time` to the time you want TAC to begin (e.g. 2 minutes in the future) +4. Navigate to the settings of `controller` and under `components > skill >` `fetchai/fetchai/tac_controller:0.22.0` `> models > parameters > args` update `registration_start_time` to the time you want TAC to begin (e.g. 2 minutes in the future) 5. Run the `controller` AEA. Navigate to its logs and copy the multiaddress displayed. Stop the `controller`. -5. Navigate to the settings of `participant_1` and under `components > connections/fetchai/p2p_libp2p:0.22.0` update as follows (make sure to replace the placeholder with the multiaddress): +5. Navigate to the settings of `participant_1` and under `components > connection >` `fetchai/p2p_libp2p:0.22.0` update as follows (make sure to replace the placeholder with the multiaddress): ``` bash { "delegate_uri": "127.0.0.1:11001", @@ -120,7 +120,7 @@ The following steps assume you have launched the AEA Manager Desktop app. } ``` -6. Navigate to the settings of `participant_2` and under `components > connections/fetchai/p2p_libp2p:0.22.0` update as follows (make sure to replace the placeholder with the multiaddress): +6. Navigate to the settings of `participant_2` and under `components > connection >` `fetchai/p2p_libp2p:0.22.0` update as follows (make sure to replace the placeholder with the multiaddress): ``` bash { "delegate_uri": "127.0.0.1:11002", diff --git a/docs/thermometer-skills.md b/docs/thermometer-skills.md index 5db274e192..4009f722e4 100644 --- a/docs/thermometer-skills.md +++ b/docs/thermometer-skills.md @@ -63,7 +63,7 @@ The following steps assume you have launched the AEA Manager Desktop app. 4. Run the `my_thermometer_aea` AEA. Navigate to its logs and copy the multiaddress displayed. -5. Navigate to the settings of the `my_thermometer_client` and under `components > connections/fetchai/p2p_libp2p:0.22.0` update as follows (make sure to replace the placeholder with the multiaddress): +5. Navigate to the settings of the `my_thermometer_client` and under `components > connection >` `fetchai/p2p_libp2p:0.22.0` update as follows (make sure to replace the placeholder with the multiaddress): ``` bash { "delegate_uri": "127.0.0.1:11001", diff --git a/docs/weather-skills.md b/docs/weather-skills.md index 8336259a12..afa5e97e41 100644 --- a/docs/weather-skills.md +++ b/docs/weather-skills.md @@ -67,7 +67,7 @@ The following steps assume you have launched the AEA Manager Desktop app. 4. Run the `my_weather_station` AEA. Navigate to its logs and copy the multiaddress displayed. -5. Navigate to the settings of the `my_weather_client` and under `components > connections/fetchai/p2p_libp2p:0.22.0` update as follows (make sure to replace the placeholder with the multiaddress): +5. Navigate to the settings of the `my_weather_client` and under `components > connection >` `fetchai/p2p_libp2p:0.22.0` update as follows (make sure to replace the placeholder with the multiaddress): ``` bash { "delegate_uri": "127.0.0.1:11001", From 1fbfebf8ddb74de9ec3b935f26c7959fe09ad266 Mon Sep 17 00:00:00 2001 From: MarcoFavorito Date: Mon, 17 May 2021 19:48:22 +0200 Subject: [PATCH 068/147] add log info message if loaded component is abstract (#2474) --- aea/aea_builder.py | 4 ++++ tests/test_aea_builder.py | 32 ++++++++++++++++++++++++++------ 2 files changed, 30 insertions(+), 6 deletions(-) diff --git a/aea/aea_builder.py b/aea/aea_builder.py index 07aa81df12..bc1dc946ad 100644 --- a/aea/aea_builder.py +++ b/aea/aea_builder.py @@ -1918,6 +1918,10 @@ def _load_and_add_components( new_configuration = self._overwrite_custom_configuration(configuration) if new_configuration.is_abstract_component: load_aea_package(configuration) + self.logger.info( + f"Package {configuration.public_id} of type {configuration.component_type} is abstract, " + f"therefore only the Python modules have been loaded." + ) continue _logger = make_component_logger(new_configuration, agent_name) component = load_component_from_config( diff --git a/tests/test_aea_builder.py b/tests/test_aea_builder.py index 29b4f6ac06..666f64dc33 100644 --- a/tests/test_aea_builder.py +++ b/tests/test_aea_builder.py @@ -596,16 +596,36 @@ def test_set_from_config_custom(): def test_load_abstract_component(): """Test abstract component loading.""" + resources = Resources() builder = AEABuilder() builder.set_name("aea_1") builder.add_private_key("fetchai") - skill = Skill.from_dir(dummy_skill_path, Mock(agent_name="name")) - skill.configuration.is_abstract = True - builder.add_component_instance(skill) - builder._load_and_add_components( - ComponentType.SKILL, Resources(), "aea_1", agent_context=Mock(agent_name="name") - ) + builder.add_component(ComponentType.SKILL, dummy_skill_path) + with mock.patch( + "aea.aea_builder.load_aea_package", return_value=True + ), mock.patch.object( + builder, + "_overwrite_custom_configuration", + return_value=Mock(is_abstract_component=True), + ), mock.patch.object( + builder.logger, "info" + ) as mock_logger: + builder._load_and_add_components( + ComponentType.SKILL, + resources, + "aea_1", + agent_context=Mock(agent_name="name"), + ) + + mock_logger.assert_called_with( + f"Package {DUMMY_SKILL_PUBLIC_ID} of type skill is abstract, " + f"therefore only the Python modules have been loaded." + ) + + assert ( + len(resources.get_all_skills()) == 0 + ), "expected 0 skills because the loaded skill is abstract" def test_find_import_order(): From 476dd3e88a1c00f1b5926885367698a754f024b1 Mon Sep 17 00:00:00 2001 From: MarcoFavorito Date: Mon, 17 May 2021 19:52:14 +0200 Subject: [PATCH 069/147] remove useless mock return value in 'test_load_abstract_component' --- tests/test_aea_builder.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/tests/test_aea_builder.py b/tests/test_aea_builder.py index 666f64dc33..27cef0b50f 100644 --- a/tests/test_aea_builder.py +++ b/tests/test_aea_builder.py @@ -602,15 +602,11 @@ def test_load_abstract_component(): builder.add_private_key("fetchai") builder.add_component(ComponentType.SKILL, dummy_skill_path) - with mock.patch( - "aea.aea_builder.load_aea_package", return_value=True - ), mock.patch.object( + with mock.patch("aea.aea_builder.load_aea_package"), mock.patch.object( builder, "_overwrite_custom_configuration", return_value=Mock(is_abstract_component=True), - ), mock.patch.object( - builder.logger, "info" - ) as mock_logger: + ), mock.patch.object(builder.logger, "info") as mock_logger: builder._load_and_add_components( ComponentType.SKILL, resources, From 3cc2a3ae30e76ff6e0ebb305b5b7da3b33d8ed44 Mon Sep 17 00:00:00 2001 From: MarcoFavorito Date: Mon, 17 May 2021 21:27:56 +0200 Subject: [PATCH 070/147] use debug level instead of info level --- aea/aea_builder.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aea/aea_builder.py b/aea/aea_builder.py index bc1dc946ad..2dad9e0013 100644 --- a/aea/aea_builder.py +++ b/aea/aea_builder.py @@ -1918,7 +1918,7 @@ def _load_and_add_components( new_configuration = self._overwrite_custom_configuration(configuration) if new_configuration.is_abstract_component: load_aea_package(configuration) - self.logger.info( + self.logger.debug( f"Package {configuration.public_id} of type {configuration.component_type} is abstract, " f"therefore only the Python modules have been loaded." ) From 27ffe24ee56b354961014feb95db13abed49bef9 Mon Sep 17 00:00:00 2001 From: MarcoFavorito Date: Mon, 17 May 2021 21:55:32 +0200 Subject: [PATCH 071/147] make test cross-platform --- tests/test_cli/test_push.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/tests/test_cli/test_push.py b/tests/test_cli/test_push.py index efe1a6aa50..7f0292bea5 100644 --- a/tests/test_cli/test_push.py +++ b/tests/test_cli/test_push.py @@ -18,6 +18,7 @@ # ------------------------------------------------------------------------------ """Test module for Registry push methods.""" import filecmp +import os.path from unittest import TestCase, mock import pytest @@ -91,9 +92,12 @@ def test_fail_no_item( self, *mocks, ): """Test fail, item_not_exists .""" + expected_path_string = os.path.join( + ".", "vendor", "fetchai", "skills", "not_exists" + ) with pytest.raises( ClickException, - match='Item "fetchai/not_exists" not found in source folder "./vendor/fetchai/skills/not_exists".', + match=f'Item "fetchai/not_exists" not found in source folder "{expected_path_string}".', ): self.invoke("push", "--local", "skill", "fetchai/not_exists") From 5098ff72e2d6b545f5cddd823cb99a2b66972524 Mon Sep 17 00:00:00 2001 From: MarcoFavorito Date: Tue, 18 May 2021 11:33:27 +0200 Subject: [PATCH 072/147] test: update log level in 'test_load_abstract_component' --- tests/test_aea_builder.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_aea_builder.py b/tests/test_aea_builder.py index 27cef0b50f..8e51efde23 100644 --- a/tests/test_aea_builder.py +++ b/tests/test_aea_builder.py @@ -606,7 +606,7 @@ def test_load_abstract_component(): builder, "_overwrite_custom_configuration", return_value=Mock(is_abstract_component=True), - ), mock.patch.object(builder.logger, "info") as mock_logger: + ), mock.patch.object(builder.logger, "debug") as mock_logger: builder._load_and_add_components( ComponentType.SKILL, resources, From 2c922fc54fb9c16623b1ffb65f2d5ce4dfb73dc3 Mon Sep 17 00:00:00 2001 From: MarcoFavorito Date: Tue, 18 May 2021 11:49:13 +0200 Subject: [PATCH 073/147] test: try to make the test 'TestPushLocally.test_fail_no_item' to pass on windows --- tests/test_cli/test_push.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_cli/test_push.py b/tests/test_cli/test_push.py index 7f0292bea5..fd0d1da985 100644 --- a/tests/test_cli/test_push.py +++ b/tests/test_cli/test_push.py @@ -93,11 +93,11 @@ def test_fail_no_item( ): """Test fail, item_not_exists .""" expected_path_string = os.path.join( - ".", "vendor", "fetchai", "skills", "not_exists" + r"\.", "vendor", "fetchai", "skills", "not_exists" ) with pytest.raises( ClickException, - match=f'Item "fetchai/not_exists" not found in source folder "{expected_path_string}".', + match=rf'Item "fetchai/not_exists" not found in source folder "{expected_path_string}"\.', ): self.invoke("push", "--local", "skill", "fetchai/not_exists") From f10c29bf89e3d22e1f39781fe53c06301c41b6a8 Mon Sep 17 00:00:00 2001 From: MarcoFavorito Date: Tue, 18 May 2021 14:06:27 +0200 Subject: [PATCH 074/147] trigger CI From c98ae38ce6764f7eb460c491bf2655583afc5089 Mon Sep 17 00:00:00 2001 From: MarcoFavorito Date: Tue, 18 May 2021 14:07:08 +0200 Subject: [PATCH 075/147] trigger CI From 91d6e113e7c7d95967c2e614061153049be4704c Mon Sep 17 00:00:00 2001 From: MarcoFavorito Date: Tue, 18 May 2021 15:15:39 +0200 Subject: [PATCH 076/147] add tests on manager with passwords --- tests/test_manager/test_manager.py | 49 ++++++++++++++++++++++++------ 1 file changed, 39 insertions(+), 10 deletions(-) diff --git a/tests/test_manager/test_manager.py b/tests/test_manager/test_manager.py index 630913c6fa..252c61dc05 100644 --- a/tests/test_manager/test_manager.py +++ b/tests/test_manager/test_manager.py @@ -20,6 +20,7 @@ import asyncio import os import re +import sys from contextlib import suppress from pathlib import Path from shutil import rmtree @@ -44,12 +45,11 @@ @patch("aea.aea_builder.AEABuilder.install_pypi_dependencies") -class TestMultiAgentManagerAsyncMode( - TestCase -): # pylint: disable=unused-argument,protected-access,attribute-defined-outside-init - """Tests for MultiAgentManager in async mode.""" +class BaseTestMultiAgentManager(TestCase): + """Base test class for multi-agent manager""" MODE = "async" + PASSWORD = None echo_skill_id = ECHO_SKILL_PUBLIC_ID @@ -62,7 +62,9 @@ def setUp(self): self.working_dir, self.project_public_id.author, self.project_public_id.name ) assert not os.path.exists(self.working_dir) - self.manager = MultiAgentManager(self.working_dir, mode=self.MODE) + self.manager = MultiAgentManager( + self.working_dir, mode=self.MODE, password=self.PASSWORD + ) def tearDown(self): """Tear down test case.""" @@ -73,10 +75,14 @@ def tearDown(self): def test_plugin_dependencies(self, *args): """Test plugin installed and loaded as a depencndecy.""" plugin_path = str(Path(ROOT_DIR) / "plugins" / "aea-ledger-fetchai") - install_cmd = f"pip install --no-deps {plugin_path}".split(" ") + install_cmd = f"{sys.executable} -m pip install --no-deps {plugin_path}".split( + " " + ) try: self.manager.start_manager() - run_install_subprocess("pip uninstall aea-ledger-fetchai -y".split(" ")) + run_install_subprocess( + f"{sys.executable} -m pip uninstall aea-ledger-fetchai -y".split(" ") + ) from aea.crypto.registries import ledger_apis_registry ledger_apis_registry.specs.pop("fetchai", None) @@ -98,7 +104,9 @@ def install_deps(*_): assert "fetchai" in ledger_apis_registry.specs finally: - run_install_subprocess("pip uninstall aea-ledger-fetchai -y".split(" ")) + run_install_subprocess( + f"{sys.executable} -m pip uninstall aea-ledger-fetchai -y".split(" ") + ) run_install_subprocess(install_cmd) def test_workdir_created_removed(self, *args): @@ -457,7 +465,7 @@ def test_issue_certificates(self, *args): assert not os.path.exists(cert_filename) priv_key_path = os.path.abspath(os.path.join(self.working_dir, "priv_key.txt")) - create_private_key("fetchai", priv_key_path) + create_private_key("fetchai", priv_key_path, password=self.PASSWORD) assert os.path.exists(priv_key_path) component_overrides = [ @@ -528,12 +536,33 @@ def test_addresses_autoadded(self, *args) -> None: assert len(agent_alias.get_connections_addresses()) == 1 -class TestMultiAgentManagerThreadedMode(TestMultiAgentManagerAsyncMode): +class TestMultiAgentManagerAsyncMode( + BaseTestMultiAgentManager +): # pylint: disable=unused-argument,protected-access,attribute-defined-outside-init + """Tests for MultiAgentManager in async mode.""" + + +class TestMultiAgentManagerAsyncModeWithPassword( + BaseTestMultiAgentManager +): # pylint: disable=unused-argument,protected-access,attribute-defined-outside-init + """Tests for MultiAgentManager in async mode, with password.""" + + PASSWORD = "password" + + +class TestMultiAgentManagerThreadedMode(BaseTestMultiAgentManager): """Tests for MultiAgentManager in threaded mode.""" MODE = "threaded" +class TestMultiAgentManagerThreadedModeWithPassword(BaseTestMultiAgentManager): + """Tests for MultiAgentManager in threaded mode, with password.""" + + MODE = "threaded" + PASSWORD = "password" + + def test_project_auto_added_removed(): """Check project auto added and auto removed on agent added/removed.""" agent_name = "test_agent" From 398475d0634ba85fffb278ba5567613bf29c6793 Mon Sep 17 00:00:00 2001 From: MarcoFavorito Date: Tue, 18 May 2021 16:30:20 +0200 Subject: [PATCH 077/147] fix: fix bandit checks --- tests/test_manager/test_manager.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_manager/test_manager.py b/tests/test_manager/test_manager.py index 252c61dc05..8bddd28074 100644 --- a/tests/test_manager/test_manager.py +++ b/tests/test_manager/test_manager.py @@ -547,7 +547,7 @@ class TestMultiAgentManagerAsyncModeWithPassword( ): # pylint: disable=unused-argument,protected-access,attribute-defined-outside-init """Tests for MultiAgentManager in async mode, with password.""" - PASSWORD = "password" + PASSWORD = "password" # nosec class TestMultiAgentManagerThreadedMode(BaseTestMultiAgentManager): @@ -560,7 +560,7 @@ class TestMultiAgentManagerThreadedModeWithPassword(BaseTestMultiAgentManager): """Tests for MultiAgentManager in threaded mode, with password.""" MODE = "threaded" - PASSWORD = "password" + PASSWORD = "password" # nosec def test_project_auto_added_removed(): From eeb4a59cdde77d8556f7d9bec2854bf571b59607 Mon Sep 17 00:00:00 2001 From: MarcoFavorito Date: Tue, 18 May 2021 21:54:36 +0200 Subject: [PATCH 078/147] add test on 'aea generate-key' with '--password' --- tests/test_cli/test_generate_key.py | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/tests/test_cli/test_generate_key.py b/tests/test_cli/test_generate_key.py index 86adbf0273..8b700cd79f 100644 --- a/tests/test_cli/test_generate_key.py +++ b/tests/test_cli/test_generate_key.py @@ -29,6 +29,7 @@ from aea.cli import cli from aea.crypto.registries import make_crypto +from aea.test_tools.test_cases import AEATestCaseEmpty from tests.conftest import ( CLI_LOG_OPTION, @@ -181,3 +182,23 @@ def teardown_class(cls): """Tear the test down.""" os.chdir(cls.cwd) shutil.rmtree(cls.t) + + +class TestGenerateKeyWithPassword(AEATestCaseEmpty): + """Test that the command 'aea generate-key' with a password argument works as expected.""" + + def test_fetchai(self): + """Test that the fetch private key is created correctly.""" + password = "password" + result = self.run_cli_command( + "generate-key", FetchAICrypto.identifier, "--password", password + ) + assert result.exit_code == 0 + assert Path(FETCHAI_PRIVATE_KEY_FILE).exists() + + # check the key can be read again with the same password. + make_crypto( + FetchAICrypto.identifier, + private_key_path=FETCHAI_PRIVATE_KEY_FILE, + password=password, + ) From 547c4d6557a79418d29f62b792a994b2211817f5 Mon Sep 17 00:00:00 2001 From: MarcoFavorito Date: Tue, 18 May 2021 22:22:01 +0200 Subject: [PATCH 079/147] add test on 'aea add-key' with '--password' --- tests/test_cli/test_add_key.py | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/tests/test_cli/test_add_key.py b/tests/test_cli/test_add_key.py index b872db19a3..45ddbed745 100644 --- a/tests/test_cli/test_add_key.py +++ b/tests/test_cli/test_add_key.py @@ -33,6 +33,7 @@ from aea.cli import cli from aea.cli.add_key import _try_add_key from aea.configurations.base import AgentConfig, DEFAULT_AEA_CONFIG_FILE +from aea.test_tools.test_cases import AEATestCaseEmpty from tests.conftest import ( AUTHOR, @@ -428,3 +429,32 @@ def test_file_not_specified_does_not_exist(self, *mocks): standalone_mode=False, catch_exceptions=False, ) + + +class TestAddKeyWithPassword(AEATestCaseEmpty): + """Test the '--password' option to 'add-key' command.""" + + FAKE_PASSWORD = "password" + + @classmethod + def setup_class(cls) -> None: + """Set up the class.""" + super().setup_class() + cls.run_cli_command( + "generate-key", + FetchAICrypto.identifier, + FETCHAI_PRIVATE_KEY_FILE, + "--password", + cls.FAKE_PASSWORD, + cwd=cls._get_cwd(), + ) + + def test_add_key_with_password(self): + """Test add key with password.""" + self.run_cli_command( + "add-key", + FetchAICrypto.identifier, + "--password", + self.FAKE_PASSWORD, + cwd=self._get_cwd(), + ) From ccd80f5811ccba5d6f409e18ee5fac3338cc013a Mon Sep 17 00:00:00 2001 From: MarcoFavorito Date: Tue, 18 May 2021 22:26:38 +0200 Subject: [PATCH 080/147] fix static check in test_manager.py --- tests/test_manager/test_manager.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/test_manager/test_manager.py b/tests/test_manager/test_manager.py index 8bddd28074..d4738e5421 100644 --- a/tests/test_manager/test_manager.py +++ b/tests/test_manager/test_manager.py @@ -25,6 +25,7 @@ from pathlib import Path from shutil import rmtree from tempfile import TemporaryDirectory +from typing import Optional from unittest.case import TestCase from unittest.mock import Mock, patch @@ -49,7 +50,7 @@ class BaseTestMultiAgentManager(TestCase): """Base test class for multi-agent manager""" MODE = "async" - PASSWORD = None + PASSWORD: Optional[str] = None echo_skill_id = ECHO_SKILL_PUBLIC_ID From dbbfc8309cb2ee14b9e74a14cf5d5b243c090b8b Mon Sep 17 00:00:00 2001 From: MarcoFavorito Date: Tue, 18 May 2021 22:38:02 +0200 Subject: [PATCH 081/147] use parametrized fixture for tests that use passwords --- tests/conftest.py | 10 ++++++ tests/test_cli/test_generate_key.py | 56 +++++++++++++---------------- 2 files changed, 35 insertions(+), 31 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index 5f98fa6a19..1021d59e52 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1437,3 +1437,13 @@ def change_directory(): yield temporary_directory finally: shutil.rmtree(temporary_directory) + + +@pytest.fixture(params=[None, "fake-password"]) +def password_or_none(request) -> Optional[str]: + """ + Return a password for testing purposes. + + Note that this is a parametrized fixture. + """ + return request.param diff --git a/tests/test_cli/test_generate_key.py b/tests/test_cli/test_generate_key.py index 8b700cd79f..918e0264ab 100644 --- a/tests/test_cli/test_generate_key.py +++ b/tests/test_cli/test_generate_key.py @@ -23,13 +23,13 @@ import shutil import tempfile from pathlib import Path +from typing import List, Optional from aea_ledger_ethereum import EthereumCrypto from aea_ledger_fetchai import FetchAICrypto from aea.cli import cli from aea.crypto.registries import make_crypto -from aea.test_tools.test_cases import AEATestCaseEmpty from tests.conftest import ( CLI_LOG_OPTION, @@ -51,26 +51,40 @@ def setup_class(cls): cls.t = tempfile.mkdtemp() os.chdir(cls.t) - def test_fetchai(self): + def _append_password_option_if_not_none( + self, args, password: Optional[str] + ) -> List: + """Append '--password' option if not None.""" + if password is None: + return args + return args + ["--password", password] + + def test_fetchai(self, password): """Test that the fetch private key is created correctly.""" - result = self.runner.invoke( - cli, [*CLI_LOG_OPTION, "generate-key", FetchAICrypto.identifier] - ) + args = [*CLI_LOG_OPTION, "generate-key", FetchAICrypto.identifier] + args = self._append_password_option_if_not_none(args, password) + result = self.runner.invoke(cli, args) assert result.exit_code == 0 assert Path(FETCHAI_PRIVATE_KEY_FILE).exists() - make_crypto(FetchAICrypto.identifier, private_key_path=FETCHAI_PRIVATE_KEY_FILE) + make_crypto( + FetchAICrypto.identifier, + private_key_path=FETCHAI_PRIVATE_KEY_FILE, + password=password, + ) Path(FETCHAI_PRIVATE_KEY_FILE).unlink() - def test_ethereum(self): + def test_ethereum(self, password): """Test that the fetch private key is created correctly.""" - result = self.runner.invoke( - cli, [*CLI_LOG_OPTION, "generate-key", EthereumCrypto.identifier] - ) + args = [*CLI_LOG_OPTION, "generate-key", EthereumCrypto.identifier] + args = self._append_password_option_if_not_none(args, password) + result = self.runner.invoke(cli, args) assert result.exit_code == 0 assert Path(ETHEREUM_PRIVATE_KEY_FILE).exists() make_crypto( - EthereumCrypto.identifier, private_key_path=ETHEREUM_PRIVATE_KEY_FILE + EthereumCrypto.identifier, + private_key_path=ETHEREUM_PRIVATE_KEY_FILE, + password=password, ) Path(ETHEREUM_PRIVATE_KEY_FILE).unlink() @@ -182,23 +196,3 @@ def teardown_class(cls): """Tear the test down.""" os.chdir(cls.cwd) shutil.rmtree(cls.t) - - -class TestGenerateKeyWithPassword(AEATestCaseEmpty): - """Test that the command 'aea generate-key' with a password argument works as expected.""" - - def test_fetchai(self): - """Test that the fetch private key is created correctly.""" - password = "password" - result = self.run_cli_command( - "generate-key", FetchAICrypto.identifier, "--password", password - ) - assert result.exit_code == 0 - assert Path(FETCHAI_PRIVATE_KEY_FILE).exists() - - # check the key can be read again with the same password. - make_crypto( - FetchAICrypto.identifier, - private_key_path=FETCHAI_PRIVATE_KEY_FILE, - password=password, - ) From bc29a0018d714af195c8d7789a7059321c86f25f Mon Sep 17 00:00:00 2001 From: MarcoFavorito Date: Tue, 18 May 2021 22:56:58 +0200 Subject: [PATCH 082/147] test: update tests - update some test_tools APIs so to include an optional parameter 'password' - add parametrized password fixture: any test that uses it will be run both with password=None and password="fake-password" - update generate-key test with new APIs - extend existing generate-wealth test with new APIs --- aea/test_tools/test_cases.py | 38 +++++++++++++++++++++++--- tests/conftest.py | 2 +- tests/test_cli/test_generate_key.py | 27 +++++++----------- tests/test_cli/test_generate_wealth.py | 8 +++--- 4 files changed, 49 insertions(+), 26 deletions(-) diff --git a/aea/test_tools/test_cases.py b/aea/test_tools/test_cases.py index 6523786e9a..ac8af5693f 100644 --- a/aea/test_tools/test_cases.py +++ b/aea/test_tools/test_cases.py @@ -579,7 +579,10 @@ def run_install(cls) -> Result: @classmethod def generate_private_key( - cls, ledger_api_id: str = DEFAULT_LEDGER, private_key_file: Optional[str] = None + cls, + ledger_api_id: str = DEFAULT_LEDGER, + private_key_file: Optional[str] = None, + password: Optional[str] = None, ) -> Result: """ Generate AEA private key with CLI command. @@ -588,12 +591,14 @@ def generate_private_key( :param ledger_api_id: ledger API ID. :param private_key_file: the private key file. + :param password: the password option. :return: Result """ cli_args = ["generate-key", ledger_api_id] if private_key_file is not None: # pragma: nocover cli_args.append(private_key_file) + cli_args += _get_password_option_args(password) return cls.run_cli_command(*cli_args, cwd=cls._get_cwd()) @classmethod @@ -602,6 +607,7 @@ def add_private_key( ledger_api_id: str = DEFAULT_LEDGER, private_key_filepath: str = DEFAULT_PRIVATE_KEY_FILE, connection: bool = False, + password: Optional[str] = None, ) -> Result: """ Add private key with CLI command. @@ -614,16 +620,22 @@ def add_private_key( :return: Result """ + password_option = _get_password_option_args(password) if connection: return cls.run_cli_command( "add-key", ledger_api_id, private_key_filepath, "--connection", + *password_option, cwd=cls._get_cwd(), ) return cls.run_cli_command( - "add-key", ledger_api_id, private_key_filepath, cwd=cls._get_cwd() + "add-key", + ledger_api_id, + private_key_filepath, + *password_option, + cwd=cls._get_cwd(), ) @classmethod @@ -661,18 +673,26 @@ def replace_private_key_in_file( f.write(private_key) @classmethod - def generate_wealth(cls, ledger_api_id: str = DEFAULT_LEDGER) -> Result: + def generate_wealth( + cls, ledger_api_id: str = DEFAULT_LEDGER, password: Optional[str] = None + ) -> Result: """ Generate wealth with CLI command. Run from agent's directory. :param ledger_api_id: ledger API ID. + :param password: the password option. :return: Result """ + password_option = _get_password_option_args(password) return cls.run_cli_command( - "generate-wealth", ledger_api_id, "--sync", cwd=cls._get_cwd() + "generate-wealth", + ledger_api_id, + *password_option, + "--sync", + cwd=cls._get_cwd(), ) @classmethod @@ -936,6 +956,16 @@ def teardown_class(cls) -> None: cls._is_teardown_class_called = True +def _get_password_option_args(password: Optional[str]): + """ + Get password option arguments. + + :param password: the password (optional). + :return: empty list if password is None, else ['--password', password]. + """ + return [] if password is None else ["--password", password] + + class AEATestCaseEmpty(BaseAEATestCase): """ Test case for a default AEA project. diff --git a/tests/conftest.py b/tests/conftest.py index 1021d59e52..f2d3999f51 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1442,7 +1442,7 @@ def change_directory(): @pytest.fixture(params=[None, "fake-password"]) def password_or_none(request) -> Optional[str]: """ - Return a password for testing purposes. + Return a password for testing purposes, including None. Note that this is a parametrized fixture. """ diff --git a/tests/test_cli/test_generate_key.py b/tests/test_cli/test_generate_key.py index 918e0264ab..2b15385450 100644 --- a/tests/test_cli/test_generate_key.py +++ b/tests/test_cli/test_generate_key.py @@ -23,7 +23,6 @@ import shutil import tempfile from pathlib import Path -from typing import List, Optional from aea_ledger_ethereum import EthereumCrypto from aea_ledger_fetchai import FetchAICrypto @@ -51,40 +50,34 @@ def setup_class(cls): cls.t = tempfile.mkdtemp() os.chdir(cls.t) - def _append_password_option_if_not_none( - self, args, password: Optional[str] - ) -> List: - """Append '--password' option if not None.""" - if password is None: - return args - return args + ["--password", password] - - def test_fetchai(self, password): + def test_fetchai(self, password_or_none): """Test that the fetch private key is created correctly.""" - args = [*CLI_LOG_OPTION, "generate-key", FetchAICrypto.identifier] - args = self._append_password_option_if_not_none(args, password) + args = [*CLI_LOG_OPTION, "generate-key", FetchAICrypto.identifier] + ( + ["--password", password_or_none] if password_or_none is not None else [] + ) result = self.runner.invoke(cli, args) assert result.exit_code == 0 assert Path(FETCHAI_PRIVATE_KEY_FILE).exists() make_crypto( FetchAICrypto.identifier, private_key_path=FETCHAI_PRIVATE_KEY_FILE, - password=password, + password=password_or_none, ) Path(FETCHAI_PRIVATE_KEY_FILE).unlink() - def test_ethereum(self, password): + def test_ethereum(self, password_or_none): """Test that the fetch private key is created correctly.""" - args = [*CLI_LOG_OPTION, "generate-key", EthereumCrypto.identifier] - args = self._append_password_option_if_not_none(args, password) + args = [*CLI_LOG_OPTION, "generate-key", EthereumCrypto.identifier] + ( + ["--password", password_or_none] if password_or_none is not None else [] + ) result = self.runner.invoke(cli, args) assert result.exit_code == 0 assert Path(ETHEREUM_PRIVATE_KEY_FILE).exists() make_crypto( EthereumCrypto.identifier, private_key_path=ETHEREUM_PRIVATE_KEY_FILE, - password=password, + password=password_or_none, ) Path(ETHEREUM_PRIVATE_KEY_FILE).unlink() diff --git a/tests/test_cli/test_generate_wealth.py b/tests/test_cli/test_generate_wealth.py index cb4fb4c2df..e98939cce1 100644 --- a/tests/test_cli/test_generate_wealth.py +++ b/tests/test_cli/test_generate_wealth.py @@ -83,17 +83,17 @@ class TestWealthCommandsPositive(AEATestCaseManyFlaky): @pytest.mark.integration @pytest.mark.flaky(reruns=MAX_FLAKY_RERUNS_INTEGRATION) - def test_wealth_commands(self): + def test_wealth_commands(self, password_or_none): """Test wealth commands.""" agent_name = "test_aea" self.create_agents(agent_name) self.set_agent_context(agent_name) - self.generate_private_key() - self.add_private_key() + self.generate_private_key(password=password_or_none) + self.add_private_key(password=password_or_none) - self.generate_wealth() + self.generate_wealth(password=password_or_none) class TestWealthCommandsNegative(AEATestCaseMany): From 77755d386c1a46c27f1bf2ae985c2fcbdf905873 Mon Sep 17 00:00:00 2001 From: MarcoFavorito Date: Tue, 18 May 2021 23:26:45 +0200 Subject: [PATCH 083/147] test: add bandit check to add-key test --- tests/test_cli/test_add_key.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_cli/test_add_key.py b/tests/test_cli/test_add_key.py index 45ddbed745..cb96cd3181 100644 --- a/tests/test_cli/test_add_key.py +++ b/tests/test_cli/test_add_key.py @@ -434,7 +434,7 @@ def test_file_not_specified_does_not_exist(self, *mocks): class TestAddKeyWithPassword(AEATestCaseEmpty): """Test the '--password' option to 'add-key' command.""" - FAKE_PASSWORD = "password" + FAKE_PASSWORD = "password" # nosec @classmethod def setup_class(cls) -> None: From 5136d3ac2c58c117c529edf44ee25fd6d4cc3214 Mon Sep 17 00:00:00 2001 From: MarcoFavorito Date: Tue, 18 May 2021 23:27:01 +0200 Subject: [PATCH 084/147] add test on 'aea get-address' command with '--password' --- tests/test_cli/test_get_address.py | 43 +++++++++++++++++++++++++++++- 1 file changed, 42 insertions(+), 1 deletion(-) diff --git a/tests/test_cli/test_get_address.py b/tests/test_cli/test_get_address.py index 6b340aff28..6b9f40be4f 100644 --- a/tests/test_cli/test_get_address.py +++ b/tests/test_cli/test_get_address.py @@ -17,7 +17,6 @@ # # ------------------------------------------------------------------------------ """This test module contains the tests for commands in aea.cli.get_address module.""" - from unittest import TestCase, mock from unittest.mock import MagicMock @@ -25,6 +24,8 @@ from aea.cli import cli from aea.cli.get_address import _try_get_address +from aea.configurations.constants import DEFAULT_LEDGER +from aea.test_tools.test_cases import AEATestCaseEmpty from tests.conftest import CLI_LOG_OPTION, COSMOS_ADDRESS_ONE, CliRunner from tests.test_cli.tools_for_testing import ContextMock @@ -67,3 +68,43 @@ def test_run_positive(self, *mocks): standalone_mode=False, ) self.assertEqual(result.exit_code, 0) + + +class TestGetAddressCommand(AEATestCaseEmpty): + """Test 'get-address' command.""" + + @classmethod + def setup_class(cls) -> None: + """ + Override the 'setup_class' method. + + This will prevent setup of tests at class-level. + """ + + def setup(self): + """Set up the test.""" + super().setup_class() + + def test_get_address(self, password_or_none): + """Run the main test.""" + self.generate_private_key(password=password_or_none) + self.add_private_key(password=password_or_none) + + password_option = ["--password", password_or_none] if password_or_none else [] + result = self.run_cli_command( + "get-address", DEFAULT_LEDGER, *password_option, cwd=self._get_cwd() + ) + + assert result.exit_code == 0 + + def teardown(self) -> None: + """Tear down the test.""" + super().teardown_class() + + @classmethod + def teardown_class(cls) -> None: + """ + Override the 'teardown_class' method. + + This will prevent teardown of tests at class-level. + """ From da640d9bdec58995aa1839d61a85aef993e0a5af Mon Sep 17 00:00:00 2001 From: MarcoFavorito Date: Wed, 19 May 2021 09:50:50 +0200 Subject: [PATCH 085/147] fix: return type missing --- aea/test_tools/test_cases.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aea/test_tools/test_cases.py b/aea/test_tools/test_cases.py index ac8af5693f..3a7ed74e38 100644 --- a/aea/test_tools/test_cases.py +++ b/aea/test_tools/test_cases.py @@ -956,7 +956,7 @@ def teardown_class(cls) -> None: cls._is_teardown_class_called = True -def _get_password_option_args(password: Optional[str]): +def _get_password_option_args(password: Optional[str]) -> List[str]: """ Get password option arguments. From 1c6a80beadea3f41648c105304290a31adfac0fc Mon Sep 17 00:00:00 2001 From: MarcoFavorito Date: Wed, 19 May 2021 09:56:17 +0200 Subject: [PATCH 086/147] test: use pattern instead of os.path.join for 'test_fail_no_item' --- tests/test_cli/test_push.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/tests/test_cli/test_push.py b/tests/test_cli/test_push.py index fd0d1da985..3bb54433dc 100644 --- a/tests/test_cli/test_push.py +++ b/tests/test_cli/test_push.py @@ -18,7 +18,6 @@ # ------------------------------------------------------------------------------ """Test module for Registry push methods.""" import filecmp -import os.path from unittest import TestCase, mock import pytest @@ -92,12 +91,12 @@ def test_fail_no_item( self, *mocks, ): """Test fail, item_not_exists .""" - expected_path_string = os.path.join( - r"\.", "vendor", "fetchai", "skills", "not_exists" + expected_path_pattern = ".*" + ".*".join( + ["vendor", "fetchai", "skills", "not_exists"] ) with pytest.raises( ClickException, - match=rf'Item "fetchai/not_exists" not found in source folder "{expected_path_string}"\.', + match=rf'Item "fetchai/not_exists" not found in source folder "{expected_path_pattern}"\.', ): self.invoke("push", "--local", "skill", "fetchai/not_exists") From e4fe341b3d833d61bf5a21c563d28917eeae7fb9 Mon Sep 17 00:00:00 2001 From: Marco Favorito Date: Wed, 19 May 2021 11:15:36 +0200 Subject: [PATCH 087/147] Update aea/test_tools/test_cases.py Co-authored-by: David Minarsch --- aea/test_tools/test_cases.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aea/test_tools/test_cases.py b/aea/test_tools/test_cases.py index 3a7ed74e38..045b2b0147 100644 --- a/aea/test_tools/test_cases.py +++ b/aea/test_tools/test_cases.py @@ -682,7 +682,7 @@ def generate_wealth( Run from agent's directory. :param ledger_api_id: ledger API ID. - :param password: the password option. + :param password: the password. :return: Result """ From 69d57cf8e60bfad787995d7c9a6291c95d7d975c Mon Sep 17 00:00:00 2001 From: Marco Favorito Date: Wed, 19 May 2021 11:15:45 +0200 Subject: [PATCH 088/147] Update aea/test_tools/test_cases.py Co-authored-by: David Minarsch --- aea/test_tools/test_cases.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aea/test_tools/test_cases.py b/aea/test_tools/test_cases.py index 045b2b0147..3cbe5fa39b 100644 --- a/aea/test_tools/test_cases.py +++ b/aea/test_tools/test_cases.py @@ -591,7 +591,7 @@ def generate_private_key( :param ledger_api_id: ledger API ID. :param private_key_file: the private key file. - :param password: the password option. + :param password: the password. :return: Result """ From c28c99ad0db878ea35a2f89c813b455734438c27 Mon Sep 17 00:00:00 2001 From: ali Date: Wed, 19 May 2021 10:21:37 +0100 Subject: [PATCH 089/147] feat: soef fixture skeleton --- tests/common/docker_image.py | 52 ++++++++++++++++++++++++++++++++++++ tests/conftest.py | 17 ++++++++++++ 2 files changed, 69 insertions(+) diff --git a/tests/common/docker_image.py b/tests/common/docker_image.py index 2c35b780de..5e716c6a37 100644 --- a/tests/common/docker_image.py +++ b/tests/common/docker_image.py @@ -337,6 +337,58 @@ def wait(self, max_attempts: int = 15, sleep_rate: float = 1.0) -> bool: return False +class SOEFDockerImage(DockerImage): + """Wrapper to SOEF Docker image.""" + + def __init__( + self, + client: DockerClient, + addr: str, + port: int = 9002, + ): + """ + Initialize the SOEF Docker image. + + :param client: the Docker client. + :param addr: the address. + :param port: the port. + """ + super().__init__(client) + self._addr = addr + self._port = port + + @property + def tag(self) -> str: + """Get the image tag.""" + return "fetchai/soef:latest" + + def _make_ports(self) -> Dict: + """Make ports dictionary for Docker.""" + return {f"{self._port}/tcp": ("0.0.0.0", self._port)} # nosec + + def create(self) -> Container: + """Create the container.""" + container = self._client.containers.run( + self.tag, + detach=True, + ports=self._make_ports() + ) + return container + + def wait(self, max_attempts: int = 15, sleep_rate: float = 1.0) -> bool: + """Wait until the image is up.""" + request = dict(jsonrpc=2.0, method="web3_clientVersion", params=[], id=1) + for i in range(max_attempts): + try: + response = requests.post(f"{self._addr}:{self._port}", json=request) + enforce(response.status_code == 200, "") + return True + except Exception: + logger.info(f"Attempt {i} failed. Retrying in {sleep_rate} seconds...") + time.sleep(sleep_rate) + return False + + class FetchLedgerDockerImage(DockerImage): """Wrapper to Fetch ledger Docker image.""" diff --git a/tests/conftest.py b/tests/conftest.py index 5f98fa6a19..33ed0f6ba2 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -109,6 +109,7 @@ FetchLedgerDockerImage, GanacheDockerImage, OEFSearchDockerImage, + SOEFDockerImage, ) from tests.data.dummy_connection.connection import DummyConnection # type: ignore @@ -730,6 +731,22 @@ def ganache( yield from _launch_image(image, timeout=timeout, max_attempts=max_attempts) +@pytest.mark.integration +@pytest.fixture(scope="function") +def soef( + soef_addr: str = "http://127.0.0.1", + soef_port: int = 9002, + timeout: float = 2.0, + max_attempts: int = 10, +): + """Launch the Ganache image.""" + client = docker.from_env() + image = SOEFDockerImage( + client, soef_addr, soef_port + ) + yield from _launch_image(image, timeout=timeout, max_attempts=max_attempts) + + @pytest.mark.integration @pytest.mark.ledger @pytest.fixture(scope="session") From 02e313ddd8ae96eddd94f5c3120395093c2bbba3 Mon Sep 17 00:00:00 2001 From: James Riehl Date: Thu, 20 May 2021 15:09:17 +0100 Subject: [PATCH 090/147] feat: upgrade oracle and oracle client contracts --- .../contracts/oracle/build/oracle.wasm | Bin 193389 -> 196065 bytes .../oracle_client/build/oracle_client.wasm | Bin 194437 -> 163649 bytes .../contracts/oracle_client/contract.py | 3 ++- .../contracts/oracle_client/contract.yaml | 2 +- .../skills/simple_oracle_client/behaviours.py | 1 + .../skills/simple_oracle_client/skill.yaml | 5 +++-- .../skills/simple_oracle_client/strategy.py | 6 ++++++ 7 files changed, 13 insertions(+), 4 deletions(-) mode change 100644 => 100755 packages/fetchai/contracts/oracle/build/oracle.wasm mode change 100644 => 100755 packages/fetchai/contracts/oracle_client/build/oracle_client.wasm diff --git a/packages/fetchai/contracts/oracle/build/oracle.wasm b/packages/fetchai/contracts/oracle/build/oracle.wasm old mode 100644 new mode 100755 index 07de37f73706c1b4e58111d96aecb8accd21da49..8a40e1da153799dd62df68f4a543b1e469463304 GIT binary patch literal 196065 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 NN75_>aS1=s^o94~c83XQQ5%JS}VO2(4UaL7cR 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 diff --git a/packages/fetchai/contracts/oracle_client/build/oracle_client.wasm b/packages/fetchai/contracts/oracle_client/build/oracle_client.wasm old mode 100644 new mode 100755 index 7c3eea5b0562859080841d14c28715782cc77f47..56ef0fa4af12679b4712b9ca7f05158138359ad7 GIT binary patch literal 163649 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

c7a(l~mSyCO>vs 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 diff --git a/packages/fetchai/contracts/oracle_client/contract.py b/packages/fetchai/contracts/oracle_client/contract.py index 9fd09a6cd5..9a193676e4 100644 --- a/packages/fetchai/contracts/oracle_client/contract.py +++ b/packages/fetchai/contracts/oracle_client/contract.py @@ -48,6 +48,7 @@ def get_query_transaction( contract_address: Address, from_address: Address, query_function: str, + amount: int = 0, gas: int = 0, tx_fee: int = 0, ) -> JSONLike: @@ -84,7 +85,7 @@ def get_query_transaction( from_address, contract_address, msg, - amount=1000000000000, + amount=amount, tx_fee=tx_fee, gas=gas, ) diff --git a/packages/fetchai/contracts/oracle_client/contract.yaml b/packages/fetchai/contracts/oracle_client/contract.yaml index 33db8feac1..1c93bb4fd7 100644 --- a/packages/fetchai/contracts/oracle_client/contract.yaml +++ b/packages/fetchai/contracts/oracle_client/contract.yaml @@ -8,7 +8,7 @@ aea_version: '>=1.0.0, <2.0.0' fingerprint: README.md: QmUBEziYn29b61higYFNsSxtnKcSNq7CRFpsQ68WjmTCcN __init__.py: QmRmf3Q5F6nQMrTEt59suBNw6zfgTVQX68E7CAuKmCnhAM - contract.py: QmeTKCAegHoi81oghXZ6aghtyBceygxK1Ue6cTo4B6v91J + contract.py: QmXtiTp3hQ87aXRPT5ivMEJPZyshKaAZkcNWPXrmt766N5 contracts/FetchOracleTestClient.sol: QmWpUJ4aBrNreiyuXe6EgfSfE7T7hWz3xHDdT7fFye3WCG fingerprint_ignore_patterns: - build/* diff --git a/packages/fetchai/skills/simple_oracle_client/behaviours.py b/packages/fetchai/skills/simple_oracle_client/behaviours.py index bf5f998e65..9243f06931 100644 --- a/packages/fetchai/skills/simple_oracle_client/behaviours.py +++ b/packages/fetchai/skills/simple_oracle_client/behaviours.py @@ -174,6 +174,7 @@ def _request_query_transaction(self) -> None: { "from_address": self.context.agent_address, "query_function": strategy.query_function, + "amount": strategy.query_oracle_fee, "gas": strategy.default_gas_query, } ), diff --git a/packages/fetchai/skills/simple_oracle_client/skill.yaml b/packages/fetchai/skills/simple_oracle_client/skill.yaml index b1757a08ed..06d3126cf9 100644 --- a/packages/fetchai/skills/simple_oracle_client/skill.yaml +++ b/packages/fetchai/skills/simple_oracle_client/skill.yaml @@ -9,10 +9,10 @@ aea_version: '>=1.0.0, <2.0.0' fingerprint: README.md: QmRqzw2aTE6rsUnoB8XvvWoF4sg4ZUpvgQwStZpAVG5sUj __init__.py: QmQPTQNhdgQSXmK4GG3XXBYZtFwKWwigdPAETFc3pMFj2b - behaviours.py: QmQdLKpeUdXumdtmLuJmK2Ji4CebEAR4stxdfTEjsJENdx + behaviours.py: QmYrAKttGKRgvkZAX2qJ5DT9VY3LAu7aWd3vT1FKEgUQCb dialogues.py: QmR1KyLjLzHGGnYtLbshvnftsTRuCEPXjs5PzJswjU4hNV handlers.py: QmesNKU9no8vivyC6KCaFenBEp33v6yzn8LCQg7zWhDFBG - strategy.py: QmP5Q6STwQzDEsPMhpzLoRjpj3qNGrdZX8VzVxVv1PZmDL + strategy.py: QmWuAsBWq1URbxmpzDmdBZFoKouRueJ8wAmSJgJqDXUzoG fingerprint_ignore_patterns: [] contracts: - fetchai/fet_erc20:0.8.0 @@ -59,6 +59,7 @@ models: ledger_id: null oracle_contract_address: null query_function: null + query_oracle_fee: 1000000000000 class_name: Strategy dependencies: aea-ledger-ethereum: diff --git a/packages/fetchai/skills/simple_oracle_client/strategy.py b/packages/fetchai/skills/simple_oracle_client/strategy.py index 45d14eaa66..253c11cacf 100644 --- a/packages/fetchai/skills/simple_oracle_client/strategy.py +++ b/packages/fetchai/skills/simple_oracle_client/strategy.py @@ -44,6 +44,7 @@ def __init__(self, **kwargs: Any) -> None: self._client_contract_address = kwargs.pop("client_contract_address", None) self._erc20_address = kwargs.pop("erc20_address", None) self._query_function = kwargs.pop("query_function", None) + self._query_oracle_fee = kwargs.pop("query_oracle_fee", 0) self._default_gas_deploy = kwargs.pop("default_gas_deploy", 0) self._default_gas_query = kwargs.pop("default_gas_query", 0) self._default_gas_approve = kwargs.pop("default_gas_approve", 0) @@ -66,6 +67,11 @@ def query_function(self) -> str: """Get the name of the oracle value query function.""" return self._query_function + @property + def query_oracle_fee(self) -> str: + """Get the fee amount for querying the oracle contract.""" + return self._query_oracle_fee + @property def default_gas_deploy(self) -> str: """Get the default gas for deploying a contract.""" From 7d76c519b18cc364693365d298f7f1fa1faafa20 Mon Sep 17 00:00:00 2001 From: James Riehl Date: Thu, 20 May 2021 15:13:54 +0100 Subject: [PATCH 091/147] chore: generate ipfs hashes --- packages/hashes.csv | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/hashes.csv b/packages/hashes.csv index 048cf7cd25..f42d717af8 100644 --- a/packages/hashes.csv +++ b/packages/hashes.csv @@ -53,8 +53,8 @@ fetchai/connections/webhook,QmQn8vSouUJrjzH7SNj148jRRDK3snRDMHMkB5GDHWBbMP fetchai/connections/yoti,QmS1J1WJQBgr847tT6eesbbU4G4UBWNb54RWzmBTFgZbrE fetchai/contracts/erc1155,QmZGci8V8dbWZuQJZk1kX2Ziod2WwkriiKpVz8CFiH2p3h fetchai/contracts/fet_erc20,QmSWRCQAfvW8jWCohyGnuYewyLeAsdn9HCGNzEExLSNgXx -fetchai/contracts/oracle,QmYhwLgYhvfZBeU1RSPQvBjekzpKMd7FMhChHgHP28JKZM -fetchai/contracts/oracle_client,QmPP1Ucdz9z8cdbChArdPYLsC1pxYY82UFRZRsun1UDvyv +fetchai/contracts/oracle,QmaGgr3vdgKdT5NHxYR4KQLtrGgorqJm7vkCmeVPVseBPi +fetchai/contracts/oracle_client,QmWHkC13YjAYYbUmXiovy5qj9fYgmGL1E2DQBB4cvZdiw7 fetchai/contracts/scaffold,QmZuYdqJtxhKQU4sDHjaarya9tF6nctRFNZqoiF7WsgbS9 fetchai/contracts/staking_erc20,QmVJZpvNmgVYWmD11Br8uytKVvYNSm6zHyrHdBNvK5Ag7s fetchai/protocols/aggregation,Qmf1cCWdpFKGUp3jZubQbFxQd5iDTWNVX2BEBTCAwmGGoG @@ -99,7 +99,7 @@ fetchai/skills/simple_aggregation,QmSDvt7i6bjmfJYJD6rD32KrJYJxFsqorG7pfiY5ue9R5q fetchai/skills/simple_buyer,QmPAVhTzgUwvMBYZvxKbHpKs7chKv6BUEaWSMCTLVAD8ia fetchai/skills/simple_data_request,QmerBkpLG4P2LPvXTQ6VkZxZE4DGXuSs8RujvTZL9nYk2J fetchai/skills/simple_oracle,QmSXnFHrYP3LUdrnMcQtkMuoXhKH3NJ8fZGef96RHGRX47 -fetchai/skills/simple_oracle_client,QmUNDGJEczurkhpEgNbuiQLjkyNpM1S4g3Kc5BNRf6MDR9 +fetchai/skills/simple_oracle_client,QmXdBNUgUFPFAyyFFRrsQxXftF6haKhA4rFsa7R6wfpXt8 fetchai/skills/simple_seller,QmcLVFyejLjJRZdR7PkpKGz3uFUBXTPfCAa3N3HR2NQu9Y fetchai/skills/simple_service_registration,QmRqH46RtqU693NZFqsjU9QS9RrRQNYhv6vEKZ7XhU7gVb fetchai/skills/simple_service_search,QmargbxeoFVqCxqxNpbWTr2byDKH32Tyw6iutLUpa63UF6 From bcf080fad1ac60ec03dc6139a6f2a3ab08af49ef Mon Sep 17 00:00:00 2001 From: ali Date: Fri, 21 May 2021 16:54:28 +0100 Subject: [PATCH 092/147] tests:erc1155 skills --- .coveragerc | 2 - .../fetchai/skills/erc1155_client/handlers.py | 2 +- .../fetchai/skills/erc1155_client/skill.yaml | 2 +- .../fetchai/skills/erc1155_deploy/handlers.py | 4 +- .../fetchai/skills/erc1155_deploy/skill.yaml | 2 +- packages/hashes.csv | 4 +- .../test_erc1155_client/__init__.py | 20 + .../test_erc1155_client/intermediate_class.py | 216 +++ .../test_erc1155_client/test_behaviours.py | 85 ++ .../test_erc1155_client/test_dialogues.py | 187 +++ .../test_erc1155_client/test_handlers.py | 850 ++++++++++++ .../test_erc1155_client/test_strategy.py | 80 ++ .../test_erc1155_deploy/__init__.py | 20 + .../test_erc1155_deploy/intermediate_class.py | 347 +++++ .../test_erc1155_deploy/test_behaviours.py | 415 ++++++ .../test_erc1155_deploy/test_dialogues.py | 219 +++ .../test_erc1155_deploy/test_handlers.py | 1171 +++++++++++++++++ .../test_erc1155_deploy/test_strategy.py | 214 +++ 18 files changed, 3831 insertions(+), 9 deletions(-) create mode 100644 tests/test_packages/test_skills/test_erc1155_client/__init__.py create mode 100644 tests/test_packages/test_skills/test_erc1155_client/intermediate_class.py create mode 100644 tests/test_packages/test_skills/test_erc1155_client/test_behaviours.py create mode 100644 tests/test_packages/test_skills/test_erc1155_client/test_dialogues.py create mode 100644 tests/test_packages/test_skills/test_erc1155_client/test_handlers.py create mode 100644 tests/test_packages/test_skills/test_erc1155_client/test_strategy.py create mode 100644 tests/test_packages/test_skills/test_erc1155_deploy/__init__.py create mode 100644 tests/test_packages/test_skills/test_erc1155_deploy/intermediate_class.py create mode 100644 tests/test_packages/test_skills/test_erc1155_deploy/test_behaviours.py create mode 100644 tests/test_packages/test_skills/test_erc1155_deploy/test_dialogues.py create mode 100644 tests/test_packages/test_skills/test_erc1155_deploy/test_handlers.py create mode 100644 tests/test_packages/test_skills/test_erc1155_deploy/test_strategy.py diff --git a/.coveragerc b/.coveragerc index 21a2be6fe7..274e6c0c04 100644 --- a/.coveragerc +++ b/.coveragerc @@ -4,8 +4,6 @@ omit = packages/fetchai/contracts/* packages/fetchai/skills/aries_alice/* packages/fetchai/skills/aries_faber/* - packages/fetchai/skills/erc1155_client/* - packages/fetchai/skills/erc1155_deploy/* packages/fetchai/skills/gym/* packages/fetchai/skills/fipa_dummy_buyer/* plugins/aea-ledger-cosmos/aea_ledger_cosmos/cosmos.py \ No newline at end of file diff --git a/packages/fetchai/skills/erc1155_client/handlers.py b/packages/fetchai/skills/erc1155_client/handlers.py index 56712bc472..c435dfc53c 100644 --- a/packages/fetchai/skills/erc1155_client/handlers.py +++ b/packages/fetchai/skills/erc1155_client/handlers.py @@ -440,7 +440,7 @@ def _handle_error( :param contract_api_dialogue: the ledger api dialogue """ self.context.logger.info( - "received ledger_api error message={} in dialogue={}.".format( + "received contract_api error message={} in dialogue={}.".format( contract_api_msg, contract_api_dialogue ) ) diff --git a/packages/fetchai/skills/erc1155_client/skill.yaml b/packages/fetchai/skills/erc1155_client/skill.yaml index 158c4ff3dd..11f5c45cac 100644 --- a/packages/fetchai/skills/erc1155_client/skill.yaml +++ b/packages/fetchai/skills/erc1155_client/skill.yaml @@ -11,7 +11,7 @@ fingerprint: __init__.py: QmerzWHZ6jU4pH71xDJqpjavxQxhWzZvkc1wqNHUCzhoog behaviours.py: QmZSUiAqMwqUb9JhcF8X4DuEVnGDtSUVbTMQoo3nnXSpVS dialogues.py: QmYGjWc223Nu1warTtVFVpSCJKexgqwu6sFRaUsV7LwCPJ - handlers.py: QmfAWpT4MC9SBwTFmfSEAa9dCxoBE9EDJjo63rmHnxXKFG + handlers.py: QmSimvAHLwz8oqF61VRPWnSih2DCDUgEzVfViNmuzddQYS strategy.py: QmavxySeUqdFjMY9Gkk4d8gWzKo5FkNmRbbGPvCAVRn7gf fingerprint_ignore_patterns: [] connections: diff --git a/packages/fetchai/skills/erc1155_deploy/handlers.py b/packages/fetchai/skills/erc1155_deploy/handlers.py index d85b2eda2b..370cd4ae84 100644 --- a/packages/fetchai/skills/erc1155_deploy/handlers.py +++ b/packages/fetchai/skills/erc1155_deploy/handlers.py @@ -363,7 +363,7 @@ def _handle_transaction_receipt(self, ledger_api_msg: LedgerApiMessage) -> None: elif strategy.is_tokens_minted: self.context.is_active = False self.context.logger.info("demo finished!") - else: + else: # pragma: no cover self.context.logger.error("unexpected transaction receipt!") else: self.context.logger.error( @@ -502,7 +502,7 @@ def _handle_error( :param contract_api_dialogue: the ledger api dialogue """ self.context.logger.info( - "received ledger_api error message={} in dialogue={}.".format( + "received contract_api error message={} in dialogue={}.".format( contract_api_msg, contract_api_dialogue ) ) diff --git a/packages/fetchai/skills/erc1155_deploy/skill.yaml b/packages/fetchai/skills/erc1155_deploy/skill.yaml index 25ccadcf9c..9e6ffebaa3 100644 --- a/packages/fetchai/skills/erc1155_deploy/skill.yaml +++ b/packages/fetchai/skills/erc1155_deploy/skill.yaml @@ -11,7 +11,7 @@ fingerprint: __init__.py: Qmd8QvPShzk1graZuA6t1bcRMAZ3XhHu6QnoqbSrrrKn8e behaviours.py: QmRZg5QSeDxk97spB4iyaPtjGuNQzMovcTq4LemxEVnTmd dialogues.py: QmU1T8Ag8kTTqhL2vPsMBd5eb6siZMkyGsTHsVHGBbLktP - handlers.py: QmbGvccjnJbp1JdyVBG7ppkdQsMA1ChMym3wEmp6YsHNcp + handlers.py: QmSeTHvMviDVQzyZpcHbNk2d1iDuJoNKutAzWnVQHv14bp strategy.py: QmRDcYs2XBT4TcrhMi81Vhg5oEQExQzkdz3dU2vmZwsRCs fingerprint_ignore_patterns: [] connections: diff --git a/packages/hashes.csv b/packages/hashes.csv index 048cf7cd25..3e64299f5c 100644 --- a/packages/hashes.csv +++ b/packages/hashes.csv @@ -82,8 +82,8 @@ fetchai/skills/confirmation_aw1,QmVkf5oHohbx5vSfY269u1MUS1c8ttkDZAgfGcnijz5AcJ fetchai/skills/confirmation_aw2,QmZ8sKX3TCLB6yzpfSEWw3R8VbWrFEt2tewqvosUNm6Vc9 fetchai/skills/confirmation_aw3,QmQ5mdPyf3ryxutuapQAS1crL8kfU16o3VXHawcpQMdoKo fetchai/skills/echo,QmZ3B4XeTuWtRNke4Q7QBuxu3aZ8duXosVbZn72RW2cdQa -fetchai/skills/erc1155_client,QmSDawkPZSiqJAAbWUcsKnpzmekVLLH9DnemDD2E8EE9Ec -fetchai/skills/erc1155_deploy,QmRHurQ2ThephQxJ8hGwCmqdfzkig94Dx3UNndNZLU8pd4 +fetchai/skills/erc1155_client,QmZkZ2EhZiVLgpWvPu5TbzBZ5v8ss51WmDuCXvjeKLt8mH +fetchai/skills/erc1155_deploy,QmaESS4NoCCToboXG6kPbBrfFDoMzBUiPyX2yfvFSoJTKP fetchai/skills/error,QmfEkTwToeAL6QnzzXBocp33kMCvzyP6F3JGUKjqFYmgHH fetchai/skills/fetch_beacon,QmfBdgU21fgA7uH1sGSLk6jF8WShfZvfEhFbr92FuA9ide fetchai/skills/fipa_dummy_buyer,QmTPUvqyf4xzfEiQS3mbMaUu56rDoz1hk427EhHu45Ygio diff --git a/tests/test_packages/test_skills/test_erc1155_client/__init__.py b/tests/test_packages/test_skills/test_erc1155_client/__init__.py new file mode 100644 index 0000000000..92f10f9ed2 --- /dev/null +++ b/tests/test_packages/test_skills/test_erc1155_client/__init__.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# ------------------------------------------------------------------------------ +# +# Copyright 2018-2019 Fetch.AI Limited +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ------------------------------------------------------------------------------ + +"""The tests module contains the tests of the packages/skills/erc1155_client dir.""" diff --git a/tests/test_packages/test_skills/test_erc1155_client/intermediate_class.py b/tests/test_packages/test_skills/test_erc1155_client/intermediate_class.py new file mode 100644 index 0000000000..84acfa971a --- /dev/null +++ b/tests/test_packages/test_skills/test_erc1155_client/intermediate_class.py @@ -0,0 +1,216 @@ +# -*- coding: utf-8 -*- +# ------------------------------------------------------------------------------ +# +# Copyright 2018-2019 Fetch.AI Limited +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ------------------------------------------------------------------------------ +"""This module sets up test environment for erc1155_client skill.""" + +from pathlib import Path +from typing import cast + +from aea.helpers.search.models import ( + Attribute, + Constraint, + ConstraintType, + DataModel, + Description, + Query, +) +from aea.helpers.transaction.base import RawMessage, RawTransaction, Terms +from aea.protocols.dialogue.base import DialogueMessage +from aea.test_tools.test_skill import BaseSkillTestCase + +from packages.fetchai.protocols.contract_api.custom_types import Kwargs +from packages.fetchai.protocols.contract_api.message import ContractApiMessage +from packages.fetchai.protocols.fipa.message import FipaMessage +from packages.fetchai.protocols.ledger_api.message import LedgerApiMessage +from packages.fetchai.protocols.oef_search.message import OefSearchMessage +from packages.fetchai.protocols.signing.message import SigningMessage +from packages.fetchai.skills.erc1155_client.behaviours import SearchBehaviour +from packages.fetchai.skills.erc1155_client.dialogues import ( + ContractApiDialogues, + DefaultDialogues, + FipaDialogues, + LedgerApiDialogues, + OefSearchDialogues, + SigningDialogues, +) +from packages.fetchai.skills.erc1155_client.handlers import ( + ContractApiHandler, + FipaHandler, + LedgerApiHandler, + OefSearchHandler, + SigningHandler, +) +from packages.fetchai.skills.erc1155_client.strategy import Strategy + +from tests.conftest import ROOT_DIR + + +class ERC1155ClientTestCase(BaseSkillTestCase): + """Sets the erc1155_client class up for testing.""" + + path_to_skill = Path(ROOT_DIR, "packages", "fetchai", "skills", "erc1155_client") + + @classmethod + def setup(cls): + """Setup the test class.""" + cls.location = {"longitude": 0.1270, "latitude": 51.5194} + cls.search_query = { + "search_key": "seller_service", + "search_value": "erc1155_contract", + "constraint_type": "==", + } + cls.search_radius = 5.0 + config_overrides = { + "models": { + "strategy": { + "args": { + "location": cls.location, + "search_query": cls.search_query, + "search_radius": cls.search_radius, + } + } + }, + } + + super().setup(config_overrides=config_overrides) + + # behaviours + cls.search_behaviour = cast( + SearchBehaviour, cls._skill.skill_context.behaviours.search + ) + + # dialogues + cls.contract_api_dialogues = cast( + ContractApiDialogues, cls._skill.skill_context.contract_api_dialogues + ) + cls.default_dialogues = cast( + DefaultDialogues, cls._skill.skill_context.default_dialogues + ) + cls.fipa_dialogues = cast( + FipaDialogues, cls._skill.skill_context.fipa_dialogues + ) + cls.ledger_api_dialogues = cast( + LedgerApiDialogues, cls._skill.skill_context.ledger_api_dialogues + ) + cls.oef_search_dialogues = cast( + OefSearchDialogues, cls._skill.skill_context.oef_search_dialogues + ) + cls.signing_dialogues = cast( + SigningDialogues, cls._skill.skill_context.signing_dialogues + ) + + # handlers + cls.fipa_handler = cast(FipaHandler, cls._skill.skill_context.handlers.fipa) + cls.oef_search_handler = cast( + OefSearchHandler, cls._skill.skill_context.handlers.oef_search + ) + cls.contract_api_handler = cast( + ContractApiHandler, cls._skill.skill_context.handlers.contract_api + ) + cls.signing_handler = cast( + SigningHandler, cls._skill.skill_context.handlers.signing + ) + cls.ledger_api_handler = cast( + LedgerApiHandler, cls._skill.skill_context.handlers.ledger_api + ) + + # models + cls.strategy = cast(Strategy, cls._skill.skill_context.strategy) + + cls.logger = cls._skill.skill_context.logger + + # mocked objects + cls.ledger_id = "some_ledger_id" + cls.contract_id = "some_contract_id" + cls.contract_address = "some_contract_address" + cls.callable = "some_callable" + cls.body = {"some_key": "some_value"} + cls.kwargs = Kwargs(cls.body) + cls.address = "some_address" + cls.mocked_terms = Terms( + cls.ledger_id, + cls._skill.skill_context.agent_address, + "counterprty", + {"currency_id": 50}, + {"good_id": -10}, + "some_nonce", + ) + cls.mocked_query = Query( + [Constraint("some_attribute_name", ConstraintType("==", "some_value"))], + DataModel( + "some_data_model_name", + [ + Attribute( + "some_attribute_name", + str, + False, + "Some attribute descriptions.", + ) + ], + ), + ) + cls.mocked_proposal = Description( + { + "contract_address": "some_contract_address", + "token_id": "123456", + "trade_nonce": "876438756348568", + "from_supply": "543", + "to_supply": "432", + "value": "67", + } + ) + cls.mocked_raw_tx = (RawTransaction(cls.ledger_id, {"some_key": "some_value"}),) + cls.mocked_raw_msg = RawMessage(cls.ledger_id, b"some_body") + + # list of messages + cls.list_of_fipa_messages = ( + DialogueMessage(FipaMessage.Performative.CFP, {"query": cls.mocked_query}), + DialogueMessage( + FipaMessage.Performative.PROPOSE, {"proposal": cls.mocked_proposal} + ), + ) + cls.list_of_oef_search_messages = ( + DialogueMessage( + OefSearchMessage.Performative.SEARCH_SERVICES, + {"query": cls.mocked_query}, + ), + ) + cls.list_of_contract_api_messages = ( + DialogueMessage( + ContractApiMessage.Performative.GET_RAW_MESSAGE, + { + "ledger_id": cls.ledger_id, + "contract_id": cls.contract_id, + "contract_address": cls.contract_address, + "callable": cls.callable, + "kwargs": cls.kwargs, + }, + ), + ) + cls.list_of_signing_messages = ( + DialogueMessage( + SigningMessage.Performative.SIGN_MESSAGE, + {"terms": cls.mocked_terms, "raw_message": cls.mocked_raw_msg}, + ), + ) + cls.list_of_ledger_api_messages = ( + DialogueMessage( + LedgerApiMessage.Performative.GET_BALANCE, + {"ledger_id": cls.ledger_id, "address": "some_address"}, + ), + ) diff --git a/tests/test_packages/test_skills/test_erc1155_client/test_behaviours.py b/tests/test_packages/test_skills/test_erc1155_client/test_behaviours.py new file mode 100644 index 0000000000..d774d6b081 --- /dev/null +++ b/tests/test_packages/test_skills/test_erc1155_client/test_behaviours.py @@ -0,0 +1,85 @@ +# -*- coding: utf-8 -*- +# ------------------------------------------------------------------------------ +# +# Copyright 2018-2019 Fetch.AI Limited +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ------------------------------------------------------------------------------ +"""This module contains the tests of the behaviour classes of the erc1155_client skill.""" + +from packages.fetchai.protocols.ledger_api.message import LedgerApiMessage +from packages.fetchai.protocols.oef_search.message import OefSearchMessage +from packages.fetchai.skills.erc1155_client.behaviours import LEDGER_API_ADDRESS + +from tests.test_packages.test_skills.test_erc1155_client.intermediate_class import ( + ERC1155ClientTestCase, +) + + +class TestSearchBehaviour(ERC1155ClientTestCase): + """Test search behaviour of erc1155_client.""" + + def test_setup(self): + """Test the setup method of the search behaviour.""" + # operation + self.search_behaviour.setup() + + # after + self.assert_quantity_in_outbox(1) + has_attributes, error_str = self.message_has_attributes( + actual_message=self.get_message_from_outbox(), + message_type=LedgerApiMessage, + performative=LedgerApiMessage.Performative.GET_BALANCE, + to=LEDGER_API_ADDRESS, + sender=str(self.skill.public_id), + ledger_id=self.strategy.ledger_id, + address=self.skill.skill_context.agent_address, + ) + assert has_attributes, error_str + + def test_act_is_searching(self): + """Test the act method of the search behaviour where is_searching is True.""" + # setup + self.strategy._is_searching = True + + # operation + self.search_behaviour.act() + + # after + self.assert_quantity_in_outbox(1) + has_attributes, error_str = self.message_has_attributes( + actual_message=self.get_message_from_outbox(), + message_type=OefSearchMessage, + performative=OefSearchMessage.Performative.SEARCH_SERVICES, + to=self.skill.skill_context.search_service_address, + sender=str(self.skill.public_id), + query=self.skill.skill_context.strategy.get_location_and_service_query(), + ) + assert has_attributes, error_str + + def test_act_not_is_searching(self): + """Test the act method of the search behaviour where is_searching is False.""" + # setup + self.strategy.is_searching = False + + # operation + self.search_behaviour.act() + + # after + self.assert_quantity_in_outbox(0) + + def test_teardown(self): + """Test the teardown method of the search behaviour.""" + assert self.search_behaviour.teardown() is None + self.assert_quantity_in_outbox(0) diff --git a/tests/test_packages/test_skills/test_erc1155_client/test_dialogues.py b/tests/test_packages/test_skills/test_erc1155_client/test_dialogues.py new file mode 100644 index 0000000000..33199fea17 --- /dev/null +++ b/tests/test_packages/test_skills/test_erc1155_client/test_dialogues.py @@ -0,0 +1,187 @@ +# -*- coding: utf-8 -*- +# ------------------------------------------------------------------------------ +# +# Copyright 2018-2019 Fetch.AI Limited +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ------------------------------------------------------------------------------ +"""This module contains the tests of the dialogue classes of the erc1155_client skill.""" + +import pytest + +from aea.exceptions import AEAEnforceError +from aea.protocols.dialogue.base import DialogueLabel +from aea.test_tools.test_skill import COUNTERPARTY_AGENT_ADDRESS + +from packages.fetchai.protocols.contract_api.message import ContractApiMessage +from packages.fetchai.protocols.default.message import DefaultMessage +from packages.fetchai.protocols.fipa.message import FipaMessage +from packages.fetchai.protocols.ledger_api.message import LedgerApiMessage +from packages.fetchai.protocols.oef_search.message import OefSearchMessage +from packages.fetchai.protocols.signing.message import SigningMessage +from packages.fetchai.skills.erc1155_client.dialogues import ( + ContractApiDialogue, + DefaultDialogue, + FipaDialogue, + LedgerApiDialogue, + OefSearchDialogue, + SigningDialogue, +) + +from tests.test_packages.test_skills.test_erc1155_client.intermediate_class import ( + ERC1155ClientTestCase, +) + + +class TestDialogues(ERC1155ClientTestCase): + """Test dialogue classes of erc1155_client.""" + + def test_contract_api_dialogue(self): + """Test the ContractApiDialogue class.""" + contract_api_dialogue = ContractApiDialogue( + DialogueLabel( + ("", ""), + COUNTERPARTY_AGENT_ADDRESS, + self.skill.skill_context.agent_address, + ), + self.skill.skill_context.agent_address, + role=ContractApiDialogue.Role.AGENT, + ) + + # associated_fipa_dialogue + with pytest.raises(ValueError, match="Associated fipa dialogue not set!"): + assert contract_api_dialogue.associated_fipa_dialogue + fipa_dialogue = FipaDialogue( + DialogueLabel( + ("", ""), + COUNTERPARTY_AGENT_ADDRESS, + self.skill.skill_context.agent_address, + ), + self.skill.skill_context.agent_address, + role=FipaDialogue.Role.BUYER, + ) + contract_api_dialogue.associated_fipa_dialogue = fipa_dialogue + with pytest.raises( + AEAEnforceError, match="Associated fipa dialogue already set!" + ): + contract_api_dialogue.associated_fipa_dialogue = fipa_dialogue + assert contract_api_dialogue.associated_fipa_dialogue == fipa_dialogue + + # terms + with pytest.raises(ValueError, match="Terms not set!"): + assert contract_api_dialogue.terms + contract_api_dialogue.terms = self.mocked_terms + with pytest.raises(AEAEnforceError, match="Terms already set!"): + contract_api_dialogue.terms = self.mocked_terms + assert contract_api_dialogue.terms == self.mocked_terms + + def test_contract_api_dialogues(self): + """Test the ContractApiDialogues class.""" + _, dialogue = self.contract_api_dialogues.create( + counterparty=COUNTERPARTY_AGENT_ADDRESS, + performative=ContractApiMessage.Performative.GET_DEPLOY_TRANSACTION, + ledger_id=self.ledger_id, + contract_id=self.contract_id, + callable=self.callable, + kwargs=self.kwargs, + ) + assert dialogue.role == ContractApiDialogue.Role.AGENT + assert dialogue.self_address == str(self.skill.skill_context.skill_id) + + def test_default_dialogues(self): + """Test the DefaultDialogues class.""" + _, dialogue = self.default_dialogues.create( + counterparty=COUNTERPARTY_AGENT_ADDRESS, + performative=DefaultMessage.Performative.BYTES, + content=b"some_content", + ) + assert dialogue.role == DefaultDialogue.Role.AGENT + assert dialogue.self_address == self.skill.skill_context.agent_address + + def test_fipa_dialogues(self): + """Test the FipaDialogues class.""" + _, dialogue = self.fipa_dialogues.create( + counterparty=COUNTERPARTY_AGENT_ADDRESS, + performative=FipaMessage.Performative.CFP, + query=self.mocked_query, + ) + assert dialogue.role == FipaDialogue.Role.SELLER + assert dialogue.self_address == self.skill.skill_context.agent_address + + def test_ledger_api_dialogues(self): + """Test the LedgerApiDialogues class.""" + _, dialogue = self.ledger_api_dialogues.create( + counterparty=COUNTERPARTY_AGENT_ADDRESS, + performative=LedgerApiMessage.Performative.GET_BALANCE, + ledger_id=self.ledger_id, + address=self.address, + ) + assert dialogue.role == LedgerApiDialogue.Role.AGENT + assert dialogue.self_address == str(self.skill.skill_context.skill_id) + + def test_oef_search_dialogues(self): + """Test the OefSearchDialogues class.""" + _, dialogue = self.oef_search_dialogues.create( + counterparty=COUNTERPARTY_AGENT_ADDRESS, + performative=OefSearchMessage.Performative.SEARCH_SERVICES, + query=self.mocked_query, + ) + assert dialogue.role == OefSearchDialogue.Role.AGENT + assert dialogue.self_address == str(self.skill.skill_context.skill_id) + + def test_signing_dialogue(self): + """Test the SigningDialogue class.""" + signing_dialogue = SigningDialogue( + DialogueLabel( + ("", ""), + COUNTERPARTY_AGENT_ADDRESS, + self.skill.skill_context.agent_address, + ), + self.skill.skill_context.agent_address, + role=ContractApiDialogue.Role.AGENT, + ) + + # associated_contract_api_dialogue + with pytest.raises( + ValueError, match="Associated contract api dialogue not set!" + ): + assert signing_dialogue.associated_contract_api_dialogue + contract_api_dialogue = ContractApiDialogue( + DialogueLabel( + ("", ""), + COUNTERPARTY_AGENT_ADDRESS, + self.skill.skill_context.agent_address, + ), + self.skill.skill_context.agent_address, + role=ContractApiDialogue.Role.AGENT, + ) + signing_dialogue.associated_contract_api_dialogue = contract_api_dialogue + with pytest.raises( + AEAEnforceError, match="Associated contract api dialogue already set!" + ): + signing_dialogue.associated_contract_api_dialogue = contract_api_dialogue + assert ( + signing_dialogue.associated_contract_api_dialogue == contract_api_dialogue + ) + + def test_signing_dialogues(self): + """Test the SigningDialogues class.""" + _, dialogue = self.signing_dialogues.create( + counterparty=COUNTERPARTY_AGENT_ADDRESS, + performative=SigningMessage.Performative.SIGN_TRANSACTION, + terms=self.mocked_terms, + raw_transaction=self.mocked_raw_tx, + ) + assert dialogue.role == SigningDialogue.Role.SKILL + assert dialogue.self_address == str(self.skill.skill_context.skill_id) diff --git a/tests/test_packages/test_skills/test_erc1155_client/test_handlers.py b/tests/test_packages/test_skills/test_erc1155_client/test_handlers.py new file mode 100644 index 0000000000..dad7ba8f09 --- /dev/null +++ b/tests/test_packages/test_skills/test_erc1155_client/test_handlers.py @@ -0,0 +1,850 @@ +# -*- coding: utf-8 -*- +# ------------------------------------------------------------------------------ +# +# Copyright 2018-2019 Fetch.AI Limited +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ------------------------------------------------------------------------------ +"""This module contains the tests of the handler classes of the erc1155_client skill.""" + +import logging +from typing import cast +from unittest.mock import patch + +from aea.helpers.search.models import Description +from aea.helpers.transaction.base import RawMessage, State, Terms +from aea.test_tools.test_skill import COUNTERPARTY_AGENT_ADDRESS + +from packages.fetchai.protocols.contract_api.message import ContractApiMessage +from packages.fetchai.protocols.default.message import DefaultMessage +from packages.fetchai.protocols.fipa.message import FipaMessage +from packages.fetchai.protocols.ledger_api.message import LedgerApiMessage +from packages.fetchai.protocols.oef_search.message import OefSearchMessage +from packages.fetchai.protocols.signing.message import SigningMessage +from packages.fetchai.skills.erc1155_client.dialogues import ( + ContractApiDialogue, + FipaDialogue, + LedgerApiDialogue, + OefSearchDialogue, + SigningDialogue, +) +from packages.fetchai.skills.erc1155_client.handlers import LEDGER_API_ADDRESS + +from tests.test_packages.test_skills.test_erc1155_client.intermediate_class import ( + ERC1155ClientTestCase, +) + + +class TestFipaHandler(ERC1155ClientTestCase): + """Test fipa handler of erc1155_client.""" + + def test_setup(self): + """Test the setup method of the fipa handler.""" + assert self.fipa_handler.setup() is None + self.assert_quantity_in_outbox(0) + + def test_handle_unidentified_dialogue(self): + """Test the _handle_unidentified_dialogue method of the fipa handler.""" + # setup + incorrect_dialogue_reference = ("", "") + incoming_message = cast( + FipaMessage, + self.build_incoming_message( + message_type=FipaMessage, + dialogue_reference=incorrect_dialogue_reference, + performative=FipaMessage.Performative.ACCEPT, + ), + ) + + # operation + with patch.object(self.logger, "log") as mock_logger: + self.fipa_handler.handle(incoming_message) + + # after + mock_logger.assert_any_call( + logging.INFO, f"unidentified dialogue for message={incoming_message}.", + ) + + self.assert_quantity_in_outbox(1) + has_attributes, error_str = self.message_has_attributes( + actual_message=self.get_message_from_outbox(), + message_type=DefaultMessage, + performative=DefaultMessage.Performative.ERROR, + to=incoming_message.sender, + sender=self.skill.skill_context.agent_address, + error_code=DefaultMessage.ErrorCode.INVALID_DIALOGUE, + error_msg="Invalid dialogue.", + error_data={"fipa_message": incoming_message.encode()}, + ) + assert has_attributes, error_str + + def test_handle_propose_i(self): + """Test the _handle_propose method of the fipa handler where all expected keys exist in the proposal.""" + # setup + fipa_dialogue = cast( + FipaDialogue, + self.prepare_skill_dialogue( + dialogues=self.fipa_dialogues, messages=self.list_of_fipa_messages[:1], + ), + ) + incoming_message = cast( + FipaMessage, + self.build_incoming_message_for_skill_dialogue( + dialogue=fipa_dialogue, + performative=FipaMessage.Performative.PROPOSE, + proposal=self.mocked_proposal, + ), + ) + + # operation + with patch.object(self.logger, "log") as mock_logger: + self.fipa_handler.handle(incoming_message) + + # after + mock_logger.assert_any_call( + logging.INFO, + f"received valid PROPOSE from sender={COUNTERPARTY_AGENT_ADDRESS[-5:]}: proposal={incoming_message.proposal.values}", + ) + + self.assert_quantity_in_outbox(1) + message = self.get_message_from_outbox() + has_attributes, error_str = self.message_has_attributes( + actual_message=message, + message_type=ContractApiMessage, + performative=ContractApiMessage.Performative.GET_RAW_MESSAGE, + to=LEDGER_API_ADDRESS, + sender=str(self.skill.skill_context.skill_id), + ledger_id=self.strategy.ledger_id, + contract_id=self.strategy.contract_id, + contract_address=incoming_message.proposal.values["contract_address"], + callable="get_hash_single", + kwargs=ContractApiMessage.Kwargs( + { + "from_address": incoming_message.sender, + "to_address": self.skill.skill_context.agent_address, + "token_id": int(incoming_message.proposal.values["token_id"]), + "from_supply": int(incoming_message.proposal.values["from_supply"]), + "to_supply": int(incoming_message.proposal.values["to_supply"]), + "value": int(incoming_message.proposal.values["value"]), + "trade_nonce": int(incoming_message.proposal.values["trade_nonce"]), + } + ), + ) + assert has_attributes, error_str + + contract_api_dialogue = cast( + ContractApiDialogue, self.contract_api_dialogues.get_dialogue(message) + ) + + expected_terms = Terms( + ledger_id=self.strategy.ledger_id, + sender_address=self.skill.skill_context.agent_address, + counterparty_address=incoming_message.sender, + amount_by_currency_id={}, + quantities_by_good_id={ + str(incoming_message.proposal.values["token_id"]): int( + incoming_message.proposal.values["from_supply"] + ) + - int(incoming_message.proposal.values["to_supply"]) + }, + is_sender_payable_tx_fee=False, + nonce=str(incoming_message.proposal.values["trade_nonce"]), + ) + assert contract_api_dialogue.terms == expected_terms + assert contract_api_dialogue.associated_fipa_dialogue == fipa_dialogue + + mock_logger.assert_any_call( + logging.INFO, "requesting single hash message from contract api...", + ) + + def test_handle_propose_ii(self): + """Test the _handle_propose method of the fipa handler where some expected keys do NOT exist in the proposal.""" + # setup + invalid_proposal = Description({"some_key": "v1", "some_key_2": "12"}) + + fipa_dialogue = cast( + FipaDialogue, + self.prepare_skill_dialogue( + dialogues=self.fipa_dialogues, messages=self.list_of_fipa_messages[:1], + ), + ) + incoming_message = cast( + FipaMessage, + self.build_incoming_message_for_skill_dialogue( + dialogue=fipa_dialogue, + performative=FipaMessage.Performative.PROPOSE, + proposal=invalid_proposal, + ), + ) + + # operation + with patch.object(self.logger, "log") as mock_logger: + self.fipa_handler.handle(incoming_message) + + # after + mock_logger.assert_any_call( + logging.INFO, + f"received invalid PROPOSE from sender={COUNTERPARTY_AGENT_ADDRESS[-5:]}: proposal={incoming_message.proposal.values}", + ) + + def test_handle_invalid(self): + """Test the _handle_invalid method of the fipa handler.""" + # setup + fipa_dialogue = cast( + FipaDialogue, + self.prepare_skill_dialogue( + dialogues=self.fipa_dialogues, messages=self.list_of_fipa_messages[:2], + ), + ) + incoming_message = cast( + FipaMessage, + self.build_incoming_message_for_skill_dialogue( + dialogue=fipa_dialogue, performative=FipaMessage.Performative.ACCEPT, + ), + ) + + # operation + with patch.object(self.logger, "log") as mock_logger: + self.fipa_handler.handle(incoming_message) + + # after + mock_logger.assert_any_call( + logging.WARNING, + f"cannot handle fipa message of performative={incoming_message.performative} in dialogue={fipa_dialogue}.", + ) + + def test_teardown(self): + """Test the teardown method of the fipa handler.""" + assert self.fipa_handler.teardown() is None + self.assert_quantity_in_outbox(0) + + +class TestOefSearchHandler(ERC1155ClientTestCase): + """Test oef_search handler of erc1155_client.""" + + is_agent_to_agent_messages = False + + def test_setup(self): + """Test the setup method of the oef_search handler.""" + assert self.oef_search_handler.setup() is None + self.assert_quantity_in_outbox(0) + + def test_handle_unidentified_dialogue(self): + """Test the _handle_unidentified_dialogue method of the oef_search handler.""" + # setup + incorrect_dialogue_reference = ("", "") + incoming_message = cast( + OefSearchMessage, + self.build_incoming_message( + message_type=OefSearchMessage, + dialogue_reference=incorrect_dialogue_reference, + performative=OefSearchMessage.Performative.OEF_ERROR, + oef_error_operation=OefSearchMessage.OefErrorOperation.REGISTER_SERVICE, + ), + ) + + # operation + with patch.object(self.logger, "log") as mock_logger: + self.oef_search_handler.handle(incoming_message) + + # after + mock_logger.assert_any_call( + logging.INFO, + f"received invalid oef_search message={incoming_message}, unidentified dialogue.", + ) + + def test_handle_error(self): + """Test the _handle_error method of the oef_search handler.""" + # setup + oef_search_dialogue = cast( + OefSearchDialogue, + self.prepare_skill_dialogue( + dialogues=self.oef_search_dialogues, + messages=self.list_of_oef_search_messages[:1], + ), + ) + incoming_message = cast( + OefSearchMessage, + self.build_incoming_message_for_skill_dialogue( + dialogue=oef_search_dialogue, + performative=OefSearchMessage.Performative.OEF_ERROR, + oef_error_operation=OefSearchMessage.OefErrorOperation.REGISTER_SERVICE, + ), + ) + + # operation + with patch.object(self.logger, "log") as mock_logger: + self.oef_search_handler.handle(incoming_message) + + # after + mock_logger.assert_any_call( + logging.INFO, + f"received oef_search error message={incoming_message} in dialogue={oef_search_dialogue}.", + ) + + def test_handle_search_i(self): + """Test the _handle_search method of the oef_search handler where the number of agents found is NOT 0.""" + # setup + agents = ("agent_1", "agent_2") + oef_search_dialogue = cast( + OefSearchDialogue, + self.prepare_skill_dialogue( + dialogues=self.oef_search_dialogues, + messages=self.list_of_oef_search_messages[:1], + ), + ) + incoming_message = cast( + OefSearchMessage, + self.build_incoming_message_for_skill_dialogue( + dialogue=oef_search_dialogue, + performative=OefSearchMessage.Performative.SEARCH_RESULT, + agents=agents, + agents_info=OefSearchMessage.AgentsInfo( + { + "agent_1": {"key_1": "value_1", "key_2": "value_2"}, + "agent_2": {"key_3": "value_3", "key_4": "value_4"}, + } + ), + ), + ) + + # before + assert self.strategy.is_searching is True + + # operation + with patch.object(self.logger, "log") as mock_logger: + self.oef_search_handler.handle(incoming_message) + + # after + mock_logger.assert_any_call( + logging.INFO, + f"found agents={list(map(lambda x: x[-5:], incoming_message.agents))}, stopping search.", + ) + + assert self.strategy.is_searching is False + + self.assert_quantity_in_outbox(len(agents)) + for agent in agents: + has_attributes, error_str = self.message_has_attributes( + actual_message=self.get_message_from_outbox(), + message_type=FipaMessage, + performative=FipaMessage.Performative.CFP, + to=agent, + sender=self.skill.skill_context.agent_address, + query=self.strategy.get_service_query(), + ) + assert has_attributes, error_str + mock_logger.assert_any_call( + logging.INFO, f"sending CFP to agent={agent[-5:]}" + ) + + def test_handle_search_ii(self): + """Test the _handle_search method of the oef_search handler where the number of agents found is 0.""" + # setup + agents = tuple() + oef_search_dialogue = cast( + OefSearchDialogue, + self.prepare_skill_dialogue( + dialogues=self.oef_search_dialogues, + messages=self.list_of_oef_search_messages[:1], + ), + ) + incoming_message = cast( + OefSearchMessage, + self.build_incoming_message_for_skill_dialogue( + dialogue=oef_search_dialogue, + performative=OefSearchMessage.Performative.SEARCH_RESULT, + agents=agents, + agents_info=OefSearchMessage.AgentsInfo({}), + ), + ) + + # operation + with patch.object(self.logger, "log") as mock_logger: + self.oef_search_handler.handle(incoming_message) + + # after + mock_logger.assert_any_call( + logging.INFO, + f"found no agents in dialogue={oef_search_dialogue}, continue searching.", + ) + + def test_handle_invalid(self): + """Test the _handle_invalid method of the oef_search handler.""" + # setup + incoming_message = cast( + OefSearchMessage, + self.build_incoming_message( + message_type=OefSearchMessage, + performative=OefSearchMessage.Performative.REGISTER_SERVICE, + service_description=self.mocked_proposal, + ), + ) + + # operation + with patch.object(self.logger, "log") as mock_logger: + self.oef_search_handler.handle(incoming_message) + + # after + mock_logger.assert_any_call( + logging.WARNING, + f"cannot handle oef_search message of performative={incoming_message.performative} in dialogue={self.oef_search_dialogues.get_dialogue(incoming_message)}.", + ) + + def test_teardown(self): + """Test the teardown method of the oef_search handler.""" + assert self.oef_search_handler.teardown() is None + self.assert_quantity_in_outbox(0) + + +class TestContractApiHandler(ERC1155ClientTestCase): + """Test contract_api handler of erc1155_client.""" + + is_agent_to_agent_messages = False + + def test_setup(self): + """Test the setup method of the contract_api handler.""" + assert self.contract_api_handler.setup() is None + self.assert_quantity_in_outbox(0) + + def test_handle_unidentified_dialogue(self): + """Test the _handle_unidentified_dialogue method of the signing handler.""" + # setup + incorrect_dialogue_reference = ("", "") + incoming_message = cast( + ContractApiMessage, + self.build_incoming_message( + message_type=ContractApiMessage, + dialogue_reference=incorrect_dialogue_reference, + performative=ContractApiMessage.Performative.STATE, + state=State(self.ledger_id, self.body), + ), + ) + + # operation + with patch.object(self.logger, "log") as mock_logger: + self.contract_api_handler.handle(incoming_message) + + # after + mock_logger.assert_any_call( + logging.INFO, + f"received invalid contract_api message={incoming_message}, unidentified dialogue.", + ) + + def test_handle_raw_message(self): + """Test the _handle_raw_message method of the signing handler.""" + # setup + contract_api_dialogue = cast( + ContractApiDialogue, + self.prepare_skill_dialogue( + dialogues=self.contract_api_dialogues, + messages=self.list_of_contract_api_messages[:1], + ), + ) + contract_api_dialogue.terms = self.mocked_terms + incoming_message = cast( + ContractApiMessage, + self.build_incoming_message_for_skill_dialogue( + dialogue=contract_api_dialogue, + performative=ContractApiMessage.Performative.RAW_MESSAGE, + raw_message=self.mocked_raw_msg, + ), + ) + + # operation + with patch.object(self.logger, "log") as mock_logger: + self.contract_api_handler.handle(incoming_message) + + # after + mock_logger.assert_any_call( + logging.INFO, f"received raw message={incoming_message}" + ) + + self.assert_quantity_in_decision_making_queue(1) + message = self.get_message_from_decision_maker_inbox() + has_attributes, error_str = self.message_has_attributes( + actual_message=message, + message_type=SigningMessage, + performative=SigningMessage.Performative.SIGN_MESSAGE, + to=self.skill.skill_context.decision_maker_address, + sender=str(self.skill.skill_context.skill_id), + raw_message=RawMessage( + incoming_message.raw_message.ledger_id, + incoming_message.raw_message.body, + is_deprecated_mode=True, + ), + terms=contract_api_dialogue.terms, + ) + assert has_attributes, error_str + + assert ( + cast( + SigningDialogue, self.signing_dialogues.get_dialogue(message) + ).associated_contract_api_dialogue + == contract_api_dialogue + ) + + mock_logger.assert_any_call( + logging.INFO, + "proposing the transaction to the decision maker. Waiting for confirmation ...", + ) + + def test_handle_error(self): + """Test the _handle_error method of the signing handler.""" + # setup + contract_api_dialogue = cast( + ContractApiDialogue, + self.prepare_skill_dialogue( + dialogues=self.contract_api_dialogues, + messages=self.list_of_contract_api_messages[:1], + ), + ) + incoming_message = cast( + ContractApiMessage, + self.build_incoming_message_for_skill_dialogue( + dialogue=contract_api_dialogue, + performative=ContractApiMessage.Performative.ERROR, + data=b"some_data", + ), + ) + + # operation + with patch.object(self.logger, "log") as mock_logger: + self.contract_api_handler.handle(incoming_message) + + # after + mock_logger.assert_any_call( + logging.INFO, + f"received contract_api error message={incoming_message} in dialogue={contract_api_dialogue}.", + ) + + def test_handle_invalid(self): + """Test the _handle_invalid method of the signing handler.""" + # setup + invalid_performative = ContractApiMessage.Performative.GET_DEPLOY_TRANSACTION + incoming_message = cast( + ContractApiMessage, + self.build_incoming_message( + message_type=ContractApiMessage, + dialogue_reference=("1", ""), + performative=invalid_performative, + ledger_id=self.ledger_id, + contract_id=self.contract_id, + callable=self.callable, + kwargs=self.kwargs, + ), + ) + + # operation + with patch.object(self.logger, "log") as mock_logger: + self.contract_api_handler.handle(incoming_message) + + # after + mock_logger.assert_any_call( + logging.WARNING, + f"cannot handle contract_api message of performative={invalid_performative} in dialogue={self.contract_api_dialogues.get_dialogue(incoming_message)}.", + ) + + def test_teardown(self): + """Test the teardown method of the contract_api handler.""" + assert self.contract_api_handler.teardown() is None + self.assert_quantity_in_outbox(0) + + +class TestSigningHandler(ERC1155ClientTestCase): + """Test signing handler of erc1155_client.""" + + is_agent_to_agent_messages = False + + def test_setup(self): + """Test the setup method of the signing handler.""" + assert self.signing_handler.setup() is None + self.assert_quantity_in_outbox(0) + + def test_handle_unidentified_dialogue(self): + """Test the _handle_unidentified_dialogue method of the signing handler.""" + # setup + incorrect_dialogue_reference = ("", "") + incoming_message = cast( + SigningMessage, + self.build_incoming_message( + message_type=SigningMessage, + dialogue_reference=incorrect_dialogue_reference, + performative=SigningMessage.Performative.ERROR, + error_code=SigningMessage.ErrorCode.UNSUCCESSFUL_MESSAGE_SIGNING, + to=str(self.skill.skill_context.skill_id), + ), + ) + + # operation + with patch.object(self.logger, "log") as mock_logger: + self.signing_handler.handle(incoming_message) + + # after + mock_logger.assert_any_call( + logging.INFO, + f"received invalid signing message={incoming_message}, unidentified dialogue.", + ) + + def test_handle_signed_message(self,): + """Test the _handle_signed_message method of the signing handler.""" + # setup + signing_counterparty = self.skill.skill_context.decision_maker_address + + fipa_dialogue = cast( + FipaDialogue, + self.prepare_skill_dialogue( + dialogues=self.fipa_dialogues, + messages=self.list_of_fipa_messages[:2], + counterparty=COUNTERPARTY_AGENT_ADDRESS, + ), + ) + signing_dialogue = cast( + SigningDialogue, + self.prepare_skill_dialogue( + dialogues=self.signing_dialogues, + messages=self.list_of_signing_messages[:1], + counterparty=signing_counterparty, + ), + ) + contract_api_dialogue = cast( + ContractApiDialogue, + self.prepare_skill_dialogue( + dialogues=self.contract_api_dialogues, + messages=self.list_of_contract_api_messages[:4], + ), + ) + + signing_dialogue.associated_contract_api_dialogue = contract_api_dialogue + contract_api_dialogue.associated_fipa_dialogue = fipa_dialogue + + incoming_message = cast( + SigningMessage, + self.build_incoming_message_for_skill_dialogue( + dialogue=signing_dialogue, + performative=SigningMessage.Performative.SIGNED_MESSAGE, + signed_message=SigningMessage.SignedMessage( + self.ledger_id, "some_body", + ), + ), + ) + + # operation + with patch.object(self.logger, "log") as mock_logger: + self.signing_handler.handle(incoming_message) + + # after + fipa_dialogue_opponent = fipa_dialogue.dialogue_label.dialogue_opponent_addr + mock_logger.assert_any_call( + logging.INFO, + f"sending ACCEPT_W_INFORM to agent={fipa_dialogue_opponent[-5:]}: tx_signature={incoming_message.signed_message}", + ) + + self.assert_quantity_in_outbox(1) + message = self.get_message_from_outbox() + has_attributes, error_str = self.message_has_attributes( + actual_message=message, + message_type=FipaMessage, + performative=FipaMessage.Performative.ACCEPT_W_INFORM, + to=fipa_dialogue_opponent, + sender=self.skill.skill_context.agent_address, + info={"tx_signature": incoming_message.signed_message.body}, + ) + assert has_attributes, error_str + + def test_handle_error(self): + """Test the _handle_error method of the signing handler.""" + # setup + signing_counterparty = self.skill.skill_context.decision_maker_address + signing_dialogue = self.prepare_skill_dialogue( + dialogues=self.signing_dialogues, + messages=self.list_of_signing_messages[:1], + counterparty=signing_counterparty, + ) + incoming_message = cast( + SigningMessage, + self.build_incoming_message_for_skill_dialogue( + dialogue=signing_dialogue, + performative=SigningMessage.Performative.ERROR, + error_code=SigningMessage.ErrorCode.UNSUCCESSFUL_TRANSACTION_SIGNING, + ), + ) + + # operation + with patch.object(self.logger, "log") as mock_logger: + self.signing_handler.handle(incoming_message) + + # after + mock_logger.assert_any_call( + logging.INFO, + f"transaction signing was not successful. Error_code={incoming_message.error_code} in dialogue={signing_dialogue}", + ) + + def test_handle_invalid(self): + """Test the _handle_invalid method of the signing handler.""" + # setup + invalid_performative = SigningMessage.Performative.SIGN_TRANSACTION + incoming_message = self.build_incoming_message( + message_type=SigningMessage, + dialogue_reference=("1", ""), + performative=invalid_performative, + terms=self.mocked_terms, + raw_transaction=SigningMessage.RawTransaction( + self.ledger_id, {"some_key": "some_value"} + ), + to=str(self.skill.skill_context.skill_id), + ) + + # operation + with patch.object(self.logger, "log") as mock_logger: + self.signing_handler.handle(incoming_message) + + # after + mock_logger.assert_any_call( + logging.WARNING, + f"cannot handle signing message of performative={invalid_performative} in dialogue={self.signing_dialogues.get_dialogue(incoming_message)}.", + ) + + def test_teardown(self): + """Test the teardown method of the signing handler.""" + assert self.signing_handler.teardown() is None + self.assert_quantity_in_outbox(0) + + +class TestLedgerApiHandler(ERC1155ClientTestCase): + """Test ledger_api handler of erc1155_client.""" + + is_agent_to_agent_messages = False + + def test_setup(self): + """Test the setup method of the ledger_api handler.""" + assert self.ledger_api_handler.setup() is None + self.assert_quantity_in_outbox(0) + + def test_handle_unidentified_dialogue(self): + """Test the _handle_unidentified_dialogue method of the ledger_api handler.""" + # setup + incorrect_dialogue_reference = ("", "") + incoming_message = cast( + LedgerApiMessage, + self.build_incoming_message( + message_type=LedgerApiMessage, + dialogue_reference=incorrect_dialogue_reference, + performative=LedgerApiMessage.Performative.BALANCE, + ledger_id=self.ledger_id, + balance=10, + ), + ) + + # operation + with patch.object(self.logger, "log") as mock_logger: + self.ledger_api_handler.handle(incoming_message) + + # after + mock_logger.assert_any_call( + logging.INFO, + f"received invalid ledger_api message={incoming_message}, unidentified dialogue.", + ) + + def test_handle_balance(self): + """Test the _handle_balance method of the ledger_api handler.""" + # setup + balance = 10 + ledger_api_dialogue = cast( + LedgerApiDialogue, + self.prepare_skill_dialogue( + dialogues=self.ledger_api_dialogues, + messages=self.list_of_ledger_api_messages[:1], + counterparty=LEDGER_API_ADDRESS, + ), + ) + incoming_message = cast( + LedgerApiMessage, + self.build_incoming_message_for_skill_dialogue( + dialogue=ledger_api_dialogue, + performative=LedgerApiMessage.Performative.BALANCE, + ledger_id=self.ledger_id, + balance=balance, + ), + ) + + # operation + with patch.object(self.logger, "log") as mock_logger: + self.ledger_api_handler.handle(incoming_message) + + # after + mock_logger.assert_any_call( + logging.INFO, + f"starting balance on {self.ledger_id} ledger={incoming_message.balance}.", + ) + + def test_handle_error(self): + """Test the _handle_error method of the ledger_api handler.""" + # setup + ledger_api_dialogue = cast( + LedgerApiDialogue, + self.prepare_skill_dialogue( + dialogues=self.ledger_api_dialogues, + messages=self.list_of_ledger_api_messages[:1], + ), + ) + incoming_message = cast( + LedgerApiMessage, + self.build_incoming_message_for_skill_dialogue( + dialogue=ledger_api_dialogue, + performative=LedgerApiMessage.Performative.ERROR, + code=1, + ), + ) + + # operation + with patch.object(self.logger, "log") as mock_logger: + self.ledger_api_handler.handle(incoming_message) + + # after + mock_logger.assert_any_call( + logging.INFO, + f"received ledger_api error message={incoming_message} in dialogue={ledger_api_dialogue}.", + ) + + def test_handle_invalid(self): + """Test the _handle_invalid method of the ledger_api handler.""" + # setup + invalid_performative = LedgerApiMessage.Performative.GET_BALANCE + incoming_message = cast( + LedgerApiMessage, + self.build_incoming_message( + message_type=LedgerApiMessage, + dialogue_reference=("1", ""), + performative=invalid_performative, + ledger_id=self.ledger_id, + address=self.address, + to=str(self.skill.skill_context.skill_id), + ), + ) + + # operation + with patch.object(self.logger, "log") as mock_logger: + self.ledger_api_handler.handle(incoming_message) + + # after + mock_logger.assert_any_call( + logging.WARNING, + f"cannot handle ledger_api message of performative={invalid_performative} in dialogue={self.ledger_api_dialogues.get_dialogue(incoming_message)}.", + ) + + def test_teardown(self): + """Test the teardown method of the ledger_api handler.""" + assert self.ledger_api_handler.teardown() is None + self.assert_quantity_in_outbox(0) diff --git a/tests/test_packages/test_skills/test_erc1155_client/test_strategy.py b/tests/test_packages/test_skills/test_erc1155_client/test_strategy.py new file mode 100644 index 0000000000..d8b90d5f51 --- /dev/null +++ b/tests/test_packages/test_skills/test_erc1155_client/test_strategy.py @@ -0,0 +1,80 @@ +# -*- coding: utf-8 -*- +# ------------------------------------------------------------------------------ +# +# Copyright 2018-2019 Fetch.AI Limited +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ------------------------------------------------------------------------------ +"""This module contains the tests of the strategy class of the erc1155_client skill.""" + +from aea.helpers.search.models import Constraint, ConstraintType, Query + +from packages.fetchai.skills.erc1155_client.strategy import ( + CONTRACT_ID, + SIMPLE_SERVICE_MODEL, +) + +from tests.test_packages.test_skills.test_erc1155_client.intermediate_class import ( + ERC1155ClientTestCase, +) + + +class TestStrategy(ERC1155ClientTestCase): + """Test Strategy of erc1155_client.""" + + def test_properties(self): + """Test the properties of Strategy class.""" + assert self.strategy.ledger_id == self.skill.skill_context.default_ledger_id + assert self.strategy.contract_id == str(CONTRACT_ID) + + def test_get_location_and_service_query(self): + """Test the get_location_and_service_query method of the Strategy class.""" + query = self.strategy.get_location_and_service_query() + + assert type(query) == Query + assert len(query.constraints) == 2 + assert query.model is None + + location_constraint = Constraint( + "location", + ConstraintType( + "distance", (self.strategy._agent_location, self.search_radius) + ), + ) + assert query.constraints[0] == location_constraint + + service_key_constraint = Constraint( + self.search_query["search_key"], + ConstraintType( + self.search_query["constraint_type"], self.search_query["search_value"], + ), + ) + assert query.constraints[1] == service_key_constraint + + def test_get_service_query(self): + """Test the get_service_query method of the Strategy class.""" + query = self.strategy.get_service_query() + + assert type(query) == Query + assert len(query.constraints) == 1 + + assert query.model == SIMPLE_SERVICE_MODEL + + service_key_constraint = Constraint( + self.search_query["search_key"], + ConstraintType( + self.search_query["constraint_type"], self.search_query["search_value"], + ), + ) + assert query.constraints[0] == service_key_constraint diff --git a/tests/test_packages/test_skills/test_erc1155_deploy/__init__.py b/tests/test_packages/test_skills/test_erc1155_deploy/__init__.py new file mode 100644 index 0000000000..2e802d3b62 --- /dev/null +++ b/tests/test_packages/test_skills/test_erc1155_deploy/__init__.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# ------------------------------------------------------------------------------ +# +# Copyright 2018-2019 Fetch.AI Limited +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ------------------------------------------------------------------------------ + +"""The tests module contains the tests of the packages/skills/erc1155_deploy dir.""" diff --git a/tests/test_packages/test_skills/test_erc1155_deploy/intermediate_class.py b/tests/test_packages/test_skills/test_erc1155_deploy/intermediate_class.py new file mode 100644 index 0000000000..446fb9eef3 --- /dev/null +++ b/tests/test_packages/test_skills/test_erc1155_deploy/intermediate_class.py @@ -0,0 +1,347 @@ +# -*- coding: utf-8 -*- +# ------------------------------------------------------------------------------ +# +# Copyright 2018-2019 Fetch.AI Limited +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ------------------------------------------------------------------------------ +"""This module sets up test environment for erc1155_deploy skill.""" + +from pathlib import Path +from typing import cast + +from aea.helpers.search.models import ( + Attribute, + Constraint, + ConstraintType, + DataModel, + Description, + Location, + Query, +) +from aea.helpers.transaction.base import ( + RawMessage, + RawTransaction, + SignedTransaction, + Terms, + TransactionDigest, + TransactionReceipt, +) +from aea.protocols.dialogue.base import DialogueMessage +from aea.test_tools.test_skill import BaseSkillTestCase + +from packages.fetchai.protocols.contract_api.custom_types import Kwargs +from packages.fetchai.protocols.contract_api.message import ContractApiMessage +from packages.fetchai.protocols.fipa.message import FipaMessage +from packages.fetchai.protocols.ledger_api.message import LedgerApiMessage +from packages.fetchai.protocols.oef_search.message import OefSearchMessage +from packages.fetchai.protocols.signing.message import SigningMessage +from packages.fetchai.skills.erc1155_deploy.behaviours import ( + ServiceRegistrationBehaviour, +) +from packages.fetchai.skills.erc1155_deploy.dialogues import ( + ContractApiDialogues, + DefaultDialogues, + FipaDialogues, + LedgerApiDialogues, + OefSearchDialogues, + SigningDialogues, +) +from packages.fetchai.skills.erc1155_deploy.handlers import ( + ContractApiHandler, + FipaHandler, + LedgerApiHandler, + OefSearchHandler, + SigningHandler, +) +from packages.fetchai.skills.erc1155_deploy.strategy import Strategy + +from tests.conftest import ROOT_DIR + + +class ERC1155DeployTestCase(BaseSkillTestCase): + """Sets the erc1155_deploy class up for testing.""" + + path_to_skill = Path(ROOT_DIR, "packages", "fetchai", "skills", "erc1155_deploy") + + @classmethod + def setup(cls): + """Setup the test class.""" + cls.location = {"longitude": 0.1270, "latitude": 51.5194} + cls.mint_quantities = [100, 100, 100, 100, 100, 100, 100, 100, 100, 100] + cls.service_data = {"key": "seller_service", "value": "some_value"} + cls.personality_data = {"piece": "genus", "value": "some_personality"} + cls.classification = {"piece": "classification", "value": "some_classification"} + cls.from_supply = 756 + cls.to_supply = 12 + cls.value = 87 + config_overrides = { + "models": { + "strategy": { + "args": { + "location": cls.location, + "mint_quantities": cls.mint_quantities, + "service_data": cls.service_data, + "personality_data": cls.personality_data, + "classification": cls.classification, + "from_supply": cls.from_supply, + "to_supply": cls.to_supply, + "value": cls.value, + } + } + }, + } + + super().setup(config_overrides=config_overrides) + + # behaviours + cls.registration_behaviour = cast( + ServiceRegistrationBehaviour, + cls._skill.skill_context.behaviours.service_registration, + ) + + # dialogues + cls.contract_api_dialogues = cast( + ContractApiDialogues, cls._skill.skill_context.contract_api_dialogues + ) + cls.default_dialogues = cast( + DefaultDialogues, cls._skill.skill_context.default_dialogues + ) + cls.fipa_dialogues = cast( + FipaDialogues, cls._skill.skill_context.fipa_dialogues + ) + cls.ledger_api_dialogues = cast( + LedgerApiDialogues, cls._skill.skill_context.ledger_api_dialogues + ) + cls.oef_search_dialogues = cast( + OefSearchDialogues, cls._skill.skill_context.oef_search_dialogues + ) + cls.signing_dialogues = cast( + SigningDialogues, cls._skill.skill_context.signing_dialogues + ) + + # handlers + cls.fipa_handler = cast(FipaHandler, cls._skill.skill_context.handlers.fipa) + cls.oef_search_handler = cast( + OefSearchHandler, cls._skill.skill_context.handlers.oef_search + ) + cls.contract_api_handler = cast( + ContractApiHandler, cls._skill.skill_context.handlers.contract_api + ) + cls.signing_handler = cast( + SigningHandler, cls._skill.skill_context.handlers.signing + ) + cls.ledger_api_handler = cast( + LedgerApiHandler, cls._skill.skill_context.handlers.ledger_api + ) + + # models + cls.strategy = cast(Strategy, cls._skill.skill_context.strategy) + + cls.logger = cls._skill.skill_context.logger + + # mocked objects + cls.ledger_id = "some_ledger_id" + cls.contract_id = "some_contract_id" + cls.contract_address = "some_contract_address" + cls.callable = "some_callable" + cls.body_dict = {"some_key": "some_value"} + cls.body_str = "some_body" + cls.body_bytes = b"some_body" + cls.kwargs = Kwargs(cls.body_dict) + cls.address = "some_address" + + cls.mocked_terms = Terms( + cls.ledger_id, + cls._skill.skill_context.agent_address, + "counterprty", + {"currency_id": 50}, + {"good_id": -10}, + "some_nonce", + ) + cls.mocked_query = Query( + [Constraint("some_attribute_name", ConstraintType("==", "some_value"))], + DataModel( + "some_data_model_name", + [ + Attribute( + "some_attribute_name", + str, + False, + "Some attribute descriptions.", + ) + ], + ), + ) + cls.mocked_proposal = Description( + { + "contract_address": "some_contract_address", + "token_id": "123456", + "trade_nonce": "876438756348568", + "from_supply": "543", + "to_supply": "432", + "value": "67", + } + ) + cls.mocked_registration_description = Description({"foo1": 1, "bar1": 2}) + + cls.mocked_raw_tx = RawTransaction(cls.ledger_id, cls.body_dict) + cls.mocked_raw_msg = RawMessage(cls.ledger_id, cls.body_bytes) + cls.mocked_tx_digest = TransactionDigest(cls.ledger_id, cls.body_str) + cls.mocked_signed_tx = SignedTransaction(cls.ledger_id, cls.body_dict) + cls.mocked_tx_receipt = TransactionReceipt( + cls.ledger_id, + {"receipt_key": "receipt_value", "contractAddress": cls.contract_address}, + {"transaction_key": "transaction_value"}, + ) + + cls.registration_message = OefSearchMessage( + dialogue_reference=("", ""), + performative=OefSearchMessage.Performative.REGISTER_SERVICE, + service_description=cls.mocked_registration_description, + ) + cls.registration_message.sender = str(cls._skill.skill_context.skill_id) + cls.registration_message.to = cls._skill.skill_context.search_service_address + + # list of messages + cls.list_of_fipa_messages = ( + DialogueMessage(FipaMessage.Performative.CFP, {"query": cls.mocked_query}), + DialogueMessage( + FipaMessage.Performative.PROPOSE, {"proposal": cls.mocked_proposal} + ), + ) + cls.list_of_contract_api_messages = ( + DialogueMessage( + ContractApiMessage.Performative.GET_RAW_TRANSACTION, + { + "ledger_id": cls.ledger_id, + "contract_id": cls.contract_id, + "contract_address": cls.contract_address, + "callable": cls.callable, + "kwargs": cls.kwargs, + }, + ), + ) + cls.list_of_signing_messages = ( + DialogueMessage( + SigningMessage.Performative.SIGN_TRANSACTION, + {"terms": cls.mocked_terms, "raw_transaction": cls.mocked_raw_tx}, + ), + ) + cls.list_of_ledger_api_balance_messages = ( + DialogueMessage( + LedgerApiMessage.Performative.GET_BALANCE, + {"ledger_id": cls.ledger_id, "address": "some_address"}, + ), + ) + + cls.list_of_ledger_api_messages = ( + DialogueMessage( + LedgerApiMessage.Performative.GET_RAW_TRANSACTION, + {"terms": cls.mocked_terms}, + ), + DialogueMessage( + LedgerApiMessage.Performative.RAW_TRANSACTION, + {"raw_transaction": cls.mocked_raw_tx}, + ), + DialogueMessage( + LedgerApiMessage.Performative.SEND_SIGNED_TRANSACTION, + {"signed_transaction": cls.mocked_signed_tx}, + ), + DialogueMessage( + LedgerApiMessage.Performative.TRANSACTION_DIGEST, + {"transaction_digest": cls.mocked_tx_digest}, + ), + DialogueMessage( + LedgerApiMessage.Performative.GET_TRANSACTION_RECEIPT, + {"transaction_digest": cls.mocked_tx_digest}, + ), + DialogueMessage( + LedgerApiMessage.Performative.TRANSACTION_RECEIPT, + {"transaction_receipt": cls.mocked_tx_receipt}, + ), + ) + cls.register_location_description = Description( + {"location": Location(51.5194, 0.1270)}, + data_model=DataModel( + "location_agent", [Attribute("location", Location, True)] + ), + ) + cls.list_of_messages_register_location = ( + DialogueMessage( + OefSearchMessage.Performative.REGISTER_SERVICE, + {"service_description": cls.register_location_description}, + is_incoming=False, + ), + ) + + cls.register_service_description = Description( + {"key": "some_key", "value": "some_value"}, + data_model=DataModel( + "set_service_key", + [Attribute("key", str, True), Attribute("value", str, True)], + ), + ) + cls.list_of_messages_register_service = ( + DialogueMessage( + OefSearchMessage.Performative.REGISTER_SERVICE, + {"service_description": cls.register_service_description}, + is_incoming=False, + ), + ) + + cls.register_genus_description = Description( + {"piece": "genus", "value": "some_value"}, + data_model=DataModel( + "personality_agent", + [Attribute("piece", str, True), Attribute("value", str, True)], + ), + ) + cls.list_of_messages_register_genus = ( + DialogueMessage( + OefSearchMessage.Performative.REGISTER_SERVICE, + {"service_description": cls.register_genus_description}, + is_incoming=False, + ), + ) + + cls.register_classification_description = Description( + {"piece": "classification", "value": "some_value"}, + data_model=DataModel( + "personality_agent", + [Attribute("piece", str, True), Attribute("value", str, True)], + ), + ) + cls.list_of_messages_register_classification = ( + DialogueMessage( + OefSearchMessage.Performative.REGISTER_SERVICE, + {"service_description": cls.register_classification_description}, + is_incoming=False, + ), + ) + + cls.register_invalid_description = Description( + {"piece": "classification", "value": "some_value"}, + data_model=DataModel( + "some_different_name", + [Attribute("piece", str, True), Attribute("value", str, True)], + ), + ) + cls.list_of_messages_register_invalid = ( + DialogueMessage( + OefSearchMessage.Performative.REGISTER_SERVICE, + {"service_description": cls.register_invalid_description}, + is_incoming=False, + ), + ) diff --git a/tests/test_packages/test_skills/test_erc1155_deploy/test_behaviours.py b/tests/test_packages/test_skills/test_erc1155_deploy/test_behaviours.py new file mode 100644 index 0000000000..9d24026f82 --- /dev/null +++ b/tests/test_packages/test_skills/test_erc1155_deploy/test_behaviours.py @@ -0,0 +1,415 @@ +# -*- coding: utf-8 -*- +# ------------------------------------------------------------------------------ +# +# Copyright 2018-2019 Fetch.AI Limited +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ------------------------------------------------------------------------------ +"""This module contains the tests of the behaviour classes of the erc1155_deploy skill.""" + +import logging +from typing import cast +from unittest.mock import patch + +from packages.fetchai.protocols.contract_api.message import ContractApiMessage +from packages.fetchai.protocols.ledger_api.message import LedgerApiMessage +from packages.fetchai.protocols.oef_search.message import OefSearchMessage +from packages.fetchai.skills.erc1155_deploy.behaviours import LEDGER_API_ADDRESS +from packages.fetchai.skills.erc1155_deploy.dialogues import ContractApiDialogue + +from tests.test_packages.test_skills.test_erc1155_deploy.intermediate_class import ( + ERC1155DeployTestCase, +) + + +class TestServiceRegistrationBehaviour(ERC1155DeployTestCase): + """Test registration behaviour of erc1155_deploy.""" + + def test_init(self): + """Test the __init__ method of the registration behaviour.""" + assert self.registration_behaviour.is_registered is False + assert self.registration_behaviour.registration_in_progress is False + assert self.registration_behaviour.failed_registration_msg is None + assert self.registration_behaviour._nb_retries == 0 + + def test_setup(self): + """Test the setup method of the registration behaviour.""" + # setup + self.strategy._is_contract_deployed = False + + # before + assert self.strategy.is_behaviour_active is True + + # operation + with patch.object(self.logger, "log") as mock_logger: + self.registration_behaviour.setup() + + # after + self.assert_quantity_in_outbox(2) + + # _request_balance + has_attributes, error_str = self.message_has_attributes( + actual_message=self.get_message_from_outbox(), + message_type=LedgerApiMessage, + performative=LedgerApiMessage.Performative.GET_BALANCE, + to=LEDGER_API_ADDRESS, + sender=str(self.skill.skill_context.skill_id), + ledger_id=self.strategy.ledger_id, + address=cast( + str, + self.skill.skill_context.agent_addresses.get(self.strategy.ledger_id), + ), + ) + assert has_attributes, error_str + + # _request_contract_deploy_transaction + assert self.strategy.is_behaviour_active is False + + has_attributes, error_str = self.message_has_attributes( + actual_message=self.get_message_from_outbox(), + message_type=ContractApiMessage, + performative=ContractApiMessage.Performative.GET_DEPLOY_TRANSACTION, + to=LEDGER_API_ADDRESS, + sender=str(self.skill.skill_context.skill_id), + ledger_id=self.strategy.ledger_id, + contract_id=self.strategy.contract_id, + callable="get_deploy_transaction", + kwargs=ContractApiMessage.Kwargs( + { + "deployer_address": self.skill.skill_context.agent_address, + "gas": self.strategy.gas, + } + ), + ) + assert has_attributes, error_str + + mock_logger.assert_any_call( + logging.INFO, "requesting contract deployment transaction..." + ) + + def test_act_i(self): + """Test the act method of the registration behaviour where failed_registration_msg is NOT None.""" + # setup + self.registration_behaviour.failed_registration_msg = self.registration_message + + with patch.object(self.logger, "log") as mock_logger: + self.registration_behaviour.act() + + # after + self.assert_quantity_in_outbox(1) + + # _retry_failed_registration + has_attributes, error_str = self.message_has_attributes( + actual_message=self.get_message_from_outbox(), + message_type=type(self.registration_message), + performative=self.registration_message.performative, + to=self.registration_message.to, + sender=str(self.skill.skill_context.skill_id), + service_description=self.registration_message.service_description, + ) + assert has_attributes, error_str + + mock_logger.assert_any_call( + logging.INFO, + f"Retrying registration on SOEF. Retry {self.registration_behaviour._nb_retries} out of {self.registration_behaviour._max_soef_registration_retries}.", + ) + assert self.registration_behaviour.failed_registration_msg is None + + def test_act_ii(self): + """Test the act method of the registration behaviour where failed_registration_msg is NOT None and max retries is reached.""" + # setup + self.registration_behaviour.failed_registration_msg = self.registration_message + self.registration_behaviour._max_soef_registration_retries = 2 + self.registration_behaviour._nb_retries = 2 + + self.registration_behaviour.act() + + # after + self.assert_quantity_in_outbox(0) + assert self.skill.skill_context.is_active is False + + def test_act_iii(self): + """Test the act method of the registration behaviour where is_behaviour_active IS False.""" + # setup + self.strategy.is_behaviour_active = False + + # operation + self.registration_behaviour.act() + + # after + self.assert_quantity_in_outbox(0) + + def test_act_iv(self): + """Test the act method of the registration behaviour where is_contract_deployed IS True and is_tokens_created IS False.""" + # setup + self.strategy.is_contract_deployed = True + self.strategy._is_tokens_created = False + self.strategy._contract_address = self.contract_address + + # operation + with patch.object(self.logger, "log") as mock_logger: + self.registration_behaviour.act() + + # after + self.assert_quantity_in_outbox(1) + + assert self.strategy.is_behaviour_active is False + + # _request_token_create_transaction + message = self.get_message_from_outbox() + has_attributes, error_str = self.message_has_attributes( + actual_message=message, + message_type=ContractApiMessage, + performative=ContractApiMessage.Performative.GET_RAW_TRANSACTION, + to=LEDGER_API_ADDRESS, + sender=str(self.skill.skill_context.skill_id), + ledger_id=self.strategy.ledger_id, + contract_id=self.strategy.contract_id, + contract_address=self.strategy.contract_address, + callable="get_create_batch_transaction", + kwargs=ContractApiMessage.Kwargs( + { + "deployer_address": self.skill.skill_context.agent_address, + "token_ids": self.strategy.token_ids, + "gas": self.strategy.gas, + } + ), + ) + assert has_attributes, error_str + + contract_api_dialogue = cast( + ContractApiDialogue, self.contract_api_dialogues.get_dialogue(message) + ) + assert contract_api_dialogue.terms == self.strategy.get_create_token_terms() + + mock_logger.assert_any_call( + logging.INFO, "requesting create batch transaction..." + ) + + def test_act_v(self): + """Test the act method of the registration behaviour where is_contract_deployed IS True, is_tokens_created IS True and is_tokens_minted is False.""" + # setup + self.strategy.is_contract_deployed = True + self.strategy._is_tokens_created = True + self.strategy._is_tokens_minted = False + self.strategy._contract_address = self.contract_address + + # operation + with patch.object(self.logger, "log") as mock_logger: + self.registration_behaviour.act() + + # after + self.assert_quantity_in_outbox(1) + + assert self.strategy.is_behaviour_active is False + + # _request_token_mint_transaction + message = self.get_message_from_outbox() + has_attributes, error_str = self.message_has_attributes( + actual_message=message, + message_type=ContractApiMessage, + performative=ContractApiMessage.Performative.GET_RAW_TRANSACTION, + to=LEDGER_API_ADDRESS, + sender=str(self.skill.skill_context.skill_id), + ledger_id=self.strategy.ledger_id, + contract_id=self.strategy.contract_id, + contract_address=self.strategy.contract_address, + callable="get_mint_batch_transaction", + kwargs=ContractApiMessage.Kwargs( + { + "deployer_address": self.skill.skill_context.agent_address, + "recipient_address": self.skill.skill_context.agent_address, + "token_ids": self.strategy.token_ids, + "mint_quantities": self.strategy.mint_quantities, + "gas": self.strategy.gas, + } + ), + ) + assert has_attributes, error_str + + contract_api_dialogue = cast( + ContractApiDialogue, self.contract_api_dialogues.get_dialogue(message) + ) + assert contract_api_dialogue.terms == self.strategy.get_mint_token_terms() + + mock_logger.assert_any_call( + logging.INFO, "requesting mint batch transaction..." + ) + + def test_act_vi(self): + """Test the act method of the registration behaviour where is_contract_deployed IS True, is_tokens_created IS True and is_tokens_minted is True and is_registered IS False.""" + # setup + self.strategy.is_contract_deployed = True + self.strategy._is_tokens_created = True + self.strategy._is_tokens_minted = True + self.strategy._contract_address = self.contract_address + + # before + assert self.registration_behaviour.registration_in_progress is False + + # operation + with patch.object( + self.strategy, + "get_location_description", + return_value=self.mocked_registration_description, + ) as mock_desc: + with patch.object(self.logger, "log") as mock_logger: + self.registration_behaviour.act() + + # after + self.assert_quantity_in_outbox(1) + + assert self.registration_behaviour.registration_in_progress is True + + # _register_agent + mock_desc.assert_called_once() + + has_attributes, error_str = self.message_has_attributes( + actual_message=self.get_message_from_outbox(), + message_type=OefSearchMessage, + performative=OefSearchMessage.Performative.REGISTER_SERVICE, + to=self.skill.skill_context.search_service_address, + sender=str(self.skill.skill_context.skill_id), + service_description=self.mocked_registration_description, + ) + assert has_attributes, error_str + + mock_logger.assert_any_call(logging.INFO, "registering agent on SOEF.") + + def test_register_service(self): + """Test the register_service method of the registration behaviour.""" + # operation + with patch.object( + self.strategy, + "get_register_service_description", + return_value=self.mocked_registration_description, + ): + with patch.object(self.logger, "log") as mock_logger: + self.registration_behaviour.register_service() + + # after + self.assert_quantity_in_outbox(1) + + message = self.get_message_from_outbox() + has_attributes, error_str = self.message_has_attributes( + actual_message=message, + message_type=OefSearchMessage, + performative=OefSearchMessage.Performative.REGISTER_SERVICE, + to=self.skill.skill_context.search_service_address, + sender=str(self.skill.skill_context.skill_id), + service_description=self.mocked_registration_description, + ) + assert has_attributes, error_str + mock_logger.assert_any_call( + logging.INFO, "registering agent's service on the SOEF." + ) + + def test_register_genus(self): + """Test the register_genus method of the registration behaviour.""" + # operation + with patch.object( + self.strategy, + "get_register_personality_description", + return_value=self.mocked_registration_description, + ): + with patch.object(self.logger, "log") as mock_logger: + self.registration_behaviour.register_genus() + + # after + self.assert_quantity_in_outbox(1) + + message = self.get_message_from_outbox() + has_attributes, error_str = self.message_has_attributes( + actual_message=message, + message_type=OefSearchMessage, + performative=OefSearchMessage.Performative.REGISTER_SERVICE, + to=self.skill.skill_context.search_service_address, + sender=str(self.skill.skill_context.skill_id), + service_description=self.mocked_registration_description, + ) + assert has_attributes, error_str + mock_logger.assert_any_call( + logging.INFO, "registering agent's personality genus on the SOEF." + ) + + def test_register_classification(self): + """Test the register_classification method of the registration behaviour.""" + # operation + with patch.object( + self.strategy, + "get_register_classification_description", + return_value=self.mocked_registration_description, + ): + with patch.object(self.logger, "log") as mock_logger: + self.registration_behaviour.register_classification() + + # after + self.assert_quantity_in_outbox(1) + + message = self.get_message_from_outbox() + has_attributes, error_str = self.message_has_attributes( + actual_message=message, + message_type=OefSearchMessage, + performative=OefSearchMessage.Performative.REGISTER_SERVICE, + to=self.skill.skill_context.search_service_address, + sender=str(self.skill.skill_context.skill_id), + service_description=self.mocked_registration_description, + ) + assert has_attributes, error_str + mock_logger.assert_any_call( + logging.INFO, "registering agent's personality classification on the SOEF." + ) + + def test_teardown(self): + """Test the teardown method of the service_registration behaviour.""" + # operation + with patch.object( + self.strategy, + "get_unregister_service_description", + return_value=self.mocked_registration_description, + ): + with patch.object( + self.strategy, + "get_location_description", + return_value=self.mocked_registration_description, + ): + with patch.object(self.logger, "log") as mock_logger: + self.registration_behaviour.teardown() + + # after + self.assert_quantity_in_outbox(2) + + # _unregister_service + has_attributes, error_str = self.message_has_attributes( + actual_message=self.get_message_from_outbox(), + message_type=OefSearchMessage, + performative=OefSearchMessage.Performative.UNREGISTER_SERVICE, + to=self.skill.skill_context.search_service_address, + sender=str(self.skill.skill_context.skill_id), + service_description=self.mocked_registration_description, + ) + assert has_attributes, error_str + mock_logger.assert_any_call(logging.INFO, "unregistering service from SOEF.") + + # _unregister_agent + has_attributes, error_str = self.message_has_attributes( + actual_message=self.get_message_from_outbox(), + message_type=OefSearchMessage, + performative=OefSearchMessage.Performative.UNREGISTER_SERVICE, + to=self.skill.skill_context.search_service_address, + sender=str(self.skill.skill_context.skill_id), + service_description=self.mocked_registration_description, + ) + assert has_attributes, error_str + mock_logger.assert_any_call(logging.INFO, "unregistering agent from SOEF.") diff --git a/tests/test_packages/test_skills/test_erc1155_deploy/test_dialogues.py b/tests/test_packages/test_skills/test_erc1155_deploy/test_dialogues.py new file mode 100644 index 0000000000..e784ad17fb --- /dev/null +++ b/tests/test_packages/test_skills/test_erc1155_deploy/test_dialogues.py @@ -0,0 +1,219 @@ +# -*- coding: utf-8 -*- +# ------------------------------------------------------------------------------ +# +# Copyright 2018-2019 Fetch.AI Limited +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ------------------------------------------------------------------------------ +"""This module contains the tests of the dialogue classes of the erc1155_deploy skill.""" + +import pytest + +from aea.exceptions import AEAEnforceError +from aea.protocols.dialogue.base import DialogueLabel +from aea.test_tools.test_skill import COUNTERPARTY_AGENT_ADDRESS + +from packages.fetchai.protocols.contract_api.message import ContractApiMessage +from packages.fetchai.protocols.default.message import DefaultMessage +from packages.fetchai.protocols.fipa.message import FipaMessage +from packages.fetchai.protocols.ledger_api.message import LedgerApiMessage +from packages.fetchai.protocols.oef_search.message import OefSearchMessage +from packages.fetchai.protocols.signing.message import SigningMessage +from packages.fetchai.skills.erc1155_deploy.dialogues import ( + ContractApiDialogue, + DefaultDialogue, + FipaDialogue, + LedgerApiDialogue, + OefSearchDialogue, + SigningDialogue, +) + +from tests.test_packages.test_skills.test_erc1155_deploy.intermediate_class import ( + ERC1155DeployTestCase, +) + + +class TestDialogues(ERC1155DeployTestCase): + """Test dialogue classes of erc1155_deploy.""" + + def test_contract_api_dialogue(self): + """Test the ContractApiDialogue class.""" + contract_api_dialogue = ContractApiDialogue( + DialogueLabel( + ("", ""), + COUNTERPARTY_AGENT_ADDRESS, + self.skill.skill_context.agent_address, + ), + self.skill.skill_context.agent_address, + role=ContractApiDialogue.Role.AGENT, + ) + + # terms + with pytest.raises(ValueError, match="Terms not set!"): + assert contract_api_dialogue.terms + contract_api_dialogue.terms = self.mocked_terms + with pytest.raises(AEAEnforceError, match="Terms already set!"): + contract_api_dialogue.terms = self.mocked_terms + assert contract_api_dialogue.terms == self.mocked_terms + + def test_contract_api_dialogues(self): + """Test the ContractApiDialogues class.""" + _, dialogue = self.contract_api_dialogues.create( + counterparty=COUNTERPARTY_AGENT_ADDRESS, + performative=ContractApiMessage.Performative.GET_DEPLOY_TRANSACTION, + ledger_id=self.ledger_id, + contract_id=self.contract_id, + callable=self.callable, + kwargs=self.kwargs, + ) + assert dialogue.role == ContractApiDialogue.Role.AGENT + assert dialogue.self_address == str(self.skill.skill_context.skill_id) + + def test_default_dialogues(self): + """Test the DefaultDialogues class.""" + _, dialogue = self.default_dialogues.create( + counterparty=COUNTERPARTY_AGENT_ADDRESS, + performative=DefaultMessage.Performative.BYTES, + content=b"some_content", + ) + assert dialogue.role == DefaultDialogue.Role.AGENT + assert dialogue.self_address == self.skill.skill_context.agent_address + + def test_fipa_dialogue(self): + """Test the FipaDialogue class.""" + fipa_dialogue = FipaDialogue( + DialogueLabel( + ("", ""), + COUNTERPARTY_AGENT_ADDRESS, + self.skill.skill_context.agent_address, + ), + self.skill.skill_context.agent_address, + role=FipaDialogue.Role.BUYER, + ) + + # proposal + with pytest.raises(ValueError, match="Proposal not set!"): + assert fipa_dialogue.proposal + fipa_dialogue.proposal = self.mocked_registration_description + with pytest.raises(AEAEnforceError, match="Proposal already set!"): + fipa_dialogue.proposal = self.mocked_registration_description + assert fipa_dialogue.proposal == self.mocked_registration_description + + def test_fipa_dialogues(self): + """Test the FipaDialogues class.""" + _, dialogue = self.fipa_dialogues.create( + counterparty=COUNTERPARTY_AGENT_ADDRESS, + performative=FipaMessage.Performative.CFP, + query=self.mocked_query, + ) + assert dialogue.role == FipaDialogue.Role.SELLER + assert dialogue.self_address == self.skill.skill_context.agent_address + + def test_ledger_api_dialogue(self): + """Test the LedgerApiDialogue class.""" + ledger_api_dialogue = LedgerApiDialogue( + DialogueLabel( + ("", ""), + COUNTERPARTY_AGENT_ADDRESS, + self.skill.skill_context.agent_address, + ), + self.skill.skill_context.agent_address, + role=ContractApiDialogue.Role.AGENT, + ) + + # associated_signing_dialogue + with pytest.raises(ValueError, match="Associated signing dialogue not set!"): + assert ledger_api_dialogue.associated_signing_dialogue + signing_dialogue = SigningDialogue( + DialogueLabel( + ("", ""), + COUNTERPARTY_AGENT_ADDRESS, + self.skill.skill_context.agent_address, + ), + self.skill.skill_context.agent_address, + role=SigningDialogue.Role.SKILL, + ) + ledger_api_dialogue.associated_signing_dialogue = signing_dialogue + with pytest.raises( + AEAEnforceError, match="Associated signing dialogue already set!" + ): + ledger_api_dialogue.associated_signing_dialogue = signing_dialogue + assert ledger_api_dialogue.associated_signing_dialogue == signing_dialogue + + def test_ledger_api_dialogues(self): + """Test the LedgerApiDialogues class.""" + _, dialogue = self.ledger_api_dialogues.create( + counterparty=COUNTERPARTY_AGENT_ADDRESS, + performative=LedgerApiMessage.Performative.GET_BALANCE, + ledger_id=self.ledger_id, + address=self.address, + ) + assert dialogue.role == LedgerApiDialogue.Role.AGENT + assert dialogue.self_address == str(self.skill.skill_context.skill_id) + + def test_oef_search_dialogues(self): + """Test the OefSearchDialogues class.""" + _, dialogue = self.oef_search_dialogues.create( + counterparty=COUNTERPARTY_AGENT_ADDRESS, + performative=OefSearchMessage.Performative.SEARCH_SERVICES, + query=self.mocked_query, + ) + assert dialogue.role == OefSearchDialogue.Role.AGENT + assert dialogue.self_address == str(self.skill.skill_context.skill_id) + + def test_signing_dialogue(self): + """Test the SigningDialogue class.""" + signing_dialogue = SigningDialogue( + DialogueLabel( + ("", ""), + COUNTERPARTY_AGENT_ADDRESS, + self.skill.skill_context.agent_address, + ), + self.skill.skill_context.agent_address, + role=ContractApiDialogue.Role.AGENT, + ) + + # associated_contract_api_dialogue + with pytest.raises( + ValueError, match="Associated contract api dialogue not set!" + ): + assert signing_dialogue.associated_contract_api_dialogue + contract_api_dialogue = ContractApiDialogue( + DialogueLabel( + ("", ""), + COUNTERPARTY_AGENT_ADDRESS, + self.skill.skill_context.agent_address, + ), + self.skill.skill_context.agent_address, + role=ContractApiDialogue.Role.AGENT, + ) + signing_dialogue.associated_contract_api_dialogue = contract_api_dialogue + with pytest.raises( + AEAEnforceError, match="Associated contract api dialogue already set!" + ): + signing_dialogue.associated_contract_api_dialogue = contract_api_dialogue + assert ( + signing_dialogue.associated_contract_api_dialogue == contract_api_dialogue + ) + + def test_signing_dialogues(self): + """Test the SigningDialogues class.""" + _, dialogue = self.signing_dialogues.create( + counterparty=COUNTERPARTY_AGENT_ADDRESS, + performative=SigningMessage.Performative.SIGN_TRANSACTION, + terms=self.mocked_terms, + raw_transaction=self.mocked_raw_tx, + ) + assert dialogue.role == SigningDialogue.Role.SKILL + assert dialogue.self_address == str(self.skill.skill_context.skill_id) diff --git a/tests/test_packages/test_skills/test_erc1155_deploy/test_handlers.py b/tests/test_packages/test_skills/test_erc1155_deploy/test_handlers.py new file mode 100644 index 0000000000..cc98097313 --- /dev/null +++ b/tests/test_packages/test_skills/test_erc1155_deploy/test_handlers.py @@ -0,0 +1,1171 @@ +# -*- coding: utf-8 -*- +# ------------------------------------------------------------------------------ +# +# Copyright 2018-2019 Fetch.AI Limited +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ------------------------------------------------------------------------------ +"""This module contains the tests of the handler classes of the erc1155_deploy skill.""" + +import logging +from typing import cast +from unittest.mock import patch + +from aea.crypto.ledger_apis import LedgerApis +from aea.helpers.transaction.base import State +from aea.protocols.dialogue.base import Dialogues +from aea.test_tools.test_skill import COUNTERPARTY_AGENT_ADDRESS + +from packages.fetchai.protocols.contract_api.message import ContractApiMessage +from packages.fetchai.protocols.default.message import DefaultMessage +from packages.fetchai.protocols.fipa.message import FipaMessage +from packages.fetchai.protocols.ledger_api.message import LedgerApiMessage +from packages.fetchai.protocols.oef_search.message import OefSearchMessage +from packages.fetchai.protocols.signing.message import SigningMessage +from packages.fetchai.skills.erc1155_deploy.dialogues import ( + ContractApiDialogue, + FipaDialogue, + LedgerApiDialogue, + OefSearchDialogue, + SigningDialogue, +) +from packages.fetchai.skills.erc1155_deploy.handlers import LEDGER_API_ADDRESS + +from tests.test_packages.test_skills.test_erc1155_deploy.intermediate_class import ( + ERC1155DeployTestCase, +) + + +class TestFipaHandler(ERC1155DeployTestCase): + """Test fipa handler of erc1155_deploy.""" + + def test_setup(self): + """Test the setup method of the fipa handler.""" + assert self.fipa_handler.setup() is None + self.assert_quantity_in_outbox(0) + + def test_handle_unidentified_dialogue(self): + """Test the _handle_unidentified_dialogue method of the fipa handler.""" + # setup + incorrect_dialogue_reference = ("", "") + incoming_message = cast( + FipaMessage, + self.build_incoming_message( + message_type=FipaMessage, + dialogue_reference=incorrect_dialogue_reference, + performative=FipaMessage.Performative.ACCEPT, + ), + ) + + # operation + with patch.object(self.logger, "log") as mock_logger: + self.fipa_handler.handle(incoming_message) + + # after + mock_logger.assert_any_call( + logging.INFO, f"unidentified dialogue for message={incoming_message}.", + ) + + self.assert_quantity_in_outbox(1) + has_attributes, error_str = self.message_has_attributes( + actual_message=self.get_message_from_outbox(), + message_type=DefaultMessage, + performative=DefaultMessage.Performative.ERROR, + to=incoming_message.sender, + sender=self.skill.skill_context.agent_address, + error_code=DefaultMessage.ErrorCode.INVALID_DIALOGUE, + error_msg="Invalid dialogue.", + error_data={"fipa_message": incoming_message.encode()}, + ) + assert has_attributes, error_str + + def test_handle_cfp_i(self): + """Test the _handle_cfp method of the fipa handler where is_tokens_minted is True.""" + # setup + self.strategy._is_tokens_minted = True + incoming_message = cast( + FipaMessage, + self.build_incoming_message( + message_type=FipaMessage, + performative=FipaMessage.Performative.CFP, + dialogue_reference=Dialogues.new_self_initiated_dialogue_reference(), + query=self.mocked_query, + ), + ) + + # operation + with patch.object( + self.strategy, "get_proposal", return_value=self.mocked_proposal + ) as mock_prop: + with patch.object(self.logger, "log") as mock_logger: + self.fipa_handler.handle(incoming_message) + + # after + mock_logger.assert_any_call( + logging.INFO, f"received CFP from sender={COUNTERPARTY_AGENT_ADDRESS[-5:]}", + ) + + mock_prop.assert_called_once() + + self.assert_quantity_in_outbox(1) + message = self.get_message_from_outbox() + has_attributes, error_str = self.message_has_attributes( + actual_message=message, + message_type=FipaMessage, + performative=FipaMessage.Performative.PROPOSE, + to=incoming_message.sender, + sender=self.skill.skill_context.agent_address, + proposal=self.mocked_proposal, + ) + assert has_attributes, error_str + + mock_logger.assert_any_call( + logging.INFO, + f"sending PROPOSE to agent={COUNTERPARTY_AGENT_ADDRESS[-5:]}: proposal={self.mocked_proposal.values}", + ) + + def test_handle_cfp_ii(self): + """Test the _handle_cfp method of the fipa handler where is_tokens_minted is False.""" + # setup + self.strategy._is_tokens_minted = False + incoming_message = cast( + FipaMessage, + self.build_incoming_message( + message_type=FipaMessage, + performative=FipaMessage.Performative.CFP, + dialogue_reference=Dialogues.new_self_initiated_dialogue_reference(), + query=self.mocked_query, + ), + ) + + # operation + with patch.object(self.logger, "log") as mock_logger: + self.fipa_handler.handle(incoming_message) + + # after + mock_logger.assert_any_call( + logging.INFO, f"received CFP from sender={COUNTERPARTY_AGENT_ADDRESS[-5:]}", + ) + + mock_logger.assert_any_call( + logging.INFO, "Contract items not minted yet. Try again later.", + ) + + self.assert_quantity_in_outbox(0) + + def test_handle_accept_w_inform_i(self): + """Test the _handle_accept_w_inform method of the fipa handler where tx_signature is NOT None.""" + # setup + tx_signature = "some_tx_signature" + self.strategy.contract_address = self.contract_address + fipa_dialogue = cast( + FipaDialogue, + self.prepare_skill_dialogue( + dialogues=self.fipa_dialogues, messages=self.list_of_fipa_messages[:2], + ), + ) + fipa_dialogue.proposal = self.mocked_proposal + incoming_message = cast( + FipaMessage, + self.build_incoming_message_for_skill_dialogue( + dialogue=fipa_dialogue, + performative=FipaMessage.Performative.ACCEPT_W_INFORM, + info={"tx_signature": tx_signature}, + ), + ) + + # operation + with patch.object( + self.strategy, "get_single_swap_terms", return_value=self.mocked_terms + ) as mock_swap: + with patch.object(self.logger, "log") as mock_logger: + self.fipa_handler.handle(incoming_message) + + # after + mock_logger.assert_any_call( + logging.INFO, + f"received ACCEPT_W_INFORM from sender={COUNTERPARTY_AGENT_ADDRESS[-5:]}: tx_signature={tx_signature}", + ) + + self.assert_quantity_in_outbox(1) + message = self.get_message_from_outbox() + has_attributes, error_str = self.message_has_attributes( + actual_message=message, + message_type=ContractApiMessage, + performative=ContractApiMessage.Performative.GET_RAW_TRANSACTION, + to=LEDGER_API_ADDRESS, + sender=str(self.skill.skill_context.skill_id), + ledger_id=self.strategy.ledger_id, + contract_id=self.strategy.contract_id, + contract_address=self.strategy.contract_address, + callable="get_atomic_swap_single_transaction", + kwargs=ContractApiMessage.Kwargs( + { + "from_address": self.skill.skill_context.agent_address, + "to_address": incoming_message.sender, + "token_id": int(fipa_dialogue.proposal.values["token_id"]), + "from_supply": int(fipa_dialogue.proposal.values["from_supply"]), + "to_supply": int(fipa_dialogue.proposal.values["to_supply"]), + "value": int(fipa_dialogue.proposal.values["value"]), + "trade_nonce": int(fipa_dialogue.proposal.values["trade_nonce"]), + "signature": tx_signature, + } + ), + ) + assert has_attributes, error_str + + mock_swap.assert_called_once() + contract_api_dialogue = cast( + ContractApiDialogue, self.contract_api_dialogues.get_dialogue(message) + ) + assert contract_api_dialogue.terms == self.mocked_terms + + mock_logger.assert_any_call( + logging.INFO, "requesting single atomic swap transaction...", + ) + + def test_handle_accept_w_inform_ii(self): + """Test the _handle_accept_w_inform method of the fipa handler where tx_signature is NOT None.""" + # setup + tx_signature = "some_tx_signature" + fipa_dialogue = cast( + FipaDialogue, + self.prepare_skill_dialogue( + dialogues=self.fipa_dialogues, messages=self.list_of_fipa_messages[:2], + ), + ) + fipa_dialogue.proposal = self.mocked_proposal + incoming_message = cast( + FipaMessage, + self.build_incoming_message_for_skill_dialogue( + dialogue=fipa_dialogue, + performative=FipaMessage.Performative.ACCEPT_W_INFORM, + info={"something": tx_signature}, + ), + ) + + # operation + with patch.object(self.logger, "log") as mock_logger: + self.fipa_handler.handle(incoming_message) + + # after + mock_logger.assert_any_call( + logging.INFO, + f"received ACCEPT_W_INFORM from sender={COUNTERPARTY_AGENT_ADDRESS[-5:]} with no signature.", + ) + + def test_handle_invalid(self): + """Test the _handle_invalid method of the fipa handler.""" + # setup + fipa_dialogue = cast( + FipaDialogue, + self.prepare_skill_dialogue( + dialogues=self.fipa_dialogues, messages=self.list_of_fipa_messages[:2], + ), + ) + incoming_message = cast( + FipaMessage, + self.build_incoming_message_for_skill_dialogue( + dialogue=fipa_dialogue, performative=FipaMessage.Performative.ACCEPT, + ), + ) + + # operation + with patch.object(self.logger, "log") as mock_logger: + self.fipa_handler.handle(incoming_message) + + # after + mock_logger.assert_any_call( + logging.WARNING, + f"cannot handle fipa message of performative={incoming_message.performative} in dialogue={fipa_dialogue}.", + ) + + def test_teardown(self): + """Test the teardown method of the fipa handler.""" + assert self.fipa_handler.teardown() is None + self.assert_quantity_in_outbox(0) + + +class TestLedgerApiHandler(ERC1155DeployTestCase): + """Test ledger_api handler of erc1155_deploy.""" + + is_agent_to_agent_messages = False + + def test_setup(self): + """Test the setup method of the ledger_api handler.""" + assert self.ledger_api_handler.setup() is None + self.assert_quantity_in_outbox(0) + + def test_handle_unidentified_dialogue(self): + """Test the _handle_unidentified_dialogue method of the ledger_api handler.""" + # setup + incorrect_dialogue_reference = ("", "") + incoming_message = cast( + LedgerApiMessage, + self.build_incoming_message( + message_type=LedgerApiMessage, + dialogue_reference=incorrect_dialogue_reference, + performative=LedgerApiMessage.Performative.BALANCE, + ledger_id=self.ledger_id, + balance=10, + ), + ) + + # operation + with patch.object(self.logger, "log") as mock_logger: + self.ledger_api_handler.handle(incoming_message) + + # after + mock_logger.assert_any_call( + logging.INFO, + f"received invalid ledger_api message={incoming_message}, unidentified dialogue.", + ) + + def test_handle_balance(self): + """Test the _handle_balance method of the ledger_api handler.""" + # setup + balance = 10 + ledger_api_dialogue = cast( + LedgerApiDialogue, + self.prepare_skill_dialogue( + dialogues=self.ledger_api_dialogues, + messages=self.list_of_ledger_api_balance_messages[:1], + counterparty=LEDGER_API_ADDRESS, + ), + ) + incoming_message = cast( + LedgerApiMessage, + self.build_incoming_message_for_skill_dialogue( + dialogue=ledger_api_dialogue, + performative=LedgerApiMessage.Performative.BALANCE, + ledger_id=self.ledger_id, + balance=balance, + ), + ) + + # operation + with patch.object(self.logger, "log") as mock_logger: + self.ledger_api_handler.handle(incoming_message) + + # after + mock_logger.assert_any_call( + logging.INFO, + f"starting balance on {self.ledger_id} ledger={incoming_message.balance}.", + ) + + def test_handle_transaction_digest(self): + """Test the _handle_transaction_digest method of the ledger_api handler.""" + # setup + ledger_api_dialogue = cast( + LedgerApiDialogue, + self.prepare_skill_dialogue( + dialogues=self.ledger_api_dialogues, + messages=self.list_of_ledger_api_messages[:3], + counterparty=LEDGER_API_ADDRESS, + ), + ) + incoming_message = cast( + LedgerApiMessage, + self.build_incoming_message_for_skill_dialogue( + dialogue=ledger_api_dialogue, + performative=LedgerApiMessage.Performative.TRANSACTION_DIGEST, + transaction_digest=self.mocked_tx_digest, + ), + ) + + # operation + with patch.object(self.logger, "log") as mock_logger: + self.ledger_api_handler.handle(incoming_message) + + # after + mock_logger.assert_any_call( + logging.INFO, + f"transaction was successfully submitted. Transaction digest={incoming_message.transaction_digest}", + ) + + self.assert_quantity_in_outbox(1) + has_attributes, error_str = self.message_has_attributes( + actual_message=self.get_message_from_outbox(), + message_type=LedgerApiMessage, + performative=LedgerApiMessage.Performative.GET_TRANSACTION_RECEIPT, + to=incoming_message.sender, + sender=str(self.skill.skill_context.skill_id), + transaction_digest=self.mocked_tx_digest, + ) + assert has_attributes, error_str + + mock_logger.assert_any_call( + logging.INFO, "requesting transaction receipt.", + ) + + def test_handle_transaction_receipt_i(self): + """Test the _handle_transaction_receipt method of the ledger_api handler where the transaction is NOT settled.""" + # setup + ledger_api_dialogue = cast( + LedgerApiDialogue, + self.prepare_skill_dialogue( + dialogues=self.ledger_api_dialogues, + messages=self.list_of_ledger_api_messages[:5], + counterparty=LEDGER_API_ADDRESS, + ), + ) + incoming_message = cast( + LedgerApiMessage, + self.build_incoming_message_for_skill_dialogue( + dialogue=ledger_api_dialogue, + performative=LedgerApiMessage.Performative.TRANSACTION_RECEIPT, + transaction_receipt=self.mocked_tx_receipt, + ), + ) + + # operation + with patch.object(self.logger, "log") as mock_logger: + with patch.object(LedgerApis, "is_transaction_settled", return_value=False): + self.ledger_api_handler.handle(incoming_message) + + # after + mock_logger.assert_any_call( + logging.ERROR, + f"transaction failed. Transaction receipt={incoming_message.transaction_receipt}", + ) + + def test_handle_transaction_receipt_ii(self): + """Test the _handle_transaction_receipt method of the ledger_api handler where is_contract_deployed is False.""" + # setup + self.strategy._is_contract_deployed = False + + ledger_api_dialogue = cast( + LedgerApiDialogue, + self.prepare_skill_dialogue( + dialogues=self.ledger_api_dialogues, + messages=self.list_of_ledger_api_messages[:5], + counterparty=LEDGER_API_ADDRESS, + ), + ) + incoming_message = cast( + LedgerApiMessage, + self.build_incoming_message_for_skill_dialogue( + dialogue=ledger_api_dialogue, + performative=LedgerApiMessage.Performative.TRANSACTION_RECEIPT, + transaction_receipt=self.mocked_tx_receipt, + ), + ) + + # operation + with patch.object(self.logger, "log") as mock_logger: + with patch.object(LedgerApis, "is_transaction_settled", return_value=True): + self.ledger_api_handler.handle(incoming_message) + + # after + mock_logger.assert_any_call( + logging.INFO, + f"transaction was successfully settled. Transaction receipt={self.mocked_tx_receipt}", + ) + + assert self.strategy.contract_address == self.contract_address + assert self.strategy.is_contract_deployed is True + assert self.strategy.is_behaviour_active is True + + def test_handle_transaction_receipt_iii(self): + """Test the _handle_transaction_receipt method of the ledger_api handler where is_tokens_created is False.""" + # setup + self.strategy._is_contract_deployed = True + self.strategy._is_tokens_created = False + + ledger_api_dialogue = cast( + LedgerApiDialogue, + self.prepare_skill_dialogue( + dialogues=self.ledger_api_dialogues, + messages=self.list_of_ledger_api_messages[:5], + counterparty=LEDGER_API_ADDRESS, + ), + ) + incoming_message = cast( + LedgerApiMessage, + self.build_incoming_message_for_skill_dialogue( + dialogue=ledger_api_dialogue, + performative=LedgerApiMessage.Performative.TRANSACTION_RECEIPT, + transaction_receipt=self.mocked_tx_receipt, + ), + ) + + # operation + with patch.object(self.logger, "log") as mock_logger: + with patch.object(LedgerApis, "is_transaction_settled", return_value=True): + self.ledger_api_handler.handle(incoming_message) + + # after + mock_logger.assert_any_call( + logging.INFO, + f"transaction was successfully settled. Transaction receipt={self.mocked_tx_receipt}", + ) + + assert self.strategy.is_tokens_created is True + assert self.strategy.is_behaviour_active is True + + def test_handle_transaction_receipt_iv(self): + """Test the _handle_transaction_receipt method of the ledger_api handler where is_tokens_minted is False.""" + # setup + self.strategy._is_contract_deployed = True + self.strategy._is_tokens_created = True + self.strategy._is_tokens_minted = False + + ledger_api_dialogue = cast( + LedgerApiDialogue, + self.prepare_skill_dialogue( + dialogues=self.ledger_api_dialogues, + messages=self.list_of_ledger_api_messages[:5], + counterparty=LEDGER_API_ADDRESS, + ), + ) + incoming_message = cast( + LedgerApiMessage, + self.build_incoming_message_for_skill_dialogue( + dialogue=ledger_api_dialogue, + performative=LedgerApiMessage.Performative.TRANSACTION_RECEIPT, + transaction_receipt=self.mocked_tx_receipt, + ), + ) + + # operation + with patch.object(self.logger, "log") as mock_logger: + with patch.object(LedgerApis, "is_transaction_settled", return_value=True): + self.ledger_api_handler.handle(incoming_message) + + # after + mock_logger.assert_any_call( + logging.INFO, + f"transaction was successfully settled. Transaction receipt={self.mocked_tx_receipt}", + ) + + assert self.strategy.is_tokens_minted is True + assert self.strategy.is_behaviour_active is True + + def test_handle_transaction_receipt_v(self): + """Test the _handle_transaction_receipt method of the ledger_api handler where is_tokens_minted is True.""" + # setup + self.strategy._is_contract_deployed = True + self.strategy._is_tokens_created = True + self.strategy._is_tokens_minted = True + + ledger_api_dialogue = cast( + LedgerApiDialogue, + self.prepare_skill_dialogue( + dialogues=self.ledger_api_dialogues, + messages=self.list_of_ledger_api_messages[:5], + counterparty=LEDGER_API_ADDRESS, + ), + ) + incoming_message = cast( + LedgerApiMessage, + self.build_incoming_message_for_skill_dialogue( + dialogue=ledger_api_dialogue, + performative=LedgerApiMessage.Performative.TRANSACTION_RECEIPT, + transaction_receipt=self.mocked_tx_receipt, + ), + ) + + # operation + with patch.object(self.logger, "log") as mock_logger: + with patch.object(LedgerApis, "is_transaction_settled", return_value=True): + self.ledger_api_handler.handle(incoming_message) + + # after + mock_logger.assert_any_call( + logging.INFO, + f"transaction was successfully settled. Transaction receipt={self.mocked_tx_receipt}", + ) + + assert self.skill.skill_context.is_active is False + mock_logger.assert_any_call(logging.INFO, "demo finished!") + + def test_handle_error(self): + """Test the _handle_error method of the ledger_api handler.""" + # setup + ledger_api_dialogue = cast( + LedgerApiDialogue, + self.prepare_skill_dialogue( + dialogues=self.ledger_api_dialogues, + messages=self.list_of_ledger_api_balance_messages[:1], + ), + ) + incoming_message = cast( + LedgerApiMessage, + self.build_incoming_message_for_skill_dialogue( + dialogue=ledger_api_dialogue, + performative=LedgerApiMessage.Performative.ERROR, + code=1, + ), + ) + + # operation + with patch.object(self.logger, "log") as mock_logger: + self.ledger_api_handler.handle(incoming_message) + + # after + mock_logger.assert_any_call( + logging.INFO, + f"received ledger_api error message={incoming_message} in dialogue={ledger_api_dialogue}.", + ) + + def test_handle_invalid(self): + """Test the _handle_invalid method of the ledger_api handler.""" + # setup + invalid_performative = LedgerApiMessage.Performative.GET_BALANCE + incoming_message = cast( + LedgerApiMessage, + self.build_incoming_message( + message_type=LedgerApiMessage, + dialogue_reference=("1", ""), + performative=invalid_performative, + ledger_id=self.ledger_id, + address=self.address, + to=str(self.skill.skill_context.skill_id), + ), + ) + + # operation + with patch.object(self.logger, "log") as mock_logger: + self.ledger_api_handler.handle(incoming_message) + + # after + mock_logger.assert_any_call( + logging.WARNING, + f"cannot handle ledger_api message of performative={invalid_performative} in dialogue={self.ledger_api_dialogues.get_dialogue(incoming_message)}.", + ) + + def test_teardown(self): + """Test the teardown method of the ledger_api handler.""" + assert self.ledger_api_handler.teardown() is None + self.assert_quantity_in_outbox(0) + + +class TestContractApiHandler(ERC1155DeployTestCase): + """Test contract_api handler of erc1155_deploy.""" + + is_agent_to_agent_messages = False + + def test_setup(self): + """Test the setup method of the contract_api handler.""" + assert self.contract_api_handler.setup() is None + self.assert_quantity_in_outbox(0) + + def test_handle_unidentified_dialogue(self): + """Test the _handle_unidentified_dialogue method of the signing handler.""" + # setup + incorrect_dialogue_reference = ("", "") + incoming_message = cast( + ContractApiMessage, + self.build_incoming_message( + message_type=ContractApiMessage, + dialogue_reference=incorrect_dialogue_reference, + performative=ContractApiMessage.Performative.STATE, + state=State(self.ledger_id, self.body_dict), + ), + ) + + # operation + with patch.object(self.logger, "log") as mock_logger: + self.contract_api_handler.handle(incoming_message) + + # after + mock_logger.assert_any_call( + logging.INFO, + f"received invalid contract_api message={incoming_message}, unidentified dialogue.", + ) + + def test_handle_raw_transaction(self): + """Test the _handle_raw_transaction method of the signing handler.""" + # setup + contract_api_dialogue = cast( + ContractApiDialogue, + self.prepare_skill_dialogue( + dialogues=self.contract_api_dialogues, + messages=self.list_of_contract_api_messages[:1], + ), + ) + contract_api_dialogue.terms = self.mocked_terms + incoming_message = cast( + ContractApiMessage, + self.build_incoming_message_for_skill_dialogue( + dialogue=contract_api_dialogue, + performative=ContractApiMessage.Performative.RAW_TRANSACTION, + raw_transaction=self.mocked_raw_tx, + ), + ) + + # operation + with patch.object(self.logger, "log") as mock_logger: + self.contract_api_handler.handle(incoming_message) + + # after + mock_logger.assert_any_call( + logging.INFO, f"received raw transaction={incoming_message}" + ) + + self.assert_quantity_in_decision_making_queue(1) + message = self.get_message_from_decision_maker_inbox() + has_attributes, error_str = self.message_has_attributes( + actual_message=message, + message_type=SigningMessage, + performative=SigningMessage.Performative.SIGN_TRANSACTION, + to=self.skill.skill_context.decision_maker_address, + sender=str(self.skill.skill_context.skill_id), + raw_transaction=self.mocked_raw_tx, + terms=contract_api_dialogue.terms, + ) + assert has_attributes, error_str + + assert ( + cast( + SigningDialogue, self.signing_dialogues.get_dialogue(message) + ).associated_contract_api_dialogue + == contract_api_dialogue + ) + + mock_logger.assert_any_call( + logging.INFO, + "proposing the transaction to the decision maker. Waiting for confirmation ...", + ) + + def test_handle_error(self): + """Test the _handle_error method of the signing handler.""" + # setup + contract_api_dialogue = cast( + ContractApiDialogue, + self.prepare_skill_dialogue( + dialogues=self.contract_api_dialogues, + messages=self.list_of_contract_api_messages[:1], + ), + ) + incoming_message = cast( + ContractApiMessage, + self.build_incoming_message_for_skill_dialogue( + dialogue=contract_api_dialogue, + performative=ContractApiMessage.Performative.ERROR, + data=b"some_data", + ), + ) + + # operation + with patch.object(self.logger, "log") as mock_logger: + self.contract_api_handler.handle(incoming_message) + + # after + mock_logger.assert_any_call( + logging.INFO, + f"received contract_api error message={incoming_message} in dialogue={contract_api_dialogue}.", + ) + + def test_handle_invalid(self): + """Test the _handle_invalid method of the signing handler.""" + # setup + invalid_performative = ContractApiMessage.Performative.GET_DEPLOY_TRANSACTION + incoming_message = cast( + ContractApiMessage, + self.build_incoming_message( + message_type=ContractApiMessage, + dialogue_reference=("1", ""), + performative=invalid_performative, + ledger_id=self.ledger_id, + contract_id=self.contract_id, + callable=self.callable, + kwargs=self.kwargs, + ), + ) + + # operation + with patch.object(self.logger, "log") as mock_logger: + self.contract_api_handler.handle(incoming_message) + + # after + mock_logger.assert_any_call( + logging.WARNING, + f"cannot handle contract_api message of performative={invalid_performative} in dialogue={self.contract_api_dialogues.get_dialogue(incoming_message)}.", + ) + + def test_teardown(self): + """Test the teardown method of the contract_api handler.""" + assert self.contract_api_handler.teardown() is None + self.assert_quantity_in_outbox(0) + + +class TestSigningHandler(ERC1155DeployTestCase): + """Test signing handler of erc1155_deploy.""" + + is_agent_to_agent_messages = False + + def test_setup(self): + """Test the setup method of the signing handler.""" + assert self.signing_handler.setup() is None + self.assert_quantity_in_outbox(0) + + def test_handle_unidentified_dialogue(self): + """Test the _handle_unidentified_dialogue method of the signing handler.""" + # setup + incorrect_dialogue_reference = ("", "") + incoming_message = cast( + SigningMessage, + self.build_incoming_message( + message_type=SigningMessage, + dialogue_reference=incorrect_dialogue_reference, + performative=SigningMessage.Performative.ERROR, + error_code=SigningMessage.ErrorCode.UNSUCCESSFUL_MESSAGE_SIGNING, + to=str(self.skill.skill_context.skill_id), + ), + ) + + # operation + with patch.object(self.logger, "log") as mock_logger: + self.signing_handler.handle(incoming_message) + + # after + mock_logger.assert_any_call( + logging.INFO, + f"received invalid signing message={incoming_message}, unidentified dialogue.", + ) + + def test_handle_signed_transaction(self,): + """Test the _handle_signed_transaction method of the signing handler.""" + # setup + signing_dialogue = cast( + SigningDialogue, + self.prepare_skill_dialogue( + dialogues=self.signing_dialogues, + messages=self.list_of_signing_messages[:1], + counterparty=self.skill.skill_context.decision_maker_address, + ), + ) + incoming_message = cast( + SigningMessage, + self.build_incoming_message_for_skill_dialogue( + dialogue=signing_dialogue, + performative=SigningMessage.Performative.SIGNED_TRANSACTION, + signed_transaction=self.mocked_signed_tx, + ), + ) + + # operation + with patch.object(self.logger, "log") as mock_logger: + self.signing_handler.handle(incoming_message) + + # after + mock_logger.assert_any_call(logging.INFO, "transaction signing was successful.") + + self.assert_quantity_in_outbox(1) + message = self.get_message_from_outbox() + has_attributes, error_str = self.message_has_attributes( + actual_message=message, + message_type=LedgerApiMessage, + performative=LedgerApiMessage.Performative.SEND_SIGNED_TRANSACTION, + to=LEDGER_API_ADDRESS, + sender=str(self.skill.skill_context.skill_id), + signed_transaction=self.mocked_signed_tx, + ) + assert has_attributes, error_str + + ledger_api_dialogue = cast( + LedgerApiDialogue, self.ledger_api_dialogues.get_dialogue(message) + ) + assert ledger_api_dialogue.associated_signing_dialogue == signing_dialogue + + mock_logger.assert_any_call(logging.INFO, "sending transaction to ledger.") + + def test_handle_error(self): + """Test the _handle_error method of the signing handler.""" + # setup + signing_counterparty = self.skill.skill_context.decision_maker_address + signing_dialogue = self.prepare_skill_dialogue( + dialogues=self.signing_dialogues, + messages=self.list_of_signing_messages[:1], + counterparty=signing_counterparty, + ) + incoming_message = cast( + SigningMessage, + self.build_incoming_message_for_skill_dialogue( + dialogue=signing_dialogue, + performative=SigningMessage.Performative.ERROR, + error_code=SigningMessage.ErrorCode.UNSUCCESSFUL_TRANSACTION_SIGNING, + ), + ) + + # operation + with patch.object(self.logger, "log") as mock_logger: + self.signing_handler.handle(incoming_message) + + # after + mock_logger.assert_any_call( + logging.INFO, + f"transaction signing was not successful. Error_code={incoming_message.error_code} in dialogue={signing_dialogue}", + ) + + def test_handle_invalid(self): + """Test the _handle_invalid method of the signing handler.""" + # setup + invalid_performative = SigningMessage.Performative.SIGN_TRANSACTION + incoming_message = self.build_incoming_message( + message_type=SigningMessage, + dialogue_reference=("1", ""), + performative=invalid_performative, + terms=self.mocked_terms, + raw_transaction=SigningMessage.RawTransaction( + self.ledger_id, {"some_key": "some_value"} + ), + to=str(self.skill.skill_context.skill_id), + ) + + # operation + with patch.object(self.logger, "log") as mock_logger: + self.signing_handler.handle(incoming_message) + + # after + mock_logger.assert_any_call( + logging.WARNING, + f"cannot handle signing message of performative={invalid_performative} in dialogue={self.signing_dialogues.get_dialogue(incoming_message)}.", + ) + + def test_teardown(self): + """Test the teardown method of the signing handler.""" + assert self.signing_handler.teardown() is None + self.assert_quantity_in_outbox(0) + + +class TestOefSearchHandler(ERC1155DeployTestCase): + """Test oef_search handler of erc1155_deploy.""" + + is_agent_to_agent_messages = False + + def test_setup(self): + """Test the setup method of the oef_search handler.""" + assert self.oef_search_handler.setup() is None + self.assert_quantity_in_outbox(0) + + def test_handle_unidentified_dialogue(self): + """Test the _handle_unidentified_dialogue method of the oef_search handler.""" + # setup + incorrect_dialogue_reference = ("", "") + incoming_message = cast( + OefSearchMessage, + self.build_incoming_message( + message_type=OefSearchMessage, + dialogue_reference=incorrect_dialogue_reference, + performative=OefSearchMessage.Performative.OEF_ERROR, + oef_error_operation=OefSearchMessage.OefErrorOperation.REGISTER_SERVICE, + ), + ) + + # operation + with patch.object(self.logger, "log") as mock_logger: + self.oef_search_handler.handle(incoming_message) + + # after + mock_logger.assert_any_call( + logging.INFO, + f"received invalid oef_search message={incoming_message}, unidentified dialogue.", + ) + + def test_handle_error(self): + """Test the _handle_error method of the oef_search handler.""" + # setup + oef_search_dialogue = cast( + OefSearchDialogue, + self.prepare_skill_dialogue( + dialogues=self.oef_search_dialogues, + messages=self.list_of_messages_register_location[:1], + ), + ) + incoming_message = cast( + OefSearchMessage, + self.build_incoming_message_for_skill_dialogue( + dialogue=oef_search_dialogue, + performative=OefSearchMessage.Performative.OEF_ERROR, + oef_error_operation=OefSearchMessage.OefErrorOperation.REGISTER_SERVICE, + ), + ) + + # operation + with patch.object(self.logger, "log") as mock_logger: + self.oef_search_handler.handle(incoming_message) + + # after + mock_logger.assert_any_call( + logging.INFO, + f"received oef_search error message={incoming_message} in dialogue={oef_search_dialogue}.", + ) + + def test_handle_success_i(self): + """Test the _handle_success method of the oef_search handler where the oef success targets register_service WITH location_agent data model description.""" + # setup + oef_dialogue = self.prepare_skill_dialogue( + dialogues=self.oef_search_dialogues, + messages=self.list_of_messages_register_location[:1], + ) + incoming_message = self.build_incoming_message_for_skill_dialogue( + dialogue=oef_dialogue, + performative=OefSearchMessage.Performative.SUCCESS, + agents_info=OefSearchMessage.AgentsInfo({"address": {"key": "value"}}), + ) + + # operation + with patch.object(self.oef_search_handler.context.logger, "log") as mock_logger: + with patch.object( + self.registration_behaviour, "register_service", + ) as mock_reg: + self.oef_search_handler.handle(incoming_message) + + # after + mock_logger.assert_any_call( + logging.INFO, + f"received oef_search success message={incoming_message} in dialogue={oef_dialogue}.", + ) + mock_reg.assert_called_once() + + def test_handle_success_ii(self): + """Test the _handle_success method of the oef_search handler where the oef success targets register_service WITH set_service_key data model description.""" + # setup + oef_dialogue = self.prepare_skill_dialogue( + dialogues=self.oef_search_dialogues, + messages=self.list_of_messages_register_service[:1], + ) + incoming_message = self.build_incoming_message_for_skill_dialogue( + dialogue=oef_dialogue, + performative=OefSearchMessage.Performative.SUCCESS, + agents_info=OefSearchMessage.AgentsInfo({"address": {"key": "value"}}), + ) + + # operation + with patch.object(self.oef_search_handler.context.logger, "log") as mock_logger: + with patch.object( + self.registration_behaviour, "register_genus", + ) as mock_reg: + self.oef_search_handler.handle(incoming_message) + + # after + mock_logger.assert_any_call( + logging.INFO, + f"received oef_search success message={incoming_message} in dialogue={oef_dialogue}.", + ) + mock_reg.assert_called_once() + + def test_handle_success_iii(self): + """Test the _handle_success method of the oef_search handler where the oef success targets register_service WITH personality_agent data model and genus value description.""" + # setup + oef_dialogue = self.prepare_skill_dialogue( + dialogues=self.oef_search_dialogues, + messages=self.list_of_messages_register_genus[:1], + ) + incoming_message = self.build_incoming_message_for_skill_dialogue( + dialogue=oef_dialogue, + performative=OefSearchMessage.Performative.SUCCESS, + agents_info=OefSearchMessage.AgentsInfo({"address": {"key": "value"}}), + ) + + # operation + with patch.object(self.oef_search_handler.context.logger, "log") as mock_logger: + with patch.object( + self.registration_behaviour, "register_classification", + ) as mock_reg: + self.oef_search_handler.handle(incoming_message) + + # after + mock_logger.assert_any_call( + logging.INFO, + f"received oef_search success message={incoming_message} in dialogue={oef_dialogue}.", + ) + mock_reg.assert_called_once() + + def test_handle_success_iv(self): + """Test the _handle_success method of the oef_search handler where the oef success targets register_service WITH personality_agent data model and classification value description.""" + # setup + oef_dialogue = self.prepare_skill_dialogue( + dialogues=self.oef_search_dialogues, + messages=self.list_of_messages_register_classification[:1], + ) + incoming_message = self.build_incoming_message_for_skill_dialogue( + dialogue=oef_dialogue, + performative=OefSearchMessage.Performative.SUCCESS, + agents_info=OefSearchMessage.AgentsInfo({"address": {"key": "value"}}), + ) + + # operation + with patch.object(self.oef_search_handler.context.logger, "log") as mock_logger: + self.oef_search_handler.handle(incoming_message) + + # after + mock_logger.assert_any_call( + logging.INFO, + f"received oef_search success message={incoming_message} in dialogue={oef_dialogue}.", + ) + + assert self.registration_behaviour.is_registered is True + assert self.registration_behaviour.registration_in_progress is False + + mock_logger.assert_any_call( + logging.INFO, + "the agent, with its genus and classification, and its service are successfully registered on the SOEF.", + ) + + def test_handle_success_v(self): + """Test the _handle_success method of the oef_search handler where the oef success targets unregister_service.""" + # setup + oef_dialogue = self.prepare_skill_dialogue( + dialogues=self.oef_search_dialogues, + messages=self.list_of_messages_register_invalid[:1], + ) + incoming_message = self.build_incoming_message_for_skill_dialogue( + dialogue=oef_dialogue, + performative=OefSearchMessage.Performative.SUCCESS, + agents_info=OefSearchMessage.AgentsInfo({"address": {"key": "value"}}), + ) + + # operation + with patch.object(self.oef_search_handler.context.logger, "log") as mock_logger: + self.oef_search_handler.handle(incoming_message) + + # after + mock_logger.assert_any_call( + logging.INFO, + f"received oef_search success message={incoming_message} in dialogue={oef_dialogue}.", + ) + mock_logger.assert_any_call( + logging.WARNING, + f"received soef SUCCESS message as a reply to the following unexpected message: {oef_dialogue.get_message_by_id(incoming_message.target)}", + ) + + def test_handle_invalid(self): + """Test the _handle_invalid method of the oef_search handler.""" + # setup + incoming_message = cast( + OefSearchMessage, + self.build_incoming_message( + message_type=OefSearchMessage, + performative=OefSearchMessage.Performative.REGISTER_SERVICE, + service_description=self.mocked_proposal, + ), + ) + + # operation + with patch.object(self.logger, "log") as mock_logger: + self.oef_search_handler.handle(incoming_message) + + # after + mock_logger.assert_any_call( + logging.WARNING, + f"cannot handle oef_search message of performative={incoming_message.performative} in dialogue={self.oef_search_dialogues.get_dialogue(incoming_message)}.", + ) + + def test_teardown(self): + """Test the teardown method of the oef_search handler.""" + assert self.oef_search_handler.teardown() is None + self.assert_quantity_in_outbox(0) diff --git a/tests/test_packages/test_skills/test_erc1155_deploy/test_strategy.py b/tests/test_packages/test_skills/test_erc1155_deploy/test_strategy.py new file mode 100644 index 0000000000..49c9f25061 --- /dev/null +++ b/tests/test_packages/test_skills/test_erc1155_deploy/test_strategy.py @@ -0,0 +1,214 @@ +# -*- coding: utf-8 -*- +# ------------------------------------------------------------------------------ +# +# Copyright 2018-2019 Fetch.AI Limited +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ------------------------------------------------------------------------------ +"""This module contains the tests of the strategy class of the erc1155_deploy skill.""" + +import pytest + +from aea.exceptions import AEAEnforceError +from aea.helpers.search.generic import ( + AGENT_LOCATION_MODEL, + AGENT_PERSONALITY_MODEL, + AGENT_REMOVE_SERVICE_MODEL, + AGENT_SET_SERVICE_MODEL, +) +from aea.helpers.search.models import Description, Location +from aea.helpers.transaction.base import Terms + +from packages.fetchai.contracts.erc1155.contract import ERC1155Contract +from packages.fetchai.skills.erc1155_deploy.strategy import SIMPLE_SERVICE_MODEL + +from tests.test_packages.test_skills.test_erc1155_deploy.intermediate_class import ( + ERC1155DeployTestCase, +) + + +class TestStrategy(ERC1155DeployTestCase): + """Test Strategy of erc1155_deploy.""" + + def test_properties(self): + """Test the properties of Strategy class.""" + assert self.strategy.ledger_id == self.skill.skill_context.default_ledger_id + assert self.strategy.contract_id == str(ERC1155Contract.contract_id) + assert self.strategy.mint_quantities == self.mint_quantities + assert self.strategy.token_ids == self.strategy._token_ids + + with pytest.raises(ValueError, match="Contract address not set!"): + assert self.strategy.contract_address + self.strategy.contract_address = self.contract_address + with pytest.raises(AEAEnforceError, match="Contract address already set!"): + self.strategy.contract_address = self.contract_address + assert self.strategy.contract_address == self.contract_address + + assert self.strategy.is_contract_deployed is False + self.strategy.is_contract_deployed = True + assert self.strategy.is_contract_deployed is True + with pytest.raises(AEAEnforceError, match="Only allowed to switch to true."): + self.strategy.is_contract_deployed = False + + assert self.strategy.is_tokens_created is False + self.strategy.is_tokens_created = True + assert self.strategy.is_tokens_created is True + with pytest.raises(AEAEnforceError, match="Only allowed to switch to true."): + self.strategy.is_tokens_created = False + + assert self.strategy.is_tokens_minted is False + self.strategy.is_tokens_minted = True + assert self.strategy.is_tokens_minted is True + with pytest.raises(AEAEnforceError, match="Only allowed to switch to true."): + self.strategy.is_tokens_minted = False + + assert self.strategy.gas == self.strategy._gas + + def test_get_location_description(self): + """Test the get_location_description method of the Strategy class.""" + description = self.strategy.get_location_description() + + assert type(description) == Description + assert description.data_model is AGENT_LOCATION_MODEL + assert description.values.get("location", "") == Location( + latitude=self.location["latitude"], longitude=self.location["longitude"] + ) + + def test_get_register_service_description(self): + """Test the get_register_service_description method of the Strategy class.""" + description = self.strategy.get_register_service_description() + + assert type(description) == Description + assert description.data_model is AGENT_SET_SERVICE_MODEL + assert description.values.get("key", "") == self.service_data["key"] + assert description.values.get("value", "") == self.service_data["value"] + + def test_get_register_personality_description(self): + """Test the get_register_personality_description method of the Strategy class.""" + description = self.strategy.get_register_personality_description() + + assert type(description) == Description + assert description.data_model is AGENT_PERSONALITY_MODEL + assert description.values.get("piece", "") == self.personality_data["piece"] + assert description.values.get("value", "") == self.personality_data["value"] + + def test_get_register_classification_description(self): + """Test the get_register_classification_description method of the Strategy class.""" + description = self.strategy.get_register_classification_description() + + assert type(description) == Description + assert description.data_model is AGENT_PERSONALITY_MODEL + assert description.values.get("piece", "") == self.classification["piece"] + assert description.values.get("value", "") == self.classification["value"] + + def test_get_service_description(self): + """Test the get_service_description method of the Strategy class.""" + description = self.strategy.get_service_description() + + assert type(description) == Description + assert description.data_model is SIMPLE_SERVICE_MODEL + assert ( + description.values.get("seller_service", "") == self.service_data["value"] + ) + + def test_get_unregister_service_description(self): + """Test the get_unregister_service_description method of the Strategy class.""" + description = self.strategy.get_unregister_service_description() + + assert type(description) == Description + assert description.data_model is AGENT_REMOVE_SERVICE_MODEL + assert description.values.get("key", "") == self.service_data["key"] + + def test_get_deploy_terms(self): + """Test the get_deploy_terms of Strategy.""" + assert self.strategy.get_deploy_terms() == Terms( + ledger_id=self.ledger_id, + sender_address=self.skill.skill_context.agent_address, + counterparty_address=self.skill.skill_context.agent_address, + amount_by_currency_id={}, + quantities_by_good_id={}, + nonce="", + ) + + def test_get_create_token_terms(self): + """Test the get_create_token_terms of Parameters.""" + assert self.strategy.get_create_token_terms() == Terms( + ledger_id=self.ledger_id, + sender_address=self.skill.skill_context.agent_address, + counterparty_address=self.skill.skill_context.agent_address, + amount_by_currency_id={}, + quantities_by_good_id={}, + nonce="", + ) + + def test_get_mint_token_terms(self): + """Test the get_mint_token_terms of Strategy.""" + assert self.strategy.get_mint_token_terms() == Terms( + ledger_id=self.ledger_id, + sender_address=self.skill.skill_context.agent_address, + counterparty_address=self.skill.skill_context.agent_address, + amount_by_currency_id={}, + quantities_by_good_id={}, + nonce="", + ) + + def test_get_proposal(self): + """Test the get_proposal of Strategy.""" + # setup + self.strategy._contract_address = self.contract_address + first_id = 8768 + self.strategy._token_ids = [first_id, 234, 879643] + + # operation + actual_proposal = self.strategy.get_proposal() + + # after + assert all( + keys in actual_proposal.values + for keys in [ + "contract_address", + "token_id", + "trade_nonce", + "from_supply", + "to_supply", + ] + ) + assert ( + actual_proposal.values.get("contract_address", "") == self.contract_address + ) + assert actual_proposal.values.get("token_id", "") == str(first_id) + assert isinstance(actual_proposal.values.get("trade_nonce", ""), str) + assert actual_proposal.values.get("from_supply", "") == str(self.from_supply) + assert actual_proposal.values.get("to_supply", "") == str(self.to_supply) + assert actual_proposal.values.get("value", "") == str(self.value) + + def test_get_single_swap_terms(self): + """Test the get_single_swap_terms of Strategy.""" + assert self.strategy.get_single_swap_terms( + self.mocked_proposal, "some_address" + ) == Terms( + ledger_id=self.ledger_id, + sender_address=self.skill.skill_context.agent_address, + counterparty_address="some_address", + amount_by_currency_id={ + str(self.mocked_proposal.values["token_id"]): int( + self.mocked_proposal.values["from_supply"] + ) + - int(self.mocked_proposal.values["to_supply"]) + }, + quantities_by_good_id={}, + is_sender_payable_tx_fee=True, + nonce=str(self.mocked_proposal.values["trade_nonce"]), + fee_by_currency_id={}, + ) From a5f991d60d41676b2e691a5fd59b1b1c11a8c869 Mon Sep 17 00:00:00 2001 From: MarcoFavorito Date: Mon, 24 May 2021 00:40:18 +0200 Subject: [PATCH 093/147] fix: update manager add check 'ensure_private_keys' at the end of AgentAlias.set_overrides, which in turn is called by 'MultiAgentManager.add_agent'. --- aea/manager/project.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/aea/manager/project.py b/aea/manager/project.py index 04d8aa2a04..8e0dd12f6f 100644 --- a/aea/manager/project.py +++ b/aea/manager/project.py @@ -301,7 +301,8 @@ def set_overrides( ) overrides["component_configurations"] = component_configurations - return self.agent_config_manager.update_config(overrides) + self.agent_config_manager.update_config(overrides) + self._ensure_private_keys() @property def agent_config_manager(self) -> AgentConfigManager: From 71440951aa9a704431ddff716a55565ea801566e Mon Sep 17 00:00:00 2001 From: ali Date: Mon, 24 May 2021 09:41:04 +0100 Subject: [PATCH 094/147] tests: fill coverage gap --- .../fetchai/skills/erc1155_client/handlers.py | 2 +- .../fetchai/skills/erc1155_client/skill.yaml | 2 +- packages/hashes.csv | 2 +- .../test_erc1155_deploy/intermediate_class.py | 2 ++ .../test_erc1155_deploy/test_strategy.py | 25 ++++++++++++++++++- 5 files changed, 29 insertions(+), 4 deletions(-) diff --git a/packages/fetchai/skills/erc1155_client/handlers.py b/packages/fetchai/skills/erc1155_client/handlers.py index c435dfc53c..fa126e0bf5 100644 --- a/packages/fetchai/skills/erc1155_client/handlers.py +++ b/packages/fetchai/skills/erc1155_client/handlers.py @@ -530,7 +530,7 @@ def _handle_signed_message( signing_dialogue.associated_contract_api_dialogue.associated_fipa_dialogue ) last_fipa_msg = fipa_dialogue.last_incoming_message - if last_fipa_msg is None: + if last_fipa_msg is None: # pragma: nocover raise ValueError("Could not retrieve last fipa message.") inform_msg = fipa_dialogue.reply( performative=FipaMessage.Performative.ACCEPT_W_INFORM, diff --git a/packages/fetchai/skills/erc1155_client/skill.yaml b/packages/fetchai/skills/erc1155_client/skill.yaml index 11f5c45cac..18f6a04e2d 100644 --- a/packages/fetchai/skills/erc1155_client/skill.yaml +++ b/packages/fetchai/skills/erc1155_client/skill.yaml @@ -11,7 +11,7 @@ fingerprint: __init__.py: QmerzWHZ6jU4pH71xDJqpjavxQxhWzZvkc1wqNHUCzhoog behaviours.py: QmZSUiAqMwqUb9JhcF8X4DuEVnGDtSUVbTMQoo3nnXSpVS dialogues.py: QmYGjWc223Nu1warTtVFVpSCJKexgqwu6sFRaUsV7LwCPJ - handlers.py: QmSimvAHLwz8oqF61VRPWnSih2DCDUgEzVfViNmuzddQYS + handlers.py: QmaPVsz9Y22fwksw1UFsDFxq7YbmWZQnbDTCBHzdfVHNH4 strategy.py: QmavxySeUqdFjMY9Gkk4d8gWzKo5FkNmRbbGPvCAVRn7gf fingerprint_ignore_patterns: [] connections: diff --git a/packages/hashes.csv b/packages/hashes.csv index 3e64299f5c..1347933cf2 100644 --- a/packages/hashes.csv +++ b/packages/hashes.csv @@ -82,7 +82,7 @@ fetchai/skills/confirmation_aw1,QmVkf5oHohbx5vSfY269u1MUS1c8ttkDZAgfGcnijz5AcJ fetchai/skills/confirmation_aw2,QmZ8sKX3TCLB6yzpfSEWw3R8VbWrFEt2tewqvosUNm6Vc9 fetchai/skills/confirmation_aw3,QmQ5mdPyf3ryxutuapQAS1crL8kfU16o3VXHawcpQMdoKo fetchai/skills/echo,QmZ3B4XeTuWtRNke4Q7QBuxu3aZ8duXosVbZn72RW2cdQa -fetchai/skills/erc1155_client,QmZkZ2EhZiVLgpWvPu5TbzBZ5v8ss51WmDuCXvjeKLt8mH +fetchai/skills/erc1155_client,Qmaif7eCKgPXgu7EbKZWjaFbwiX6jDkkFLEGRm5VmhD1UJ fetchai/skills/erc1155_deploy,QmaESS4NoCCToboXG6kPbBrfFDoMzBUiPyX2yfvFSoJTKP fetchai/skills/error,QmfEkTwToeAL6QnzzXBocp33kMCvzyP6F3JGUKjqFYmgHH fetchai/skills/fetch_beacon,QmfBdgU21fgA7uH1sGSLk6jF8WShfZvfEhFbr92FuA9ide diff --git a/tests/test_packages/test_skills/test_erc1155_deploy/intermediate_class.py b/tests/test_packages/test_skills/test_erc1155_deploy/intermediate_class.py index 446fb9eef3..df032c5dfb 100644 --- a/tests/test_packages/test_skills/test_erc1155_deploy/intermediate_class.py +++ b/tests/test_packages/test_skills/test_erc1155_deploy/intermediate_class.py @@ -86,6 +86,7 @@ def setup(cls): cls.from_supply = 756 cls.to_supply = 12 cls.value = 87 + cls.token_type = 2 config_overrides = { "models": { "strategy": { @@ -98,6 +99,7 @@ def setup(cls): "from_supply": cls.from_supply, "to_supply": cls.to_supply, "value": cls.value, + "token_type": cls.token_type, } } }, diff --git a/tests/test_packages/test_skills/test_erc1155_deploy/test_strategy.py b/tests/test_packages/test_skills/test_erc1155_deploy/test_strategy.py index 49c9f25061..025202e48d 100644 --- a/tests/test_packages/test_skills/test_erc1155_deploy/test_strategy.py +++ b/tests/test_packages/test_skills/test_erc1155_deploy/test_strategy.py @@ -31,7 +31,10 @@ from aea.helpers.transaction.base import Terms from packages.fetchai.contracts.erc1155.contract import ERC1155Contract -from packages.fetchai.skills.erc1155_deploy.strategy import SIMPLE_SERVICE_MODEL +from packages.fetchai.skills.erc1155_deploy.strategy import ( + SIMPLE_SERVICE_MODEL, + Strategy, +) from tests.test_packages.test_skills.test_erc1155_deploy.intermediate_class import ( ERC1155DeployTestCase, @@ -41,6 +44,22 @@ class TestStrategy(ERC1155DeployTestCase): """Test Strategy of erc1155_deploy.""" + def test__init__(self): + """Test the properties of Strategy class.""" + assert Strategy( + location=self.location, + mint_quantities=[1, 1, 1, 1, 1, 1, 1, 1, 1, 1], + service_data=self.service_data, + personality_data=self.personality_data, + classification=self.classification, + from_supply=self.from_supply, + to_supply=self.to_supply, + value=self.value, + token_type=1, + name="strategy", + skill_context=self.skill.skill_context, + ) + def test_properties(self): """Test the properties of Strategy class.""" assert self.strategy.ledger_id == self.skill.skill_context.default_ledger_id @@ -48,6 +67,10 @@ def test_properties(self): assert self.strategy.mint_quantities == self.mint_quantities assert self.strategy.token_ids == self.strategy._token_ids + self.strategy._token_ids = None + with pytest.raises(ValueError, match="Token ids not set."): + assert self.strategy.token_ids + with pytest.raises(ValueError, match="Contract address not set!"): assert self.strategy.contract_address self.strategy.contract_address = self.contract_address From 0e8cc81637a092ceb3ed028e2c229d52e52f5926 Mon Sep 17 00:00:00 2001 From: ali Date: Mon, 24 May 2021 10:32:34 +0100 Subject: [PATCH 095/147] feat: soef DockerImage wrapper and test fixture --- tests/common/docker_image.py | 9 ++------- tests/conftest.py | 4 +--- 2 files changed, 3 insertions(+), 10 deletions(-) diff --git a/tests/common/docker_image.py b/tests/common/docker_image.py index 5e716c6a37..09c0f9dc1c 100644 --- a/tests/common/docker_image.py +++ b/tests/common/docker_image.py @@ -341,10 +341,7 @@ class SOEFDockerImage(DockerImage): """Wrapper to SOEF Docker image.""" def __init__( - self, - client: DockerClient, - addr: str, - port: int = 9002, + self, client: DockerClient, addr: str, port: int = 9002, ): """ Initialize the SOEF Docker image. @@ -369,9 +366,7 @@ def _make_ports(self) -> Dict: def create(self) -> Container: """Create the container.""" container = self._client.containers.run( - self.tag, - detach=True, - ports=self._make_ports() + self.tag, detach=True, ports=self._make_ports() ) return container diff --git a/tests/conftest.py b/tests/conftest.py index 33ed0f6ba2..ecd8d017f1 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -741,9 +741,7 @@ def soef( ): """Launch the Ganache image.""" client = docker.from_env() - image = SOEFDockerImage( - client, soef_addr, soef_port - ) + image = SOEFDockerImage(client, soef_addr, soef_port) yield from _launch_image(image, timeout=timeout, max_attempts=max_attempts) From 093a5e736d9b544ed51b7b7dc9dad2926be9870c Mon Sep 17 00:00:00 2001 From: MarcoFavorito Date: Mon, 24 May 2021 13:11:21 +0200 Subject: [PATCH 096/147] feat: warning message in mam in case exception occurs but no error clbk registered --- aea/manager/manager.py | 28 ++++++++++++++++++++++++++-- 1 file changed, 26 insertions(+), 2 deletions(-) diff --git a/aea/manager/manager.py b/aea/manager/manager.py index ecc2e1754a..96c13a5114 100644 --- a/aea/manager/manager.py +++ b/aea/manager/manager.py @@ -203,6 +203,10 @@ def __init__( self._mode = mode self._password = password + # this flags will control whether we have already printed the warning message + # for a certain agent + self._warning_message_printed_for_agent: Dict[str, bool] = {} + @property def data_dir(self) -> str: """Get the certs directory.""" @@ -263,8 +267,11 @@ async def _manager_loop(self) -> None: agent_name = agents_run_tasks_futures[task] self._agents_tasks.pop(agent_name) if task.exception(): - for callback in self._error_callbacks: - callback(agent_name, task.exception()) + if len(self._error_callbacks) == 0: + self._print_exception_occurred_but_no_error_callback(agent_name) + else: + for callback in self._error_callbacks: + callback(agent_name, task.exception()) else: await task @@ -843,3 +850,20 @@ def _save_state(self) -> None: """ with open_file(self._save_path, "w") as f: json.dump(self.dict_state, f, indent=4, sort_keys=True) + + def _print_exception_occurred_but_no_error_callback(self, agent_name: str) -> None: + """ + Print a warning message when an exception occurred but no error callback is registered. + + :param agent_name: the agent name. + :return: None + """ + if self._warning_message_printed_for_agent.get(agent_name, False): + return + self._warning_message_printed_for_agent[agent_name] = True + self._print_exception_occurred_but_no_error_callback(agent_name) + print( + f"WARNING: An exception occurred during the execution of agent '{agent_name}', " + f"but since no error callback was found the exception is handled silently. Please " + f"add an error callback using the method 'add_error_callback' of the MultiAgentManager instance." + ) From 16f94d268dda07b231e7c3154cb785507d3fc9ff Mon Sep 17 00:00:00 2001 From: "Yuri (solarw) Turchenkov" Date: Mon, 24 May 2021 16:58:42 +0300 Subject: [PATCH 097/147] win install script: golang and gcc addded --- docs/quickstart.md | 7 ++++++- scripts/install.ps1 | 8 ++++++++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/docs/quickstart.md b/docs/quickstart.md index 8b6724e6de..b4da74256d 100644 --- a/docs/quickstart.md +++ b/docs/quickstart.md @@ -9,7 +9,12 @@ This example will take you through a simple AEA to familiarise you with the basi The AEA framework can be used on `Windows`, `Ubuntu/Debian` and `MacOS`. You need Python 3.6 or higher as well as Go 1.14.2 or higher installed. -​ + +​GCC installation is required: +* Ubuntu: `apt-get install gcc` +* Windows (with choco installed https://chocolatey.org/): `choco install mingw` +* MacOS X (with home brew): `brew install gcc` + ### Option 1: Manual system preparation Install a compatible Python and Go version on your system (see this external resource for a comprehensive guide). diff --git a/scripts/install.ps1 b/scripts/install.ps1 index ffb9fd115c..ec9954d98d 100644 --- a/scripts/install.ps1 +++ b/scripts/install.ps1 @@ -25,6 +25,13 @@ function install_build_tools { } + +function instal_choco_golang_gcc { + echo "Choco, golang and gcc will be installed" + echo "You'll be asked for admin shell" + sleep 5 + Start-Process powershell -Verb runAs -ArgumentList "Set-ExecutionPolicy Bypass -Scope Process -Force; [System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol -bor 3072; iex ((New-Object System.Net.WebClient).DownloadString('https://chocolatey.org/install.ps1')); choco install -y golang mingw" +} function install_aea { echo "Install aea" $output=pip install aea[all]==1.0.1 --force --no-cache-dir 2>&1 |out-string; @@ -66,6 +73,7 @@ function main{ install_build_tools refresh-path install_aea + instal_choco_golang_gcc pause } From 860ca66cb6ac75bf1e68834ce5ffa484fb29d940 Mon Sep 17 00:00:00 2001 From: MarcoFavorito Date: Mon, 24 May 2021 21:28:33 +0200 Subject: [PATCH 098/147] add tests on 'get-wealth' command with '--password' flag --- aea/test_tools/test_cases.py | 9 +++++++-- tests/conftest.py | 20 ++++++++++++++++++++ tests/test_cli/test_get_address.py | 27 ++------------------------- tests/test_cli/test_get_wealth.py | 18 +++++++++++++++++- 4 files changed, 46 insertions(+), 28 deletions(-) diff --git a/aea/test_tools/test_cases.py b/aea/test_tools/test_cases.py index 3cbe5fa39b..d4fee6f8c2 100644 --- a/aea/test_tools/test_cases.py +++ b/aea/test_tools/test_cases.py @@ -696,7 +696,9 @@ def generate_wealth( ) @classmethod - def get_wealth(cls, ledger_api_id: str = DEFAULT_LEDGER) -> str: + def get_wealth( + cls, ledger_api_id: str = DEFAULT_LEDGER, password: Optional[str] = None + ) -> str: """ Get wealth with CLI command. @@ -706,7 +708,10 @@ def get_wealth(cls, ledger_api_id: str = DEFAULT_LEDGER) -> str: :return: command line output """ - cls.run_cli_command("get-wealth", ledger_api_id, cwd=cls._get_cwd()) + password_option = _get_password_option_args(password) + cls.run_cli_command( + "get-wealth", ledger_api_id, *password_option, cwd=cls._get_cwd() + ) if cls.last_cli_runner_result is None: raise ValueError("Runner result not set!") # pragma: nocover return str(cls.last_cli_runner_result.stdout_bytes, "utf-8") diff --git a/tests/conftest.py b/tests/conftest.py index f2d3999f51..528a4dc759 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -83,10 +83,12 @@ ) from aea.crypto.registries import ledger_apis_registry, make_crypto from aea.crypto.wallet import CryptoStore +from aea.exceptions import enforce from aea.helpers.base import CertRequest, SimpleId, cd from aea.identity.base import Identity from aea.test_tools.click_testing import CliRunner as ImportedCliRunner from aea.test_tools.constants import DEFAULT_AUTHOR +from aea.test_tools.test_cases import BaseAEATestCase from packages.fetchai.connections.local.connection import LocalNode, OEFLocalConnection from packages.fetchai.connections.oef.connection import OEFConnection @@ -1447,3 +1449,21 @@ def password_or_none(request) -> Optional[str]: Note that this is a parametrized fixture. """ return request.param + + +def method_scope(cls): + """ + Class decorator to make the setup/teardown to have the 'method' scope. + + :param cls: the class. It must be a subclass of + :return: + """ + enforce( + issubclass(cls, BaseAEATestCase), + "cannot use decorator if class is not instance of BaseAEATestCase", + ) + cls.setup_class = classmethod(lambda _cls: None) + cls.teardown_class = classmethod(lambda _cls: None) + cls.setup = lambda self: super(cls, self).setup_class() + cls.teardown = lambda self: super(cls, self).teardown_class() + return cls diff --git a/tests/test_cli/test_get_address.py b/tests/test_cli/test_get_address.py index 6b9f40be4f..9c9300a2d2 100644 --- a/tests/test_cli/test_get_address.py +++ b/tests/test_cli/test_get_address.py @@ -27,7 +27,7 @@ from aea.configurations.constants import DEFAULT_LEDGER from aea.test_tools.test_cases import AEATestCaseEmpty -from tests.conftest import CLI_LOG_OPTION, COSMOS_ADDRESS_ONE, CliRunner +from tests.conftest import CLI_LOG_OPTION, COSMOS_ADDRESS_ONE, CliRunner, method_scope from tests.test_cli.tools_for_testing import ContextMock @@ -70,21 +70,10 @@ def test_run_positive(self, *mocks): self.assertEqual(result.exit_code, 0) +@method_scope class TestGetAddressCommand(AEATestCaseEmpty): """Test 'get-address' command.""" - @classmethod - def setup_class(cls) -> None: - """ - Override the 'setup_class' method. - - This will prevent setup of tests at class-level. - """ - - def setup(self): - """Set up the test.""" - super().setup_class() - def test_get_address(self, password_or_none): """Run the main test.""" self.generate_private_key(password=password_or_none) @@ -96,15 +85,3 @@ def test_get_address(self, password_or_none): ) assert result.exit_code == 0 - - def teardown(self) -> None: - """Tear down the test.""" - super().teardown_class() - - @classmethod - def teardown_class(cls) -> None: - """ - Override the 'teardown_class' method. - - This will prevent teardown of tests at class-level. - """ diff --git a/tests/test_cli/test_get_wealth.py b/tests/test_cli/test_get_wealth.py index 00dae0145d..7b99811a6d 100644 --- a/tests/test_cli/test_get_wealth.py +++ b/tests/test_cli/test_get_wealth.py @@ -24,8 +24,9 @@ from aea.cli import cli from aea.cli.get_wealth import _try_get_wealth +from aea.test_tools.test_cases import AEATestCaseEmpty -from tests.conftest import CLI_LOG_OPTION, CliRunner +from tests.conftest import CLI_LOG_OPTION, CliRunner, method_scope from tests.test_cli.tools_for_testing import ContextMock @@ -65,3 +66,18 @@ def test_run_positive(self, *mocks): standalone_mode=False, ) self.assertEqual(result.exit_code, 0) + + +@method_scope +class TestGetWealth(AEATestCaseEmpty): + """Test 'get-wealth' command.""" + + @mock.patch("click.echo") + def test_get_wealth(self, _echo_mock, password_or_none): + """Run the main test.""" + self.generate_private_key(password=password_or_none) + self.add_private_key(password=password_or_none) + self.get_wealth(password=password_or_none) + + expected_wealth = 0 + _echo_mock.assert_called_with(expected_wealth) From 2811c30f59e0f860ea4df274cf74471045543912 Mon Sep 17 00:00:00 2001 From: MarcoFavorito Date: Mon, 24 May 2021 21:44:11 +0200 Subject: [PATCH 099/147] test: add tests on 'get-multiaddress' command with '--password' flag --- tests/test_cli/test_get_multiaddress.py | 156 +++++++++++++++++------- 1 file changed, 114 insertions(+), 42 deletions(-) diff --git a/tests/test_cli/test_get_multiaddress.py b/tests/test_cli/test_get_multiaddress.py index 00699910c4..87611b6764 100644 --- a/tests/test_cli/test_get_multiaddress.py +++ b/tests/test_cli/test_get_multiaddress.py @@ -23,23 +23,32 @@ import pytest from aea_ledger_fetchai import FetchAICrypto -from aea.test_tools.test_cases import AEATestCaseEmpty +from aea.test_tools.test_cases import AEATestCaseEmpty, _get_password_option_args from packages.fetchai.connections.stub.connection import ( PUBLIC_ID as STUB_CONNECTION_PUBLIC_ID, ) +from tests.conftest import method_scope + +@method_scope class TestGetMultiAddressCommandPositive(AEATestCaseEmpty): """Test case for CLI get-multiaddress command.""" - def test_run(self, *mocks): + def test_run(self, password_or_none): """Run the test.""" - self.generate_private_key(FetchAICrypto.identifier) - self.add_private_key(FetchAICrypto.identifier, connection=False) + self.generate_private_key(FetchAICrypto.identifier, password=password_or_none) + self.add_private_key( + FetchAICrypto.identifier, connection=False, password=password_or_none + ) + password_options = _get_password_option_args(password_or_none) result = self.run_cli_command( - "get-multiaddress", FetchAICrypto.identifier, cwd=self.current_agent_context + "get-multiaddress", + FetchAICrypto.identifier, + *password_options, + cwd=self.current_agent_context, ) assert result.exit_code == 0 @@ -47,18 +56,23 @@ def test_run(self, *mocks): base58.b58decode(result.stdout) +@method_scope class TestGetMultiAddressCommandConnectionPositive(AEATestCaseEmpty): """Test case for CLI get-multiaddress command with --connection flag.""" - def test_run(self, *mocks): + def test_run(self, password_or_none): """Run the test.""" - self.generate_private_key(FetchAICrypto.identifier) - self.add_private_key(FetchAICrypto.identifier, connection=True) + self.generate_private_key(FetchAICrypto.identifier, password=password_or_none) + self.add_private_key( + FetchAICrypto.identifier, connection=True, password=password_or_none + ) + password_options = _get_password_option_args(password_or_none) result = self.run_cli_command( "get-multiaddress", FetchAICrypto.identifier, "--connection", + *password_options, cwd=self.current_agent_context, ) @@ -67,20 +81,24 @@ def test_run(self, *mocks): base58.b58decode(result.stdout) +@method_scope class TestGetMultiAddressCommandConnectionIdPositive(AEATestCaseEmpty): """Test case for CLI get-multiaddress command with --connection flag.""" - def test_run(self, *mocks): + def test_run(self, password_or_none): """Run the test.""" self.add_item("connection", str(STUB_CONNECTION_PUBLIC_ID)) - self.generate_private_key(FetchAICrypto.identifier) - self.add_private_key(FetchAICrypto.identifier, connection=True) + self.generate_private_key(FetchAICrypto.identifier, password=password_or_none) + self.add_private_key( + FetchAICrypto.identifier, connection=True, password=password_or_none + ) self.nested_set_config( "vendor.fetchai.connections.stub.config", {"host": "127.0.0.1", "port": 10000}, ) + password_options = _get_password_option_args(password_or_none) result = self.run_cli_command( "get-multiaddress", FetchAICrypto.identifier, @@ -91,6 +109,7 @@ def test_run(self, *mocks): "host", "--port-field", "port", + *password_options, cwd=self.current_agent_context, ) @@ -102,19 +121,23 @@ def test_run(self, *mocks): base58.b58decode(base58_addr) +@method_scope class TestGetMultiAddressCommandConnectionIdURIPositive(AEATestCaseEmpty): """Test case for CLI get-multiaddress command with --connection flag and --uri.""" - def test_run(self, *mocks): + def test_run(self, password_or_none): """Run the test.""" self.add_item("connection", str(STUB_CONNECTION_PUBLIC_ID)) - self.generate_private_key(FetchAICrypto.identifier) - self.add_private_key(FetchAICrypto.identifier, connection=True) + self.generate_private_key(FetchAICrypto.identifier, password=password_or_none) + self.add_private_key( + FetchAICrypto.identifier, connection=True, password=password_or_none + ) self.nested_set_config( "vendor.fetchai.connections.stub.config", {"public_uri": "127.0.0.1:10000"} ) + password_options = _get_password_option_args(password_or_none) result = self.run_cli_command( "get-multiaddress", FetchAICrypto.identifier, @@ -123,6 +146,7 @@ def test_run(self, *mocks): str(STUB_CONNECTION_PUBLIC_ID), "--uri-field", "public_uri", + *password_options, cwd=self.current_agent_context, ) @@ -134,18 +158,23 @@ def test_run(self, *mocks): base58.b58decode(base58_addr) +@method_scope class TestGetMultiAddressCommandConnectionNegative(AEATestCaseEmpty): """Test case for CLI get-multiaddress command with --connection flag.""" - def test_run(self, *mocks): + def test_run(self, password_or_none): """Run the test.""" - self.generate_private_key(FetchAICrypto.identifier) - self.add_private_key(FetchAICrypto.identifier, connection=True) + self.generate_private_key(FetchAICrypto.identifier, password=password_or_none) + self.add_private_key( + FetchAICrypto.identifier, connection=True, password=password_or_none + ) + password_options = _get_password_option_args(password_or_none) result = self.run_cli_command( "get-multiaddress", FetchAICrypto.identifier, "--connection", + *password_options, cwd=self.current_agent_context, ) @@ -154,10 +183,11 @@ def test_run(self, *mocks): base58.b58decode(result.stdout) +@method_scope class TestGetMultiAddressCommandNegativeMissingKey(AEATestCaseEmpty): """Test case for CLI get-multiaddress when the key is missing.""" - def test_run(self, *mocks): + def test_run(self, password_or_none): """Run the test.""" # this will cause exception because no key is added to the AEA project. with pytest.raises( @@ -166,13 +196,16 @@ def test_run(self, *mocks): FetchAICrypto.identifier ), ): + password_options = _get_password_option_args(password_or_none) self.run_cli_command( "get-multiaddress", FetchAICrypto.identifier, + *password_options, cwd=self.current_agent_context, ) +@method_scope class TestGetMultiAddressCommandNegativePeerId(AEATestCaseEmpty): """Test case for CLI get-multiaddress when the peer id computation raises an error.""" @@ -180,30 +213,38 @@ class TestGetMultiAddressCommandNegativePeerId(AEATestCaseEmpty): "aea.cli.get_multiaddress.MultiAddr.__init__", side_effect=Exception("test error"), ) - def test_run(self, *mocks): + def test_run(self, _mock, password_or_none): """Run the test.""" - self.generate_private_key(FetchAICrypto.identifier) - self.add_private_key(FetchAICrypto.identifier, connection=False) + self.generate_private_key(FetchAICrypto.identifier, password=password_or_none) + self.add_private_key( + FetchAICrypto.identifier, connection=False, password=password_or_none + ) # this will cause exception because no key is added to the AEA project. + password_options = _get_password_option_args(password_or_none) with pytest.raises(Exception, match="test error"): self.run_cli_command( "get-multiaddress", FetchAICrypto.identifier, + *password_options, cwd=self.current_agent_context, ) +@method_scope class TestGetMultiAddressCommandNegativeBadHostField(AEATestCaseEmpty): """Test case for CLI get-multiaddress when the host field is missing.""" - def test_run(self, *mocks): + def test_run(self, password_or_none): """Run the test.""" self.add_item("connection", str(STUB_CONNECTION_PUBLIC_ID)) - self.generate_private_key(FetchAICrypto.identifier) - self.add_private_key(FetchAICrypto.identifier, connection=True) + self.generate_private_key(FetchAICrypto.identifier, password=password_or_none) + self.add_private_key( + FetchAICrypto.identifier, connection=True, password=password_or_none + ) # this will cause exception because no host configuration is in stub connection by default. + password_options = _get_password_option_args(password_or_none) with pytest.raises( Exception, match="Host field 'some_host' not present in connection configuration fetchai/stub:0.20.0", @@ -218,24 +259,29 @@ def test_run(self, *mocks): "some_host", "--port-field", "some_port", + *password_options, cwd=self.current_agent_context, ) +@method_scope class TestGetMultiAddressCommandNegativeBadPortField(AEATestCaseEmpty): """Test case for CLI get-multiaddress when the port field is missing.""" - def test_run(self, *mocks): + def test_run(self, password_or_none): """Run the test.""" self.add_item("connection", str(STUB_CONNECTION_PUBLIC_ID)) - self.generate_private_key(FetchAICrypto.identifier) - self.add_private_key(FetchAICrypto.identifier, connection=True) + self.generate_private_key(FetchAICrypto.identifier, password=password_or_none) + self.add_private_key( + FetchAICrypto.identifier, connection=True, password=password_or_none + ) self.nested_set_config( "vendor.fetchai.connections.stub.config", {"host": "127.0.0.1"} ) # this will cause exception because no port configuration is in stub connection by default. + password_options = _get_password_option_args(password_or_none) with pytest.raises( Exception, match="Port field 'some_port' not present in connection configuration fetchai/stub:0.20.0", @@ -250,19 +296,24 @@ def test_run(self, *mocks): "host", "--port-field", "some_port", + *password_options, cwd=self.current_agent_context, ) +@method_scope class TestGetMultiAddressCommandNegativeBadConnectionId(AEATestCaseEmpty): """Test case for CLI get-multiaddress when the connection id is missing.""" - def test_run(self, *mocks): + def test_run(self, password_or_none): """Run the test.""" - self.generate_private_key(FetchAICrypto.identifier) - self.add_private_key(FetchAICrypto.identifier, connection=True) + self.generate_private_key(FetchAICrypto.identifier, password=password_or_none) + self.add_private_key( + FetchAICrypto.identifier, connection=True, password=password_or_none + ) # this will cause exception because a bad public id is provided. + password_options = _get_password_option_args(password_or_none) connection_id = "some_author/some_connection:0.1.0" with pytest.raises( Exception, @@ -274,10 +325,12 @@ def test_run(self, *mocks): "--connection", "--connection-id", connection_id, + *password_options, cwd=self.current_agent_context, ) +@method_scope class TestGetMultiAddressCommandNegativeFullMultiaddrComputation(AEATestCaseEmpty): """Test case for CLI get-multiaddress when an error occurs in the computation of the full multiaddr.""" @@ -285,11 +338,13 @@ class TestGetMultiAddressCommandNegativeFullMultiaddrComputation(AEATestCaseEmpt "aea.cli.get_multiaddress.MultiAddr.__init__", side_effect=Exception("test error"), ) - def test_run(self, *mocks): + def test_run(self, _mock, password_or_none): """Run the test.""" self.add_item("connection", str(STUB_CONNECTION_PUBLIC_ID)) - self.generate_private_key(FetchAICrypto.identifier) - self.add_private_key(FetchAICrypto.identifier, connection=True) + self.generate_private_key(FetchAICrypto.identifier, password=password_or_none) + self.add_private_key( + FetchAICrypto.identifier, connection=True, password=password_or_none + ) self.nested_set_config( "vendor.fetchai.connections.stub.config", @@ -297,6 +352,7 @@ def test_run(self, *mocks): ) # this will cause exception due to the mocking. + password_options = _get_password_option_args(password_or_none) with pytest.raises( Exception, match="An error occurred while creating the multiaddress: test error", @@ -311,20 +367,25 @@ def test_run(self, *mocks): "host", "--port-field", "port", + *password_options, cwd=self.current_agent_context, ) +@method_scope class TestGetMultiAddressCommandNegativeOnlyHostSpecified(AEATestCaseEmpty): """Test case for CLI get-multiaddress when only the host field is specified.""" - def test_run(self, *mocks): + def test_run(self, password_or_none): """Run the test.""" self.add_item("connection", str(STUB_CONNECTION_PUBLIC_ID)) - self.generate_private_key(FetchAICrypto.identifier) - self.add_private_key(FetchAICrypto.identifier, connection=True) + self.generate_private_key(FetchAICrypto.identifier, password=password_or_none) + self.add_private_key( + FetchAICrypto.identifier, connection=True, password=password_or_none + ) # this will cause exception because only the host, and not the port, are specified. + password_options = _get_password_option_args(password_or_none) with pytest.raises( Exception, match="-h/--host-field and -p/--port-field must be specified together.", @@ -337,20 +398,25 @@ def test_run(self, *mocks): str(STUB_CONNECTION_PUBLIC_ID), "--host-field", "some_host", + *password_options, cwd=self.current_agent_context, ) +@method_scope class TestGetMultiAddressCommandNegativeUriNotExisting(AEATestCaseEmpty): """Test case for CLI get-multiaddress when the URI field doesn't exists.""" - def test_run(self, *mocks): + def test_run(self, password_or_none): """Run the test.""" self.add_item("connection", str(STUB_CONNECTION_PUBLIC_ID)) - self.generate_private_key(FetchAICrypto.identifier) - self.add_private_key(FetchAICrypto.identifier, connection=True) + self.generate_private_key(FetchAICrypto.identifier, password=password_or_none) + self.add_private_key( + FetchAICrypto.identifier, connection=True, password=password_or_none + ) # this will cause exception because only the host, and not the port, are specified. + password_options = _get_password_option_args(password_or_none) with pytest.raises( Exception, match="URI field 'some_uri' not present in connection configuration fetchai/stub:0.20.0", @@ -363,18 +429,22 @@ def test_run(self, *mocks): str(STUB_CONNECTION_PUBLIC_ID), "--uri-field", "some_uri", + *password_options, cwd=self.current_agent_context, ) +@method_scope class TestGetMultiAddressCommandNegativeBadUri(AEATestCaseEmpty): """Test case for CLI get-multiaddress when we cannot parse the URI field.""" - def test_run(self, *mocks): + def test_run(self, password_or_none): """Run the test.""" self.add_item("connection", str(STUB_CONNECTION_PUBLIC_ID)) - self.generate_private_key(FetchAICrypto.identifier) - self.add_private_key(FetchAICrypto.identifier, connection=True) + self.generate_private_key(FetchAICrypto.identifier, password=password_or_none) + self.add_private_key( + FetchAICrypto.identifier, connection=True, password=password_or_none + ) self.nested_set_config( "vendor.fetchai.connections.stub.config", @@ -382,6 +452,7 @@ def test_run(self, *mocks): ) # this will cause exception because only the host, and not the port, are specified. + password_options = _get_password_option_args(password_or_none) with pytest.raises( Exception, match=r"Cannot extract host and port from some_uri: 'some-unparsable_URI'. Reason: URI Doesn't match regex '", @@ -394,5 +465,6 @@ def test_run(self, *mocks): str(STUB_CONNECTION_PUBLIC_ID), "--uri-field", "some_uri", + *password_options, cwd=self.current_agent_context, ) From 37f0c2da76d60a6d4a1de46c8a0af77f62bde1e8 Mon Sep 17 00:00:00 2001 From: MarcoFavorito Date: Mon, 24 May 2021 23:22:18 +0200 Subject: [PATCH 100/147] test: add tests on 'issue-certificates' command with '--password' flag --- tests/conftest.py | 6 +- tests/test_cli/test_issue_certificates.py | 72 +++++++++++++---------- 2 files changed, 44 insertions(+), 34 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index 528a4dc759..47c85ca03a 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1462,8 +1462,10 @@ def method_scope(cls): issubclass(cls, BaseAEATestCase), "cannot use decorator if class is not instance of BaseAEATestCase", ) + old_setup_class = cls.setup_class + old_teardown_class = cls.teardown_class cls.setup_class = classmethod(lambda _cls: None) cls.teardown_class = classmethod(lambda _cls: None) - cls.setup = lambda self: super(cls, self).setup_class() - cls.teardown = lambda self: super(cls, self).teardown_class() + cls.setup = lambda self: old_setup_class() + cls.teardown = lambda self: old_teardown_class() return cls diff --git a/tests/test_cli/test_issue_certificates.py b/tests/test_cli/test_issue_certificates.py index 1ba49e1795..94deee4c35 100644 --- a/tests/test_cli/test_issue_certificates.py +++ b/tests/test_cli/test_issue_certificates.py @@ -29,15 +29,9 @@ from aea.cli.utils.config import dump_item_config from aea.helpers.base import CertRequest -from aea.test_tools.test_cases import AEATestCaseEmpty - -from tests.conftest import ( - CUR_PATH, - ETHEREUM_PRIVATE_KEY_FILE, - ETHEREUM_PRIVATE_KEY_PATH, - FETCHAI_PRIVATE_KEY_FILE, - FETCHAI_PRIVATE_KEY_PATH, -) +from aea.test_tools.test_cases import AEATestCaseEmpty, _get_password_option_args + +from tests.conftest import CUR_PATH, ETHEREUM_PRIVATE_KEY_FILE, FETCHAI_PRIVATE_KEY_FILE from tests.data.dummy_connection.connection import DummyConnection @@ -101,20 +95,26 @@ def setup_class(cls): [cls.cert_request_1, cls.cert_request_2], DummyConnection.connection_id.name ) - # add fetchai key and connection key - shutil.copy( - FETCHAI_PRIVATE_KEY_PATH, - os.path.join(cls.current_agent_context, FETCHAI_PRIVATE_KEY_FILE), - ) - cls.add_private_key() - cls.add_private_key(connection=True) - - def test_issue_certificate(self): + def test_issue_certificate(self, password_or_none): """Test 'aea issue-certificates' in case of success.""" - result = self.run_cli_command("issue-certificates", cwd=self._get_cwd()) + # setup: add private key with password + self.generate_private_key(password=password_or_none) + self.add_private_key(password=password_or_none) + self.add_private_key(connection=True, password=password_or_none) + + # issue certificates and check + password_options = _get_password_option_args(password_or_none) + result = self.run_cli_command( + "issue-certificates", *password_options, cwd=self._get_cwd() + ) self._check_signature(self.cert_id_1, self.expected_path_1, result.stdout) self._check_signature(self.cert_id_2, self.expected_path_2, result.stdout) + # teardown: remove private key + Path(self._get_cwd(), FETCHAI_PRIVATE_KEY_FILE).unlink() + self.remove_private_key() + self.remove_private_key(connection=True) + def _check_signature(self, cert_id, filename, stdout): """Check signature has been generated correctly.""" path = Path(self.current_agent_context, filename) @@ -176,27 +176,35 @@ def setup_class(cls): new_cert_requests = f"[{json_3}, {json_4}]" cls.set_config(dotted_path, new_cert_requests, type_="list") - # add ethereum key and connection key - shutil.copy( - ETHEREUM_PRIVATE_KEY_PATH, - os.path.join(cls.current_agent_context, ETHEREUM_PRIVATE_KEY_FILE), + def test_issue_certificate(self, password_or_none): + """Test 'aea issue-certificates' in case of success.""" + # setup: add private key with password + ledger_id = EthereumCrypto.identifier + self.generate_private_key( + ledger_id, ETHEREUM_PRIVATE_KEY_FILE, password=password_or_none ) - cls.add_private_key( - ledger_api_id=EthereumCrypto.identifier, - private_key_filepath=ETHEREUM_PRIVATE_KEY_FILE, + self.add_private_key( + ledger_id, ETHEREUM_PRIVATE_KEY_FILE, password=password_or_none ) - cls.add_private_key( - ledger_api_id=EthereumCrypto.identifier, - private_key_filepath=ETHEREUM_PRIVATE_KEY_FILE, + self.add_private_key( + ledger_id, + ETHEREUM_PRIVATE_KEY_FILE, connection=True, + password=password_or_none, ) - def test_issue_certificate(self): - """Test 'aea issue-certificates' in case of success.""" - result = self.run_cli_command("issue-certificates", cwd=self._get_cwd()) + password_options = _get_password_option_args(password_or_none) + result = self.run_cli_command( + "issue-certificates", *password_options, cwd=self._get_cwd() + ) self._check_signature(self.cert_id_3, self.expected_path_3, result.stdout) self._check_signature(self.cert_id_4, self.expected_path_4, result.stdout) + # teardown: remove private key + Path(self._get_cwd(), ETHEREUM_PRIVATE_KEY_FILE).unlink() + self.remove_private_key(ledger_id) + self.remove_private_key(ledger_id, connection=True) + class TestIssueCertificatesWrongConnectionKey(BaseTestIssueCertificates): """Test 'aea issue-certificates' when a bad connection key id is provided.""" From 1e67135497980327d24adbbbd1549c14980ce119 Mon Sep 17 00:00:00 2001 From: MarcoFavorito Date: Mon, 24 May 2021 23:39:31 +0200 Subject: [PATCH 101/147] test: add tests on 'launch' command with '--password' flag --- aea/aea_builder.py | 8 +++++++- aea/launcher.py | 4 +++- tests/test_cli/test_launch.py | 38 +++++++++++++++++++++++++++++++---- 3 files changed, 44 insertions(+), 6 deletions(-) diff --git a/aea/aea_builder.py b/aea/aea_builder.py index 2dad9e0013..f993229dc0 100644 --- a/aea/aea_builder.py +++ b/aea/aea_builder.py @@ -1844,7 +1844,10 @@ def _find_import_order( @classmethod def from_aea_project( - cls, aea_project_path: PathLike, skip_consistency_check: bool = False, + cls, + aea_project_path: PathLike, + skip_consistency_check: bool = False, + password: Optional[str] = None, ) -> "AEABuilder": """ Construct the builder from an AEA project. @@ -1858,6 +1861,7 @@ def from_aea_project( :param aea_project_path: path to the AEA project. :param skip_consistency_check: if True, the consistency check are skipped. + :param password: the password to encrypt/decrypt private keys. :return: an AEABuilder. """ aea_project_path = Path(aea_project_path) @@ -1869,6 +1873,7 @@ def from_aea_project( aea_project_path, substitude_env_vars=False, private_key_helper=private_key_verify, + password=password, ).dump_config() # just validate @@ -1876,6 +1881,7 @@ def from_aea_project( aea_project_path, substitude_env_vars=True, private_key_helper=private_key_verify, + password=password, ).agent_config builder = AEABuilder(with_default_packages=False) diff --git a/aea/launcher.py b/aea/launcher.py index d0aaed0f72..54b3cd9680 100644 --- a/aea/launcher.py +++ b/aea/launcher.py @@ -57,7 +57,9 @@ def load_agent(agent_dir: Union[PathLike, str], password: Optional[str] = None) :return: AEA instance """ with cd(agent_dir): - return AEABuilder.from_aea_project(".").build(password=password) + return AEABuilder.from_aea_project(".", password=password).build( + password=password + ) def _set_logger( diff --git a/tests/test_cli/test_launch.py b/tests/test_cli/test_launch.py index 6a45cf56df..e0ea0c6f9d 100644 --- a/tests/test_cli/test_launch.py +++ b/tests/test_cli/test_launch.py @@ -53,6 +53,8 @@ class BaseLaunchTestCase: """Base Test case for launch tests.""" + PASSWORD: Optional[str] = None + @contextmanager def _cli_launch( self, agents: List[str], options: Optional[List[str]] = None @@ -65,6 +67,7 @@ def _cli_launch( :return: PexpectWrapper """ + password_options = self.get_password_args(self.PASSWORD) proc = PexpectWrapper( # nosec [ sys.executable, @@ -73,6 +76,7 @@ def _cli_launch( "-v", "DEBUG", "launch", + *password_options, *(options or []), *(agents or []), ], @@ -112,6 +116,7 @@ def setup_class(cls): src_dir = cls.cwd / Path(ROOT_DIR, dir_path) shutil.copytree(str(src_dir), str(tmp_dir)) os.chdir(cls.t) + password_option = cls.get_password_args(cls.PASSWORD) result = cls.runner.invoke( cli, [*CLI_LOG_OPTION, "init", "--local", "--author", AUTHOR] ) @@ -122,12 +127,19 @@ def setup_class(cls): assert result.exit_code == 0 os.chdir(cls.agent_name_1) result = cls.runner.invoke( - cli, [*CLI_LOG_OPTION, "generate-key", FetchAICrypto.identifier] + cli, + [ + *CLI_LOG_OPTION, + "generate-key", + FetchAICrypto.identifier, + *password_option, + ], ) assert result.exit_code == 0 result = cls.runner.invoke( - cli, [*CLI_LOG_OPTION, "add-key", FetchAICrypto.identifier] + cli, + [*CLI_LOG_OPTION, "add-key", FetchAICrypto.identifier, *password_option], ) assert result.exit_code == 0 os.chdir(cls.t) @@ -137,16 +149,28 @@ def setup_class(cls): assert result.exit_code == 0 os.chdir(cls.agent_name_2) result = cls.runner.invoke( - cli, [*CLI_LOG_OPTION, "generate-key", FetchAICrypto.identifier] + cli, + [ + *CLI_LOG_OPTION, + "generate-key", + FetchAICrypto.identifier, + *password_option, + ], ) assert result.exit_code == 0 result = cls.runner.invoke( - cli, [*CLI_LOG_OPTION, "add-key", FetchAICrypto.identifier] + cli, + [*CLI_LOG_OPTION, "add-key", FetchAICrypto.identifier, *password_option], ) assert result.exit_code == 0 os.chdir(cls.t) + @classmethod + def get_password_args(cls, password: Optional[str]) -> List[str]: + """Get password arguments.""" + return [] if password is None else ["--password", password] + @classmethod def teardown_class(cls): """Tear the test down.""" @@ -176,6 +200,12 @@ def test_exit_code_equal_to_zero(self): ) +class TestLaunchWithPassword(TestLaunch): + """Test that the command 'aea launch --password ' works as expected.""" + + PASSWORD = "fake-password" + + class TestLaunchWithOneFailingAgent(BaseLaunchTestCase): """Test aea launch when there is a failing agent..""" From bcaf78de7cfa36d8e0e689a2b1f780ca2a5dd9ab Mon Sep 17 00:00:00 2001 From: MarcoFavorito Date: Mon, 24 May 2021 23:44:55 +0200 Subject: [PATCH 102/147] test: add tests on 'run' command with '--password' flag --- aea/cli/run.py | 4 ++-- tests/test_cli/test_launch.py | 2 +- tests/test_cli/test_run.py | 14 +++++++++----- 3 files changed, 12 insertions(+), 8 deletions(-) diff --git a/aea/cli/run.py b/aea/cli/run.py index d560163fb5..aa4709dd99 100644 --- a/aea/cli/run.py +++ b/aea/cli/run.py @@ -90,7 +90,7 @@ def run( profiling = int(profiling) if profiling > 0: with _profiling_context(period=profiling): - run_aea(ctx, connection_ids, env_file, is_install_deps) + run_aea(ctx, connection_ids, env_file, is_install_deps, password) return run_aea(ctx, connection_ids, env_file, is_install_deps, password) @@ -192,7 +192,7 @@ def _build_aea( """Build the AEA.""" try: builder = AEABuilder.from_aea_project( - Path("."), skip_consistency_check=skip_consistency_check + Path("."), skip_consistency_check=skip_consistency_check, password=password ) aea = builder.build(connection_ids=connection_ids, password=password) return aea diff --git a/tests/test_cli/test_launch.py b/tests/test_cli/test_launch.py index e0ea0c6f9d..0313bdfcbe 100644 --- a/tests/test_cli/test_launch.py +++ b/tests/test_cli/test_launch.py @@ -53,7 +53,7 @@ class BaseLaunchTestCase: """Base Test case for launch tests.""" - PASSWORD: Optional[str] = None + PASSWORD: Optional[str] = None # nosec @contextmanager def _cli_launch( diff --git a/tests/test_cli/test_run.py b/tests/test_cli/test_run.py index f653dbeff2..b9eb170119 100644 --- a/tests/test_cli/test_run.py +++ b/tests/test_cli/test_run.py @@ -41,7 +41,7 @@ DEFAULT_CONNECTION_CONFIG_FILE, ) from aea.exceptions import AEAPackageLoadingError -from aea.test_tools.test_cases import AEATestCaseEmpty +from aea.test_tools.test_cases import AEATestCaseEmpty, _get_password_option_args from packages.fetchai.connections.http_client.connection import ( PUBLIC_ID as HTTP_ClIENT_PUBLIC_ID, @@ -63,7 +63,7 @@ @pytest.mark.flaky(reruns=MAX_FLAKY_RERUNS) -def test_run(): +def test_run(password_or_none): """Test that the command 'aea run' works as expected.""" runner = CliRunner() agent_name = "myagent" @@ -71,6 +71,7 @@ def test_run(): t = tempfile.mkdtemp() # copy the 'packages' directory in the parent of the agent folder. shutil.copytree(Path(ROOT_DIR, "packages"), Path(t, "packages")) + password_options = _get_password_option_args(password_or_none) os.chdir(t) result = runner.invoke( @@ -84,11 +85,14 @@ def test_run(): os.chdir(Path(t, agent_name)) result = runner.invoke( - cli, [*CLI_LOG_OPTION, "generate-key", FetchAICrypto.identifier] + cli, + [*CLI_LOG_OPTION, "generate-key", FetchAICrypto.identifier, *password_options], ) assert result.exit_code == 0 - result = runner.invoke(cli, [*CLI_LOG_OPTION, "add-key", FetchAICrypto.identifier]) + result = runner.invoke( + cli, [*CLI_LOG_OPTION, "add-key", FetchAICrypto.identifier, *password_options] + ) assert result.exit_code == 0 result = runner.invoke( @@ -111,7 +115,7 @@ def test_run(): try: process = PexpectWrapper( # nosec - [sys.executable, "-m", "aea.cli", "run"], + [sys.executable, "-m", "aea.cli", "run", *password_options], env=os.environ.copy(), maxread=10000, encoding="utf-8", From 1a0d36ace6407e07cd3ed4a8840d62a67733c35a Mon Sep 17 00:00:00 2001 From: MarcoFavorito Date: Mon, 24 May 2021 23:57:39 +0200 Subject: [PATCH 103/147] test: add tests on 'transfer' command with '--password' flag --- tests/test_cli/test_transfer.py | 98 +++++++++++++++++++++++++++++---- 1 file changed, 86 insertions(+), 12 deletions(-) diff --git a/tests/test_cli/test_transfer.py b/tests/test_cli/test_transfer.py index df9dc3245b..2707f8065e 100644 --- a/tests/test_cli/test_transfer.py +++ b/tests/test_cli/test_transfer.py @@ -20,6 +20,7 @@ import random import string from pathlib import Path +from typing import List, Optional from unittest.mock import patch import pytest @@ -43,6 +44,7 @@ class TestCliTransferFetchAINetwork(AEATestCaseEmpty): LEDGER_ID = FetchAICrypto.identifier ANOTHER_LEDGER_ID = CosmosCrypto.identifier + PASSWORD: Optional[str] = None @classmethod def setup_class(cls): @@ -61,16 +63,28 @@ def gen_key(cls, agent_name: str) -> None: """Generate crypto key.""" cls.set_agent_context(agent_name) key_file = f"{cls.LEDGER_ID}.key" + password_options = cls.get_password_args(cls.PASSWORD) assert cls.run_cli_command( - "generate-key", cls.LEDGER_ID, key_file, cwd=cls._get_cwd() + "generate-key", + cls.LEDGER_ID, + key_file, + *password_options, + cwd=cls._get_cwd(), ) assert cls.run_cli_command( - "add-key", cls.LEDGER_ID, key_file, cwd=cls._get_cwd() + "add-key", cls.LEDGER_ID, key_file, *password_options, cwd=cls._get_cwd() ) + @classmethod + def get_password_args(cls, password: Optional[str]) -> List[str]: + """Get password arguments.""" + return [] if password is None else ["--password", password] + def get_address(self) -> str: """Get current agent address.""" - result = self.invoke("get-address", self.LEDGER_ID) + result = self.invoke( + "get-address", self.LEDGER_ID, *self.get_password_args(self.PASSWORD) + ) return result.stdout_bytes.decode("utf-8").strip() def get_balance(self) -> int: @@ -80,20 +94,22 @@ def get_balance(self) -> int: Path("."), substitude_env_vars=False, private_key_helper=private_key_verify, + password=self.PASSWORD, ).agent_config - wallet = get_wallet_from_agent_config(agent_config) + wallet = get_wallet_from_agent_config(agent_config, password=self.PASSWORD) return int(try_get_balance(agent_config, wallet, self.LEDGER_ID)) @pytest.mark.flaky(reruns=MAX_FLAKY_RERUNS) def test_integration(self): """Perform integration tests of cli transfer command with real transfer.""" self.set_agent_context(self.agent_name2) + password_option = self.get_password_args(self.PASSWORD) agent2_balance = self.get_balance() agent2_address = self.get_address() assert agent2_balance == 0 self.set_agent_context(self.agent_name) - self.generate_wealth() + self.generate_wealth(password=self.PASSWORD) wait_for_condition(lambda: self.get_balance() > 0, timeout=15, period=1) @@ -104,7 +120,13 @@ def test_integration(self): fee = round(agent1_balance / 20) self.invoke( - "transfer", self.LEDGER_ID, agent2_address, str(amount), str(fee), "-y" + "transfer", + self.LEDGER_ID, + agent2_address, + str(amount), + str(fee), + "-y", + *password_option, ) wait_for_condition( @@ -127,8 +149,15 @@ def test_yes_option_enabled( self, wait_tx_settled_mock, confirm_mock, do_transfer_mock ): """Test yes option is enabled.""" + password_option = self.get_password_args(self.PASSWORD) self.invoke( - "transfer", self.LEDGER_ID, self.get_address(), "100000", "100", "-y" + "transfer", + self.LEDGER_ID, + self.get_address(), + "100000", + "100", + "-y", + *password_option, ) confirm_mock.assert_not_called() @@ -139,7 +168,15 @@ def test_yes_option_disabled( self, wait_tx_settled_mock, confirm_mock, do_transfer_mock ): """Test yes option is disabled.""" - self.invoke("transfer", self.LEDGER_ID, self.get_address(), "100000", "100") + password_option = self.get_password_args(self.PASSWORD) + self.invoke( + "transfer", + self.LEDGER_ID, + self.get_address(), + "100000", + "100", + *password_option, + ) confirm_mock.assert_called_once() @patch("aea.cli.transfer.do_transfer", return_value="some_digest") @@ -149,8 +186,15 @@ def test_sync_option_enabled( self, wait_tx_settled_mock, confirm_mock, do_transfer_mock ): """Test sync option is enabled.""" + password_option = self.get_password_args(self.PASSWORD) self.invoke( - "transfer", self.LEDGER_ID, self.get_address(), "100000", "100", "-y" + "transfer", + self.LEDGER_ID, + self.get_address(), + "100000", + "100", + "-y", + *password_option, ) wait_tx_settled_mock.assert_not_called() @@ -161,6 +205,7 @@ def test_sync_option_disabled( self, wait_tx_settled_mock, confirm_mock, do_transfer_mock ): """Test sync option is disabled.""" + password_option = self.get_password_args(self.PASSWORD) self.invoke( "transfer", self.LEDGER_ID, @@ -169,6 +214,7 @@ def test_sync_option_disabled( "100", "-y", "--sync", + *password_option, ) wait_tx_settled_mock.assert_called_once() @@ -178,17 +224,31 @@ def test_sync_option_disabled( def test_failed_on_send(self, wait_tx_settled_mock, confirm_mock, do_transfer_mock): """Test fail to send a transaction.""" with pytest.raises(ClickException, match=r"Failed to send a transaction!"): - self.invoke("transfer", self.LEDGER_ID, self.get_address(), "100000", "100") + password_option = self.get_password_args(self.PASSWORD) + self.invoke( + "transfer", + self.LEDGER_ID, + self.get_address(), + "100000", + "100", + *password_option, + ) @patch("aea.cli.transfer.click.confirm", return_value=None) @patch("aea.cli.transfer.wait_tx_settled", return_value=None) def test_no_wallet_registered(self, wait_tx_settled_mock, confirm_mock): """Test no wallet for crypto id registered.""" + password_option = self.get_password_args(self.PASSWORD) with pytest.raises( ClickException, match=r"No private key registered for `.*` in wallet!" ): self.invoke( - "transfer", self.ANOTHER_LEDGER_ID, self.get_address(), "100000", "100" + "transfer", + self.ANOTHER_LEDGER_ID, + self.get_address(), + "100000", + "100", + *password_option, ) @patch("aea.cli.transfer.try_get_balance", return_value=10) @@ -198,11 +258,19 @@ def test_balance_too_low( self, wait_tx_settled_mock, confirm_mock, do_transfer_mock ): """Test balance too low exception.""" + password_option = self.get_password_args(self.PASSWORD) with pytest.raises( ClickException, match=r"Balance is not enough! Available=[0-9]+, required=[0-9]+!", ): - self.invoke("transfer", self.LEDGER_ID, self.get_address(), "100000", "100") + self.invoke( + "transfer", + self.LEDGER_ID, + self.get_address(), + "100000", + "100", + *password_option, + ) @patch( "aea.cli.transfer.LedgerApis.is_transaction_settled", side_effects=[False, True] @@ -216,3 +284,9 @@ def test_wait_tx_settled_timeout(self, is_transaction_settled_mock): """Test wait tx settle fails with timeout error.""" with pytest.raises(TimeoutError): wait_tx_settled("some", "some", timeout=0.5) + + +class TestCliTransferFetchAINetworkWithPassword(TestCliTransferFetchAINetwork): + """Test cli transfer command, with '--password' option.""" + + PASSWORD = "fake-password" From 1dc4494a85c0b2c0d9e36e276c256b33e3d9f2af Mon Sep 17 00:00:00 2001 From: MarcoFavorito Date: Tue, 25 May 2021 00:00:49 +0200 Subject: [PATCH 104/147] test: fix bandit checks on PASSWORD fields --- tests/test_cli/test_launch.py | 2 +- tests/test_cli/test_transfer.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_cli/test_launch.py b/tests/test_cli/test_launch.py index 0313bdfcbe..5a51758b0c 100644 --- a/tests/test_cli/test_launch.py +++ b/tests/test_cli/test_launch.py @@ -203,7 +203,7 @@ def test_exit_code_equal_to_zero(self): class TestLaunchWithPassword(TestLaunch): """Test that the command 'aea launch --password ' works as expected.""" - PASSWORD = "fake-password" + PASSWORD = "fake-password" # nosec class TestLaunchWithOneFailingAgent(BaseLaunchTestCase): diff --git a/tests/test_cli/test_transfer.py b/tests/test_cli/test_transfer.py index 2707f8065e..83d3696eda 100644 --- a/tests/test_cli/test_transfer.py +++ b/tests/test_cli/test_transfer.py @@ -289,4 +289,4 @@ def test_wait_tx_settled_timeout(self, is_transaction_settled_mock): class TestCliTransferFetchAINetworkWithPassword(TestCliTransferFetchAINetwork): """Test cli transfer command, with '--password' option.""" - PASSWORD = "fake-password" + PASSWORD = "fake-password" # nosec From decea2487d928772458a1686a51a7a7fe5270699 Mon Sep 17 00:00:00 2001 From: David Minarsch Date: Tue, 25 May 2021 09:54:27 +0100 Subject: [PATCH 105/147] feat: add darglint to CI --- .github/workflows/workflow.yml | 2 ++ Makefile | 1 + Pipfile | 1 + setup.cfg | 5 +++++ tox.ini | 7 +++++++ 5 files changed, 16 insertions(+) diff --git a/.github/workflows/workflow.yml b/.github/workflows/workflow.yml index 01183d1f22..459611f875 100644 --- a/.github/workflows/workflow.yml +++ b/.github/workflows/workflow.yml @@ -73,6 +73,8 @@ jobs: tox -e black-check tox -e isort-check tox -e flake8 + - name: Docstring check + tox -e darglint - name: Unused code check run: tox -e vulture - name: Static type check diff --git a/Makefile b/Makefile index 16be07f074..241d78c40a 100644 --- a/Makefile +++ b/Makefile @@ -46,6 +46,7 @@ lint: isort aea benchmark examples packages plugins scripts tests flake8 aea benchmark examples packages plugins scripts tests vulture aea scripts/whitelist.py --exclude "*_pb2.py" + darglint aea benchmark examples packages .PHONY: pylint pylint: diff --git a/Pipfile b/Pipfile index 61f5f8c611..3949763be2 100644 --- a/Pipfile +++ b/Pipfile @@ -17,6 +17,7 @@ bech32 = "==1.2.0" black = "==19.10b0" bs4 = "==0.0.1" colorlog = "==4.1.0" +darglint = "==1.8.0" defusedxml = "==0.6.0" docker = "==4.2.0" ecdsa = ">=0.15" diff --git a/setup.cfg b/setup.cfg index ca841b97e6..74534e38ef 100644 --- a/setup.cfg +++ b/setup.cfg @@ -215,3 +215,8 @@ ignore_missing_imports = True [mypy-packages/fetchai/protocols/yoti/yoti_pb2] ignore_errors = True + +[darglint] +docstring_style=sphinx +strictness=short +ignore_regex=.*_pb2\.py diff --git a/tox.ini b/tox.ini index 45683fb68a..5f2015ce5e 100644 --- a/tox.ini +++ b/tox.ini @@ -275,6 +275,13 @@ deps = vulture==2.3 commands = vulture aea scripts/whitelist.py --exclude "*_pb2.py" +[testenv:darglint] +skipsdist = True +skip_install = True +deps = + darglint==1.8.0 +commands = darglint aea benchmark examples packages + [testenv:check_doc_links] skipsdist = True usedevelop = True From 4c195ba0e7528b4f012b77275069273f291fe8f0 Mon Sep 17 00:00:00 2001 From: David Minarsch Date: Tue, 25 May 2021 10:07:15 +0100 Subject: [PATCH 106/147] fix: github workflow typo --- .github/workflows/workflow.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/workflow.yml b/.github/workflows/workflow.yml index 459611f875..ba1aff252e 100644 --- a/.github/workflows/workflow.yml +++ b/.github/workflows/workflow.yml @@ -74,7 +74,7 @@ jobs: tox -e isort-check tox -e flake8 - name: Docstring check - tox -e darglint + run: tox -e darglint - name: Unused code check run: tox -e vulture - name: Static type check From 3d5920ef0f814200dc42bfd009feb5766a5105a3 Mon Sep 17 00:00:00 2001 From: David Minarsch Date: Tue, 25 May 2021 10:16:11 +0100 Subject: [PATCH 107/147] chore: adapt darglint settings to ignore exceptions for now --- setup.cfg | 1 + 1 file changed, 1 insertion(+) diff --git a/setup.cfg b/setup.cfg index 74534e38ef..cfcbc7f7fb 100644 --- a/setup.cfg +++ b/setup.cfg @@ -220,3 +220,4 @@ ignore_errors = True docstring_style=sphinx strictness=short ignore_regex=.*_pb2\.py +ignore=DAR401 From 4feed0ee6e69a17f254d0931b55f074ba66a198a Mon Sep 17 00:00:00 2001 From: MarcoFavorito Date: Tue, 25 May 2021 12:06:08 +0200 Subject: [PATCH 108/147] fix: fix minor bugs in MultiAgentManager - remove useless call to the warning printing method - ensure private keys only if overrides are provided --- aea/manager/manager.py | 1 - aea/manager/project.py | 3 ++- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/aea/manager/manager.py b/aea/manager/manager.py index 96c13a5114..894705fe47 100644 --- a/aea/manager/manager.py +++ b/aea/manager/manager.py @@ -861,7 +861,6 @@ def _print_exception_occurred_but_no_error_callback(self, agent_name: str) -> No if self._warning_message_printed_for_agent.get(agent_name, False): return self._warning_message_printed_for_agent[agent_name] = True - self._print_exception_occurred_but_no_error_callback(agent_name) print( f"WARNING: An exception occurred during the execution of agent '{agent_name}', " f"but since no error callback was found the exception is handled silently. Please " diff --git a/aea/manager/project.py b/aea/manager/project.py index 8e0dd12f6f..39e8ef8a2b 100644 --- a/aea/manager/project.py +++ b/aea/manager/project.py @@ -302,7 +302,8 @@ def set_overrides( overrides["component_configurations"] = component_configurations self.agent_config_manager.update_config(overrides) - self._ensure_private_keys() + if overrides: + self._ensure_private_keys() @property def agent_config_manager(self) -> AgentConfigManager: From b597c1c665ff009c4d250a7903b56668a1290818 Mon Sep 17 00:00:00 2001 From: ali Date: Tue, 25 May 2021 19:53:56 +0100 Subject: [PATCH 109/147] tests: aries alice --- .coveragerc | 1 - packages/fetchai/protocols/default/README.md | 1 + .../fetchai/protocols/default/protocol.yaml | 2 +- .../fetchai/skills/aries_alice/behaviours.py | 16 +- .../fetchai/skills/aries_alice/dialogues.py | 2 +- .../fetchai/skills/aries_alice/handlers.py | 69 +-- .../fetchai/skills/aries_alice/skill.yaml | 16 +- .../fetchai/skills/aries_alice/strategy.py | 4 +- packages/hashes.csv | 4 +- .../test_skills/test_aries_alice/__init__.py | 20 + .../test_aries_alice/intermediate_class.py | 245 +++++++++ .../test_aries_alice/test_behaviours.py | 266 ++++++++++ .../test_aries_alice/test_dialogues.py | 72 +++ .../test_aries_alice/test_handlers.py | 494 ++++++++++++++++++ .../test_aries_alice/test_strategy.py | 86 +++ 15 files changed, 1234 insertions(+), 64 deletions(-) create mode 100644 tests/test_packages/test_skills/test_aries_alice/__init__.py create mode 100644 tests/test_packages/test_skills/test_aries_alice/intermediate_class.py create mode 100644 tests/test_packages/test_skills/test_aries_alice/test_behaviours.py create mode 100644 tests/test_packages/test_skills/test_aries_alice/test_dialogues.py create mode 100644 tests/test_packages/test_skills/test_aries_alice/test_handlers.py create mode 100644 tests/test_packages/test_skills/test_aries_alice/test_strategy.py diff --git a/.coveragerc b/.coveragerc index 274e6c0c04..e2e5c9c798 100644 --- a/.coveragerc +++ b/.coveragerc @@ -2,7 +2,6 @@ omit = */.tox/* packages/fetchai/contracts/* - packages/fetchai/skills/aries_alice/* packages/fetchai/skills/aries_faber/* packages/fetchai/skills/gym/* packages/fetchai/skills/fipa_dummy_buyer/* diff --git a/packages/fetchai/protocols/default/README.md b/packages/fetchai/protocols/default/README.md index c71f97e9ee..8133e98f5c 100644 --- a/packages/fetchai/protocols/default/README.md +++ b/packages/fetchai/protocols/default/README.md @@ -1,3 +1,4 @@ + # Default Protocol ## Description diff --git a/packages/fetchai/protocols/default/protocol.yaml b/packages/fetchai/protocols/default/protocol.yaml index b29b7e80eb..d327db9f1c 100644 --- a/packages/fetchai/protocols/default/protocol.yaml +++ b/packages/fetchai/protocols/default/protocol.yaml @@ -7,7 +7,7 @@ description: A protocol for exchanging any bytes message. license: Apache-2.0 aea_version: '>=1.0.0, <2.0.0' fingerprint: - README.md: QmNN1eJSSeAGs4uny1tqWkeQwyr6SVV2QgKAoPz18W5jnV + README.md: QmREaHgRR2rzAitxqGH3LurXciboG7JgPK82noBizRe2yy __init__.py: QmZL6rcTUHASkTVncagHTWrKvLiJrwPfHmduqekEM5R2Rb custom_types.py: QmbAu5CYv5VtKKbEtzCbGQGE5cPZezDjubLNbiTPycXrHv default.proto: QmWYzTSHVbz7FBS84iKFMhGSXPxay2mss29vY7ufz2BFJ8 diff --git a/packages/fetchai/skills/aries_alice/behaviours.py b/packages/fetchai/skills/aries_alice/behaviours.py index 7d7dfc46cb..19bab645f1 100644 --- a/packages/fetchai/skills/aries_alice/behaviours.py +++ b/packages/fetchai/skills/aries_alice/behaviours.py @@ -34,7 +34,7 @@ HttpDialogues, OefSearchDialogues, ) -from packages.fetchai.skills.aries_alice.strategy import AliceStrategy +from packages.fetchai.skills.aries_alice.strategy import Strategy DEFAULT_MAX_SOEF_REGISTRATION_RETRIES = 5 @@ -83,7 +83,7 @@ def send_http_request_message( body=b"" if content is None else json.dumps(content).encode("utf-8"), ) # send - self.context.outbox.put_message(message=request_http_message,) + self.context.outbox.put_message(message=request_http_message) def setup(self) -> None: """ @@ -164,7 +164,7 @@ def _register_agent(self) -> None: :return: None """ - strategy = cast(AliceStrategy, self.context.strategy) + strategy = cast(Strategy, self.context.strategy) description = strategy.get_location_description() self._register(description, "registering agent on SOEF.") @@ -174,7 +174,7 @@ def register_service(self) -> None: :return: None """ - strategy = cast(AliceStrategy, self.context.strategy) + strategy = cast(Strategy, self.context.strategy) description = strategy.get_register_service_description() self._register(description, "registering agent's service on the SOEF.") @@ -184,7 +184,7 @@ def register_genus(self) -> None: :return: None """ - strategy = cast(AliceStrategy, self.context.strategy) + strategy = cast(Strategy, self.context.strategy) description = strategy.get_register_personality_description() self._register( description, "registering agent's personality genus on the SOEF." @@ -196,7 +196,7 @@ def register_classification(self) -> None: :return: None """ - strategy = cast(AliceStrategy, self.context.strategy) + strategy = cast(Strategy, self.context.strategy) description = strategy.get_register_classification_description() self._register( description, "registering agent's personality classification on the SOEF." @@ -208,7 +208,7 @@ def _unregister_service(self) -> None: :return: None """ - strategy = cast(AliceStrategy, self.context.strategy) + strategy = cast(Strategy, self.context.strategy) description = strategy.get_unregister_service_description() oef_search_dialogues = cast( OefSearchDialogues, self.context.oef_search_dialogues @@ -227,7 +227,7 @@ def _unregister_agent(self) -> None: :return: None """ - strategy = cast(AliceStrategy, self.context.strategy) + strategy = cast(Strategy, self.context.strategy) description = strategy.get_location_description() oef_search_dialogues = cast( OefSearchDialogues, self.context.oef_search_dialogues diff --git a/packages/fetchai/skills/aries_alice/dialogues.py b/packages/fetchai/skills/aries_alice/dialogues.py index 33b2fc4db1..8e65385bfa 100644 --- a/packages/fetchai/skills/aries_alice/dialogues.py +++ b/packages/fetchai/skills/aries_alice/dialogues.py @@ -139,6 +139,6 @@ def role_from_first_message( # pylint: disable=unused-argument BaseOefSearchDialogues.__init__( self, - self_address=self.context.agent_address, + self_address=str(self.skill_id), role_from_first_message=role_from_first_message, ) diff --git a/packages/fetchai/skills/aries_alice/handlers.py b/packages/fetchai/skills/aries_alice/handlers.py index 1175b7d47d..ed01cfb08d 100644 --- a/packages/fetchai/skills/aries_alice/handlers.py +++ b/packages/fetchai/skills/aries_alice/handlers.py @@ -43,21 +43,15 @@ ) from packages.fetchai.skills.aries_alice.strategy import ( ADMIN_COMMAND_RECEIVE_INVITE, - AliceStrategy, + Strategy, ) -class AliceDefaultHandler(Handler): +class DefaultHandler(Handler): """This class represents alice's handler for default messages.""" SUPPORTED_PROTOCOL = DefaultMessage.protocol_id # type: Optional[PublicId] - def __init__(self, **kwargs: Any) -> None: - """Initialize the handler.""" - super().__init__(**kwargs) - - self.handled_message: Optional[DefaultMessage] = None - def _handle_received_invite(self, invite_detail: Dict[str, str]) -> Optional[str]: """ Prepare an invitation detail received from Faber_AEA to be send to the Alice ACA. @@ -114,24 +108,24 @@ def handle(self, message: Message) -> None: :return: None """ message = cast(DefaultMessage, message) - default_dialogues = cast(DefaultDialogues, self.context.default_dialogues) - strategy = cast(AliceStrategy, self.context.strategy) + # recover dialogue + default_dialogues = cast(DefaultDialogues, self.context.default_dialogues) + default_dialogue = cast( + Optional[DefaultDialogue], default_dialogues.update(message) + ) + if default_dialogue is None: + self.context.logger.error( + "alice -> default_handler -> handle(): something went wrong when adding the incoming default message to the dialogue." + ) + return - self.handled_message = message if message.performative == DefaultMessage.Performative.BYTES: - http_dialogue = cast( - Optional[DefaultDialogue], default_dialogues.update(message) - ) - if http_dialogue is None: - self.context.logger.exception( - "alice -> default_handler -> handle(): something went wrong when adding the incoming HTTP response message to the dialogue." - ) - return content_bytes = message.content content = json.loads(content_bytes) self.context.logger.info("Received message content:" + str(content)) if "@type" in content: + strategy = cast(Strategy, self.context.strategy) details = self._handle_received_invite(content) self.context.behaviours.alice.send_http_request_message( method="POST", @@ -147,7 +141,7 @@ def teardown(self) -> None: """ -class AliceHttpHandler(Handler): +class HttpHandler(Handler): """This class represents alice's handler for HTTP messages.""" SUPPORTED_PROTOCOL = HttpMessage.protocol_id # type: Optional[PublicId] @@ -159,8 +153,6 @@ def __init__(self, **kwargs: Any) -> None: self.connection_id = None # type: Optional[str] self.is_connected_to_Faber = False - self.handled_message: Optional[HttpMessage] = None - def setup(self) -> None: """ Implement the setup. @@ -176,16 +168,17 @@ def handle(self, message: Message) -> None: :return: None """ message = cast(HttpMessage, message) + + # recover dialogue http_dialogues = cast(HttpDialogues, self.context.http_dialogues) + http_dialogue = cast(Optional[HttpDialogue], http_dialogues.update(message)) + if http_dialogue is None: + self.context.logger.error( + "alice -> http_handler -> handle() -> REQUEST: something went wrong when adding the incoming HTTP webhook request message to the dialogue." + ) + return - self.handled_message = message if message.performative == HttpMessage.Performative.REQUEST: # webhook - http_dialogue = cast(Optional[HttpDialogue], http_dialogues.update(message)) - if http_dialogue is None: - self.context.logger.exception( - "alice -> http_handler -> handle() -> REQUEST: something went wrong when adding the incoming HTTP webhook request message to the dialogue." - ) - return content_bytes = message.body content = json.loads(content_bytes) self.context.logger.info("Received webhook message content:" + str(content)) @@ -197,29 +190,23 @@ def handle(self, message: Message) -> None: elif ( message.performative == HttpMessage.Performative.RESPONSE ): # response to http_client request - http_dialogue = cast(Optional[HttpDialogue], http_dialogues.update(message)) - if http_dialogue is None: - self.context.logger.exception( - "alice -> http_handler -> handle() -> RESPONSE: something went wrong when adding the incoming HTTP response message to the dialogue." - ) - return content_bytes = message.body - content = content_bytes.decode("utf-8") + content = json.loads(content_bytes) if "Error" in content: self.context.logger.error( "Something went wrong after I sent the administrative command of 'invitation receive'" ) else: self.context.logger.info( - "Received http response message content:" + str(content) + f"Received http response message content:{str(content)}" ) if "connection_id" in content: connection = content self.connection_id = content["connection_id"] invitation = connection["invitation"] - self.context.logger.info("invitation response: " + str(connection)) - self.context.logger.info("connection id: " + self.connection_id) # type: ignore - self.context.logger.info("invitation: " + str(invitation)) + self.context.logger.info(f"invitation response: {str(connection)}") + self.context.logger.info(f"connection id: {self.connection_id}") # type: ignore + self.context.logger.info(f"invitation: {str(invitation)}") def teardown(self) -> None: """ @@ -229,7 +216,7 @@ def teardown(self) -> None: """ -class AliceOefSearchHandler(Handler): +class OefSearchHandler(Handler): """This class implements an OEF search handler.""" SUPPORTED_PROTOCOL = OefSearchMessage.protocol_id # type: Optional[PublicId] diff --git a/packages/fetchai/skills/aries_alice/skill.yaml b/packages/fetchai/skills/aries_alice/skill.yaml index a486329605..026ad1956f 100644 --- a/packages/fetchai/skills/aries_alice/skill.yaml +++ b/packages/fetchai/skills/aries_alice/skill.yaml @@ -9,10 +9,10 @@ aea_version: '>=1.0.0, <2.0.0' fingerprint: README.md: Qmc88RFakLqDTqT42YGJDCDrH22tW2dkCBAs8wLKMGt5TV __init__.py: QmTiLfLMvHMUzrgQ7tMtYewLrz5DDihRV2YoLkq6ycCBby - behaviours.py: QmeVpN2b4M3w84Q8MeBxfcdSB7tFQcgDYHgAcWEqEyibcC - dialogues.py: QmS74KVR3EHXSTFa7P1dcqbJsFjNNo4gPzekhKXfKbD3Wj - handlers.py: QmXmxZ8kcpPHPZBCUUzoBoma7UjyYPayBdLkDaCHtHQGTr - strategy.py: Qmb6naFGGHgcirrwNf6VUxyVa4BYKNFwyCttqkKn7mER8U + behaviours.py: QmZUD3AYp9Tw51zFC5tyS49yunAZvXD6qtWX8buu7jou11 + dialogues.py: QmPPyGaizHVdJdLzuD4xebhSx6jehNc3oQ9UnZBu6xqu3b + handlers.py: QmZSeg7xCYXWwAc7yDYQg5pPpfGdQWJNFB2jfGjWeH1HPM + strategy.py: Qmaa1usbr2duj3yMm2gF5AbuCpBSyFfkRVHVceeQT8XVGE fingerprint_ignore_patterns: [] connections: - fetchai/http_client:0.22.0 @@ -31,13 +31,13 @@ behaviours: handlers: default: args: {} - class_name: AliceDefaultHandler + class_name: DefaultHandler http: args: {} - class_name: AliceHttpHandler + class_name: HttpHandler oef_search: args: {} - class_name: AliceOefSearchHandler + class_name: OefSearchHandler models: default_dialogues: args: {} @@ -64,6 +64,6 @@ models: service_data: key: intro_service value: intro_alice - class_name: AliceStrategy + class_name: Strategy dependencies: {} is_abstract: false diff --git a/packages/fetchai/skills/aries_alice/strategy.py b/packages/fetchai/skills/aries_alice/strategy.py index f26d8c3ecd..1a038bbc27 100644 --- a/packages/fetchai/skills/aries_alice/strategy.py +++ b/packages/fetchai/skills/aries_alice/strategy.py @@ -50,7 +50,7 @@ DEFAULT_CLASSIFICATION = {"piece": "classification", "value": "identity.aries.alice"} -class AliceStrategy(Model): +class Strategy(Model): """This class defines a strategy for the agent.""" def __init__(self, **kwargs: Any) -> None: @@ -66,7 +66,7 @@ def __init__(self, **kwargs: Any) -> None: self._admin_host = kwargs.pop("admin_host", DEFAULT_ADMIN_HOST) self._admin_port = kwargs.pop("admin_port", DEFAULT_ADMIN_PORT) - self._admin_url = "http://{}:{}".format(self.admin_host, self.admin_port) + self._admin_url = f"http://{self.admin_host}:{self.admin_port}" # search location = kwargs.pop("location", DEFAULT_LOCATION) diff --git a/packages/hashes.csv b/packages/hashes.csv index b93a30df5a..e9043d27ec 100644 --- a/packages/hashes.csv +++ b/packages/hashes.csv @@ -59,7 +59,7 @@ fetchai/contracts/scaffold,QmZuYdqJtxhKQU4sDHjaarya9tF6nctRFNZqoiF7WsgbS9 fetchai/contracts/staking_erc20,QmVJZpvNmgVYWmD11Br8uytKVvYNSm6zHyrHdBNvK5Ag7s fetchai/protocols/aggregation,Qmf1cCWdpFKGUp3jZubQbFxQd5iDTWNVX2BEBTCAwmGGoG fetchai/protocols/contract_api,QmYjgYKBM9ATJ9S2ReNVFy7GEQzBA6CGmea5giyzJVUV84 -fetchai/protocols/default,QmR8HQP3oLYStwME45MztU8Pg2TFn9wFHJGiLgJpQdnHdd +fetchai/protocols/default,QmaraosuS34mxDK7RSV8nS4wHhDBytQeX11JNiaCpsheRV fetchai/protocols/fipa,QmcZgYbB17yQydsy4YMSv5ZpV6jq2PrveSepAqtk48ANUo fetchai/protocols/gym,QmUJdZ4tHi5r2EsfCrLVNaQeSU1sXCo1SbiMEh76Rg2sUg fetchai/protocols/http,QmXTLCKrPtSoVWuuSHj2n9QH4Mi4zjtfe8bixYbPqW8dHC @@ -74,7 +74,7 @@ fetchai/protocols/state_update,QmcLkEEmAxaynioNobJvaMdWUJuG8AG2BzxCYwX6kJhhA5 fetchai/protocols/tac,QmZGEpFDwtUU7ykRmwr3Scjmw69CixH7ZFMNptYWSZ5eBe fetchai/protocols/yoti,QmRcYAyxk2JjhTKHqVYnYiZ8zzck6qzsRuqRnBxPREK5Fe fetchai/skills/advanced_data_request,Qmdcmy14MMTUvjB1nHXfH26eZbYiJm3GGKhX84opMqhJ3b -fetchai/skills/aries_alice,QmRaWnVu8oEHFULU8Q4Kf4YzASZ72MD8YAoNmmoVgH3ziG +fetchai/skills/aries_alice,QmdyTucTSKyjLSEJfa43gP5sXg5uScEbzX6PtaN11EFfEs fetchai/skills/aries_faber,QmPpRGezMuMNzZmZkRLeDN3gMbCoGz9mKJ1M2J6iFxJyfX fetchai/skills/carpark_client,QmSSpTz49mF439YiVcxA3ddPaU9du3ptzQZ25HZtkL4Rys fetchai/skills/carpark_detection,QmUoVRaJqbo47tNMzmbsJ18gm1WNYGwCBZTPYvFuvG4is2 diff --git a/tests/test_packages/test_skills/test_aries_alice/__init__.py b/tests/test_packages/test_skills/test_aries_alice/__init__.py new file mode 100644 index 0000000000..b34c0aab10 --- /dev/null +++ b/tests/test_packages/test_skills/test_aries_alice/__init__.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# ------------------------------------------------------------------------------ +# +# Copyright 2018-2019 Fetch.AI Limited +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ------------------------------------------------------------------------------ + +"""The tests module contains the tests of the packages/skills/aries_alice dir.""" diff --git a/tests/test_packages/test_skills/test_aries_alice/intermediate_class.py b/tests/test_packages/test_skills/test_aries_alice/intermediate_class.py new file mode 100644 index 0000000000..2cf9755e01 --- /dev/null +++ b/tests/test_packages/test_skills/test_aries_alice/intermediate_class.py @@ -0,0 +1,245 @@ +# -*- coding: utf-8 -*- +# ------------------------------------------------------------------------------ +# +# Copyright 2018-2019 Fetch.AI Limited +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ------------------------------------------------------------------------------ +"""This module sets up test environment for aries_alice skill.""" + +import json +from pathlib import Path +from typing import cast + +from aea.helpers.search.models import ( + Attribute, + Constraint, + ConstraintType, + DataModel, + Description, + Location, + Query, +) +from aea.protocols.dialogue.base import DialogueMessage +from aea.test_tools.test_skill import BaseSkillTestCase + +from packages.fetchai.protocols.http.message import HttpMessage +from packages.fetchai.protocols.oef_search.message import OefSearchMessage +from packages.fetchai.skills.aries_alice.behaviours import AliceBehaviour +from packages.fetchai.skills.aries_alice.dialogues import ( + DefaultDialogues, + HttpDialogues, + OefSearchDialogues, +) +from packages.fetchai.skills.aries_alice.handlers import ( + DefaultHandler, + HttpHandler, + OefSearchHandler, +) +from packages.fetchai.skills.aries_alice.strategy import Strategy + +from tests.conftest import ROOT_DIR + + +class AriesAliceTestCase(BaseSkillTestCase): + """Sets the aries_alice class up for testing.""" + + path_to_skill = Path(ROOT_DIR, "packages", "fetchai", "skills", "aries_alice") + + @classmethod + def setup(cls): + """Setup the test class.""" + cls.location = {"longitude": 0.1270, "latitude": 51.5194} + cls.service_data = {"key": "seller_service", "value": "some_value"} + cls.personality_data = {"piece": "genus", "value": "some_personality"} + cls.classification = {"piece": "classification", "value": "some_classification"} + cls.admin_host = "127.0.0.1" + cls.admin_port = 8067 + config_overrides = { + "models": { + "strategy": { + "args": { + "location": cls.location, + "service_data": cls.service_data, + "personality_data": cls.personality_data, + "classification": cls.classification, + "admin_host": cls.admin_host, + "admin_port": cls.admin_port, + } + } + }, + } + + super().setup(config_overrides=config_overrides) + + # behaviours + cls.alice_behaviour = cast( + AliceBehaviour, cls._skill.skill_context.behaviours.alice, + ) + + # dialogues + cls.default_dialogues = cast( + DefaultDialogues, cls._skill.skill_context.default_dialogues + ) + cls.http_dialogues = cast( + HttpDialogues, cls._skill.skill_context.http_dialogues + ) + cls.oef_search_dialogues = cast( + OefSearchDialogues, cls._skill.skill_context.oef_search_dialogues + ) + + # handlers + cls.default_handler = cast( + DefaultHandler, cls._skill.skill_context.handlers.default + ) + cls.http_handler = cast(HttpHandler, cls._skill.skill_context.handlers.http) + cls.oef_search_handler = cast( + OefSearchHandler, cls._skill.skill_context.handlers.oef_search + ) + + # models + cls.strategy = cast(Strategy, cls._skill.skill_context.strategy) + + cls.logger = cls._skill.skill_context.logger + + # mocked objects + cls.mocked_method = "SOME_METHOD" + cls.mocked_url = "www.some-url.com" + cls.mocked_version = "some_version" + cls.mocked_headers = "some_headers" + cls.body_dict = {"some_key": "some_value"} + cls.body_str = "some_body" + cls.mocked_body_bytes = json.dumps(cls.body_str).encode("utf-8") + cls.mocked_query = Query( + [Constraint("some_attribute_name", ConstraintType("==", "some_value"))], + DataModel( + "some_data_model_name", + [ + Attribute( + "some_attribute_name", + str, + False, + "Some attribute descriptions.", + ) + ], + ), + ) + cls.mocked_proposal = Description( + { + "contract_address": "some_contract_address", + "token_id": "123456", + "trade_nonce": "876438756348568", + "from_supply": "543", + "to_supply": "432", + "value": "67", + } + ) + cls.mocked_registration_description = Description({"foo1": 1, "bar1": 2}) + + cls.registration_message = OefSearchMessage( + dialogue_reference=("", ""), + performative=OefSearchMessage.Performative.REGISTER_SERVICE, + service_description=cls.mocked_registration_description, + ) + cls.registration_message.sender = str(cls._skill.skill_context.skill_id) + cls.registration_message.to = cls._skill.skill_context.search_service_address + + # list of messages + cls.list_of_http_messages = ( + DialogueMessage( + HttpMessage.Performative.REQUEST, + { + "method": cls.mocked_method, + "url": cls.mocked_url, + "headers": cls.mocked_headers, + "version": cls.mocked_version, + "body": cls.mocked_body_bytes, + }, + is_incoming=False, + ), + ) + + cls.register_location_description = Description( + {"location": Location(51.5194, 0.1270)}, + data_model=DataModel( + "location_agent", [Attribute("location", Location, True)] + ), + ) + cls.list_of_messages_register_location = ( + DialogueMessage( + OefSearchMessage.Performative.REGISTER_SERVICE, + {"service_description": cls.register_location_description}, + is_incoming=False, + ), + ) + + cls.register_service_description = Description( + {"key": "some_key", "value": "some_value"}, + data_model=DataModel( + "set_service_key", + [Attribute("key", str, True), Attribute("value", str, True)], + ), + ) + cls.list_of_messages_register_service = ( + DialogueMessage( + OefSearchMessage.Performative.REGISTER_SERVICE, + {"service_description": cls.register_service_description}, + is_incoming=False, + ), + ) + + cls.register_genus_description = Description( + {"piece": "genus", "value": "some_value"}, + data_model=DataModel( + "personality_agent", + [Attribute("piece", str, True), Attribute("value", str, True)], + ), + ) + cls.list_of_messages_register_genus = ( + DialogueMessage( + OefSearchMessage.Performative.REGISTER_SERVICE, + {"service_description": cls.register_genus_description}, + is_incoming=False, + ), + ) + + cls.register_classification_description = Description( + {"piece": "classification", "value": "some_value"}, + data_model=DataModel( + "personality_agent", + [Attribute("piece", str, True), Attribute("value", str, True)], + ), + ) + cls.list_of_messages_register_classification = ( + DialogueMessage( + OefSearchMessage.Performative.REGISTER_SERVICE, + {"service_description": cls.register_classification_description}, + is_incoming=False, + ), + ) + + cls.register_invalid_description = Description( + {"piece": "classification", "value": "some_value"}, + data_model=DataModel( + "some_different_name", + [Attribute("piece", str, True), Attribute("value", str, True)], + ), + ) + cls.list_of_messages_register_invalid = ( + DialogueMessage( + OefSearchMessage.Performative.REGISTER_SERVICE, + {"service_description": cls.register_invalid_description}, + is_incoming=False, + ), + ) diff --git a/tests/test_packages/test_skills/test_aries_alice/test_behaviours.py b/tests/test_packages/test_skills/test_aries_alice/test_behaviours.py new file mode 100644 index 0000000000..50df6ec3a0 --- /dev/null +++ b/tests/test_packages/test_skills/test_aries_alice/test_behaviours.py @@ -0,0 +1,266 @@ +# -*- coding: utf-8 -*- +# ------------------------------------------------------------------------------ +# +# Copyright 2018-2019 Fetch.AI Limited +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ------------------------------------------------------------------------------ +"""This module contains the tests of the behaviour classes of the aries_alice skill.""" + +import json +import logging +from unittest.mock import patch + +from packages.fetchai.protocols.http.message import HttpMessage +from packages.fetchai.protocols.oef_search.message import OefSearchMessage +from packages.fetchai.skills.aries_alice.behaviours import HTTP_CLIENT_PUBLIC_ID + +from tests.test_packages.test_skills.test_aries_alice.intermediate_class import ( + AriesAliceTestCase, +) + + +class TestAliceBehaviour(AriesAliceTestCase): + """Test registration behaviour of aries_alice.""" + + def test_init(self): + """Test the __init__ method of the alice behaviour.""" + assert self.alice_behaviour.failed_registration_msg is None + assert self.alice_behaviour._nb_retries == 0 + + def test_send_http_request_message(self): + """Test the send_http_request_message method of the registration behaviour.""" + # operation + self.alice_behaviour.send_http_request_message( + self.mocked_method, self.mocked_url, self.body_dict + ) + + # after + self.assert_quantity_in_outbox(1) + + message = self.get_message_from_outbox() + has_attributes, error_str = self.message_has_attributes( + actual_message=message, + message_type=HttpMessage, + performative=HttpMessage.Performative.REQUEST, + to=str(HTTP_CLIENT_PUBLIC_ID), + sender=str(self.skill.skill_context.skill_id), + method=self.mocked_method, + url=self.mocked_url, + headers="", + version="", + body=json.dumps(self.body_dict).encode("utf-8"), + ) + assert has_attributes, error_str + + def test_setup(self): + """Test the setup method of the alice behaviour.""" + # operation + with patch.object( + self.strategy, + "get_location_description", + return_value=self.mocked_registration_description, + ) as mock_desc: + with patch.object(self.logger, "log") as mock_logger: + self.alice_behaviour.setup() + + # after + self.assert_quantity_in_outbox(1) + + mock_logger.assert_any_call( + logging.INFO, f"My address is: {self.skill.skill_context.agent_address}" + ) + + mock_desc.assert_called_once() + + # _register_agent + has_attributes, error_str = self.message_has_attributes( + actual_message=self.get_message_from_outbox(), + message_type=OefSearchMessage, + performative=OefSearchMessage.Performative.REGISTER_SERVICE, + to=self.skill.skill_context.search_service_address, + sender=str(self.skill.skill_context.skill_id), + service_description=self.mocked_registration_description, + ) + assert has_attributes, error_str + + mock_logger.assert_any_call(logging.INFO, "registering agent on SOEF.") + + def test_act_i(self): + """Test the act method of the registration behaviour where failed_registration_msg is NOT None.""" + # setup + self.alice_behaviour.failed_registration_msg = self.registration_message + + with patch.object(self.logger, "log") as mock_logger: + self.alice_behaviour.act() + + # after + self.assert_quantity_in_outbox(1) + + # _retry_failed_registration + has_attributes, error_str = self.message_has_attributes( + actual_message=self.get_message_from_outbox(), + message_type=type(self.registration_message), + performative=self.registration_message.performative, + to=self.registration_message.to, + sender=str(self.skill.skill_context.skill_id), + service_description=self.registration_message.service_description, + ) + assert has_attributes, error_str + + mock_logger.assert_any_call( + logging.INFO, + f"Retrying registration on SOEF. Retry {self.alice_behaviour._nb_retries} out of {self.alice_behaviour._max_soef_registration_retries}.", + ) + assert self.alice_behaviour.failed_registration_msg is None + + def test_act_ii(self): + """Test the act method of the registration behaviour where failed_registration_msg is NOT None and max retries is reached.""" + # setup + self.alice_behaviour.failed_registration_msg = self.registration_message + self.alice_behaviour._max_soef_registration_retries = 2 + self.alice_behaviour._nb_retries = 2 + + self.alice_behaviour.act() + + # after + self.assert_quantity_in_outbox(0) + assert self.skill.skill_context.is_active is False + + def test_register_service(self): + """Test the register_service method of the registration behaviour.""" + # operation + with patch.object( + self.strategy, + "get_register_service_description", + return_value=self.mocked_registration_description, + ): + with patch.object(self.logger, "log") as mock_logger: + self.alice_behaviour.register_service() + + # after + self.assert_quantity_in_outbox(1) + + message = self.get_message_from_outbox() + has_attributes, error_str = self.message_has_attributes( + actual_message=message, + message_type=OefSearchMessage, + performative=OefSearchMessage.Performative.REGISTER_SERVICE, + to=self.skill.skill_context.search_service_address, + sender=str(self.skill.skill_context.skill_id), + service_description=self.mocked_registration_description, + ) + assert has_attributes, error_str + mock_logger.assert_any_call( + logging.INFO, "registering agent's service on the SOEF." + ) + + def test_register_genus(self): + """Test the register_genus method of the registration behaviour.""" + # operation + with patch.object( + self.strategy, + "get_register_personality_description", + return_value=self.mocked_registration_description, + ): + with patch.object(self.logger, "log") as mock_logger: + self.alice_behaviour.register_genus() + + # after + self.assert_quantity_in_outbox(1) + + message = self.get_message_from_outbox() + has_attributes, error_str = self.message_has_attributes( + actual_message=message, + message_type=OefSearchMessage, + performative=OefSearchMessage.Performative.REGISTER_SERVICE, + to=self.skill.skill_context.search_service_address, + sender=str(self.skill.skill_context.skill_id), + service_description=self.mocked_registration_description, + ) + assert has_attributes, error_str + mock_logger.assert_any_call( + logging.INFO, "registering agent's personality genus on the SOEF." + ) + + def test_register_classification(self): + """Test the register_classification method of the registration behaviour.""" + # operation + with patch.object( + self.strategy, + "get_register_classification_description", + return_value=self.mocked_registration_description, + ): + with patch.object(self.logger, "log") as mock_logger: + self.alice_behaviour.register_classification() + + # after + self.assert_quantity_in_outbox(1) + + message = self.get_message_from_outbox() + has_attributes, error_str = self.message_has_attributes( + actual_message=message, + message_type=OefSearchMessage, + performative=OefSearchMessage.Performative.REGISTER_SERVICE, + to=self.skill.skill_context.search_service_address, + sender=str(self.skill.skill_context.skill_id), + service_description=self.mocked_registration_description, + ) + assert has_attributes, error_str + mock_logger.assert_any_call( + logging.INFO, "registering agent's personality classification on the SOEF." + ) + + def test_teardown(self): + """Test the teardown method of the service_registration behaviour.""" + # operation + with patch.object( + self.strategy, + "get_unregister_service_description", + return_value=self.mocked_registration_description, + ): + with patch.object( + self.strategy, + "get_location_description", + return_value=self.mocked_registration_description, + ): + with patch.object(self.logger, "log") as mock_logger: + self.alice_behaviour.teardown() + + # after + self.assert_quantity_in_outbox(2) + + # _unregister_service + has_attributes, error_str = self.message_has_attributes( + actual_message=self.get_message_from_outbox(), + message_type=OefSearchMessage, + performative=OefSearchMessage.Performative.UNREGISTER_SERVICE, + to=self.skill.skill_context.search_service_address, + sender=str(self.skill.skill_context.skill_id), + service_description=self.mocked_registration_description, + ) + assert has_attributes, error_str + mock_logger.assert_any_call(logging.INFO, "unregistering service from SOEF.") + + # _unregister_agent + has_attributes, error_str = self.message_has_attributes( + actual_message=self.get_message_from_outbox(), + message_type=OefSearchMessage, + performative=OefSearchMessage.Performative.UNREGISTER_SERVICE, + to=self.skill.skill_context.search_service_address, + sender=str(self.skill.skill_context.skill_id), + service_description=self.mocked_registration_description, + ) + assert has_attributes, error_str + mock_logger.assert_any_call(logging.INFO, "unregistering agent from SOEF.") diff --git a/tests/test_packages/test_skills/test_aries_alice/test_dialogues.py b/tests/test_packages/test_skills/test_aries_alice/test_dialogues.py new file mode 100644 index 0000000000..b554a7c4c6 --- /dev/null +++ b/tests/test_packages/test_skills/test_aries_alice/test_dialogues.py @@ -0,0 +1,72 @@ +# -*- coding: utf-8 -*- +# ------------------------------------------------------------------------------ +# +# Copyright 2018-2019 Fetch.AI Limited +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ------------------------------------------------------------------------------ +"""This module contains the tests of the dialogue classes of the aries_alice skill.""" + +from aea.test_tools.test_skill import COUNTERPARTY_AGENT_ADDRESS + +from packages.fetchai.protocols.default.message import DefaultMessage +from packages.fetchai.protocols.http.message import HttpMessage +from packages.fetchai.protocols.oef_search.message import OefSearchMessage +from packages.fetchai.skills.aries_alice.dialogues import ( + DefaultDialogue, + HttpDialogue, + OefSearchDialogue, +) + +from tests.test_packages.test_skills.test_aries_alice.intermediate_class import ( + AriesAliceTestCase, +) + + +class TestDialogues(AriesAliceTestCase): + """Test dialogue classes of aries_alice.""" + + def test_default_dialogues(self): + """Test the DefaultDialogues class.""" + _, dialogue = self.default_dialogues.create( + counterparty=COUNTERPARTY_AGENT_ADDRESS, + performative=DefaultMessage.Performative.BYTES, + content=b"some_content", + ) + assert dialogue.role == DefaultDialogue.Role.AGENT + assert dialogue.self_address == self.skill.skill_context.agent_address + + def test_http_dialogues(self): + """Test the HttpDialogues class.""" + _, dialogue = self.http_dialogues.create( + counterparty=COUNTERPARTY_AGENT_ADDRESS, + performative=HttpMessage.Performative.REQUEST, + method=self.mocked_method, + url=self.mocked_url, + version=self.mocked_version, + headers=self.mocked_headers, + body=self.mocked_body_bytes, + ) + assert dialogue.role == HttpDialogue.Role.CLIENT + assert dialogue.self_address == str(self.skill.skill_context.skill_id) + + def test_oef_search_dialogues(self): + """Test the OefSearchDialogues class.""" + _, dialogue = self.oef_search_dialogues.create( + counterparty=COUNTERPARTY_AGENT_ADDRESS, + performative=OefSearchMessage.Performative.SEARCH_SERVICES, + query=self.mocked_query, + ) + assert dialogue.role == OefSearchDialogue.Role.AGENT + assert dialogue.self_address == str(self.skill.skill_context.skill_id) diff --git a/tests/test_packages/test_skills/test_aries_alice/test_handlers.py b/tests/test_packages/test_skills/test_aries_alice/test_handlers.py new file mode 100644 index 0000000000..7e97f5ba5f --- /dev/null +++ b/tests/test_packages/test_skills/test_aries_alice/test_handlers.py @@ -0,0 +1,494 @@ +# -*- coding: utf-8 -*- +# ------------------------------------------------------------------------------ +# +# Copyright 2018-2019 Fetch.AI Limited +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ------------------------------------------------------------------------------ +"""This module contains the tests of the handler classes of the aries_alice skill.""" +import json +import logging +from typing import cast +from unittest.mock import patch + +from aea.protocols.dialogue.base import Dialogues + +from packages.fetchai.protocols.default.message import DefaultMessage +from packages.fetchai.protocols.http.message import HttpMessage +from packages.fetchai.protocols.oef_search.message import OefSearchMessage +from packages.fetchai.skills.aries_alice.dialogues import ( + HttpDialogue, + OefSearchDialogue, +) +from packages.fetchai.skills.aries_alice.handlers import ADMIN_COMMAND_RECEIVE_INVITE + +from tests.test_packages.test_skills.test_aries_alice.intermediate_class import ( + AriesAliceTestCase, +) + + +class TestDefaultHandler(AriesAliceTestCase): + """Test default handler of aries_alice.""" + + def test_setup(self): + """Test the setup method of the default handler.""" + assert self.default_handler.setup() is None + self.assert_quantity_in_outbox(0) + + def test_handle_i(self): + """Test the handle method of the default handler where @type is in content.""" + # setup + content = "@type=something" + content_bytes = json.dumps(content).encode("utf-8") + details = "some_details" + incoming_message = cast( + DefaultMessage, + self.build_incoming_message( + message_type=DefaultMessage, + performative=DefaultMessage.Performative.BYTES, + dialogue_reference=Dialogues.new_self_initiated_dialogue_reference(), + content=content_bytes, + ), + ) + + # operation + with patch.object( + self.default_handler, "_handle_received_invite", return_value=details + ) as mock_invite: + with patch.object( + self.alice_behaviour, "send_http_request_message" + ) as mock_send: + with patch.object(self.logger, "log") as mock_logger: + self.default_handler.handle(incoming_message) + + # after + mock_logger.assert_any_call( + logging.INFO, f"Received message content:{content}", + ) + mock_invite.assert_called_once() + mock_send.assert_any_call( + method="POST", + url=self.strategy.admin_url + ADMIN_COMMAND_RECEIVE_INVITE, + content=details, + ) + + def test_handle_ii(self): + """Test the handle method of the default handler where http_dialogue is None.""" + # setup + details = "some_details" + incoming_message = cast( + DefaultMessage, + self.build_incoming_message( + message_type=DefaultMessage, + performative=DefaultMessage.Performative.ERROR, + dialogue_reference=("", ""), + error_code=DefaultMessage.ErrorCode.INVALID_DIALOGUE, + error_msg="some_error_msg", + error_data={"some_key": b"some_bytes"}, + ), + ) + + # operation + with patch.object( + self.default_handler, "_handle_received_invite", return_value=details + ) as mock_invite: + with patch.object( + self.alice_behaviour, "send_http_request_message" + ) as mock_send: + with patch.object(self.logger, "log") as mock_logger: + self.default_handler.handle(incoming_message) + + # after + mock_logger.assert_any_call( + logging.ERROR, + "alice -> default_handler -> handle(): something went wrong when adding the incoming default message to the dialogue.", + ) + mock_invite.assert_not_called() + mock_send.assert_not_called() + + def test_teardown(self): + """Test the teardown method of the default handler.""" + assert self.default_handler.teardown() is None + self.assert_quantity_in_outbox(0) + + +class TestHttpHandler(AriesAliceTestCase): + """Test http handler of aries_alice.""" + + is_agent_to_agent_messages = False + + def test_setup(self): + """Test the setup method of the http_handler handler.""" + assert self.http_handler.setup() is None + self.assert_quantity_in_outbox(0) + + def test_handle_unidentified_dialogue(self): + """Test the handle method of the http handler where incoming message is invalid.""" + # setup + incorrect_dialogue_reference = ("", "") + incoming_message = cast( + HttpMessage, + self.build_incoming_message( + message_type=HttpMessage, + dialogue_reference=incorrect_dialogue_reference, + performative=HttpMessage.Performative.REQUEST, + method=self.mocked_method, + url=self.mocked_url, + headers=self.mocked_headers, + version=self.mocked_version, + body=self.mocked_body_bytes, + ), + ) + + # operation + with patch.object(self.logger, "log") as mock_logger: + self.http_handler.handle(incoming_message) + + # after + mock_logger.assert_any_call( + logging.ERROR, + "alice -> http_handler -> handle() -> REQUEST: something went wrong when adding the incoming HTTP webhook request message to the dialogue.", + ) + + def test_handle_request(self): + """Test the handle method of the http handler where performative is REQUEST.""" + # setup + self.http_handler.connection_id = 123 + self.http_handler.is_connected_to_Faber = False + + body = {"connection_id": 123, "state": "active"} + mocked_body_bytes = json.dumps(body).encode("utf-8") + incoming_message = cast( + HttpMessage, + self.build_incoming_message( + message_type=HttpMessage, + performative=HttpMessage.Performative.REQUEST, + method=self.mocked_method, + url=self.mocked_url, + headers=self.mocked_headers, + version=self.mocked_version, + body=mocked_body_bytes, + ), + ) + + # operation + with patch.object(self.logger, "log") as mock_logger: + self.http_handler.handle(incoming_message) + + # after + mock_logger.assert_any_call(logging.INFO, "Connected to Faber") + assert self.http_handler.is_connected_to_Faber is True + + def test_handle_response_i(self): + """Test the handle method of the http handler where performative is RESPONSE and content has Error.""" + # setup + http_dialogue = cast( + HttpDialogue, + self.prepare_skill_dialogue( + dialogues=self.http_dialogues, messages=self.list_of_http_messages[:1], + ), + ) + + body = {"Error": "something"} + mocked_body_bytes = json.dumps(body).encode("utf-8") + incoming_message = cast( + HttpMessage, + self.build_incoming_message_for_skill_dialogue( + dialogue=http_dialogue, + performative=HttpMessage.Performative.RESPONSE, + status_code=200, + status_text="some_status_code", + headers=self.mocked_headers, + version=self.mocked_version, + body=mocked_body_bytes, + ), + ) + + # operation + with patch.object(self.logger, "log") as mock_logger: + self.http_handler.handle(incoming_message) + + # after + mock_logger.assert_any_call( + logging.ERROR, + "Something went wrong after I sent the administrative command of 'invitation receive'", + ) + + def test_handle_response_ii(self): + """Test the handle method of the http handler where performative is RESPONSE and content does NOT have Error.""" + # setup + connection_id = 2342 + invitation = {"some_key": "some_value"} + http_dialogue = cast( + HttpDialogue, + self.prepare_skill_dialogue( + dialogues=self.http_dialogues, messages=self.list_of_http_messages[:1], + ), + ) + + body = {"connection_id": connection_id, "invitation": invitation} + mocked_body_bytes = json.dumps(body).encode("utf-8") + incoming_message = cast( + HttpMessage, + self.build_incoming_message_for_skill_dialogue( + dialogue=http_dialogue, + performative=HttpMessage.Performative.RESPONSE, + status_code=200, + status_text="some_status_code", + headers=self.mocked_headers, + version=self.mocked_version, + body=mocked_body_bytes, + ), + ) + + # operation + with patch.object(self.logger, "log") as mock_logger: + self.http_handler.handle(incoming_message) + + # after + mock_logger.assert_any_call( + logging.INFO, f"Received http response message content:{body}" + ) + assert self.http_handler.connection_id == connection_id + mock_logger.assert_any_call(logging.INFO, f"invitation response: {str(body)}") + mock_logger.assert_any_call(logging.INFO, f"connection id: {connection_id}") + mock_logger.assert_any_call(logging.INFO, f"invitation: {str(invitation)}") + + def test_teardown(self): + """Test the teardown method of the http handler.""" + assert self.http_handler.teardown() is None + self.assert_quantity_in_outbox(0) + + +class TestOefSearchHandler(AriesAliceTestCase): + """Test oef_search handler of aries_alice.""" + + is_agent_to_agent_messages = False + + def test_setup(self): + """Test the setup method of the oef_search handler.""" + assert self.oef_search_handler.setup() is None + self.assert_quantity_in_outbox(0) + + def test_handle_unidentified_dialogue(self): + """Test the _handle_unidentified_dialogue method of the oef_search handler.""" + # setup + incorrect_dialogue_reference = ("", "") + incoming_message = cast( + OefSearchMessage, + self.build_incoming_message( + message_type=OefSearchMessage, + dialogue_reference=incorrect_dialogue_reference, + performative=OefSearchMessage.Performative.OEF_ERROR, + oef_error_operation=OefSearchMessage.OefErrorOperation.REGISTER_SERVICE, + ), + ) + + # operation + with patch.object(self.logger, "log") as mock_logger: + self.oef_search_handler.handle(incoming_message) + + # after + mock_logger.assert_any_call( + logging.INFO, + f"received invalid oef_search message={incoming_message}, unidentified dialogue.", + ) + + def test_handle_error(self): + """Test the _handle_error method of the oef_search handler.""" + # setup + oef_search_dialogue = cast( + OefSearchDialogue, + self.prepare_skill_dialogue( + dialogues=self.oef_search_dialogues, + messages=self.list_of_messages_register_location[:1], + ), + ) + incoming_message = cast( + OefSearchMessage, + self.build_incoming_message_for_skill_dialogue( + dialogue=oef_search_dialogue, + performative=OefSearchMessage.Performative.OEF_ERROR, + oef_error_operation=OefSearchMessage.OefErrorOperation.REGISTER_SERVICE, + ), + ) + + # operation + with patch.object(self.logger, "log") as mock_logger: + self.oef_search_handler.handle(incoming_message) + + # after + mock_logger.assert_any_call( + logging.INFO, + f"received oef_search error message={incoming_message} in dialogue={oef_search_dialogue}.", + ) + + def test_handle_success_i(self): + """Test the _handle_success method of the oef_search handler where the oef success targets register_service WITH location_agent data model description.""" + # setup + oef_dialogue = self.prepare_skill_dialogue( + dialogues=self.oef_search_dialogues, + messages=self.list_of_messages_register_location[:1], + ) + incoming_message = self.build_incoming_message_for_skill_dialogue( + dialogue=oef_dialogue, + performative=OefSearchMessage.Performative.SUCCESS, + agents_info=OefSearchMessage.AgentsInfo({"address": {"key": "value"}}), + ) + + # operation + with patch.object(self.oef_search_handler.context.logger, "log") as mock_logger: + with patch.object(self.alice_behaviour, "register_service",) as mock_reg: + self.oef_search_handler.handle(incoming_message) + + # after + mock_logger.assert_any_call( + logging.INFO, + f"received oef_search success message={incoming_message} in dialogue={oef_dialogue}.", + ) + mock_reg.assert_called_once() + + def test_handle_success_ii(self): + """Test the _handle_success method of the oef_search handler where the oef success targets register_service WITH set_service_key data model description.""" + # setup + oef_dialogue = self.prepare_skill_dialogue( + dialogues=self.oef_search_dialogues, + messages=self.list_of_messages_register_service[:1], + ) + incoming_message = self.build_incoming_message_for_skill_dialogue( + dialogue=oef_dialogue, + performative=OefSearchMessage.Performative.SUCCESS, + agents_info=OefSearchMessage.AgentsInfo({"address": {"key": "value"}}), + ) + + # operation + with patch.object(self.oef_search_handler.context.logger, "log") as mock_logger: + with patch.object(self.alice_behaviour, "register_genus",) as mock_reg: + self.oef_search_handler.handle(incoming_message) + + # after + mock_logger.assert_any_call( + logging.INFO, + f"received oef_search success message={incoming_message} in dialogue={oef_dialogue}.", + ) + mock_reg.assert_called_once() + + def test_handle_success_iii(self): + """Test the _handle_success method of the oef_search handler where the oef success targets register_service WITH personality_agent data model and genus value description.""" + # setup + oef_dialogue = self.prepare_skill_dialogue( + dialogues=self.oef_search_dialogues, + messages=self.list_of_messages_register_genus[:1], + ) + incoming_message = self.build_incoming_message_for_skill_dialogue( + dialogue=oef_dialogue, + performative=OefSearchMessage.Performative.SUCCESS, + agents_info=OefSearchMessage.AgentsInfo({"address": {"key": "value"}}), + ) + + # operation + with patch.object(self.oef_search_handler.context.logger, "log") as mock_logger: + with patch.object( + self.alice_behaviour, "register_classification", + ) as mock_reg: + self.oef_search_handler.handle(incoming_message) + + # after + mock_logger.assert_any_call( + logging.INFO, + f"received oef_search success message={incoming_message} in dialogue={oef_dialogue}.", + ) + mock_reg.assert_called_once() + + def test_handle_success_iv(self): + """Test the _handle_success method of the oef_search handler where the oef success targets register_service WITH personality_agent data model and classification value description.""" + # setup + oef_dialogue = self.prepare_skill_dialogue( + dialogues=self.oef_search_dialogues, + messages=self.list_of_messages_register_classification[:1], + ) + incoming_message = self.build_incoming_message_for_skill_dialogue( + dialogue=oef_dialogue, + performative=OefSearchMessage.Performative.SUCCESS, + agents_info=OefSearchMessage.AgentsInfo({"address": {"key": "value"}}), + ) + + # operation + with patch.object(self.oef_search_handler.context.logger, "log") as mock_logger: + self.oef_search_handler.handle(incoming_message) + + # after + mock_logger.assert_any_call( + logging.INFO, + f"received oef_search success message={incoming_message} in dialogue={oef_dialogue}.", + ) + + mock_logger.assert_any_call( + logging.INFO, + "the agent, with its genus and classification, and its service are successfully registered on the SOEF.", + ) + + def test_handle_success_v(self): + """Test the _handle_success method of the oef_search handler where the oef success targets unregister_service.""" + # setup + oef_dialogue = self.prepare_skill_dialogue( + dialogues=self.oef_search_dialogues, + messages=self.list_of_messages_register_invalid[:1], + ) + incoming_message = self.build_incoming_message_for_skill_dialogue( + dialogue=oef_dialogue, + performative=OefSearchMessage.Performative.SUCCESS, + agents_info=OefSearchMessage.AgentsInfo({"address": {"key": "value"}}), + ) + + # operation + with patch.object(self.oef_search_handler.context.logger, "log") as mock_logger: + self.oef_search_handler.handle(incoming_message) + + # after + mock_logger.assert_any_call( + logging.INFO, + f"received oef_search success message={incoming_message} in dialogue={oef_dialogue}.", + ) + mock_logger.assert_any_call( + logging.WARNING, + f"received soef SUCCESS message as a reply to the following unexpected message: {oef_dialogue.get_message_by_id(incoming_message.target)}", + ) + + def test_handle_invalid(self): + """Test the _handle_invalid method of the oef_search handler.""" + # setup + incoming_message = cast( + OefSearchMessage, + self.build_incoming_message( + message_type=OefSearchMessage, + performative=OefSearchMessage.Performative.REGISTER_SERVICE, + service_description=self.mocked_proposal, + ), + ) + + # operation + with patch.object(self.logger, "log") as mock_logger: + self.oef_search_handler.handle(incoming_message) + + # after + mock_logger.assert_any_call( + logging.WARNING, + f"cannot handle oef_search message of performative={incoming_message.performative} in dialogue={self.oef_search_dialogues.get_dialogue(incoming_message)}.", + ) + + def test_teardown(self): + """Test the teardown method of the oef_search handler.""" + assert self.oef_search_handler.teardown() is None + self.assert_quantity_in_outbox(0) diff --git a/tests/test_packages/test_skills/test_aries_alice/test_strategy.py b/tests/test_packages/test_skills/test_aries_alice/test_strategy.py new file mode 100644 index 0000000000..70c04f2792 --- /dev/null +++ b/tests/test_packages/test_skills/test_aries_alice/test_strategy.py @@ -0,0 +1,86 @@ +# -*- coding: utf-8 -*- +# ------------------------------------------------------------------------------ +# +# Copyright 2018-2019 Fetch.AI Limited +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ------------------------------------------------------------------------------ +"""This module contains the tests of the strategy class of the aries_alice skill.""" + +from aea.helpers.search.generic import ( + AGENT_LOCATION_MODEL, + AGENT_PERSONALITY_MODEL, + AGENT_REMOVE_SERVICE_MODEL, + AGENT_SET_SERVICE_MODEL, +) +from aea.helpers.search.models import Description, Location + +from tests.test_packages.test_skills.test_aries_alice.intermediate_class import ( + AriesAliceTestCase, +) + + +class TestStrategy(AriesAliceTestCase): + """Test Strategy of aries_alice.""" + + def test_properties(self): + """Test the properties of Strategy class.""" + assert self.strategy.admin_host == self.admin_host + assert self.strategy.admin_port == self.admin_port + assert self.strategy.admin_url == f"http://{self.admin_host}:{self.admin_port}" + + def test_get_location_description(self): + """Test the get_location_description method of the Strategy class.""" + description = self.strategy.get_location_description() + + assert type(description) == Description + assert description.data_model is AGENT_LOCATION_MODEL + assert description.values.get("location", "") == Location( + latitude=self.location["latitude"], longitude=self.location["longitude"] + ) + + def test_get_register_service_description(self): + """Test the get_register_service_description method of the Strategy class.""" + description = self.strategy.get_register_service_description() + + assert type(description) == Description + assert description.data_model is AGENT_SET_SERVICE_MODEL + assert description.values.get("key", "") == self.service_data["key"] + assert description.values.get("value", "") == self.service_data["value"] + + def test_get_register_personality_description(self): + """Test the get_register_personality_description method of the Strategy class.""" + description = self.strategy.get_register_personality_description() + + assert type(description) == Description + assert description.data_model is AGENT_PERSONALITY_MODEL + assert description.values.get("piece", "") == self.personality_data["piece"] + assert description.values.get("value", "") == self.personality_data["value"] + + def test_get_register_classification_description(self): + """Test the get_register_classification_description method of the Strategy class.""" + description = self.strategy.get_register_classification_description() + + assert type(description) == Description + assert description.data_model is AGENT_PERSONALITY_MODEL + assert description.values.get("piece", "") == self.classification["piece"] + assert description.values.get("value", "") == self.classification["value"] + + def test_get_unregister_service_description(self): + """Test the get_unregister_service_description method of the Strategy class.""" + description = self.strategy.get_unregister_service_description() + + assert type(description) == Description + assert description.data_model is AGENT_REMOVE_SERVICE_MODEL + assert description.values.get("key", "") == self.service_data["key"] From cfddaca5d82873ab0b7d5625aabe64cba3dbed82 Mon Sep 17 00:00:00 2001 From: ali Date: Wed, 26 May 2021 09:49:15 +0100 Subject: [PATCH 110/147] tests: coverage gap --- packages/fetchai/skills/aries_alice/handlers.py | 4 +++- packages/fetchai/skills/aries_alice/skill.yaml | 2 +- packages/hashes.csv | 2 +- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/packages/fetchai/skills/aries_alice/handlers.py b/packages/fetchai/skills/aries_alice/handlers.py index ed01cfb08d..524f992928 100644 --- a/packages/fetchai/skills/aries_alice/handlers.py +++ b/packages/fetchai/skills/aries_alice/handlers.py @@ -52,7 +52,9 @@ class DefaultHandler(Handler): SUPPORTED_PROTOCOL = DefaultMessage.protocol_id # type: Optional[PublicId] - def _handle_received_invite(self, invite_detail: Dict[str, str]) -> Optional[str]: + def _handle_received_invite( + self, invite_detail: Dict[str, str] + ) -> Optional[str]: # pragma: no cover """ Prepare an invitation detail received from Faber_AEA to be send to the Alice ACA. diff --git a/packages/fetchai/skills/aries_alice/skill.yaml b/packages/fetchai/skills/aries_alice/skill.yaml index 026ad1956f..65b5b09f54 100644 --- a/packages/fetchai/skills/aries_alice/skill.yaml +++ b/packages/fetchai/skills/aries_alice/skill.yaml @@ -11,7 +11,7 @@ fingerprint: __init__.py: QmTiLfLMvHMUzrgQ7tMtYewLrz5DDihRV2YoLkq6ycCBby behaviours.py: QmZUD3AYp9Tw51zFC5tyS49yunAZvXD6qtWX8buu7jou11 dialogues.py: QmPPyGaizHVdJdLzuD4xebhSx6jehNc3oQ9UnZBu6xqu3b - handlers.py: QmZSeg7xCYXWwAc7yDYQg5pPpfGdQWJNFB2jfGjWeH1HPM + handlers.py: QmZMH8RvMCXHXYKz4fS6AhnfiuUHNDXtfnEqhxwgwECnW5 strategy.py: Qmaa1usbr2duj3yMm2gF5AbuCpBSyFfkRVHVceeQT8XVGE fingerprint_ignore_patterns: [] connections: diff --git a/packages/hashes.csv b/packages/hashes.csv index e9043d27ec..71e8200cc8 100644 --- a/packages/hashes.csv +++ b/packages/hashes.csv @@ -74,7 +74,7 @@ fetchai/protocols/state_update,QmcLkEEmAxaynioNobJvaMdWUJuG8AG2BzxCYwX6kJhhA5 fetchai/protocols/tac,QmZGEpFDwtUU7ykRmwr3Scjmw69CixH7ZFMNptYWSZ5eBe fetchai/protocols/yoti,QmRcYAyxk2JjhTKHqVYnYiZ8zzck6qzsRuqRnBxPREK5Fe fetchai/skills/advanced_data_request,Qmdcmy14MMTUvjB1nHXfH26eZbYiJm3GGKhX84opMqhJ3b -fetchai/skills/aries_alice,QmdyTucTSKyjLSEJfa43gP5sXg5uScEbzX6PtaN11EFfEs +fetchai/skills/aries_alice,Qmd5kKqMsHsP97vrgrVm6nexqvznaU7JCFPRrEgaCnoJek fetchai/skills/aries_faber,QmPpRGezMuMNzZmZkRLeDN3gMbCoGz9mKJ1M2J6iFxJyfX fetchai/skills/carpark_client,QmSSpTz49mF439YiVcxA3ddPaU9du3ptzQZ25HZtkL4Rys fetchai/skills/carpark_detection,QmUoVRaJqbo47tNMzmbsJ18gm1WNYGwCBZTPYvFuvG4is2 From a29118fda72b573ce8c828118222dd7dda2a1fce Mon Sep 17 00:00:00 2001 From: ali Date: Wed, 26 May 2021 12:20:08 +0100 Subject: [PATCH 111/147] cleanup: address some PR comments --- tests/common/docker_image.py | 6 ++++-- tests/conftest.py | 4 ++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/tests/common/docker_image.py b/tests/common/docker_image.py index 09c0f9dc1c..a329b3c083 100644 --- a/tests/common/docker_image.py +++ b/tests/common/docker_image.py @@ -340,8 +340,10 @@ def wait(self, max_attempts: int = 15, sleep_rate: float = 1.0) -> bool: class SOEFDockerImage(DockerImage): """Wrapper to SOEF Docker image.""" + PORT = 9002 + def __init__( - self, client: DockerClient, addr: str, port: int = 9002, + self, client: DockerClient, addr: str, port: int = PORT, ): """ Initialize the SOEF Docker image. @@ -357,7 +359,7 @@ def __init__( @property def tag(self) -> str: """Get the image tag.""" - return "fetchai/soef:latest" + return "gcr.io/fetch-ai-images/soef:9e78611" def _make_ports(self) -> Dict: """Make ports dictionary for Docker.""" diff --git a/tests/conftest.py b/tests/conftest.py index ecd8d017f1..5736b0f665 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -732,14 +732,14 @@ def ganache( @pytest.mark.integration -@pytest.fixture(scope="function") +@pytest.fixture(scope="class") def soef( soef_addr: str = "http://127.0.0.1", soef_port: int = 9002, timeout: float = 2.0, max_attempts: int = 10, ): - """Launch the Ganache image.""" + """Launch the soef image.""" client = docker.from_env() image = SOEFDockerImage(client, soef_addr, soef_port) yield from _launch_image(image, timeout=timeout, max_attempts=max_attempts) From 880d0d8280ee82581bce0d3824338cfc9f53c4b6 Mon Sep 17 00:00:00 2001 From: ali Date: Wed, 26 May 2021 16:30:19 +0100 Subject: [PATCH 112/147] fix: ml bug --- .../fetchai/skills/ml_train/behaviours.py | 3 +-- packages/fetchai/skills/ml_train/handlers.py | 25 ++++++------------- 2 files changed, 8 insertions(+), 20 deletions(-) diff --git a/packages/fetchai/skills/ml_train/behaviours.py b/packages/fetchai/skills/ml_train/behaviours.py index 09674c08e9..8232819fc4 100644 --- a/packages/fetchai/skills/ml_train/behaviours.py +++ b/packages/fetchai/skills/ml_train/behaviours.py @@ -64,8 +64,7 @@ def act(self) -> None: if not result.successful(): return - ml_task = result.get() - strategy.weights = ml_task.result + strategy.weights = result.get() strategy.current_task_id = None if len(strategy.data) > 0: diff --git a/packages/fetchai/skills/ml_train/handlers.py b/packages/fetchai/skills/ml_train/handlers.py index ac77100ac0..fe60478048 100644 --- a/packages/fetchai/skills/ml_train/handlers.py +++ b/packages/fetchai/skills/ml_train/handlers.py @@ -129,16 +129,16 @@ def _handle_terms( :param ml_trade_dialogue: the dialogue object :return: None """ - terms = ml_trade_msg.terms + proposal_terms = ml_trade_msg.terms self.context.logger.info( "received terms message from {}: terms={}".format( - ml_trade_msg.sender[-5:], terms.values + ml_trade_msg.sender[-5:], proposal_terms.values ) ) strategy = cast(Strategy, self.context.strategy) - acceptable = strategy.is_acceptable_terms(terms) - affordable = strategy.is_affordable_terms(terms) + acceptable = strategy.is_acceptable_terms(proposal_terms) + affordable = strategy.is_affordable_terms(proposal_terms) if not (acceptable and affordable): self.context.logger.info( "rejecting, terms are not acceptable and/or affordable" @@ -146,19 +146,8 @@ def _handle_terms( return if strategy.is_ledger_tx: - # construct a tx for settlement on the ledger - ledger_api_dialogues = cast( - LedgerApiDialogues, self.context.ledger_api_dialogues - ) - terms_ = strategy.terms_from_proposal(ml_trade_msg.terms) - ml_trade_dialogue.terms = terms_ - _, ledger_api_dialogue = ledger_api_dialogues.create( - counterparty=LEDGER_API_ADDRESS, - performative=LedgerApiMessage.Performative.GET_RAW_TRANSACTION, - terms=terms_, - ) - ledger_api_dialogue = cast(LedgerApiDialogue, ledger_api_dialogue) - ledger_api_dialogue.associated_ml_trade_dialogue = ml_trade_dialogue + terms = strategy.terms_from_proposal(proposal_terms) + ml_trade_dialogue.terms = terms tx_behaviour = cast( TransactionBehaviour, self.context.behaviours.transaction ) @@ -169,7 +158,7 @@ def _handle_terms( performative=MlTradeMessage.Performative.ACCEPT, target_message=ml_trade_msg, tx_digest=DUMMY_DIGEST, - terms=terms, + terms=proposal_terms, ) self.context.outbox.put_message(message=ml_accept) self.context.logger.info("sending dummy transaction digest ...") From 39dec4bff987506f083aedd299d2a743e0f2fe9e Mon Sep 17 00:00:00 2001 From: ali Date: Wed, 26 May 2021 16:39:31 +0100 Subject: [PATCH 113/147] cleanup: hashes --- packages/fetchai/skills/ml_train/skill.yaml | 4 ++-- packages/hashes.csv | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/fetchai/skills/ml_train/skill.yaml b/packages/fetchai/skills/ml_train/skill.yaml index b03ba31cc4..ade16121e1 100644 --- a/packages/fetchai/skills/ml_train/skill.yaml +++ b/packages/fetchai/skills/ml_train/skill.yaml @@ -9,9 +9,9 @@ aea_version: '>=1.0.0, <2.0.0' fingerprint: README.md: QmUk1XEpYnxte5GKZjSvJY2LJPpVzzP6zLvnxT2Y1g9f4m __init__.py: QmbHXvYhja888nF1egE4jaTHATDp6HqbpsHuJNtPPkvATb - behaviours.py: QmQUJo7J7KuBU9gSsm13AQt1QcEUjS3nqhpEKGr95PX9Db + behaviours.py: QmZUQhKCzAM7ZR9zZeRb75wKKqVs1qS7NegKyZdLAbP8Yp dialogues.py: QmUpSz6RH7QvRjDx2JvyHeZuta2xeU27ddoeJApWRxHmbw - handlers.py: QmWdoURnNHax4dYy7LPHuwwZRvKReqtgTJ7Gk1zeyASKrR + handlers.py: Qmc9U5Rhq2oaEyfb7mEqUifCHtaqcWmDgTE39QCsFqYpC1 strategy.py: QmYBdahEjUas3ohvTTgR2XUAM4AZQ9KddME9NAALavUJ3Q tasks.py: QmUGPgSX3HbhoPsYY8XSrLMrq5BQo1nk4o7oZBa4uG3ZPB fingerprint_ignore_patterns: [] diff --git a/packages/hashes.csv b/packages/hashes.csv index b93a30df5a..004a0da98b 100644 --- a/packages/hashes.csv +++ b/packages/hashes.csv @@ -92,7 +92,7 @@ fetchai/skills/generic_seller,QmUgaZtETcg9G7NWLs1nkwtiaqnmH1wsmvSWkMjNeGjPX4 fetchai/skills/gym,Qma8bQQzEG8vibYfBhDAoGTu4Tu8GKKQcmiLQdoaSHtyV9 fetchai/skills/http_echo,QmUtHKVRkDh12UiuJoh2WMQ4FSBgWVEmT2AuXoTDpg76Cb fetchai/skills/ml_data_provider,Qmbz5a4YmZu9SWnnHuh7coTsNwaTBHr1gLhsEK9Nrc667Y -fetchai/skills/ml_train,QmSrv5JRSXttWGzrYgjbCytQfW9UH3HJ8B5nWXNoso6Ykv +fetchai/skills/ml_train,QmUCs5XVpisQpMC8j5Y46qFhp5ux2yD6DpehZqWiox8Arr fetchai/skills/registration_aw1,QmRkDTaeWzSSDSMQ6EGvKntU6fmfbfuC18jJumFdFfDPZK fetchai/skills/scaffold,QmewidPpxgAAWn7hPtv6TkTotHCdG29VP7ECojzbz3H73Q fetchai/skills/simple_aggregation,QmSDvt7i6bjmfJYJD6rD32KrJYJxFsqorG7pfiY5ue9R5q From d44a47c82083409e31bfb6edc5693c7e02d6a658 Mon Sep 17 00:00:00 2001 From: ali Date: Wed, 26 May 2021 16:53:41 +0100 Subject: [PATCH 114/147] tests: fix ml test --- .../test_skills/test_ml_train/test_behaviours.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/tests/test_packages/test_skills/test_ml_train/test_behaviours.py b/tests/test_packages/test_skills/test_ml_train/test_behaviours.py index cf1e230e42..3a8ba08290 100644 --- a/tests/test_packages/test_skills/test_ml_train/test_behaviours.py +++ b/tests/test_packages/test_skills/test_ml_train/test_behaviours.py @@ -29,7 +29,7 @@ from aea.helpers.search.models import Description from aea.protocols.dialogue.base import DialogueMessage -from aea.skills.tasks import Task, TaskManager +from aea.skills.tasks import TaskManager from aea.test_tools.test_skill import BaseSkillTestCase from packages.fetchai.protocols.ledger_api.message import LedgerApiMessage @@ -126,14 +126,12 @@ def test_act_task_ready_and_successful(self): """Test the act method of the search behaviour where task is ready and successful.""" # setup self.strategy._current_task_id = 1 - - mock_task = Mock(wraps=Task) - mock_task.result = "some_weights" + mocked_weights = "some_weights" mock_task_result = Mock(wraps=ApplyResult) mock_task_result.ready.return_value = True mock_task_result.successful.return_value = True - mock_task_result.get.return_value = mock_task + mock_task_result.get.return_value = mocked_weights # operation with patch.object( @@ -144,7 +142,7 @@ def test_act_task_ready_and_successful(self): # after assert self.strategy._current_task_id is None - assert self.strategy._weights == "some_weights" + assert self.strategy._weights == mocked_weights mock_generic_act.assert_called_once() def test_act_data_exists(self): From 010b31753f5f3cfa413d794e57979cd77e96283e Mon Sep 17 00:00:00 2001 From: ali Date: Wed, 26 May 2021 17:00:06 +0100 Subject: [PATCH 115/147] docs: updating ml skill demo docs --- docs/ml-skills.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/docs/ml-skills.md b/docs/ml-skills.md index 04a8bf2d5e..2259670bcb 100644 --- a/docs/ml-skills.md +++ b/docs/ml-skills.md @@ -84,7 +84,8 @@ The following steps assume you have launched the AEA Manager Desktop app. 6. Run the `ml_model_trainer`. -In the AEA's logs, you should see the agent trading successfully. +In the AEA's logs, you should see the agents trading successfully, and the training agent training its machine learning model using the data purchased. +The trainer keeps purchasing data and training its model until stopped.
## Option 2: CLI approach @@ -271,7 +272,8 @@ Then run the model trainer AEA: aea run ``` -You can see that the AEAs find each other, negotiate and eventually trade. +You can see that the AEAs find each other, negotiate and eventually trade. After the trade, the model trainer AEA trains its ML model using the data it has purchased. +This AEA keeps purchasing data and training its model until stopped. #### Cleaning up From b83a661885da441a9dfafba899e50cb0c3e17176 Mon Sep 17 00:00:00 2001 From: ali Date: Thu, 27 May 2021 11:39:33 +0100 Subject: [PATCH 116/147] tests" Aries faber --- .coveragerc | 1 - .../fetchai/skills/aries_faber/behaviours.py | 7 +- .../fetchai/skills/aries_faber/dialogues.py | 2 +- .../fetchai/skills/aries_faber/handlers.py | 72 +-- .../fetchai/skills/aries_faber/skill.yaml | 16 +- .../fetchai/skills/aries_faber/strategy.py | 4 +- packages/hashes.csv | 2 +- .../test_aries_alice/test_behaviours.py | 16 +- .../test_skills/test_aries_faber/__init__.py | 20 + .../test_aries_faber/intermediate_class.py | 166 ++++++ .../test_aries_faber/test_behaviours.py | 104 ++++ .../test_aries_faber/test_dialogues.py | 72 +++ .../test_aries_faber/test_handlers.py | 542 ++++++++++++++++++ .../test_aries_faber/test_strategy.py | 71 +++ 14 files changed, 1028 insertions(+), 67 deletions(-) create mode 100644 tests/test_packages/test_skills/test_aries_faber/__init__.py create mode 100644 tests/test_packages/test_skills/test_aries_faber/intermediate_class.py create mode 100644 tests/test_packages/test_skills/test_aries_faber/test_behaviours.py create mode 100644 tests/test_packages/test_skills/test_aries_faber/test_dialogues.py create mode 100644 tests/test_packages/test_skills/test_aries_faber/test_handlers.py create mode 100644 tests/test_packages/test_skills/test_aries_faber/test_strategy.py diff --git a/.coveragerc b/.coveragerc index e2e5c9c798..efa6236ac9 100644 --- a/.coveragerc +++ b/.coveragerc @@ -2,7 +2,6 @@ omit = */.tox/* packages/fetchai/contracts/* - packages/fetchai/skills/aries_faber/* packages/fetchai/skills/gym/* packages/fetchai/skills/fipa_dummy_buyer/* plugins/aea-ledger-cosmos/aea_ledger_cosmos/cosmos.py \ No newline at end of file diff --git a/packages/fetchai/skills/aries_faber/behaviours.py b/packages/fetchai/skills/aries_faber/behaviours.py index 3b5f5df5f7..693777b97f 100644 --- a/packages/fetchai/skills/aries_faber/behaviours.py +++ b/packages/fetchai/skills/aries_faber/behaviours.py @@ -33,7 +33,7 @@ HttpDialogues, OefSearchDialogues, ) -from packages.fetchai.skills.aries_faber.strategy import FaberStrategy +from packages.fetchai.skills.aries_faber.strategy import Strategy DEFAULT_SEARCH_INTERVAL = 5.0 @@ -83,7 +83,7 @@ def setup(self) -> None: :return: None """ - strategy = cast(FaberStrategy, self.context.strategy) + strategy = cast(Strategy, self.context.strategy) strategy.is_searching = True def act(self) -> None: @@ -92,7 +92,7 @@ def act(self) -> None: :return: None """ - strategy = cast(FaberStrategy, self.context.strategy) + strategy = cast(Strategy, self.context.strategy) if strategy.is_searching: query = strategy.get_location_and_service_query() oef_search_dialogues = cast( @@ -101,7 +101,6 @@ def act(self) -> None: oef_search_msg, _ = oef_search_dialogues.create( counterparty=self.context.search_service_address, performative=OefSearchMessage.Performative.SEARCH_SERVICES, - dialogue_reference=oef_search_dialogues.new_self_initiated_dialogue_reference(), query=query, ) self.context.outbox.put_message(message=oef_search_msg) diff --git a/packages/fetchai/skills/aries_faber/dialogues.py b/packages/fetchai/skills/aries_faber/dialogues.py index 971f8510b9..d017b7a1a3 100644 --- a/packages/fetchai/skills/aries_faber/dialogues.py +++ b/packages/fetchai/skills/aries_faber/dialogues.py @@ -139,6 +139,6 @@ def role_from_first_message( # pylint: disable=unused-argument BaseOefSearchDialogues.__init__( self, - self_address=self.context.agent_address, + self_address=str(self.context.skill_id), role_from_first_message=role_from_first_message, ) diff --git a/packages/fetchai/skills/aries_faber/handlers.py b/packages/fetchai/skills/aries_faber/handlers.py index cf69c4a137..6230148068 100644 --- a/packages/fetchai/skills/aries_faber/handlers.py +++ b/packages/fetchai/skills/aries_faber/handlers.py @@ -43,15 +43,16 @@ ADMIN_COMMAND_SCEHMAS, ADMIN_COMMAND_STATUS, FABER_ACA_IDENTITY, - FaberStrategy, LEDGER_COMMAND_REGISTER_DID, + Strategy, ) +DEFAULT_SEARCH_INTERVAL = 5.0 SUPPORT_REVOCATION = False -class FaberHTTPHandler(Handler): +class HttpHandler(Handler): """This class represents faber's handler for default messages.""" SUPPORTED_PROTOCOL = HttpMessage.protocol_id # type: Optional[PublicId] @@ -89,7 +90,7 @@ def _send_default_message(self, content: Dict) -> None: :return: None """ # context - strategy = cast(FaberStrategy, self.context.strategy) + strategy = cast(Strategy, self.context.strategy) default_dialogues = cast(DefaultDialogues, self.context.default_dialogues) # default message @@ -107,8 +108,8 @@ def _register_did(self) -> None: :return: None """ - strategy = cast(FaberStrategy, self.context.strategy) - self.context.logger.info("Registering Faber_ACA with seed " + str(self.seed)) + strategy = cast(Strategy, self.context.strategy) + self.context.logger.info(f"Registering Faber_ACA with seed {str(self.seed)}") data = {"alias": self.faber_identity, "seed": self.seed, "role": "TRUST_ANCHOR"} self.context.behaviours.faber.send_http_request_message( method="POST", @@ -128,15 +129,15 @@ def _register_schema( :return: None """ - strategy = cast(FaberStrategy, self.context.strategy) + strategy = cast(Strategy, self.context.strategy) schema_body = { "schema_name": schema_name, "schema_version": version, "attributes": schema_attrs, } - self.context.logger.info("Registering schema " + str(schema_body)) + self.context.logger.info(f"Registering schema {str(schema_body)}") # The following call isn't responded to. This is most probably because of missing options when running the accompanying ACA. - # THe accompanying ACA is not properly connected to the von network ledger (missing pointer to genesis file/wallet type) + # The accompanying ACA is not properly connected to the von network ledger (missing pointer to genesis file/wallet type) self.context.behaviours.faber.send_http_request_message( method="POST", url=strategy.admin_url + ADMIN_COMMAND_SCEHMAS, @@ -150,7 +151,7 @@ def _register_creddef(self, schema_id: str) -> None: :param schema_id: the id of the schema definition registered on the ledger :return: None """ - strategy = cast(FaberStrategy, self.context.strategy) + strategy = cast(Strategy, self.context.strategy) credential_definition_body = { "schema_id": schema_id, "support_revocation": SUPPORT_REVOCATION, @@ -176,32 +177,30 @@ def handle(self, message: Message) -> None: :return: None """ message = cast(HttpMessage, message) + + # recover dialogue http_dialogues = cast(HttpDialogues, self.context.http_dialogues) - strategy = cast(FaberStrategy, self.context.strategy) + http_dialogue = cast(Optional[HttpDialogue], http_dialogues.update(message)) + if http_dialogue is None: + self.context.logger.error( + "something went wrong when adding the incoming HTTP message to the dialogue." + ) + return + + strategy = cast(Strategy, self.context.strategy) if ( message.performative == HttpMessage.Performative.RESPONSE and message.status_code == 200 ): # response to http request - http_dialogue = cast(Optional[HttpDialogue], http_dialogues.update(message)) - if http_dialogue is None: - self.context.logger.exception( - "faber -> http_handler -> handle() -> RESPONSE: " - "something went wrong when adding the incoming HTTP response message to the dialogue." - ) - return - content_bytes = message.body # type: ignore content = json.loads(content_bytes) - self.context.logger.info("Received message: " + str(content)) + self.context.logger.info(f"Received message: {str(content)}") if "version" in content: # response to /status self._register_did() elif "did" in content: + self.context.logger.info(f"Received DID: {self.did}") self.did = content["did"] - if self.did is not None: - self.context.logger.info("Got DID: " + self.did) - else: - self.context.logger.info("DID is None") self._register_schema( schema_name="degree schema", version="0.0.1", @@ -220,9 +219,9 @@ def handle(self, message: Message) -> None: connection = content self.connection_id = content["connection_id"] invitation = connection["invitation"] - self.context.logger.info("connection: " + str(connection)) - self.context.logger.info("connection id: " + self.connection_id) # type: ignore - self.context.logger.info("invitation: " + str(invitation)) + self.context.logger.info(f"connection: {str(connection)}") + self.context.logger.info(f"connection id: {self.connection_id}") # type: ignore + self.context.logger.info(f"invitation: {str(invitation)}") self.context.logger.info( "Sent invitation to Alice. Waiting for the invitation from Alice to finalise the connection..." ) @@ -230,16 +229,9 @@ def handle(self, message: Message) -> None: elif ( message.performative == HttpMessage.Performative.REQUEST ): # webhook request - http_dialogue = cast(Optional[HttpDialogue], http_dialogues.update(message)) - if http_dialogue is None: - self.context.logger.exception( - "faber -> http_handler -> handle() -> REQUEST: " - "something went wrong when adding the incoming HTTP webhook request message to the dialogue." - ) - return content_bytes = message.body content = json.loads(content_bytes) - self.context.logger.info("Received webhook message content:" + str(content)) + self.context.logger.info(f"Received webhook message content:{str(content)}") if "connection_id" in content: if content["connection_id"] == self.connection_id: if content["state"] == "active" and not self.is_connected_to_Alice: @@ -254,7 +246,7 @@ def teardown(self) -> None: """ -class FaberOefSearchHandler(Handler): +class OefSearchHandler(Handler): """This class implements an OEF search handler.""" SUPPORTED_PROTOCOL = OefSearchMessage.protocol_id # type: Optional[PublicId] @@ -336,18 +328,14 @@ def _handle_search(self, oef_search_msg: OefSearchMessage) -> None: """ if len(oef_search_msg.agents) != 1: self.context.logger.info( - "did not find Alice. found {} agents. continue searching.".format( - len(oef_search_msg.agents) - ) + f"did not find Alice. found {len(oef_search_msg.agents)} agents. continue searching." ) return self.context.logger.info( - "found Alice with address {}, stopping search.".format( - oef_search_msg.agents[0] - ) + f"found Alice with address {oef_search_msg.agents[0]}, stopping search." ) - strategy = cast(FaberStrategy, self.context.strategy) + strategy = cast(Strategy, self.context.strategy) strategy.is_searching = False # stopping search # set alice address diff --git a/packages/fetchai/skills/aries_faber/skill.yaml b/packages/fetchai/skills/aries_faber/skill.yaml index a21faa80f0..e5629ff9a5 100644 --- a/packages/fetchai/skills/aries_faber/skill.yaml +++ b/packages/fetchai/skills/aries_faber/skill.yaml @@ -9,10 +9,10 @@ aea_version: '>=1.0.0, <2.0.0' fingerprint: README.md: QmUQB9uBtWGWY5zETSyJnbPZixRj1c4suedVwGPegrTQWs __init__.py: QmQUvEQaaNhSK3ZB35QXnKx93FPb5p9sWysFHuDP7zNsBu - behaviours.py: QmWCwW1oWBWMvPnPRt2v88aTBtEteTVaDrXfHsWAvWoZVj - dialogues.py: QmckxpUSmT9iNGDp7q1GaTtYPJDSMgHro9Y7z9zVWt2pdp - handlers.py: Qmab7KMKAwLfuWVGgfuYBFbLtQuTHoFA2NBqsWD4WkAbhU - strategy.py: QmPEoVGckc8e9L1c4rRa7aYDKMQWg6o8UbN5QgDd1M4c32 + behaviours.py: QmTw8RsAFpYKjfnxNyN3aoV4jDAxNXYsoFpvBxnJpmGUGx + dialogues.py: QmRDG1pHppiRJqdZ2yWRY44h2tFVq2XnKVQw3EZ5TVg1oG + handlers.py: QmeSyQ3Wd7pxghUasKuj4CeLuvSD71LMKk2jaAi94ETanz + strategy.py: QmUFzkeRJGeT4eybSsQGkV3c1iQ2Fj4YDkpmmYk4nV6HmM fingerprint_ignore_patterns: [] connections: - fetchai/http_client:0.22.0 @@ -25,15 +25,15 @@ skills: [] behaviours: faber: args: - services_interval: 20 + search_interval: 5 class_name: FaberBehaviour handlers: http: args: {} - class_name: FaberHTTPHandler + class_name: HttpHandler oef_search: args: {} - class_name: FaberOefSearchHandler + class_name: OefSearchHandler models: default_dialogues: args: {} @@ -57,6 +57,6 @@ models: search_key: intro_service search_value: intro_alice search_radius: 5.0 - class_name: FaberStrategy + class_name: Strategy dependencies: {} is_abstract: false diff --git a/packages/fetchai/skills/aries_faber/strategy.py b/packages/fetchai/skills/aries_faber/strategy.py index e9d0d2d076..d80b844407 100644 --- a/packages/fetchai/skills/aries_faber/strategy.py +++ b/packages/fetchai/skills/aries_faber/strategy.py @@ -52,7 +52,7 @@ DEFAULT_SEARCH_RADIUS = 5.0 -class FaberStrategy(Model): +class Strategy(Model): """This class defines a strategy for the agent.""" def __init__(self, **kwargs: Any) -> None: @@ -67,7 +67,7 @@ def __init__(self, **kwargs: Any) -> None: self._ledger_url = kwargs.pop("ledger_url", DEFAULT_LEDGER_URL) # derived config - self._admin_url = "http://{}:{}".format(self.admin_host, self.admin_port) + self._admin_url = f"http://{self.admin_host}:{self.admin_port}" self._alice_aea_address = "" # Search diff --git a/packages/hashes.csv b/packages/hashes.csv index 71e8200cc8..90c00ee01d 100644 --- a/packages/hashes.csv +++ b/packages/hashes.csv @@ -75,7 +75,7 @@ fetchai/protocols/tac,QmZGEpFDwtUU7ykRmwr3Scjmw69CixH7ZFMNptYWSZ5eBe fetchai/protocols/yoti,QmRcYAyxk2JjhTKHqVYnYiZ8zzck6qzsRuqRnBxPREK5Fe fetchai/skills/advanced_data_request,Qmdcmy14MMTUvjB1nHXfH26eZbYiJm3GGKhX84opMqhJ3b fetchai/skills/aries_alice,Qmd5kKqMsHsP97vrgrVm6nexqvznaU7JCFPRrEgaCnoJek -fetchai/skills/aries_faber,QmPpRGezMuMNzZmZkRLeDN3gMbCoGz9mKJ1M2J6iFxJyfX +fetchai/skills/aries_faber,QmQEbRz5bTycF9FcA47bunidftqRvpKwsdPtZyGVvs1wXQ fetchai/skills/carpark_client,QmSSpTz49mF439YiVcxA3ddPaU9du3ptzQZ25HZtkL4Rys fetchai/skills/carpark_detection,QmUoVRaJqbo47tNMzmbsJ18gm1WNYGwCBZTPYvFuvG4is2 fetchai/skills/confirmation_aw1,QmVkf5oHohbx5vSfY269u1MUS1c8ttkDZAgfGcnijz5AcJ diff --git a/tests/test_packages/test_skills/test_aries_alice/test_behaviours.py b/tests/test_packages/test_skills/test_aries_alice/test_behaviours.py index 50df6ec3a0..0a81f8ae3f 100644 --- a/tests/test_packages/test_skills/test_aries_alice/test_behaviours.py +++ b/tests/test_packages/test_skills/test_aries_alice/test_behaviours.py @@ -32,7 +32,7 @@ class TestAliceBehaviour(AriesAliceTestCase): - """Test registration behaviour of aries_alice.""" + """Test alice behaviour of aries_alice.""" def test_init(self): """Test the __init__ method of the alice behaviour.""" @@ -40,7 +40,7 @@ def test_init(self): assert self.alice_behaviour._nb_retries == 0 def test_send_http_request_message(self): - """Test the send_http_request_message method of the registration behaviour.""" + """Test the send_http_request_message method of the alice behaviour.""" # operation self.alice_behaviour.send_http_request_message( self.mocked_method, self.mocked_url, self.body_dict @@ -98,7 +98,7 @@ def test_setup(self): mock_logger.assert_any_call(logging.INFO, "registering agent on SOEF.") def test_act_i(self): - """Test the act method of the registration behaviour where failed_registration_msg is NOT None.""" + """Test the act method of the alice behaviour where failed_registration_msg is NOT None.""" # setup self.alice_behaviour.failed_registration_msg = self.registration_message @@ -126,7 +126,7 @@ def test_act_i(self): assert self.alice_behaviour.failed_registration_msg is None def test_act_ii(self): - """Test the act method of the registration behaviour where failed_registration_msg is NOT None and max retries is reached.""" + """Test the act method of the alice behaviour where failed_registration_msg is NOT None and max retries is reached.""" # setup self.alice_behaviour.failed_registration_msg = self.registration_message self.alice_behaviour._max_soef_registration_retries = 2 @@ -139,7 +139,7 @@ def test_act_ii(self): assert self.skill.skill_context.is_active is False def test_register_service(self): - """Test the register_service method of the registration behaviour.""" + """Test the register_service method of the alice behaviour.""" # operation with patch.object( self.strategy, @@ -167,7 +167,7 @@ def test_register_service(self): ) def test_register_genus(self): - """Test the register_genus method of the registration behaviour.""" + """Test the register_genus method of the alice behaviour.""" # operation with patch.object( self.strategy, @@ -195,7 +195,7 @@ def test_register_genus(self): ) def test_register_classification(self): - """Test the register_classification method of the registration behaviour.""" + """Test the register_classification method of the alice behaviour.""" # operation with patch.object( self.strategy, @@ -223,7 +223,7 @@ def test_register_classification(self): ) def test_teardown(self): - """Test the teardown method of the service_registration behaviour.""" + """Test the teardown method of the alice behaviour.""" # operation with patch.object( self.strategy, diff --git a/tests/test_packages/test_skills/test_aries_faber/__init__.py b/tests/test_packages/test_skills/test_aries_faber/__init__.py new file mode 100644 index 0000000000..ded9c792cf --- /dev/null +++ b/tests/test_packages/test_skills/test_aries_faber/__init__.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# ------------------------------------------------------------------------------ +# +# Copyright 2018-2019 Fetch.AI Limited +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ------------------------------------------------------------------------------ + +"""The tests module contains the tests of the packages/skills/aries_faber dir.""" diff --git a/tests/test_packages/test_skills/test_aries_faber/intermediate_class.py b/tests/test_packages/test_skills/test_aries_faber/intermediate_class.py new file mode 100644 index 0000000000..9acdbb107a --- /dev/null +++ b/tests/test_packages/test_skills/test_aries_faber/intermediate_class.py @@ -0,0 +1,166 @@ +# -*- coding: utf-8 -*- +# ------------------------------------------------------------------------------ +# +# Copyright 2018-2019 Fetch.AI Limited +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ------------------------------------------------------------------------------ +"""This module sets up test environment for aries_faber skill.""" + +import json +from pathlib import Path +from typing import cast + +from aea.helpers.search.models import ( + Attribute, + Constraint, + ConstraintType, + DataModel, + Description, + Query, +) +from aea.protocols.dialogue.base import DialogueMessage +from aea.test_tools.test_skill import BaseSkillTestCase + +from packages.fetchai.protocols.http.message import HttpMessage +from packages.fetchai.protocols.oef_search.message import OefSearchMessage +from packages.fetchai.skills.aries_faber.behaviours import FaberBehaviour +from packages.fetchai.skills.aries_faber.dialogues import ( + DefaultDialogues, + HttpDialogues, + OefSearchDialogues, +) +from packages.fetchai.skills.aries_faber.handlers import HttpHandler, OefSearchHandler +from packages.fetchai.skills.aries_faber.strategy import Strategy + +from tests.conftest import ROOT_DIR + + +class AriesFaberTestCase(BaseSkillTestCase): + """Sets the aries_faber class up for testing.""" + + path_to_skill = Path(ROOT_DIR, "packages", "fetchai", "skills", "aries_faber") + + @classmethod + def setup(cls): + """Setup the test class.""" + cls.location = {"longitude": 0.1270, "latitude": 51.5194} + cls.search_query = { + "search_key": "intro_service", + "search_value": "intro_alice", + "constraint_type": "==", + } + cls.search_radius = 5.0 + cls.admin_host = "127.0.0.1" + cls.admin_port = 8021 + cls.ledger_url = "http://127.0.0.1:9000" + config_overrides = { + "models": { + "strategy": { + "args": { + "location": cls.location, + "search_query": cls.search_query, + "search_radius": cls.search_radius, + "admin_host": cls.admin_host, + "admin_port": cls.admin_port, + "ledger_url": cls.ledger_url, + } + } + }, + } + + super().setup(config_overrides=config_overrides) + + # behaviours + cls.faber_behaviour = cast( + FaberBehaviour, cls._skill.skill_context.behaviours.faber, + ) + + # dialogues + cls.default_dialogues = cast( + DefaultDialogues, cls._skill.skill_context.default_dialogues + ) + cls.http_dialogues = cast( + HttpDialogues, cls._skill.skill_context.http_dialogues + ) + cls.oef_search_dialogues = cast( + OefSearchDialogues, cls._skill.skill_context.oef_search_dialogues + ) + + # handlers + cls.http_handler = cast(HttpHandler, cls._skill.skill_context.handlers.http) + cls.oef_search_handler = cast( + OefSearchHandler, cls._skill.skill_context.handlers.oef_search + ) + + # models + cls.strategy = cast(Strategy, cls._skill.skill_context.strategy) + + cls.logger = cls._skill.skill_context.logger + + # mocked objects + cls.mocked_method = "SOME_METHOD" + cls.mocked_url = "www.some-url.com" + cls.mocked_version = "some_version" + cls.mocked_headers = "some_headers" + cls.body_dict = {"some_key": "some_value"} + cls.body_str = "some_body" + cls.body_bytes = b"some_body" + cls.mocked_body_bytes = json.dumps(cls.body_str).encode("utf-8") + cls.mocked_query = Query( + [Constraint("some_attribute_name", ConstraintType("==", "some_value"))], + DataModel( + "some_data_model_name", + [ + Attribute( + "some_attribute_name", + str, + False, + "Some attribute descriptions.", + ) + ], + ), + ) + cls.mocked_proposal = Description( + { + "contract_address": "some_contract_address", + "token_id": "123456", + "trade_nonce": "876438756348568", + "from_supply": "543", + "to_supply": "432", + "value": "67", + } + ) + + # list of messages + cls.list_of_http_messages = ( + DialogueMessage( + HttpMessage.Performative.REQUEST, + { + "method": cls.mocked_method, + "url": cls.mocked_url, + "headers": cls.mocked_headers, + "version": cls.mocked_version, + "body": cls.mocked_body_bytes, + }, + is_incoming=False, + ), + ) + + cls.list_of_oef_search_messages = ( + DialogueMessage( + OefSearchMessage.Performative.SEARCH_SERVICES, + {"query": cls.mocked_query}, + ), + ) diff --git a/tests/test_packages/test_skills/test_aries_faber/test_behaviours.py b/tests/test_packages/test_skills/test_aries_faber/test_behaviours.py new file mode 100644 index 0000000000..4e2ee3ed57 --- /dev/null +++ b/tests/test_packages/test_skills/test_aries_faber/test_behaviours.py @@ -0,0 +1,104 @@ +# -*- coding: utf-8 -*- +# ------------------------------------------------------------------------------ +# +# Copyright 2018-2019 Fetch.AI Limited +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ------------------------------------------------------------------------------ +"""This module contains the tests of the behaviour classes of the aries_faber skill.""" + +import json +import logging +from unittest.mock import patch + +from packages.fetchai.protocols.http.message import HttpMessage +from packages.fetchai.protocols.oef_search.message import OefSearchMessage +from packages.fetchai.skills.aries_faber.behaviours import HTTP_CLIENT_PUBLIC_ID + +from tests.test_packages.test_skills.test_aries_faber.intermediate_class import ( + AriesFaberTestCase, +) + + +class TestFaberBehaviour(AriesFaberTestCase): + """Test registration behaviour of aries_faber.""" + + def test_send_http_request_message(self): + """Test the send_http_request_message method of the faber behaviour.""" + # operation + self.faber_behaviour.send_http_request_message( + self.mocked_method, self.mocked_url, self.body_dict + ) + + # after + self.assert_quantity_in_outbox(1) + + message = self.get_message_from_outbox() + has_attributes, error_str = self.message_has_attributes( + actual_message=message, + message_type=HttpMessage, + performative=HttpMessage.Performative.REQUEST, + to=str(HTTP_CLIENT_PUBLIC_ID), + sender=str(self.skill.skill_context.skill_id), + method=self.mocked_method, + url=self.mocked_url, + headers="", + version="", + body=json.dumps(self.body_dict).encode("utf-8"), + ) + assert has_attributes, error_str + + def test_setup(self): + """Test the setup method of the faber behaviour.""" + # operation + self.faber_behaviour.setup() + + # after + self.assert_quantity_in_outbox(0) + assert self.strategy.is_searching is True + + def test_act_is_searching(self): + """Test the act method of the faber behaviour where is_searching is True.""" + # setup + self.strategy._is_searching = True + + # operation + with patch.object( + self.strategy, + "get_location_and_service_query", + return_value=self.mocked_query, + ): + with patch.object(self.logger, "log") as mock_logger: + self.faber_behaviour.act() + + # after + self.assert_quantity_in_outbox(1) + has_attributes, error_str = self.message_has_attributes( + actual_message=self.get_message_from_outbox(), + message_type=OefSearchMessage, + performative=OefSearchMessage.Performative.SEARCH_SERVICES, + to=self.skill.skill_context.search_service_address, + sender=str(self.skill.public_id), + query=self.mocked_query, + ) + assert has_attributes, error_str + + mock_logger.assert_any_call( + logging.INFO, "Searching for Alice on SOEF...", + ) + + def test_teardown(self): + """Test the teardown method of the service_registration behaviour.""" + self.faber_behaviour.teardown() + self.assert_quantity_in_outbox(0) diff --git a/tests/test_packages/test_skills/test_aries_faber/test_dialogues.py b/tests/test_packages/test_skills/test_aries_faber/test_dialogues.py new file mode 100644 index 0000000000..54f0af1e5f --- /dev/null +++ b/tests/test_packages/test_skills/test_aries_faber/test_dialogues.py @@ -0,0 +1,72 @@ +# -*- coding: utf-8 -*- +# ------------------------------------------------------------------------------ +# +# Copyright 2018-2019 Fetch.AI Limited +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ------------------------------------------------------------------------------ +"""This module contains the tests of the dialogue classes of the aries_faber skill.""" + +from aea.test_tools.test_skill import COUNTERPARTY_AGENT_ADDRESS + +from packages.fetchai.protocols.default.message import DefaultMessage +from packages.fetchai.protocols.http.message import HttpMessage +from packages.fetchai.protocols.oef_search.message import OefSearchMessage +from packages.fetchai.skills.aries_faber.dialogues import ( + DefaultDialogue, + HttpDialogue, + OefSearchDialogue, +) + +from tests.test_packages.test_skills.test_aries_faber.intermediate_class import ( + AriesFaberTestCase, +) + + +class TestDialogues(AriesFaberTestCase): + """Test dialogue classes of aries_faber.""" + + def test_default_dialogues(self): + """Test the DefaultDialogues class.""" + _, dialogue = self.default_dialogues.create( + counterparty=COUNTERPARTY_AGENT_ADDRESS, + performative=DefaultMessage.Performative.BYTES, + content=self.body_bytes, + ) + assert dialogue.role == DefaultDialogue.Role.AGENT + assert dialogue.self_address == self.skill.skill_context.agent_address + + def test_http_dialogues(self): + """Test the HttpDialogues class.""" + _, dialogue = self.http_dialogues.create( + counterparty=COUNTERPARTY_AGENT_ADDRESS, + performative=HttpMessage.Performative.REQUEST, + method=self.mocked_method, + url=self.mocked_url, + version=self.mocked_version, + headers=self.mocked_headers, + body=self.mocked_body_bytes, + ) + assert dialogue.role == HttpDialogue.Role.CLIENT + assert dialogue.self_address == str(self.skill.skill_context.skill_id) + + def test_oef_search_dialogues(self): + """Test the OefSearchDialogues class.""" + _, dialogue = self.oef_search_dialogues.create( + counterparty=COUNTERPARTY_AGENT_ADDRESS, + performative=OefSearchMessage.Performative.SEARCH_SERVICES, + query=self.mocked_query, + ) + assert dialogue.role == OefSearchDialogue.Role.AGENT + assert dialogue.self_address == str(self.skill.skill_context.skill_id) diff --git a/tests/test_packages/test_skills/test_aries_faber/test_handlers.py b/tests/test_packages/test_skills/test_aries_faber/test_handlers.py new file mode 100644 index 0000000000..48055c3870 --- /dev/null +++ b/tests/test_packages/test_skills/test_aries_faber/test_handlers.py @@ -0,0 +1,542 @@ +# -*- coding: utf-8 -*- +# ------------------------------------------------------------------------------ +# +# Copyright 2018-2019 Fetch.AI Limited +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ------------------------------------------------------------------------------ +"""This module contains the tests of the handler classes of the aries_faber skill.""" +import json +import logging +from typing import cast +from unittest.mock import patch + +from packages.fetchai.protocols.default.message import DefaultMessage +from packages.fetchai.protocols.http.message import HttpMessage +from packages.fetchai.protocols.oef_search.message import OefSearchMessage +from packages.fetchai.skills.aries_faber.dialogues import ( + HttpDialogue, + OefSearchDialogue, +) +from packages.fetchai.skills.aries_faber.handlers import SUPPORT_REVOCATION +from packages.fetchai.skills.aries_faber.strategy import ( + ADMIN_COMMAND_CREATE_INVITATION, + ADMIN_COMMAND_CREDDEF, + ADMIN_COMMAND_SCEHMAS, + ADMIN_COMMAND_STATUS, + FABER_ACA_IDENTITY, + LEDGER_COMMAND_REGISTER_DID, +) + +from tests.test_packages.test_skills.test_aries_faber.intermediate_class import ( + AriesFaberTestCase, +) + + +class TestHttpHandler(AriesFaberTestCase): + """Test http handler of aries_faber.""" + + is_agent_to_agent_messages = False + + def test__init__i(self): + """Test the __init__ method of the http_request behaviour.""" + assert self.http_handler.faber_identity == FABER_ACA_IDENTITY + assert self.http_handler.seed[:-6] == "d_000000000000000000000000" + assert self.http_handler.did is None + assert self.http_handler._schema_id is None + assert self.http_handler.credential_definition_id is None + assert self.http_handler.connection_id is None + assert self.http_handler.is_connected_to_Alice is False + + def test_setup(self): + """Test the setup method of the http_handler handler.""" + assert self.http_handler.setup() is None + self.assert_quantity_in_outbox(0) + + def test_handle_unidentified_dialogue(self): + """Test the handle method of the http handler where incoming message is invalid.""" + # setup + incorrect_dialogue_reference = ("", "") + incoming_message = cast( + HttpMessage, + self.build_incoming_message( + message_type=HttpMessage, + dialogue_reference=incorrect_dialogue_reference, + performative=HttpMessage.Performative.REQUEST, + method=self.mocked_method, + url=self.mocked_url, + headers=self.mocked_headers, + version=self.mocked_version, + body=self.mocked_body_bytes, + ), + ) + + # operation + with patch.object(self.logger, "log") as mock_logger: + self.http_handler.handle(incoming_message) + + # after + mock_logger.assert_any_call( + logging.ERROR, + "something went wrong when adding the incoming HTTP message to the dialogue.", + ) + + def test_handle_request(self): + """Test the handle method of the http handler where performative is REQUEST.""" + # setup + self.http_handler.connection_id = 123 + self.http_handler.is_connected_to_Faber = False + + body = {"connection_id": 123, "state": "active"} + mocked_body_bytes = json.dumps(body).encode("utf-8") + incoming_message = cast( + HttpMessage, + self.build_incoming_message( + message_type=HttpMessage, + performative=HttpMessage.Performative.REQUEST, + method=self.mocked_method, + url=self.mocked_url, + headers=self.mocked_headers, + version=self.mocked_version, + body=mocked_body_bytes, + ), + ) + + # operation + with patch.object(self.logger, "log") as mock_logger: + self.http_handler.handle(incoming_message) + + # after + mock_logger.assert_any_call( + logging.INFO, f"Received webhook message content:{str(body)}" + ) + mock_logger.assert_any_call(logging.INFO, "Connected to Alice") + assert self.http_handler.is_connected_to_Alice is True + + def test_handle_response_i(self): + """Test the handle method of the http handler where performative is RESPONSE and content has version.""" + # setup + data = { + "alias": self.http_handler.faber_identity, + "seed": self.http_handler.seed, + "role": "TRUST_ANCHOR", + } + http_dialogue = cast( + HttpDialogue, + self.prepare_skill_dialogue( + dialogues=self.http_dialogues, messages=self.list_of_http_messages[:1], + ), + ) + + body = {"version": "some_version"} + mocked_body_bytes = json.dumps(body).encode("utf-8") + incoming_message = cast( + HttpMessage, + self.build_incoming_message_for_skill_dialogue( + dialogue=http_dialogue, + performative=HttpMessage.Performative.RESPONSE, + status_code=200, + status_text="some_status_code", + headers=self.mocked_headers, + version=self.mocked_version, + body=mocked_body_bytes, + ), + ) + + # operation + with patch.object( + self.faber_behaviour, "send_http_request_message" + ) as mock_http_req: + with patch.object(self.logger, "log") as mock_logger: + self.http_handler.handle(incoming_message) + + # after + mock_logger.assert_any_call(logging.INFO, f"Received message: {str(body)}") + + mock_logger.assert_any_call( + logging.INFO, + f"Registering Faber_ACA with seed {str(self.http_handler.seed)}", + ) + mock_http_req.assert_any_call( + method="POST", + url=self.strategy.ledger_url + LEDGER_COMMAND_REGISTER_DID, + content=data, + ) + + def test_handle_response_ii(self): + """Test the handle method of the http handler where performative is RESPONSE and content has did.""" + # setup + did = "some_did" + schema_body = { + "schema_name": "degree schema", + "schema_version": "0.0.1", + "attributes": ["name", "date", "degree", "age", "timestamp"], + } + http_dialogue = cast( + HttpDialogue, + self.prepare_skill_dialogue( + dialogues=self.http_dialogues, messages=self.list_of_http_messages[:1], + ), + ) + + body = {"did": did} + mocked_body_bytes = json.dumps(body).encode("utf-8") + incoming_message = cast( + HttpMessage, + self.build_incoming_message_for_skill_dialogue( + dialogue=http_dialogue, + performative=HttpMessage.Performative.RESPONSE, + status_code=200, + status_text="some_status_code", + headers=self.mocked_headers, + version=self.mocked_version, + body=mocked_body_bytes, + ), + ) + + # operation + with patch.object( + self.faber_behaviour, "send_http_request_message" + ) as mock_http_req: + with patch.object(self.logger, "log") as mock_logger: + self.http_handler.handle(incoming_message) + + # after + mock_logger.assert_any_call(logging.INFO, f"Received message: {str(body)}") + + mock_logger.assert_any_call( + logging.INFO, f"Registering schema {str(schema_body)}" + ) + assert self.http_handler.did == did + mock_http_req.assert_any_call( + method="POST", + url=self.strategy.admin_url + ADMIN_COMMAND_SCEHMAS, + content=schema_body, + ) + + def test_handle_response_iii(self): + """Test the handle method of the http handler where performative is RESPONSE and content has schema_id.""" + # setup + schema_id = "some_schema_id" + credential_definition_body = { + "schema_id": schema_id, + "support_revocation": SUPPORT_REVOCATION, + } + http_dialogue = cast( + HttpDialogue, + self.prepare_skill_dialogue( + dialogues=self.http_dialogues, messages=self.list_of_http_messages[:1], + ), + ) + + body = {"schema_id": schema_id} + mocked_body_bytes = json.dumps(body).encode("utf-8") + incoming_message = cast( + HttpMessage, + self.build_incoming_message_for_skill_dialogue( + dialogue=http_dialogue, + performative=HttpMessage.Performative.RESPONSE, + status_code=200, + status_text="some_status_code", + headers=self.mocked_headers, + version=self.mocked_version, + body=mocked_body_bytes, + ), + ) + + # operation + with patch.object( + self.faber_behaviour, "send_http_request_message" + ) as mock_http_req: + with patch.object(self.logger, "log") as mock_logger: + self.http_handler.handle(incoming_message) + + # after + mock_logger.assert_any_call(logging.INFO, f"Received message: {str(body)}") + + assert self.http_handler.schema_id == schema_id + mock_http_req.assert_any_call( + method="POST", + url=self.strategy.admin_url + ADMIN_COMMAND_CREDDEF, + content=credential_definition_body, + ) + + def test_handle_response_iv(self): + """Test the handle method of the http handler where performative is RESPONSE and content has credential_definition_id.""" + # setup + credential_definition_id = "some_credential_definition_id" + http_dialogue = cast( + HttpDialogue, + self.prepare_skill_dialogue( + dialogues=self.http_dialogues, messages=self.list_of_http_messages[:1], + ), + ) + + body = {"credential_definition_id": credential_definition_id} + mocked_body_bytes = json.dumps(body).encode("utf-8") + incoming_message = cast( + HttpMessage, + self.build_incoming_message_for_skill_dialogue( + dialogue=http_dialogue, + performative=HttpMessage.Performative.RESPONSE, + status_code=200, + status_text="some_status_code", + headers=self.mocked_headers, + version=self.mocked_version, + body=mocked_body_bytes, + ), + ) + + # operation + with patch.object( + self.faber_behaviour, "send_http_request_message" + ) as mock_http_req: + with patch.object(self.logger, "log") as mock_logger: + self.http_handler.handle(incoming_message) + + # after + mock_logger.assert_any_call(logging.INFO, f"Received message: {str(body)}") + + assert self.http_handler.credential_definition_id == credential_definition_id + mock_http_req.assert_any_call( + method="POST", url=self.strategy.admin_url + ADMIN_COMMAND_CREATE_INVITATION + ) + + def test_handle_response_v(self): + """Test the handle method of the http handler where performative is RESPONSE and content has connection_id.""" + # setup + connection_id = 2342 + invitation = {"some_key": "some_value"} + http_dialogue = cast( + HttpDialogue, + self.prepare_skill_dialogue( + dialogues=self.http_dialogues, messages=self.list_of_http_messages[:1], + ), + ) + + body = {"connection_id": connection_id, "invitation": invitation} + mocked_body_bytes = json.dumps(body).encode("utf-8") + incoming_message = cast( + HttpMessage, + self.build_incoming_message_for_skill_dialogue( + dialogue=http_dialogue, + performative=HttpMessage.Performative.RESPONSE, + status_code=200, + status_text="some_status_code", + headers=self.mocked_headers, + version=self.mocked_version, + body=mocked_body_bytes, + ), + ) + + # operation + with patch.object(self.logger, "log") as mock_logger: + self.http_handler.handle(incoming_message) + + # after + self.assert_quantity_in_outbox(1) + + mock_logger.assert_any_call(logging.INFO, f"Received message: {str(body)}") + + assert self.http_handler.connection_id == connection_id + mock_logger.assert_any_call(logging.INFO, f"connection: {str(body)}") + mock_logger.assert_any_call(logging.INFO, f"connection id: {connection_id}") + mock_logger.assert_any_call(logging.INFO, f"invitation: {str(invitation)}") + mock_logger.assert_any_call( + logging.INFO, + "Sent invitation to Alice. Waiting for the invitation from Alice to finalise the connection...", + ) + + # _send_default_message + message = self.get_message_from_outbox() + has_attributes, error_str = self.message_has_attributes( + actual_message=message, + message_type=DefaultMessage, + performative=DefaultMessage.Performative.BYTES, + to=self.strategy.alice_aea_address, + sender=self.skill.skill_context.agent_address, + content=json.dumps(invitation).encode("utf-8"), + ) + assert has_attributes, error_str + + def test_teardown(self): + """Test the teardown method of the http handler.""" + assert self.http_handler.teardown() is None + self.assert_quantity_in_outbox(0) + + +class TestOefSearchHandler(AriesFaberTestCase): + """Test oef_search handler of aries_faber.""" + + is_agent_to_agent_messages = False + + def test_setup(self): + """Test the setup method of the oef_search handler.""" + assert self.oef_search_handler.setup() is None + self.assert_quantity_in_outbox(0) + + def test_handle_unidentified_dialogue(self): + """Test the _handle_unidentified_dialogue method of the oef_search handler.""" + # setup + incorrect_dialogue_reference = ("", "") + incoming_message = cast( + OefSearchMessage, + self.build_incoming_message( + message_type=OefSearchMessage, + dialogue_reference=incorrect_dialogue_reference, + performative=OefSearchMessage.Performative.OEF_ERROR, + oef_error_operation=OefSearchMessage.OefErrorOperation.REGISTER_SERVICE, + ), + ) + + # operation + with patch.object(self.logger, "log") as mock_logger: + self.oef_search_handler.handle(incoming_message) + + # after + mock_logger.assert_any_call( + logging.INFO, + f"received invalid oef_search message={incoming_message}, unidentified dialogue.", + ) + + def test_handle_error(self): + """Test the _handle_error method of the oef_search handler.""" + # setup + oef_search_dialogue = cast( + OefSearchDialogue, + self.prepare_skill_dialogue( + dialogues=self.oef_search_dialogues, + messages=self.list_of_oef_search_messages[:1], + ), + ) + incoming_message = cast( + OefSearchMessage, + self.build_incoming_message_for_skill_dialogue( + dialogue=oef_search_dialogue, + performative=OefSearchMessage.Performative.OEF_ERROR, + oef_error_operation=OefSearchMessage.OefErrorOperation.REGISTER_SERVICE, + ), + ) + + # operation + with patch.object(self.logger, "log") as mock_logger: + self.oef_search_handler.handle(incoming_message) + + # after + mock_logger.assert_any_call( + logging.INFO, + f"received oef_search error message={incoming_message} in dialogue={oef_search_dialogue}.", + ) + + def test_handle_search_i(self): + """Test the _handle_search method of the oef_search handler where the number of agents found is NOT 0.""" + # setup + alice_address = "alice" + agents = (alice_address,) + oef_search_dialogue = cast( + OefSearchDialogue, + self.prepare_skill_dialogue( + dialogues=self.oef_search_dialogues, + messages=self.list_of_oef_search_messages[:1], + ), + ) + incoming_message = cast( + OefSearchMessage, + self.build_incoming_message_for_skill_dialogue( + dialogue=oef_search_dialogue, + performative=OefSearchMessage.Performative.SEARCH_RESULT, + agents=agents, + agents_info=OefSearchMessage.AgentsInfo( + { + "agent_1": {"key_1": "value_1", "key_2": "value_2"}, + "agent_2": {"key_3": "value_3", "key_4": "value_4"}, + } + ), + ), + ) + + # operation + with patch.object( + self.faber_behaviour, "send_http_request_message" + ) as mock_http_req: + with patch.object(self.logger, "log") as mock_logger: + self.oef_search_handler.handle(incoming_message) + + # after + mock_logger.assert_any_call( + logging.INFO, f"found Alice with address {alice_address}, stopping search.", + ) + + assert self.strategy.is_searching is False + assert self.strategy.alice_aea_address is alice_address + mock_http_req.assert_any_call( + "GET", self.strategy.admin_url + ADMIN_COMMAND_STATUS + ) + + def test_handle_search_ii(self): + """Test the _handle_search method of the oef_search handler where the number of agents found is 0.""" + # setup + agents = tuple() + oef_search_dialogue = cast( + OefSearchDialogue, + self.prepare_skill_dialogue( + dialogues=self.oef_search_dialogues, + messages=self.list_of_oef_search_messages[:1], + ), + ) + incoming_message = cast( + OefSearchMessage, + self.build_incoming_message_for_skill_dialogue( + dialogue=oef_search_dialogue, + performative=OefSearchMessage.Performative.SEARCH_RESULT, + agents=agents, + agents_info=OefSearchMessage.AgentsInfo({}), + ), + ) + + # operation + with patch.object(self.logger, "log") as mock_logger: + self.oef_search_handler.handle(incoming_message) + + # after + mock_logger.assert_any_call( + logging.INFO, "did not find Alice. found 0 agents. continue searching.", + ) + + def test_handle_invalid(self): + """Test the _handle_invalid method of the oef_search handler.""" + # setup + incoming_message = cast( + OefSearchMessage, + self.build_incoming_message( + message_type=OefSearchMessage, + performative=OefSearchMessage.Performative.REGISTER_SERVICE, + service_description=self.mocked_proposal, + ), + ) + + # operation + with patch.object(self.logger, "log") as mock_logger: + self.oef_search_handler.handle(incoming_message) + + # after + mock_logger.assert_any_call( + logging.WARNING, + f"cannot handle oef_search message of performative={incoming_message.performative} in dialogue={self.oef_search_dialogues.get_dialogue(incoming_message)}.", + ) + + def test_teardown(self): + """Test the teardown method of the oef_search handler.""" + assert self.oef_search_handler.teardown() is None + self.assert_quantity_in_outbox(0) diff --git a/tests/test_packages/test_skills/test_aries_faber/test_strategy.py b/tests/test_packages/test_skills/test_aries_faber/test_strategy.py new file mode 100644 index 0000000000..88a20881bf --- /dev/null +++ b/tests/test_packages/test_skills/test_aries_faber/test_strategy.py @@ -0,0 +1,71 @@ +# -*- coding: utf-8 -*- +# ------------------------------------------------------------------------------ +# +# Copyright 2018-2019 Fetch.AI Limited +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ------------------------------------------------------------------------------ +"""This module contains the tests of the strategy class of the aries_faber skill.""" + +import pytest + +from aea.exceptions import AEAEnforceError +from aea.helpers.search.models import Constraint, ConstraintType, Query + +from tests.test_packages.test_skills.test_aries_faber.intermediate_class import ( + AriesFaberTestCase, +) + + +class TestStrategy(AriesFaberTestCase): + """Test Strategy of aries_faber.""" + + def test_properties(self): + """Test the properties of Strategy class.""" + assert self.strategy.admin_host == self.admin_host + assert self.strategy.admin_port == self.admin_port + assert self.strategy.ledger_url == self.ledger_url + assert self.strategy.admin_url == f"http://{self.admin_host}:{self.admin_port}" + assert self.strategy.alice_aea_address == "" + self.strategy.alice_aea_address = "some_address" + assert self.strategy.alice_aea_address == "some_address" + assert self.strategy.is_searching is False + with pytest.raises(AEAEnforceError, match="Can only set bool on is_searching!"): + self.strategy.is_searching = "some_value" + self.strategy.is_searching = True + assert self.strategy.is_searching is True + + def test_get_location_and_service_query(self): + """Test the get_location_and_service_query method of the Strategy class.""" + query = self.strategy.get_location_and_service_query() + + assert type(query) == Query + assert len(query.constraints) == 2 + assert query.model is None + + location_constraint = Constraint( + "location", + ConstraintType( + "distance", (self.strategy._agent_location, self.search_radius) + ), + ) + assert query.constraints[0] == location_constraint + + service_key_constraint = Constraint( + self.search_query["search_key"], + ConstraintType( + self.search_query["constraint_type"], self.search_query["search_value"], + ), + ) + assert query.constraints[1] == service_key_constraint From ce083642df66f368de3964a37b81cf06a082fc4a Mon Sep 17 00:00:00 2001 From: ali Date: Thu, 27 May 2021 11:46:53 +0100 Subject: [PATCH 117/147] cleanup: remove mistakenly added new line from default protocol readme --- packages/fetchai/protocols/default/README.md | 1 - packages/fetchai/protocols/default/protocol.yaml | 2 +- packages/hashes.csv | 2 +- 3 files changed, 2 insertions(+), 3 deletions(-) diff --git a/packages/fetchai/protocols/default/README.md b/packages/fetchai/protocols/default/README.md index 8133e98f5c..c71f97e9ee 100644 --- a/packages/fetchai/protocols/default/README.md +++ b/packages/fetchai/protocols/default/README.md @@ -1,4 +1,3 @@ - # Default Protocol ## Description diff --git a/packages/fetchai/protocols/default/protocol.yaml b/packages/fetchai/protocols/default/protocol.yaml index d327db9f1c..b29b7e80eb 100644 --- a/packages/fetchai/protocols/default/protocol.yaml +++ b/packages/fetchai/protocols/default/protocol.yaml @@ -7,7 +7,7 @@ description: A protocol for exchanging any bytes message. license: Apache-2.0 aea_version: '>=1.0.0, <2.0.0' fingerprint: - README.md: QmREaHgRR2rzAitxqGH3LurXciboG7JgPK82noBizRe2yy + README.md: QmNN1eJSSeAGs4uny1tqWkeQwyr6SVV2QgKAoPz18W5jnV __init__.py: QmZL6rcTUHASkTVncagHTWrKvLiJrwPfHmduqekEM5R2Rb custom_types.py: QmbAu5CYv5VtKKbEtzCbGQGE5cPZezDjubLNbiTPycXrHv default.proto: QmWYzTSHVbz7FBS84iKFMhGSXPxay2mss29vY7ufz2BFJ8 diff --git a/packages/hashes.csv b/packages/hashes.csv index 90c00ee01d..b747663fac 100644 --- a/packages/hashes.csv +++ b/packages/hashes.csv @@ -59,7 +59,7 @@ fetchai/contracts/scaffold,QmZuYdqJtxhKQU4sDHjaarya9tF6nctRFNZqoiF7WsgbS9 fetchai/contracts/staking_erc20,QmVJZpvNmgVYWmD11Br8uytKVvYNSm6zHyrHdBNvK5Ag7s fetchai/protocols/aggregation,Qmf1cCWdpFKGUp3jZubQbFxQd5iDTWNVX2BEBTCAwmGGoG fetchai/protocols/contract_api,QmYjgYKBM9ATJ9S2ReNVFy7GEQzBA6CGmea5giyzJVUV84 -fetchai/protocols/default,QmaraosuS34mxDK7RSV8nS4wHhDBytQeX11JNiaCpsheRV +fetchai/protocols/default,QmR8HQP3oLYStwME45MztU8Pg2TFn9wFHJGiLgJpQdnHdd fetchai/protocols/fipa,QmcZgYbB17yQydsy4YMSv5ZpV6jq2PrveSepAqtk48ANUo fetchai/protocols/gym,QmUJdZ4tHi5r2EsfCrLVNaQeSU1sXCo1SbiMEh76Rg2sUg fetchai/protocols/http,QmXTLCKrPtSoVWuuSHj2n9QH4Mi4zjtfe8bixYbPqW8dHC From 69977410463a59103f1a95dc4408b2cf24dd906a Mon Sep 17 00:00:00 2001 From: ali Date: Fri, 28 May 2021 10:10:34 +0100 Subject: [PATCH 118/147] tests: fill missing coverage --- .../test_skills/test_aries_faber/test_handlers.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/tests/test_packages/test_skills/test_aries_faber/test_handlers.py b/tests/test_packages/test_skills/test_aries_faber/test_handlers.py index 48055c3870..6dd889b0c9 100644 --- a/tests/test_packages/test_skills/test_aries_faber/test_handlers.py +++ b/tests/test_packages/test_skills/test_aries_faber/test_handlers.py @@ -22,6 +22,8 @@ from typing import cast from unittest.mock import patch +import pytest + from packages.fetchai.protocols.default.message import DefaultMessage from packages.fetchai.protocols.http.message import HttpMessage from packages.fetchai.protocols.oef_search.message import OefSearchMessage @@ -64,6 +66,14 @@ def test_setup(self): assert self.http_handler.setup() is None self.assert_quantity_in_outbox(0) + def test_properties(self): + """Test the properties of the http_handler handler.""" + self.http_handler._schema_id = None + with pytest.raises(ValueError, match="schema_id not set"): + assert self.http_handler.schema_id is None + self.http_handler._schema_id = "some_schema_id" + assert self.http_handler.schema_id == "some_schema_id" + def test_handle_unidentified_dialogue(self): """Test the handle method of the http handler where incoming message is invalid.""" # setup From ded7d6a2ca9dfa7aebecc7fe2350e6dd83f4fba4 Mon Sep 17 00:00:00 2001 From: "Yuri (solarw) Turchenkov" Date: Fri, 28 May 2021 15:01:41 +0300 Subject: [PATCH 119/147] aea push --local: fix for proper author --- aea/cli/push.py | 2 +- tests/test_cli/test_push.py | 29 +++++++++++++++++++++++++++-- 2 files changed, 28 insertions(+), 3 deletions(-) diff --git a/aea/cli/push.py b/aea/cli/push.py index ec9795aa25..6970ae2aa1 100644 --- a/aea/cli/push.py +++ b/aea/cli/push.py @@ -121,7 +121,7 @@ def _save_item_locally(ctx: Context, item_type: str, item_id: PublicId) -> None: except ValueError as e: # pragma: nocover raise click.ClickException(str(e)) target_path = try_get_item_target_path( - registry_path, ctx.agent_config.author, item_type_plural, item_id.name, + registry_path, item_id.author, item_type_plural, item_id.name, ) copytree(source_path, target_path) click.echo( diff --git a/tests/test_cli/test_push.py b/tests/test_cli/test_push.py index 3bb54433dc..9e23b49553 100644 --- a/tests/test_cli/test_push.py +++ b/tests/test_cli/test_push.py @@ -18,6 +18,7 @@ # ------------------------------------------------------------------------------ """Test module for Registry push methods.""" import filecmp +import os from unittest import TestCase, mock import pytest @@ -69,7 +70,7 @@ def test_save_item_locally_positive( @mock.patch("aea.cli.push.copytree") class TestPushLocally(AEATestCaseEmpty): - """Test case for clu push --local.""" + """Test case for cli push --local.""" ITEM_PUBLIC_ID = PUBLIC_ID ITEM_TYPE = "skill" @@ -84,8 +85,32 @@ def test_vendor_ok( self, copy_tree_mock, ): """Test ok for vendor's item.""" - self.invoke("push", "--local", "skill", "fetchai/echo") + with mock.patch("os.path.exists", side_effect=[False, True, False]): + self.invoke("push", "--local", "skill", "fetchai/echo") copy_tree_mock.assert_called_once() + src_path, dst_path = copy_tree_mock.mock_calls[0][1] + # check for correct author, type, name + assert ( + os.path.normpath(src_path).split(os.sep)[-3:] + == os.path.normpath(dst_path).split(os.sep)[-3:] + ) + + def test_user_ok( + self, copy_tree_mock, + ): + """Test ok for users's item.""" + with mock.patch( + "aea.cli.push.try_get_item_source_path", + return_value=f"{self.author}/skills/echo", + ), mock.patch("aea.cli.push.check_package_public_id"): + self.invoke("push", "--local", "skill", f"{self.author}/echo") + copy_tree_mock.assert_called_once() + src_path, dst_path = copy_tree_mock.mock_calls[0][1] + # check for correct author, type, name + assert ( + os.path.normpath(src_path).split(os.sep)[-3:] + == os.path.normpath(dst_path).split(os.sep)[-3:] + ) def test_fail_no_item( self, *mocks, From bb55e2a62e690dd86d7434d99cc226fefcb571da Mon Sep 17 00:00:00 2001 From: "Yuri (solarw) Turchenkov" Date: Fri, 28 May 2021 17:49:13 +0300 Subject: [PATCH 120/147] fix for aea get-multiaddress --- aea/cli/get_multiaddress.py | 40 ++++++++++---------- aea/test_tools/generic.py | 1 - tests/test_cli/test_get_multiaddress.py | 50 +++++++++++++++++++++++-- 3 files changed, 67 insertions(+), 24 deletions(-) diff --git a/aea/cli/get_multiaddress.py b/aea/cli/get_multiaddress.py index f12dccfd39..133f881a76 100644 --- a/aea/cli/get_multiaddress.py +++ b/aea/cli/get_multiaddress.py @@ -16,7 +16,6 @@ # limitations under the License. # # ------------------------------------------------------------------------------ - """Implementation of the 'aea get_multiaddress' subcommand.""" import re import typing @@ -27,12 +26,11 @@ from click import ClickException from aea.cli.utils.click_utils import PublicIdParameter, password_option -from aea.cli.utils.config import load_item_config from aea.cli.utils.context import Context from aea.cli.utils.decorators import check_aea_project -from aea.cli.utils.package_utils import get_package_path_unified -from aea.configurations.base import ConnectionConfig, PublicId -from aea.configurations.constants import CONNECTION +from aea.configurations.base import PublicId +from aea.configurations.constants import CONNECTIONS +from aea.configurations.manager import AgentConfigManager from aea.crypto.base import Crypto from aea.crypto.registries import crypto_registry from aea.exceptions import enforce @@ -155,7 +153,8 @@ def _try_get_peerid(crypto: Crypto) -> str: def _read_host_and_port_from_config( - connection_config: ConnectionConfig, + connection_config: dict, + connection_id: PublicId, uri_field: str, host_field: Optional[str], port_field: Optional[str], @@ -174,26 +173,26 @@ def _read_host_and_port_from_config( host_is_none and not port_is_none ) if not host_is_none and not port_is_none: - if host_field not in connection_config.config: + if host_field not in connection_config: raise ClickException( - f"Host field '{host_field}' not present in connection configuration {connection_config.public_id}" + f"Host field '{host_field}' not present in connection configuration {connection_id}" ) - if port_field not in connection_config.config: + if port_field not in connection_config: raise ClickException( - f"Port field '{port_field}' not present in connection configuration {connection_config.public_id}" + f"Port field '{port_field}' not present in connection configuration {connection_id}" ) - host = connection_config.config[host_field] - port = int(connection_config.config[port_field]) + host = connection_config[host_field] + port = int(connection_config[port_field]) return host, port if one_is_none: raise ClickException( "-h/--host-field and -p/--port-field must be specified together." ) - if uri_field not in connection_config.config: + if uri_field not in connection_config: raise ClickException( - f"URI field '{uri_field}' not present in connection configuration {connection_config.public_id}" + f"URI field '{uri_field}' not present in connection configuration {connection_id}" ) - url_value = connection_config.config[uri_field] + url_value = connection_config[uri_field] try: m = URI_REGEX.search(url_value) enforce(m is not None, f"URI Doesn't match regex '{URI_REGEX}'") @@ -232,15 +231,16 @@ def _try_get_connection_multiaddress( if connection_id not in ctx.agent_config.connections: raise ValueError(f"Cannot find connection with the public id {connection_id}.") - package_path = Path( - get_package_path_unified(ctx.cwd, ctx.agent_config, CONNECTION, connection_id) - ) + agent_config_manager = AgentConfigManager.load(ctx.cwd) connection_config = cast( - ConnectionConfig, load_item_config(CONNECTION, package_path) + dict, + agent_config_manager.get_variable( + f"vendor.{connection_id.author}.{CONNECTIONS}.{connection_id.name}.config" + ), ) host, port = _read_host_and_port_from_config( - connection_config, uri_field, host_field, port_field + connection_config, connection_id, uri_field, host_field, port_field ) try: diff --git a/aea/test_tools/generic.py b/aea/test_tools/generic.py index 6bd180f195..8bb81c4405 100644 --- a/aea/test_tools/generic.py +++ b/aea/test_tools/generic.py @@ -194,7 +194,6 @@ def nested_set_config( settings_keys, config_file_path, config_loader, _ = handle_dotted_path( dotted_path, author ) - with open_file(config_file_path) as fp: config = config_loader.load(fp) diff --git a/tests/test_cli/test_get_multiaddress.py b/tests/test_cli/test_get_multiaddress.py index 00699910c4..84b34581ad 100644 --- a/tests/test_cli/test_get_multiaddress.py +++ b/tests/test_cli/test_get_multiaddress.py @@ -25,6 +25,9 @@ from aea.test_tools.test_cases import AEATestCaseEmpty +from packages.fetchai.connections.p2p_libp2p.connection import ( + PUBLIC_ID as P2P_CONNECTION_PUBLIC_ID, +) from packages.fetchai.connections.stub.connection import ( PUBLIC_ID as STUB_CONNECTION_PUBLIC_ID, ) @@ -64,7 +67,6 @@ def test_run(self, *mocks): assert result.exit_code == 0 # test we can decode the output - base58.b58decode(result.stdout) class TestGetMultiAddressCommandConnectionIdPositive(AEATestCaseEmpty): @@ -111,8 +113,10 @@ def test_run(self, *mocks): self.generate_private_key(FetchAICrypto.identifier) self.add_private_key(FetchAICrypto.identifier, connection=True) + port = 10101 + host = "127.0.0.1" self.nested_set_config( - "vendor.fetchai.connections.stub.config", {"public_uri": "127.0.0.1:10000"} + "vendor.fetchai.connections.stub.config", {"public_uri": f"{host}:{port}"} ) result = self.run_cli_command( @@ -128,7 +132,47 @@ def test_run(self, *mocks): assert result.exit_code == 0 # multiaddr test - expected_multiaddr_prefix = "/dns4/127.0.0.1/tcp/10000/p2p/" + expected_multiaddr_prefix = f"/dns4/{host}/tcp/{port}/p2p/" + assert expected_multiaddr_prefix in result.stdout + base58_addr = str(result.stdout).replace(expected_multiaddr_prefix, "") + base58.b58decode(base58_addr) + + +class TestGetMultiAddressCommandConnectionIdURIAgentOverridesPositive(AEATestCaseEmpty): + """Test case for CLI get-multiaddress command with --connection flag and --uri for agent overrides.""" + + def test_run(self, *mocks): + """Run the test.""" + self.add_item("connection", str(P2P_CONNECTION_PUBLIC_ID)) + self.generate_private_key(FetchAICrypto.identifier) + self.add_private_key(FetchAICrypto.identifier, connection=True) + + port = 10101 + host = "127.0.0.1" + self.run_cli_command( + "config", + "set", + "--type", + "dict", + "vendor.fetchai.connections.p2p_libp2p.config", + f'{{"public_uri": "{host}:{port}"}}', + cwd=self.current_agent_context, + ) + + result = self.run_cli_command( + "get-multiaddress", + FetchAICrypto.identifier, + "--connection", + "--connection-id", + str(P2P_CONNECTION_PUBLIC_ID), + "--uri-field", + "public_uri", + cwd=self.current_agent_context, + ) + + assert result.exit_code == 0 + # multiaddr test + expected_multiaddr_prefix = f"/dns4/{host}/tcp/{port}/p2p/" assert expected_multiaddr_prefix in result.stdout base58_addr = str(result.stdout).replace(expected_multiaddr_prefix, "") base58.b58decode(base58_addr) From c4c82da656e1333a8aaaa234399330f66ace6628 Mon Sep 17 00:00:00 2001 From: David Minarsch Date: Fri, 28 May 2021 21:47:17 +0100 Subject: [PATCH 121/147] chore: fix docstrings for aea root modules --- aea/aea.py | 16 ++++++--------- aea/aea_builder.py | 49 ++++++++++++++++++++-------------------------- aea/agent.py | 29 +++++++-------------------- aea/agent_loop.py | 15 +++++++------- aea/exceptions.py | 3 +-- aea/launcher.py | 2 -- aea/multiplexer.py | 34 ++++++++------------------------ aea/runtime.py | 7 +++++-- 8 files changed, 55 insertions(+), 100 deletions(-) diff --git a/aea/aea.py b/aea/aea.py index dad5f530b9..2137469db7 100644 --- a/aea/aea.py +++ b/aea/aea.py @@ -108,8 +108,12 @@ def __init__( :param period: period to call agent's act :param execution_timeout: amount of time to limit single act/handle to execute. :param max_reactions: the processing rate of envelopes per tick (i.e. single loop). + :param error_handler_class: the class implementing the error handler + :param error_handler_config: the configuration of the error handler :param decision_maker_handler_class: the class implementing the decision maker handler to be used. + :param decision_maker_handler_config: the configuration of the decision maker handler :param skill_exception_policy: the skill exception policy enum + :param connection_exception_policy: the connection exception policy enum :param loop_mode: loop_mode to choose agent run loop. :param runtime_mode: runtime mode (async, threaded) to run AEA in. :param default_ledger: default ledger id @@ -121,8 +125,6 @@ def __init__( :param storage_uri: optional uri to set generic storage :param task_manager_mode: task manager mode (threaded) to run tasks with. :param kwargs: keyword arguments to be attached in the agent context namespace. - - :return: None """ self._skills_exception_policy = skill_exception_policy @@ -266,8 +268,6 @@ def setup(self) -> None: Set up the agent. Calls setup() on the resources. - - :return: None """ self.resources.setup() @@ -276,8 +276,6 @@ def act(self) -> None: Perform actions. Adds new handlers and behaviours for use/execution by the runtime. - - :return: None """ self.filter.handle_new_handlers_and_behaviours() @@ -457,8 +455,6 @@ def teardown(self) -> None: Performs the following: - tears down the resources. - - :return: None """ self.resources.teardown() @@ -466,6 +462,7 @@ def get_task_result(self, task_id: int) -> AsyncResult: """ Get the result from a task. + :param task_id: the id of the task :return: async result for task_id """ return self.runtime.task_manager.get_task_result(task_id) @@ -482,7 +479,6 @@ def enqueue_task( :param func: the callable instance to be enqueued :param args: the positional arguments to be passed to the function. :param kwargs: the keyword arguments to be passed to the function. - :return the task id to get the the result. - :raises ValueError: if the task manager is not running. + :return: the task id to get the the result. """ return self.runtime.task_manager.enqueue_task(func, args, kwargs) diff --git a/aea/aea_builder.py b/aea/aea_builder.py index 2dad9e0013..57d78e1c77 100644 --- a/aea/aea_builder.py +++ b/aea/aea_builder.py @@ -174,10 +174,9 @@ def contracts(self) -> Dict[ComponentId, ContractConfig]: def add_component(self, configuration: ComponentConfiguration) -> None: """ - Add a component to the dependency manager.. + Add a component to the dependency manager. :param configuration: the component configuration to add. - :return: None """ # add to main index self._dependencies[configuration.component_id] = configuration @@ -199,7 +198,7 @@ def remove_component(self, component_id: ComponentId) -> None: """ Remove a component. - :return None + :param component_id: the component id :raises ValueError: if some component depends on this package. """ if component_id not in self.all_dependencies: @@ -333,6 +332,8 @@ def __init__( Initialize the builder. :param with_default_packages: add the default packages. + :param registry_dir: the registry directory. + :param build_dir_root: the root of the build directory. """ WithLogger.__init__(self, logger=_default_logger) self.registry_dir = os.path.join(os.getcwd(), registry_dir) @@ -351,7 +352,6 @@ def reset(self, is_full_reset: bool = False) -> None: - component instances :param is_full_reset: whether it is a full reset or not. - :return: None """ self._reset(is_full_reset) @@ -360,7 +360,6 @@ def _reset(self, is_full_reset: bool = False) -> None: Reset the builder (private usage). :param is_full_reset: whether it is a full reset or not. - :return: None. """ self._name: Optional[str] = None self._private_key_paths: Dict[str, Optional[str]] = {} @@ -597,9 +596,9 @@ def set_connection_exception_policy( self, connection_exception_policy: Optional[ExceptionPolicyEnum] ) -> "AEABuilder": # pragma: nocover """ - Set skill exception policy. + Set connection exception policy. - :param skill_exception_policy: the policy + :param connection_exception_policy: the policy :return: self """ @@ -679,7 +678,7 @@ def set_storage_uri( """ Set the storage uri. - :param storage uri: storage uri + :param storage_uri: storage uri :return: self """ self._storage_uri = storage_uri @@ -755,7 +754,6 @@ def _check_can_remove(self, component_id: ComponentId) -> None: Check if a component can be removed. :param component_id: the component id. - :return: None :raises ValueError: if the component is already present. """ if component_id not in self._package_dependency_manager.all_dependencies: @@ -770,7 +768,6 @@ def _check_can_add(self, configuration: ComponentConfiguration) -> None: Check if the component can be added, given its configuration. :param configuration: the configuration of the component. - :return: None """ self._check_configuration_not_already_added(configuration) self._check_package_dependencies(configuration) @@ -950,8 +947,6 @@ def _set_component_build_directory( Set component build directory, create if not presents. :param configuration: component configuration - - :return: None """ configuration.build_directory = os.path.join( self.get_build_root_directory(), @@ -970,6 +965,7 @@ def add_component_instance(self, component: Component) -> "AEABuilder": You will have to `reset()` the builder before calling `build()` again. :param component: Component instance already initialized. + :return: self """ self._to_reset = True self._check_can_add(component.configuration) @@ -1145,10 +1141,9 @@ def _run_build_entrypoint( Run a build entrypoint script. :param build_entrypoint: the path to the build script relative to directory. - :param directory: the directory root for the entrypoint path. + :param source_directory: the source directory. + :param target_directory: the target directory. :param logger: logger - - :return: None """ cls._check_valid_entrypoint(build_entrypoint, source_directory) @@ -1172,6 +1167,8 @@ def _run_in_subprocess( """ Run in subprocess. + :param command: command to run + :param source_directory: source directory :return: stdout, stderr, code """ res = subprocess.run( # nosec @@ -1335,7 +1332,6 @@ def build( # pylint: disable=unsubscriptable-object :param connection_ids: select only these connections to run the AEA. :param password: the password to encrypt/decrypt the private key. :return: the AEA object. - :raises ValueError: if we cannot """ datadir = self._get_data_dir() self._check_we_can_build() @@ -1591,8 +1587,7 @@ def _check_configuration_not_already_added( """ Check the component configuration has not already been added. - :param configuration: the configuration being added - :return: None + :param configuration: the component configuration being added :raises AEAException: if the component is already present. """ if ( @@ -1611,7 +1606,7 @@ def _check_package_dependencies( """ Check that we have all the dependencies needed to the package. - :return: None + :param configuration: the component configuration :raises AEAException: if there's a missing dependency. """ not_supported_packages = configuration.package_dependencies.difference( @@ -1632,7 +1627,6 @@ def _check_pypi_dependencies(self, configuration: ComponentConfiguration) -> Non Check that PyPI dependencies of a package don't conflict with the existing ones. :param configuration: the component configuration. - :return: None :raises AEAException: if some PyPI dependency is conflicting. """ all_pypi_dependencies = self._package_dependency_manager.pypi_dependencies @@ -1682,7 +1676,6 @@ def _check_valid_entrypoint(build_entrypoint: str, directory: str) -> None: :param build_entrypoint: the build entrypoint. :param directory: the directory from where to start reading the script. - :return: None """ enforce( build_entrypoint is not None, @@ -1716,8 +1709,6 @@ def set_from_configuration( :param agent_configuration: AgentConfig to get values from. :param aea_project_path: PathLike root directory of the agent project. :param skip_consistency_check: if True, the consistency check are skipped. - - :return: None """ # set name and other configurations self.set_name(agent_configuration.name) @@ -1799,7 +1790,8 @@ def _find_import_order( aea_project_path: Path, skip_consistency_check: bool, ) -> List[ComponentId]: - """Find import order for skills/connections. + """ + Find import order for skills/connections. We need to handle skills and connections separately, since skills/connections can depend on each other. @@ -1807,6 +1799,11 @@ def _find_import_order( - load the skill/connection configurations to find the import order - detect if there are cycles - import skills/connections from the leaves of the dependency graph, by finding a topological ordering. + + :param component_ids: component ids to check + :param aea_project_path: project path to AEA + :param skip_consistency_check: consistency check of AEA + :return: list of component ids ordered for import """ # the adjacency list for the inverse dependency graph dependency_to_supported_dependencies: Dict[ @@ -1903,7 +1900,6 @@ def _load_and_add_components( :param resources: the resources object to populate. :param agent_name: the AEA name for logging purposes. :param kwargs: keyword argument to forward to the component loader. - :return: None """ for configuration in self._package_dependency_manager.get_components_by_type( component_type @@ -1972,7 +1968,6 @@ def _add_components_of_type( :param agent_configuration: the agent configuration from where to retrieve the components. :param aea_project_path: path to the AEA project. :param skip_consistency_check: if true, skip consistency checks. - :return: None """ public_ids = getattr(agent_configuration, component_type.to_plural()) component_ids = [ @@ -2000,8 +1995,6 @@ def _preliminary_checks_before_build(self) -> None: Do consistency check on build parameters. - Check that the specified default ledger is in the list of specified required ledgers. - - :return: None """ default_ledger = self.get_default_ledger() required_ledgers = self.get_required_ledgers() diff --git a/aea/agent.py b/aea/agent.py index 3f22aca18f..e8bc911d59 100644 --- a/aea/agent.py +++ b/aea/agent.py @@ -73,8 +73,8 @@ def __init__( :param runtime_mode: runtime mode to up agent. :param storage_uri: optional uri to set generic storage :param task_manager_mode: task manager mode. - - :return: None + :param logger: the logger. + :param task_manager_mode: mode of the task manager. """ WithLogger.__init__(self, logger=logger) self._identity = identity @@ -173,6 +173,8 @@ def tick(self) -> int: # pragma: nocover Get the tick or agent loop count. Each agent loop (one call to each one of act(), react(), update()) increments the tick. + + :return: tick count """ return self._tick @@ -196,11 +198,7 @@ def runtime(self) -> BaseRuntime: return self._runtime def setup(self) -> None: - """ - Set up the agent. - - :return: None - """ + """Set up the agent.""" raise NotImplementedError # pragma: nocover def start(self) -> None: @@ -211,8 +209,6 @@ def start(self) -> None: - calls start() on runtime. - waits for runtime to complete running (blocking) - - :return: None """ was_started = self.runtime.start() @@ -226,16 +222,11 @@ def handle_envelope(self, envelope: Envelope) -> None: Handle an envelope. :param envelope: the envelope to handle. - :return: None """ raise NotImplementedError # pragma: nocover def act(self) -> None: - """ - Perform actions on period. - - :return: None - """ + """Perform actions on period.""" raise NotImplementedError # pragma: nocover def stop(self) -> None: @@ -246,18 +237,12 @@ def stop(self) -> None: - calls stop() on runtime - waits for runtime to stop (blocking) - - :return: None """ self.runtime.stop() self.runtime.wait_completed(sync=True) def teardown(self) -> None: - """ - Tear down the agent. - - :return: None - """ + """Tear down the agent.""" raise NotImplementedError # pragma: nocover def get_periodic_tasks( diff --git a/aea/agent_loop.py b/aea/agent_loop.py index 222f7e0a83..5ca2fe96b4 100644 --- a/aea/agent_loop.py +++ b/aea/agent_loop.py @@ -67,6 +67,7 @@ def __init__( :param agent: Agent or AEA to run. :param loop: optional asyncio event loop. if not specified a new loop will be created. + :param threaded: if True, run in threaded mode, else async """ logger = get_logger(__name__, agent.name) WithLogger.__init__(self, logger) @@ -166,10 +167,10 @@ def send_to_skill( """ Send message or envelope to another skill. - :param message_or_envelope: envelope to send to another skill. - if message passed it will be wrapped into envelope with optional envelope context. + If message passed it will be wrapped into envelope with optional envelope context. - :return: None + :param message_or_envelope: envelope to send to another skill. + :param context: envelope context """ @property @@ -222,10 +223,10 @@ def send_to_skill( """ Send message or envelope to another skill. - :param message_or_envelope: envelope to send to another skill. - if message passed it will be wrapped into envelope with optional envelope context. + If message passed it will be wrapped into envelope with optional envelope context. - :return: None + :param message_or_envelope: envelope to send to another skill. + :param context: envelope context """ if isinstance(message_or_envelope, Envelope): envelope = message_or_envelope @@ -255,8 +256,6 @@ def _periodic_task_exception_callback( # pylint: disable=unused-argument :param task_callable: function to be called :param: exc: Exception raised - - :return: None """ self._exceptions.append(exc) diff --git a/aea/exceptions.py b/aea/exceptions.py index 167b611da3..bdf6a5b502 100644 --- a/aea/exceptions.py +++ b/aea/exceptions.py @@ -84,8 +84,6 @@ def __init__(self, reraise: Optional[Exception] = None) -> None: Init _StopRuntime exception. :param reraise: exception to reraise. - - :return: None """ self.reraise = reraise super().__init__("Stop runtime exception.") @@ -111,6 +109,7 @@ def parse_exception(exception: Exception, limit: int = -1) -> str: """ Parse an exception to get the relevant lines. + :param exception: the exception to be parsed :param limit: the limit :return: exception as string """ diff --git a/aea/launcher.py b/aea/launcher.py index d0aaed0f72..6ce31707a8 100644 --- a/aea/launcher.py +++ b/aea/launcher.py @@ -87,8 +87,6 @@ def _run_agent( :param stop_event: multithreading Event to stop agent run. :param log_level: debug level applied for AEA in subprocess :param password: the password to encrypt/decrypt the private key. - - :return: None """ import asyncio # pylint: disable=import-outside-toplevel import select # pylint: disable=import-outside-toplevel diff --git a/aea/multiplexer.py b/aea/multiplexer.py index cbef46958a..87cf4c7a11 100644 --- a/aea/multiplexer.py +++ b/aea/multiplexer.py @@ -106,7 +106,12 @@ def __init__( This information is used for envelopes which don't specify any routing context. If connections is None, this parameter is ignored. :param loop: the event loop to run the multiplexer. If None, a new event loop is created. + :param exception_policy: the exception policy used for connections. + :param threaded: if True, run in threaded mode, else async :param agent_name: the name of the agent that owns the multiplexer, for logging purposes. + :param default_routing: default routing map + :param default_connection: default connection + :param protocols: protocols used """ self._exception_policy: ExceptionPolicyEnum = exception_policy logger = get_logger(__name__, agent_name) @@ -232,7 +237,6 @@ def set_loop(self, loop: AbstractEventLoop) -> None: Set event loop and all event loop related objects. :param loop: asyncio event loop. - :return: None """ self._loop = loop self._lock = asyncio.Lock(loop=self._loop) @@ -243,8 +247,6 @@ def _handle_exception(self, fn: Callable, exc: Exception) -> None: :param fn: a method where it raised .send .connect etc :param exc: exception - - :return: None. """ if self._exception_policy == ExceptionPolicyEnum.just_log: self.logger.exception(f"Exception raised in {fn}") @@ -261,7 +263,6 @@ def add_connection(self, connection: Connection, is_default: bool = False) -> No :param connection: the connection to add. :param is_default: whether the connection added should be the default one. - :return: None """ if connection.connection_id in self._id_to_connection: # pragma: nocover self.logger.warning( @@ -277,7 +278,6 @@ def _connection_consistency_checks(self) -> None: """ Do some consistency checks on the multiplexer connections. - :return: None :raise AEAEnforceError: if an inconsistency is found. """ if len(self.connections) == 0: @@ -427,7 +427,6 @@ async def _connect_one(self, connection_id: PublicId) -> None: Set a connection up. :param connection_id: the id of the connection. - :return: None """ connection = self._id_to_connection[connection_id] self.logger.debug("Processing connection {}".format(connection.connection_id)) @@ -467,7 +466,6 @@ async def _disconnect_one(self, connection_id: PublicId) -> None: Tear a connection down. :param connection_id: the id of the connection. - :return: None """ connection = self._id_to_connection[connection_id] self.logger.debug("Processing connection {}".format(connection.connection_id)) @@ -553,8 +551,6 @@ async def _send(self, envelope: Envelope) -> None: Send an envelope. :param envelope: the envelope to send. - :return: None - :raises ValueError: if the connection id provided is not valid. """ envelope_protocol_id = self._get_protocol_id_for_envelope(envelope) connection_id = self._get_connection_id_from_envelope( @@ -717,7 +713,6 @@ async def _put(self, envelope: Envelope) -> None: running on a different thread than the one used in this function. :param envelope: the envelope to be sent. - :return: None """ await self.out_queue.put(envelope) @@ -729,7 +724,6 @@ def put(self, envelope: Envelope) -> None: running on a different thread than the one used in this function. :param envelope: the envelope to be sent. - :return: None """ if self._threaded: self._loop.call_soon_threadsafe(self.out_queue.put_nowait, envelope) @@ -748,7 +742,6 @@ def _setup( :param connections: the connections to use. It will replace the other ones. :param default_routing: the default routing. :param default_connection: the default connection. - :return: None. """ self.default_routing = default_routing or {} @@ -785,11 +778,8 @@ def __init__(self, *args: Any, **kwargs: Any) -> None: """ Initialize the connection multiplexer. - :param connections: a sequence of connections. - :param default_connection_index: the index of the connection to use as default. - | this information is used for envelopes which - | don't specify any routing context. - :param loop: the event loop to run the multiplexer. If None, a new event loop is created. + :param args: arguments + :param kwargs: keyword arguments """ super().__init__(*args, **kwargs) self._sync_lock = threading.Lock() @@ -805,7 +795,6 @@ def set_loop(self, loop: AbstractEventLoop) -> None: Set event loop and all event loop related objects. :param loop: asyncio event loop. - :return: None """ super().set_loop(loop) self._thread_runner = ThreadedAsyncRunner(self._loop) @@ -858,7 +847,6 @@ def put(self, envelope: Envelope) -> None: # type: ignore # cause overrides co running on a different thread than the one used in this function. :param envelope: the envelope to be sent. - :return: None """ self._thread_runner.call(super()._put(envelope)) # .result(240) @@ -931,11 +919,7 @@ async def async_get(self) -> Envelope: return envelope async def async_wait(self) -> None: - """ - Check for a envelope on the in queue. - - :return: the envelope object. - """ + """Check for a envelope on the in queue.""" self._multiplexer.logger.debug( "Checks for envelope presents in queue async way..." ) @@ -967,7 +951,6 @@ def put(self, envelope: Envelope) -> None: Put an envelope into the queue. :param envelope: the envelope. - :return: None """ self._multiplexer.logger.debug(f"Put an envelope in the queue: {envelope}.") if not isinstance(envelope.message, Message): @@ -991,7 +974,6 @@ def put_message( :param message: the message :param context: the envelope context - :return: None """ if not isinstance(message, Message): raise ValueError("Provided message not of type Message.") diff --git a/aea/runtime.py b/aea/runtime.py index 40b4728686..6d758d6e81 100644 --- a/aea/runtime.py +++ b/aea/runtime.py @@ -78,9 +78,11 @@ def __init__( Init runtime. :param agent: Agent to run. + :param multiplexer_options: options for the multiplexer. :param loop_mode: agent main loop mode. :param loop: optional event loop. if not provided a new one will be created. - :return: None + :param threaded: if True, run in threaded mode, else async + :param task_manager_mode: mode of the task manager. """ Runnable.__init__(self, threaded=threaded, loop=loop if not threaded else None) logger = get_logger(__name__, agent.name) @@ -272,9 +274,10 @@ def __init__( Init runtime. :param agent: Agent to run. + :param multiplexer_options: options for the multiplexer. :param loop_mode: agent main loop mode. :param loop: optional event loop. if not provided a new one will be created. - :return: None + :param threaded: if True, run in threaded mode, else async """ super().__init__( agent=agent, From 8e856f924ffe3ea285294516b68a28f336143bb7 Mon Sep 17 00:00:00 2001 From: MarcoFavorito Date: Sat, 29 May 2021 17:24:11 +0200 Subject: [PATCH 122/147] feat: add default error callback the default error callback will be removed once the client of the class will adds a custom error callback. --- aea/manager/manager.py | 42 +++++++++++++++++++++++------- tests/test_manager/test_manager.py | 18 ++++++++++++- 2 files changed, 49 insertions(+), 11 deletions(-) diff --git a/aea/manager/manager.py b/aea/manager/manager.py index 894705fe47..c39ee9d572 100644 --- a/aea/manager/manager.py +++ b/aea/manager/manager.py @@ -186,7 +186,10 @@ def __init__( self._loop: Optional[asyncio.AbstractEventLoop] = None self._event: Optional[asyncio.Event] = None - self._error_callbacks: List[Callable[[str, BaseException], None]] = [] + self._error_callbacks: List[Callable[[str, BaseException], None]] = [ + self._default_error_callback + ] + self._custom_callback_added: bool = False self._last_start_status: Optional[ Tuple[ bool, @@ -267,11 +270,8 @@ async def _manager_loop(self) -> None: agent_name = agents_run_tasks_futures[task] self._agents_tasks.pop(agent_name) if task.exception(): - if len(self._error_callbacks) == 0: - self._print_exception_occurred_but_no_error_callback(agent_name) - else: - for callback in self._error_callbacks: - callback(agent_name, task.exception()) + for callback in self._error_callbacks: + callback(agent_name, task.exception()) else: await task @@ -279,6 +279,10 @@ def add_error_callback( self, error_callback: Callable[[str, BaseException], None] ) -> None: """Add error callback to call on error raised.""" + if len(self._error_callbacks) == 1 and not self._custom_callback_added: + # only default callback present, reset before adding new callback + self._custom_callback_added = True + self._error_callbacks = [] self._error_callbacks.append(error_callback) def start_manager( @@ -851,7 +855,24 @@ def _save_state(self) -> None: with open_file(self._save_path, "w") as f: json.dump(self.dict_state, f, indent=4, sort_keys=True) - def _print_exception_occurred_but_no_error_callback(self, agent_name: str) -> None: + def _default_error_callback( + self, agent_name: str, exception: BaseException + ) -> None: + """ + Handle errors from running agents. + + This is the default error callback. To replace it + with another one, use the method 'add_error_callback'. + + :param agent_name: the agent name + :param exception: the caught exception + :return None + """ + self._print_exception_occurred_but_no_error_callback(agent_name, exception) + + def _print_exception_occurred_but_no_error_callback( + self, agent_name: str, exception: BaseException + ) -> None: """ Print a warning message when an exception occurred but no error callback is registered. @@ -862,7 +883,8 @@ def _print_exception_occurred_but_no_error_callback(self, agent_name: str) -> No return self._warning_message_printed_for_agent[agent_name] = True print( - f"WARNING: An exception occurred during the execution of agent '{agent_name}', " - f"but since no error callback was found the exception is handled silently. Please " - f"add an error callback using the method 'add_error_callback' of the MultiAgentManager instance." + f"WARNING: An exception occurred during the execution of agent '{agent_name}':\n", + str(exception), + f"\nHowever, since no error callback was found the exception is handled silently. Please " + f"add an error callback using the method 'add_error_callback' of the MultiAgentManager instance.", ) diff --git a/tests/test_manager/test_manager.py b/tests/test_manager/test_manager.py index d4738e5421..906155bed8 100644 --- a/tests/test_manager/test_manager.py +++ b/tests/test_manager/test_manager.py @@ -260,7 +260,7 @@ def test_agent_actually_running(self, *args): wait_for_condition(lambda: act_mock.call_count > 0, timeout=10) def test_exception_handling(self, *args): - """Test erro callback works.""" + """Test error callback works.""" self.test_add_agent() self.manager.start_all_agents() agent = self.manager._agents_tasks[self.agent_name].agent @@ -275,6 +275,22 @@ def test_exception_handling(self, *args): self.manager.start_all_agents() wait_for_condition(lambda: callback_mock.call_count > 0, timeout=10) + def test_default_exception_handling(self, *args): + """Test that the default error callback works.""" + self.test_add_agent() + self.manager.start_all_agents() + agent = self.manager._agents_tasks[self.agent_name].agent + behaviour = agent.resources.get_behaviour(self.echo_skill_id, "echo") + assert behaviour + + with patch.object( + self.manager, "_print_exception_occurred_but_no_error_callback" + ) as callback_mock: + with patch.object(behaviour, "act", side_effect=ValueError("expected")): + self.manager.start_all_agents() + wait_for_condition(lambda: callback_mock.call_count > 0, timeout=10) + callback_mock.assert_called_once() + def test_stop_from_exception_handling(self, *args): """Test stop MultiAgentManager from error callback.""" self.test_add_agent() From e0940f821b656bee2d92a3adcebc175b8b434ccf Mon Sep 17 00:00:00 2001 From: Marco Favorito Date: Sat, 29 May 2021 17:25:47 +0200 Subject: [PATCH 123/147] Update aea/manager/project.py Co-authored-by: David Minarsch --- aea/manager/project.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aea/manager/project.py b/aea/manager/project.py index 39e8ef8a2b..9d45985d11 100644 --- a/aea/manager/project.py +++ b/aea/manager/project.py @@ -302,7 +302,7 @@ def set_overrides( overrides["component_configurations"] = component_configurations self.agent_config_manager.update_config(overrides) - if overrides: + if agent_overrides: self._ensure_private_keys() @property From 1c4ba2ca040cd454058448076534b9ece094a8bd Mon Sep 17 00:00:00 2001 From: MarcoFavorito Date: Sat, 29 May 2021 17:57:28 +0200 Subject: [PATCH 124/147] fix: f-string with no variable --- aea/manager/manager.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/aea/manager/manager.py b/aea/manager/manager.py index c39ee9d572..6e42241b6d 100644 --- a/aea/manager/manager.py +++ b/aea/manager/manager.py @@ -885,6 +885,6 @@ def _print_exception_occurred_but_no_error_callback( print( f"WARNING: An exception occurred during the execution of agent '{agent_name}':\n", str(exception), - f"\nHowever, since no error callback was found the exception is handled silently. Please " - f"add an error callback using the method 'add_error_callback' of the MultiAgentManager instance.", + "\nHowever, since no error callback was found the exception is handled silently. Please " + "add an error callback using the method 'add_error_callback' of the MultiAgentManager instance.", ) From 6742942c34e8e27259f6169bb7574e871ab58a77 Mon Sep 17 00:00:00 2001 From: MarcoFavorito Date: Mon, 31 May 2021 09:58:03 +0200 Subject: [PATCH 125/147] test: update libp2p test after new ack mechanism (#2435) The test involved was in a temporary state, waiting for improvements on the libp2p implementation. Now we do not need to resend the envelope, as it will be retransmitted by the connection logic. --- .../test_p2p_libp2p_client/test_communication.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/test_packages/test_connections/test_p2p_libp2p_client/test_communication.py b/tests/test_packages/test_connections/test_p2p_libp2p_client/test_communication.py index df497928b6..19365cf65d 100644 --- a/tests/test_packages/test_connections/test_p2p_libp2p_client/test_communication.py +++ b/tests/test_packages/test_connections/test_p2p_libp2p_client/test_communication.py @@ -779,7 +779,6 @@ def test_envelope_received(self): RegexComparator("Connection error:.*Try to reconnect and read again") ) # proceed as usual. Now we expect the connection to have reconnected successfully - self.multiplexer_client_2.put(envelope) delivered_envelope = self.multiplexer_client_1.get(block=True, timeout=20) assert delivered_envelope is not None From 05b7444a2b5be5fb44a6e9397939a5ed9578db92 Mon Sep 17 00:00:00 2001 From: David Minarsch Date: Tue, 1 Jun 2021 12:09:55 +0100 Subject: [PATCH 126/147] chore: update docstrings in aea c-d components --- aea/components/base.py | 2 +- aea/components/loader.py | 6 +++++ aea/configurations/base.py | 26 ++++++++++--------- aea/configurations/data_types.py | 11 +++++--- aea/configurations/loader.py | 6 ++--- aea/configurations/manager.py | 8 +++--- aea/configurations/pypi.py | 1 - aea/configurations/utils.py | 2 -- aea/configurations/validation.py | 8 +++--- aea/connections/base.py | 6 +++++ aea/connections/scaffold/connection.py | 35 ++++++++++++++++---------- aea/context/base.py | 13 ++++++---- aea/contracts/base.py | 6 +++++ aea/contracts/scaffold/contract.py | 3 +++ aea/crypto/base.py | 6 ++--- aea/crypto/helpers.py | 5 ---- aea/crypto/ledger_apis.py | 5 +++- aea/decision_maker/base.py | 15 +++-------- aea/decision_maker/default.py | 6 +---- aea/decision_maker/gop.py | 26 +++++++------------ aea/decision_maker/scaffold.py | 1 - 21 files changed, 104 insertions(+), 93 deletions(-) diff --git a/aea/components/base.py b/aea/components/base.py index 826dae2a3b..60167a19b2 100644 --- a/aea/components/base.py +++ b/aea/components/base.py @@ -55,6 +55,7 @@ def __init__( :param configuration: the package configuration. :param is_vendor: whether the package is vendorized. + :param kwargs: the keyword arguments for the logger. """ WithLogger.__init__(self, **kwargs) self._configuration = configuration @@ -120,7 +121,6 @@ def load_aea_package(configuration: ComponentConfiguration) -> None: It adds all the __init__.py modules into `sys.modules`. :param configuration: the configuration object. - :return: None """ dir_ = configuration.directory if dir_ is None: # pragma: nocover diff --git a/aea/components/loader.py b/aea/components/loader.py index 15bd988b95..a497fcfda4 100644 --- a/aea/components/loader.py +++ b/aea/components/loader.py @@ -59,6 +59,8 @@ def load_component_from_config( # type: ignore Load a component from a directory. :param configuration: the component configuration. + :param args: the positional arguments. + :param kwargs: the keyword arguments. :return: the component instance. """ component_type = configuration.component_type @@ -98,6 +100,8 @@ def _handle_error_while_loading_component_module_not_found( - "'{}' is not a valid type name, choose one of ['protocols', 'connections', 'skills', 'contracts']" - "The package '{}/{}' of type '{}' exists, but cannot find module '{}'" + :param configuration: the configuration + :param e: the exception :raises ModuleNotFoundError: if it is not :raises AEAPackageLoadingError: the same exception, but prepending an informative message. """ @@ -165,6 +169,8 @@ def _handle_error_while_loading_component_generic_error( """ Handle Exception for AEA packages. + :param configuration: the configuration + :param e: the exception :raises Exception: the same exception, but prepending an informative message. """ e_str = parse_exception(e) diff --git a/aea/configurations/base.py b/aea/configurations/base.py index b1af0dc1a3..6811e543cc 100644 --- a/aea/configurations/base.py +++ b/aea/configurations/base.py @@ -243,10 +243,7 @@ def __init__( :param author: the author of the package. :param version: the version of the package (SemVer format). :param license_: the license. - :param aea_version: either a fixed version, or a set of specifiers - describing the AEA versions allowed. - (default: empty string - no constraint). - The fixed version is interpreted with the specifier '=='. + :param aea_version: either a fixed version, or a set of specifiers describing the AEA versions allowed. (default: empty string - no constraint). The fixed version is interpreted with the specifier '=='. :param fingerprint: the fingerprint. :param fingerprint_ignore_patterns: a list of file patterns to ignore files to fingerprint. :param build_entrypoint: path to a script to execute at build time. @@ -357,7 +354,7 @@ def update(self, data: Dict, env_vars_friendly: bool = False) -> None: Update configuration with other data. :param data: the data to replace. - :return: None + :param env_vars_friendly: whether or not it is env vars friendly. """ if not data: # do nothing if nothing to update return @@ -389,9 +386,13 @@ def _create_or_update_from_json( raise NotImplementedError # pragma: nocover def make_resulting_config_data(self, overrides: Dict) -> Dict: - """Make config data with overrides applied. + """ + Make config data with overrides applied. + + Does not update config, just creates json representation. - Does not update config, just creates json representation + :param overrides: the overrides + :return: config with overrides applied """ current_config = self.json recursive_update(current_config, overrides, allow_new_values=True) @@ -523,7 +524,8 @@ def check_fingerprint(self, directory: Path) -> None: """ Check that the fingerprint are correct against a directory path. - :raises ValueError if: + :param directory: the directory path. + :raises ValueError: if - the argument is not a valid package directory - the fingerprints do not match. """ @@ -537,7 +539,8 @@ def check_public_id_consistency(self, directory: Path) -> None: """ Check that the public ids in the init file match the config. - :raises ValueError if: + :param directory: the directory path. + :raises ValueError: if - the argument is not a valid package directory - the public ids do not match. """ @@ -861,8 +864,8 @@ def __init__( """ Initialize a skill component configuration. - :param skill_component_type: the skill component type. :param class_name: the class name of the component. + :param file_path: the file path. :param args: keyword arguments. """ self.class_name = class_name @@ -1538,7 +1541,7 @@ def update(self, data: Dict, env_vars_friendly: bool = False) -> None: mapping from ComponentId to configurations. :param data: the data to replace. - :return: None + :param env_vars_friendly: whether or not it is env vars friendly. """ data = copy(data) # update component parts @@ -1843,7 +1846,6 @@ def _compare_fingerprints( :param item_type: the type of the item. :param is_recursive: look up sub directories for files to fingerprint - :return: None :raises ValueError: if the fingerprints do not match. """ expected_fingerprints = package_configuration.fingerprint diff --git a/aea/configurations/data_types.py b/aea/configurations/data_types.py index d2b287febf..79f5ed6c6e 100644 --- a/aea/configurations/data_types.py +++ b/aea/configurations/data_types.py @@ -147,6 +147,7 @@ def to_plural(self) -> str: >>> PackageType.CONTRACT.to_plural() 'contracts' + :return: pluralised package type """ return self.value + "s" @@ -174,6 +175,8 @@ def plurals() -> Collection[str]: # pylint: disable=unsubscriptable-object >>> ComponentType.plurals() ['protocols', 'connections', 'skills', 'contracts'] + + :return: list of all pluralised component types """ return list(map(lambda x: x.to_plural(), ComponentType)) @@ -189,6 +192,8 @@ def to_plural(self) -> str: 'skills' >>> ComponentType.CONTRACT.to_plural() 'contracts' + + :return: pluralised component type """ return self.value + "s" @@ -424,8 +429,10 @@ def __lt__(self, other: Any) -> bool: >>> public_id_1 < public_id_3 Traceback (most recent call last): ... - ValueError: The public IDs author_1/name_1:0.1.0 and author_1/name_2:0.1.0 cannot be compared. Their author or name attributes are different. + :param other: the object to compate to + :raises ValueError: The public IDs author_1/name_1:0.1.0 and author_1/name_2:0.1.0 cannot be compared. Their author or name attributes are different. + :return: whether or not the inequality is satisfied """ if ( isinstance(other, PublicId) @@ -807,7 +814,6 @@ def create(self, item_id: str, item: T) -> None: :param item_id: the item id. :param item: the item to be added. - :return: None :raises ValueError: if the item with the same id is already in the collection. """ if item_id in self._items_by_id: @@ -829,7 +835,6 @@ def update(self, item_id: str, item: T) -> None: :param item_id: the item id. :param item: the item to be added. - :return: None """ self._items_by_id[item_id] = item diff --git a/aea/configurations/loader.py b/aea/configurations/loader.py index ebf4733982..8b15892f2e 100644 --- a/aea/configurations/loader.py +++ b/aea/configurations/loader.py @@ -78,7 +78,6 @@ def validate(self, json_data: Dict) -> None: Validate a JSON object. :param json_data: the JSON data. - :return: None. """ self.validator.validate(json_data) @@ -167,7 +166,6 @@ def dump(self, configuration: T, file_pointer: TextIO) -> None: :param configuration: the configuration to be dumped. :param file_pointer: the file pointer to the configuration file - :return: None """ if self.configuration_class.package_type == PackageType.AGENT: self._dump_agent_config(cast(AgentConfig, configuration), file_pointer) @@ -204,6 +202,7 @@ def load_agent_config_from_json( Load agent configuration from configuration json data. :param configuration_json: list of dicts with aea configuration + :param validate: whether or not to validate :return: AgentConfig instance """ @@ -305,7 +304,8 @@ def from_package_type( """ Get a config loader from the configuration type. - :param configuration_type: the configuration type + :param configuration_type: the configuration type. + :return: configuration loader """ config_class: Type[PackageConfiguration] = PACKAGE_TYPE_TO_CONFIG_CLASS[ PackageType(configuration_type) diff --git a/aea/configurations/manager.py b/aea/configurations/manager.py index 85eb8e442e..9eccb48fe9 100644 --- a/aea/configurations/manager.py +++ b/aea/configurations/manager.py @@ -91,9 +91,8 @@ def _try_get_configuration_object_from_aea_config( The result is not guaranteed because there might not be any - :param ctx: the CLI context. - :param component_id: the component id whose prefix points to the relevant - custom configuration in the AEA configuration file. + :param agent_config: the agent configuration. + :param component_id: the component id whose prefix points to the relevant custom configuration in the AEA configuration file. :return: the configuration object to get/set an attribute. """ if component_id is None: @@ -303,6 +302,7 @@ def __init__( :param agent_config: AgentConfig to manage. :param aea_project_directory: directory where project for agent_config placed. + :param env_vars_friendly: whether or not it is env vars friendly """ self.agent_config = agent_config self.aea_project_directory = aea_project_directory @@ -366,8 +366,6 @@ def set_variable(self, path: VariablePath, value: JSON_TYPES) -> None: :param path: str dotted path or List[Union[ComponentId, str]] :param value: one of the json friendly objects. - - :return: None """ component_id, json_path = self._parse_path(path) data = self._make_dict_for_path_and_value(json_path, value) diff --git a/aea/configurations/pypi.py b/aea/configurations/pypi.py index 7b44a5a622..e1d889259a 100644 --- a/aea/configurations/pypi.py +++ b/aea/configurations/pypi.py @@ -51,7 +51,6 @@ def _handle_compatibility_operator( :param all_specifiers: the list of all specifiers (to be populated). :param operator_to_specifiers: a mapping from operator to specifiers (to be populated). :param specifier: the specifier to process. - :return: None """ spec_version = Version(specifier.version) base_version = spec_version.base_version diff --git a/aea/configurations/utils.py b/aea/configurations/utils.py index 20bbe53898..dabe0a7cc9 100644 --- a/aea/configurations/utils.py +++ b/aea/configurations/utils.py @@ -61,7 +61,6 @@ def _( :param arg: the agent configuration. :param replacements: the replacement mapping. - :return: None """ _replace_component_id( arg, @@ -172,7 +171,6 @@ def _replace_component_id( :param config: the component configuration to update. :param types_to_update: the types to update. :param replacements: the replacements. - :return: """ for component_type in types_to_update: public_id_set = getattr(config, component_type.to_plural(), set()) diff --git a/aea/configurations/validation.py b/aea/configurations/validation.py index 068fe81b87..2719ff6de0 100644 --- a/aea/configurations/validation.py +++ b/aea/configurations/validation.py @@ -130,6 +130,7 @@ def __init__(self, schema_filename: str, env_vars_friendly: bool = False) -> Non Initialize the parser for configuration files. :param schema_filename: the path to the JSON-schema file in 'aea/configurations/schemas'. + :param env_vars_friendly: whether or not it is env var friendly. """ base_uri = Path(_SCHEMAS_DIR) with open_file(base_uri / schema_filename) as fp: @@ -187,7 +188,6 @@ def validate_component_configuration( :param configuration: the configuration dictionary. :param env_vars_friendly: bool, if set True, will not raise errors over the env variable definitions. - :return: None :raises ValueError: if the configuration is not valid. """ schema_file = _get_path_to_custom_config_schema_from_type( @@ -211,7 +211,6 @@ def validate(self, json_data: Dict) -> None: Validate a JSON object against the right JSON schema. :param json_data: the JSON data. - :return: None. """ if json_data.get("type", AGENT) == AGENT: json_data_copy = deepcopy(json_data) @@ -250,9 +249,7 @@ def validate_agent_components_configuration( """ Validate agent component configurations overrides. - :param component_configurations: - - :return: None + :param component_configurations: the component configurations to validate. """ for idx, component_configuration_json in enumerate(component_configurations): component_id = self.split_component_id_and_config( @@ -281,6 +278,7 @@ def validate_data_with_pattern( """ Validate data dict with pattern dict for attributes present and type match. + :param data: data dict to validate :param pattern: dict with pattern to check over :param excludes: list of tuples of str of paths to be skipped during the check :param skip_env_vars: is set True will not check data type over env variables. diff --git a/aea/connections/base.py b/aea/connections/base.py index a0f3032c03..7edcfba67e 100644 --- a/aea/connections/base.py +++ b/aea/connections/base.py @@ -83,6 +83,7 @@ def __init__( :param crypto_store: the crypto store for encrypted communication. :param restricted_to_protocols: the set of protocols ids of the only supported protocols for this connection. :param excluded_protocols: the set of protocols ids that we want to exclude for this connection. + :param kwargs: keyword arguments passed to component base """ enforce(configuration is not None, "The configuration must be provided.") super().__init__(configuration, **kwargs) @@ -222,6 +223,8 @@ async def receive(self, *args: Any, **kwargs: Any) -> Optional["Envelope"]: """ Receive an envelope. + :param args: positional arguments + :param kwargs: keyword arguments :return: the received envelope, or None if an error occurred. """ @@ -241,6 +244,7 @@ def from_dir( :param identity: the identity object. :param crypto_store: object to access the connection crypto objects. :param data_dir: the assets directory. + :param kwargs: keyword arguments passed to connection base :return: the connection object. """ configuration = cast( @@ -268,6 +272,7 @@ def from_config( :param identity: the identity object. :param crypto_store: object to access the connection crypto objects. :param data_dir: the directory of the AEA project data. + :param kwargs: keyword arguments passed to component base :return: an instance of the concrete connection class. """ configuration = cast(ConnectionConfig, configuration) @@ -356,6 +361,7 @@ def __init__( :param crypto_store: the crypto store for encrypted communication. :param restricted_to_protocols: the set of protocols ids of the only supported protocols for this connection. :param excluded_protocols: the set of protocols ids that we want to exclude for this connection. + :param kwargs: keyword arguments passed to connection base """ super().__init__( configuration=configuration, diff --git a/aea/connections/scaffold/connection.py b/aea/connections/scaffold/connection.py index 2eb86199be..ab411dcb8c 100644 --- a/aea/connections/scaffold/connection.py +++ b/aea/connections/scaffold/connection.py @@ -45,11 +45,15 @@ def __init__(self, **kwargs: Any) -> None: The configuration must be specified if and only if the following parameters are None: connection_id, excluded_protocols or restricted_to_protocols. - :param configuration: the connection configuration. - :param identity: the identity object held by the agent. - :param crypto_store: the crypto store for encrypted communication. - :param restricted_to_protocols: the set of protocols ids of the only supported protocols for this connection. - :param excluded_protocols: the set of protocols ids that we want to exclude for this connection. + Possible keyword arguments: + - configuration: the connection configuration. + - data_dir: directory where to put local files. + - identity: the identity object held by the agent. + - crypto_store: the crypto store for encrypted communication. + - restricted_to_protocols: the set of protocols ids of the only supported protocols for this connection. + - excluded_protocols: the set of protocols ids that we want to exclude for this connection. + + :param kwargs: keyword arguments passed to component base """ super().__init__(**kwargs) # pragma: no cover @@ -74,7 +78,6 @@ async def send(self, envelope: Envelope) -> None: Send an envelope. :param envelope: the envelope to send. - :return: None """ raise NotImplementedError # pragma: no cover @@ -82,7 +85,9 @@ async def receive(self, *args: Any, **kwargs: Any) -> Optional[Envelope]: """ Receive an envelope. Blocking. - :return: the envelope received, or None. + :param args: arguments to receive + :param kwargs: keyword arguments to receive + :return: the envelope received, if present. """ raise NotImplementedError # pragma: no cover @@ -101,11 +106,16 @@ def __init__(self, *args: Any, **kwargs: Any) -> None: # pragma: no cover The configuration must be specified if and only if the following parameters are None: connection_id, excluded_protocols or restricted_to_protocols. - :param configuration: the connection configuration. - :param identity: the identity object held by the agent. - :param crypto_store: the crypto store for encrypted communication. - :param restricted_to_protocols: the set of protocols ids of the only supported protocols for this connection. - :param excluded_protocols: the set of protocols ids that we want to exclude for this connection. + Possible arguments: + - configuration: the connection configuration. + - data_dir: directory where to put local files. + - identity: the identity object held by the agent. + - crypto_store: the crypto store for encrypted communication. + - restricted_to_protocols: the set of protocols ids of the only supported protocols for this connection. + - excluded_protocols: the set of protocols ids that we want to exclude for this connection. + + :param args: arguments passed to component base + :param kwargs: keyword arguments passed to component base """ super().__init__(*args, **kwargs) raise NotImplementedError @@ -141,7 +151,6 @@ def on_send(self, envelope: Envelope) -> None: Send an envelope. :param envelope: the envelope to send. - :return: None. """ raise NotImplementedError # pragma: no cover diff --git a/aea/context/base.py b/aea/context/base.py index 7a3362917a..0d83a61e1c 100644 --- a/aea/context/base.py +++ b/aea/context/base.py @@ -83,13 +83,14 @@ def __init__( :param decision_maker_handler_context: the decision maker's name space :param task_manager: the task manager :param default_ledger_id: the default ledger id - :param ledger_it_to_currency_denom: mapping from ledger ids to currency denominations + :param currency_denominations: mapping from ledger ids to currency denominations :param default_connection: the default connection :param default_routing: the default routing :param search_service_address: the address of the search service :param decision_maker_address: the address of the decision maker :param data_dir: directory where to put local files. :param storage_callable: function that returns optional storage attached to agent. + :param send_to_skill: callable for sending envelopes to skills. :param kwargs: keyword arguments to be attached in the agent context namespace. """ self._shared_state = {} # type: Dict[str, Any] @@ -118,14 +119,14 @@ def send_to_skill( """ Send message or envelope to another skill. - :param message_or_envelope: envelope to send to another skill. - if message passed it will be wrapped into envelope with optional envelope context. + If message passed it will be wrapped into envelope with optional envelope context. - :return: None + :param message_or_envelope: envelope to send to another skill. + :param context: the optional envelope context """ if self._send_to_skill is None: # pragma: nocover raise ValueError("Send to skill feature is not supported") - return self._send_to_skill(message_or_envelope, context) + self._send_to_skill(message_or_envelope, context) @property def storage(self) -> Optional[Storage]: @@ -145,6 +146,8 @@ def shared_state(self) -> Dict[str, Any]: The shared state is the only object which skills can use to exchange state directly. It is accessible (read and write) from all skills. + + :return: dictionary of the shared state. """ return self._shared_state diff --git a/aea/contracts/base.py b/aea/contracts/base.py index 2981c0b42e..aea5959548 100644 --- a/aea/contracts/base.py +++ b/aea/contracts/base.py @@ -50,6 +50,7 @@ def __init__(self, contract_config: ContractConfig, **kwargs: Any) -> None: Initialize the contract. :param contract_config: the contract configurations. + :param kwargs: the keyword arguments. """ super().__init__(contract_config, **kwargs) @@ -88,6 +89,7 @@ def from_dir(cls, directory: str, **kwargs: Any) -> "Contract": Load the protocol from a directory. :param directory: the directory to the skill package. + :param kwargs: the keyword arguments. :return: the contract object. """ configuration = cast( @@ -103,6 +105,7 @@ def from_config(cls, configuration: ContractConfig, **kwargs: Any) -> "Contract" Load contract from configuration. :param configuration: the contract configuration. + :param kwargs: the keyword arguments. :return: the contract object. """ if configuration.directory is None: # pragma: nocover @@ -160,6 +163,7 @@ def get_raw_transaction( :param ledger_api: the ledger apis. :param contract_address: the contract address. + :param kwargs: the keyword arguments. :return: the tx """ raise NotImplementedError @@ -176,6 +180,7 @@ def get_raw_message( :param ledger_api: the ledger apis. :param contract_address: the contract address. + :param kwargs: the keyword arguments. :return: the tx """ raise NotImplementedError @@ -192,6 +197,7 @@ def get_state( :param ledger_api: the ledger apis. :param contract_address: the contract address. + :param kwargs: the keyword arguments. :return: the tx """ raise NotImplementedError diff --git a/aea/contracts/scaffold/contract.py b/aea/contracts/scaffold/contract.py index ad6fffcda6..6c73ff2e1f 100644 --- a/aea/contracts/scaffold/contract.py +++ b/aea/contracts/scaffold/contract.py @@ -44,6 +44,7 @@ def get_raw_transaction( :param ledger_api: the ledger apis. :param contract_address: the contract address. + :param kwargs: the keyword arguments. :return: the tx """ raise NotImplementedError @@ -60,6 +61,7 @@ def get_raw_message( :param ledger_api: the ledger apis. :param contract_address: the contract address. + :param kwargs: the keyword arguments. :return: the tx """ raise NotImplementedError @@ -76,6 +78,7 @@ def get_state( :param ledger_api: the ledger apis. :param contract_address: the contract address. + :param kwargs: the keyword arguments. :return: the tx """ raise NotImplementedError diff --git a/aea/crypto/base.py b/aea/crypto/base.py index 64a3612731..832d2892cc 100644 --- a/aea/crypto/base.py +++ b/aea/crypto/base.py @@ -161,7 +161,6 @@ def dump(self, private_key_file: str, password: Optional[str] = None) -> None: :param private_key_file: the file where the key is stored. :param password: the password to encrypt/decrypt the private key. - :return: None """ if password is None: with open(private_key_file, "wb") as fpb: @@ -176,7 +175,6 @@ def encrypt(self, password: str) -> str: """ Encrypt the private key and return in json. - :param private_key: the raw private key. :param password: the password to decrypt. :return: json string containing encrypted private key. """ @@ -202,7 +200,7 @@ def is_transaction_settled(tx_receipt: JSONLike) -> bool: """ Check whether a transaction is settled or not. - :param tx_digest: the digest associated to the transaction. + :param tx_receipt: the receipt associated to the transaction. :return: True if the transaction has been settled, False o/w. """ @@ -370,6 +368,7 @@ def get_transfer_transaction( :param amount: the amount of wealth to be transferred. :param tx_fee: the transaction fee. :param tx_nonce: verifies the authenticity of the tx + :param kwargs: the keyword arguments. :return: the transfer transaction """ @@ -425,6 +424,7 @@ def get_deploy_transaction( :param contract_interface: the contract interface. :param deployer_address: The address that will deploy the contract. + :param kwargs: the keyword arguments. :returns tx: the transaction dictionary. """ diff --git a/aea/crypto/helpers.py b/aea/crypto/helpers.py index 81ebe26baa..78438770e0 100644 --- a/aea/crypto/helpers.py +++ b/aea/crypto/helpers.py @@ -44,7 +44,6 @@ def try_validate_private_key_path( :param ledger_id: one of 'fetchai', 'ethereum' :param private_key_path: the path to the private key. :param password: the password to encrypt/decrypt the private key. - :return: None :raises: ValueError if the identifier is invalid. """ try: @@ -68,7 +67,6 @@ def create_private_key( :param ledger_id: the ledger identifier. :param private_key_file: the private key file. :param password: the password to encrypt/decrypt the private key. - :return: None :raises: ValueError if the identifier is invalid. """ crypto = make_crypto(ledger_id) @@ -85,7 +83,6 @@ def try_generate_testnet_wealth( :param address: the address to check for :param url: the url :param _sync: whether to wait to sync or not; currently unused - :return: None """ faucet_api = make_faucet_api(identifier) if faucet_api is not None: @@ -101,8 +98,6 @@ def private_key_verify( :param aea_conf: AgentConfig :param aea_project_path: Path, where project placed. :param password: the password to encrypt/decrypt the private key. - - :return: None """ for identifier, _ in aea_conf.private_key_paths.read_all(): if identifier not in crypto_registry.supported_ids: # pragma: nocover diff --git a/aea/crypto/ledger_apis.py b/aea/crypto/ledger_apis.py index a8ed8343b3..8bb10a2861 100644 --- a/aea/crypto/ledger_apis.py +++ b/aea/crypto/ledger_apis.py @@ -128,6 +128,7 @@ def get_transfer_transaction( :param amount: the amount :param tx_nonce: verifies the authenticity of the tx :param tx_fee: the tx fee + :param kwargs: the keyword arguments. :return: tx """ @@ -321,7 +322,9 @@ def is_valid_address(identifier: str, address: Address) -> bool: """ Check if the address is valid. - :param address: the address to validate + :param identifier: ledger identifier. + :param address: the address to validate. + :return: whether it is a valid address or not. """ identifier = ( identifier diff --git a/aea/decision_maker/base.py b/aea/decision_maker/base.py index 8817be65e2..db9402c10a 100644 --- a/aea/decision_maker/base.py +++ b/aea/decision_maker/base.py @@ -55,7 +55,6 @@ def set(self, **kwargs: Any) -> None: Set values on the ownership state. :param kwargs: the relevant keyword arguments - :return: None """ @abstractmethod @@ -66,7 +65,6 @@ def apply_delta(self, **kwargs: Any) -> None: This method is used to apply a raw state update without a transaction. :param kwargs: the relevant keyword arguments - :return: None """ @property @@ -174,8 +172,9 @@ def put( # pylint: disable=arguments-differ ignored in that case). :param internal_message: the internal message to put on the queue + :param block: whether to block or not + :param timeout: timeout on block :raises: ValueError, if the item is not an internal message - :return: None """ if not (isinstance(internal_message, Message) or internal_message is None): raise ValueError("Only messages are allowed!") @@ -191,7 +190,6 @@ def put_nowait( # pylint: disable=arguments-differ :param internal_message: the internal message to put on the queue :raises: ValueError, if the item is not an internal message - :return: None """ if not (isinstance(internal_message, Message) or internal_message is None): raise ValueError("Only messages are allowed!") @@ -201,8 +199,9 @@ def get(self, block: bool = True, timeout: Optional[float] = None) -> None: """ Inaccessible get method. + :param block: whether to block or not + :param timeout: timeout on block :raises: ValueError, access not permitted. - :return: None """ raise ValueError("Access not permitted!") @@ -211,7 +210,6 @@ def get_nowait(self) -> None: Inaccessible get_nowait method. :raises: ValueError, access not permitted. - :return: None """ raise ValueError("Access not permitted!") @@ -297,7 +295,6 @@ def handle(self, message: Message) -> None: Handle an internal message from the skills. :param message: the internal message - :return: None """ @@ -318,7 +315,6 @@ def __init__(self, decision_maker_handler: DecisionMakerHandler,) -> None: """ Initialize the decision maker. - :param agent_name: the agent name :param decision_maker_handler: the decision maker handler """ WithLogger.__init__(self, logger=decision_maker_handler.logger) @@ -382,8 +378,6 @@ def execute(self) -> None: Performs the following while not stopped: - gets internal messages from the in queue and calls handle() on them - - :return: None """ while not self._stopped: message = self.message_in_queue.protected_get( @@ -405,6 +399,5 @@ def handle(self, message: Message) -> None: Handle an internal message from the skills. :param message: the internal message - :return: None """ self.decision_maker_handler.handle(message) diff --git a/aea/decision_maker/default.py b/aea/decision_maker/default.py index fe0b158cd9..3eb534682b 100644 --- a/aea/decision_maker/default.py +++ b/aea/decision_maker/default.py @@ -50,7 +50,7 @@ def __init__(self, self_address: Address, **kwargs: Any) -> None: Initialize dialogues. :param self_address: the address of the entity for whom dialogues are maintained - :return: None + :param kwargs: the keyword arguments """ def role_from_first_message( # pylint: disable=unused-argument @@ -108,7 +108,6 @@ def handle(self, message: Message) -> None: Handle an internal message from the skills. :param message: the internal message - :return: None """ if isinstance(message, self.signing_msg_class): self._handle_signing_message(message) @@ -124,7 +123,6 @@ def _handle_signing_message(self, signing_msg: SigningMessage) -> None: Handle a signing message. :param signing_msg: the transaction message - :return: None """ signing_dialogue = self.signing_dialogues.update(signing_msg) # type: ignore if signing_dialogue is None or not isinstance( @@ -159,7 +157,6 @@ def _handle_message_signing( :param signing_msg: the signing message :param signing_dialogue: the signing dialogue - :return: None """ performative = self.signing_msg_class.Performative.ERROR kwargs = { @@ -191,7 +188,6 @@ def _handle_transaction_signing( :param signing_msg: the signing message :param signing_dialogue: the signing dialogue - :return: None """ performative = self.signing_msg_class.Performative.ERROR kwargs = { diff --git a/aea/decision_maker/gop.py b/aea/decision_maker/gop.py index 6e6ef0523b..4a171cf498 100644 --- a/aea/decision_maker/gop.py +++ b/aea/decision_maker/gop.py @@ -80,7 +80,6 @@ def update(self, new_status: Status) -> None: Update the goal pursuit readiness. :param new_status: the new status - :return: None """ self._status = new_status @@ -91,11 +90,7 @@ class OwnershipState(BaseOwnershipState): __slots__ = ("_amount_by_currency_id", "_quantities_by_good_id") def __init__(self) -> None: - """ - Instantiate an ownership state object. - - :param decision_maker: the decision maker - """ + """Instantiate an ownership state object.""" self._amount_by_currency_id = None # type: Optional[CurrencyHoldings] self._quantities_by_good_id = None # type: Optional[GoodHoldings] @@ -110,6 +105,7 @@ def set( # pylint: disable=arguments-differ :param amount_by_currency_id: the currency endowment of the agent in this state. :param quantities_by_good_id: the good endowment of the agent in this state. + :param kwargs: the keyword arguments. """ if amount_by_currency_id is None: # pragma: nocover raise ValueError("Must provide amount_by_currency_id.") @@ -136,7 +132,7 @@ def apply_delta( # pylint: disable=arguments-differ :param delta_amount_by_currency_id: the delta in the currency amounts :param delta_quantities_by_good_id: the delta in the quantities by good - :return: None + :param kwargs: the keyword arguments """ if delta_amount_by_currency_id is None: # pragma: nocover raise ValueError("Must provide delta_amount_by_currency_id.") @@ -249,7 +245,6 @@ def update(self, terms: Terms) -> None: Update the agent state from a transaction. :param terms: the transaction terms - :return: None """ if self._amount_by_currency_id is None or self._quantities_by_good_id is None: raise ValueError( # pragma: nocover @@ -304,6 +299,7 @@ def set( # pylint: disable=arguments-differ :param exchange_params_by_currency_id: the exchange params. :param utility_params_by_good_id: the utility params for every asset. + :param kwargs: the keyword arguments. """ if exchange_params_by_currency_id is None: # pragma: nocover raise ValueError("Must provide exchange_params_by_currency_id.") @@ -322,7 +318,7 @@ def is_initialized(self) -> bool: """ Get the initialization status. - Returns True if exchange_params_by_currency_id and utility_params_by_good_id are not None. + :return: True if exchange_params_by_currency_id and utility_params_by_good_id are not None. """ return (self._exchange_params_by_currency_id is not None) and ( self._utility_params_by_good_id is not None @@ -399,6 +395,7 @@ def marginal_utility( # pylint: disable=arguments-differ :param ownership_state: the ownership state against which to compute the marginal utility. :param delta_quantities_by_good_id: the change in good holdings :param delta_amount_by_currency_id: the change in money holdings + :param kwargs: the keyword arguments :return: the marginal utility score """ enforce(self.is_initialized, "Preferences params not set!") @@ -521,7 +518,7 @@ def __init__(self, self_address: Address, **kwargs: Any) -> None: Initialize dialogues. :param self_address: the address of the entity for whom dialogues are maintained - :return: None + :param kwargs: the keyword arguments """ def role_from_first_message( # pylint: disable=unused-argument @@ -559,7 +556,7 @@ def __init__(self, self_address: Address, **kwargs: Any) -> None: Initialize dialogues. :param self_address: the address of the entity for whom dialogues are maintained - :return: None + :param kwargs: the keyword arguments """ def role_from_first_message( # pylint: disable=unused-argument @@ -619,7 +616,6 @@ def handle(self, message: Message) -> None: Handle an internal message from the skills. :param message: the internal message - :return: None """ if isinstance(message, self.signing_msg_class): self._handle_signing_message(message) @@ -637,7 +633,6 @@ def _handle_signing_message(self, signing_msg: SigningMessage) -> None: Handle a signing message. :param signing_msg: the transaction message - :return: None """ if not self.context.goal_pursuit_readiness.is_ready: self.logger.debug( @@ -680,7 +675,6 @@ def _handle_message_signing( :param signing_msg: the signing message :param signing_dialogue: the signing dialogue - :return: None """ performative = self.signing_msg_class.Performative.ERROR kwargs = { @@ -713,7 +707,6 @@ def _handle_transaction_signing( :param signing_msg: the signing message :param signing_dialogue: the signing dialogue - :return: None """ performative = self.signing_msg_class.Performative.ERROR kwargs = { @@ -752,8 +745,7 @@ def _handle_state_update_message( """ Handle a state update message. - :param state_update_message: the state update message - :return: None + :param state_update_msg: the state update message """ state_update_dialogue = self.state_update_dialogues.update(state_update_msg) if state_update_dialogue is None or not isinstance( diff --git a/aea/decision_maker/scaffold.py b/aea/decision_maker/scaffold.py index 5711516871..5232f51929 100644 --- a/aea/decision_maker/scaffold.py +++ b/aea/decision_maker/scaffold.py @@ -58,6 +58,5 @@ def handle(self, message: Message) -> None: - check transactions satisfy the preferences :param message: the message - :return: None """ raise NotImplementedError From 77eecfe3647b0cd9a76b8458206f1e06a1bcf79f Mon Sep 17 00:00:00 2001 From: David Minarsch Date: Tue, 1 Jun 2021 14:02:31 +0100 Subject: [PATCH 127/147] chore: fix docstrings in aea modules e-t --- aea/error_handler/default.py | 5 +- aea/error_handler/scaffold.py | 3 -- aea/helpers/acn/agent_record.py | 8 +-- aea/helpers/async_friendly_queue.py | 7 ++- aea/helpers/async_utils.py | 9 ++-- aea/helpers/base.py | 29 ++++++----- aea/helpers/exec_timeout.py | 45 +++-------------- aea/helpers/file_io.py | 6 ++- aea/helpers/install_dependency.py | 3 +- aea/helpers/ipfs/base.py | 3 +- aea/helpers/logging.py | 1 + aea/helpers/multiaddr/base.py | 2 +- aea/helpers/multiple_executor.py | 7 +-- aea/helpers/pipe.py | 20 +++++--- .../preference_representations/base.py | 3 +- aea/helpers/profiling.py | 2 + aea/helpers/search/models.py | 14 +++--- aea/helpers/storage/backends/base.py | 3 +- aea/helpers/storage/backends/sqlite.py | 9 +--- aea/helpers/storage/generic_storage.py | 2 - aea/helpers/sym_link.py | 4 ++ aea/helpers/transaction/base.py | 18 +++---- aea/helpers/yaml_utils.py | 8 ++- aea/mail/base.py | 1 - aea/manager/manager.py | 33 ++++++------- aea/manager/project.py | 9 ++-- aea/protocols/base.py | 13 ++--- aea/protocols/dialogue/base.py | 23 +++------ aea/protocols/generator/base.py | 5 +- aea/protocols/generator/common.py | 14 +----- .../generator/extract_specification.py | 6 +-- aea/protocols/generator/validate.py | 1 + aea/protocols/scaffold/message.py | 1 + aea/registries/base.py | 39 +++------------ aea/registries/filter.py | 6 +-- aea/registries/resources.py | 14 +----- aea/skills/base.py | 49 +++++++++---------- aea/skills/behaviours.py | 25 +++++----- aea/skills/scaffold/behaviours.py | 18 ++----- aea/skills/scaffold/handlers.py | 13 +---- aea/skills/tasks.py | 46 +++++------------ aea/test_tools/generic.py | 8 +-- aea/test_tools/test_cases.py | 41 +++++----------- aea/test_tools/test_skill.py | 2 +- 44 files changed, 201 insertions(+), 377 deletions(-) diff --git a/aea/error_handler/default.py b/aea/error_handler/default.py index 249df74fa0..a8bc9d05be 100644 --- a/aea/error_handler/default.py +++ b/aea/error_handler/default.py @@ -47,7 +47,7 @@ def send_unsupported_protocol(self, envelope: Envelope, logger: Logger) -> None: Handle the received envelope in case the protocol is not supported. :param envelope: the envelope - :return: None + :param logger: the logger """ self.unsupported_protocol_count += 1 logger.warning( @@ -63,7 +63,6 @@ def send_decoding_error( :param envelope: the envelope :param exception: the exception raised during decoding :param logger: the logger - :return: None """ self.decoding_error_count += 1 logger.warning( @@ -78,7 +77,7 @@ def send_no_active_handler( :param envelope: the envelope :param reason: the reason for the failure - :return: None + :param logger: the logger """ self.no_active_handler_count += 1 logger.warning( diff --git a/aea/error_handler/scaffold.py b/aea/error_handler/scaffold.py index 20872196b7..5d8a94f138 100644 --- a/aea/error_handler/scaffold.py +++ b/aea/error_handler/scaffold.py @@ -33,7 +33,6 @@ def send_unsupported_protocol(self, envelope: Envelope, logger: Logger) -> None: :param envelope: the envelope :param logger: the logger - :return: None """ raise NotImplementedError @@ -46,7 +45,6 @@ def send_decoding_error( :param envelope: the envelope :param exception: the exception raised during decoding :param logger: the logger - :return: None """ raise NotImplementedError @@ -59,6 +57,5 @@ def send_no_active_handler( :param envelope: the envelope :param reason: the reason for the failure :param logger: the logger - :return: None """ raise NotImplementedError diff --git a/aea/helpers/acn/agent_record.py b/aea/helpers/acn/agent_record.py index fb5eeb17b7..37e8a25b20 100644 --- a/aea/helpers/acn/agent_record.py +++ b/aea/helpers/acn/agent_record.py @@ -59,12 +59,8 @@ def __init__( :param representative_public_key: representative's public key :param identifier: certificate identifier. :param ledger_id: ledger identifier the request is referring to. - :param not_before: specify the lower bound for certificate validity. - If it is a string, it must follow the format: 'YYYY-MM-DD'. It - will be interpreted as timezone UTC-0. - :param not_before: specify the lower bound for certificate validity. - if it is a string, it must follow the format: 'YYYY-MM-DD' It - will be interpreted as timezone UTC-0. + :param not_before: specify the lower bound for certificate validity. If it is a string, it must follow the format: 'YYYY-MM-DD'. It will be interpreted as timezone UTC-0. + :param not_after: specify the lower bound for certificate validity. If it is a string, it must follow the format: 'YYYY-MM-DD'. It will be interpreted as timezone UTC-0. :param message_format: message format used for signing :param signature: proof-of-representation of this AgentRecord """ diff --git a/aea/helpers/async_friendly_queue.py b/aea/helpers/async_friendly_queue.py index 9e628bbe69..7c2e4f541d 100644 --- a/aea/helpers/async_friendly_queue.py +++ b/aea/helpers/async_friendly_queue.py @@ -40,7 +40,8 @@ def put( # pylint: disable=signature-differs Put an item into the queue. :param item: item to put in the queue - :param args, kwargs: similar to queue.Queue.put + :param args: similar to queue.Queue.put + :param kwargs: similar to queue.Queue.put """ super().put(item, *args, **kwargs) if self._non_empty_waiters: @@ -62,7 +63,9 @@ def get( # pylint: disable=signature-differs """ Get an item into the queue. - :param args, kwargs: similar to queue.Queue.get + :param args: similar to queue.Queue.get + :param kwargs: similar to queue.Queue.get + :return: similar to queue.Queue.get """ return super().get(*args, **kwargs) diff --git a/aea/helpers/async_utils.py b/aea/helpers/async_utils.py index 9892108078..854c330572 100644 --- a/aea/helpers/async_utils.py +++ b/aea/helpers/async_utils.py @@ -102,8 +102,6 @@ def add_callback(self, callback_fn: Callable[[Any], None]) -> None: Add callback to track state changes. :param callback_fn: callable object to be called on state changed. - - :return: None """ self._callbacks.append(callback_fn) @@ -176,8 +174,7 @@ def transit( :param initial: set state on context enter, not_set by default :param success: set state on context block done, not_set by default :param fail: set state on context block raises exception, not_set by default - - :return: None + :yield: generator """ try: if initial is not not_set: @@ -291,6 +288,7 @@ def result(self, timeout: Optional[float] = None) -> Any: Wait for coroutine execution result. :param timeout: optional timeout to wait in seconds. + :return: result """ return self._future.result(timeout) @@ -339,6 +337,7 @@ def call(self, coro: Awaitable) -> Any: Run a coroutine inside the event loop. :param coro: a coroutine to run. + :return: task """ return AnotherThreadTask(coro, self._loop) @@ -379,8 +378,6 @@ def __init__( :param loop: asyncio event loop to use. :param threaded: bool. start in thread if True. - - :return: None """ if loop and threaded: raise ValueError( diff --git a/aea/helpers/base.py b/aea/helpers/base.py index d6ba4dcd30..d559e977c3 100644 --- a/aea/helpers/base.py +++ b/aea/helpers/base.py @@ -117,7 +117,7 @@ def load_module(dotted_path: str, filepath: Path) -> types.ModuleType: :param dotted_path: the dotted save_path of the package/module. :param filepath: the file to the package/module. - :return: None + :return: module type :raises ValueError: if the filepath provided is not a module. :raises Exception: if the execution of the module raises exception. """ @@ -132,7 +132,6 @@ def load_env_file(env_file: str) -> None: Load the content of the environment file into the process environment. :param env_file: save_path to the env file. - :return: None. """ load_dotenv(dotenv_path=Path(env_file), override=False) @@ -149,7 +148,6 @@ def sigint_crossplatform(process: subprocess.Popen) -> None: # pragma: nocover 'send_signal' that gives more flexibility in this terms. :param process: the process to send the signal to. - :return: None """ if os.name == "posix": process.send_signal(signal.SIGINT) # pylint: disable=no-member @@ -165,6 +163,8 @@ def win_popen_kwargs() -> dict: Help to handle ctrl c properly. Return empty dict if platform is not win32 + + :return: windows popen kwargs """ kwargs: dict = {} @@ -184,8 +184,7 @@ def send_control_c( Send ctrl-C cross-platform to terminate a subprocess. :param process: the process to send the signal to. - - :return: None + :param kill_group: whether or not to kill group """ if platform.system() == "Windows": if process.stdin: # cause ctrl-c event will be handled with stdin @@ -303,6 +302,7 @@ def try_decorator( :param error_message: message template with one `{}` for exception :param default_return: value to return on exception, by default None :param logger_method: name of the logger method or callable to print logs + :return: the callable """ # for pydocstyle @@ -343,6 +343,7 @@ def retry_decorator( :param error_message: message template with one `{}` for exception :param delay: number of seconds to sleep between retries. default 0 :param logger_method: name of the logger method or callable to print logs + :return: the callable """ # for pydocstyle @@ -372,6 +373,7 @@ def exception_log_and_reraise(log_method: Callable, message: str) -> Generator: :param log_method: function to print log :param message: message template to add error text. + :yield: the generator """ try: yield @@ -406,7 +408,7 @@ def recursive_update( :param to_update: the dictionary to update. :param new_values: the dictionary of new values to replace. - :return: None + :param allow_new_values: whether or not to allow new values. """ for key, value in new_values.items(): if (not allow_new_values) and key not in to_update: @@ -646,12 +648,8 @@ def __init__( :param public_key: the public key, or the key id. :param identifier: certificate identifier. :param ledger_id: ledger identifier the request is referring to. - :param not_before: specify the lower bound for certificate validity. - If it is a string, it must follow the format: 'YYYY-MM-DD'. It - will be interpreted as timezone UTC-0. - :param not_before: specify the lower bound for certificate validity. - if it is a string, it must follow the format: 'YYYY-MM-DD' It - will be interpreted as timezone UTC-0. + :param not_before: specify the lower bound for certificate validity. If it is a string, it must follow the format: 'YYYY-MM-DD'. It will be interpreted as timezone UTC-0. + :param not_after: specify the lower bound for certificate validity. If it is a string, it must follow the format: 'YYYY-MM-DD'. It will be interpreted as timezone UTC-0. :param message_format: message format used for signing :param save_path: the save_path where to save the certificate. """ @@ -705,6 +703,8 @@ def _parse_public_key(self, public_key_str: str) -> None: It first tries to parse it as an identifier, and in case of failure as a sequence of hexadecimals, starting with "0x". + + :param public_key_str: the public key """ with contextlib.suppress(ValueError): # if this raises ValueError, we don't return @@ -775,6 +775,8 @@ def save_path(self) -> Path: Note: if the path is *not* absolute, then the actual save path might depend on the context. + + :return: the save path """ return self._save_path @@ -840,6 +842,7 @@ def construct_message( :param not_before_string: signature not valid before :param not_after_string: signature not valid after :param message_format: message format used for signing + :return: the message """ message = message_format.format( public_key=public_key, @@ -956,6 +959,8 @@ def myfunction(): def myfunction(): ... + :param decorator: a decorator callable + :return: a decorator callable """ @wraps(decorator) diff --git a/aea/helpers/exec_timeout.py b/aea/helpers/exec_timeout.py index 436e99318e..3ef99be12c 100644 --- a/aea/helpers/exec_timeout.py +++ b/aea/helpers/exec_timeout.py @@ -42,11 +42,7 @@ def __init__(self) -> None: self._cancelled_by_timeout = False def set_cancelled_by_timeout(self) -> None: - """ - Set code was terminated cause timeout. - - :return: None - """ + """Set code was terminated cause timeout.""" self._cancelled_by_timeout = True def is_cancelled_by_timeout(self) -> bool: @@ -105,7 +101,9 @@ def __exit__( """ Exit context manager. - :return: bool + :param exc_type: the exception type + :param exc_val: the exception + :param exc_tb: the traceback """ if self.timeout: self._remove_timeout_watch() @@ -119,8 +117,6 @@ def _set_timeout_watch(self) -> None: Start control over execution time. Should be implemented in concrete class. - - :return: None """ raise NotImplementedError # pragma: nocover @@ -130,8 +126,6 @@ def _remove_timeout_watch(self) -> None: Stop control over execution time. Should be implemented in concrete class. - - :return: None """ raise NotImplementedError # pragma: nocover @@ -144,20 +138,12 @@ class ExecTimeoutSigAlarm(BaseExecTimeout): # pylint: disable=too-few-public-me """ def _set_timeout_watch(self) -> None: - """ - Start control over execution time. - - :return: None - """ + """Start control over execution time.""" signal.setitimer(signal.ITIMER_REAL, self.timeout, 0) signal.signal(signal.SIGALRM, self._on_timeout) def _remove_timeout_watch(self) -> None: - """ - Stop control over execution time. - - :return: None - """ + """Stop control over execution time.""" signal.setitimer(signal.ITIMER_REAL, 0, 0) @@ -193,8 +179,6 @@ def start(cls) -> None: Start supervisor thread to check timeouts. Supervisor starts once but number of start counted. - - :return: None """ with cls._lock: cls._start_count += 1 @@ -217,7 +201,6 @@ def stop(cls, force: bool = False) -> None: Actual stop performed on force == True or if number of stops == number of starts :param force: force stop regardless number of start. - :return: None """ with cls._lock: if not cls._supervisor_thread: # pragma: nocover @@ -249,21 +232,13 @@ async def wait_stopped() -> None: cls._loop.run_until_complete(wait_stopped()) # type: ignore async def _guard_task(self) -> None: - """ - Task to terminate thread on timeout. - - :return: None - """ + """Task to terminate thread on timeout.""" await asyncio.sleep(self.timeout) self._set_thread_exception(self._thread_id, self.exception_class) # type: ignore @staticmethod def _set_thread_exception(thread_id: int, exception_class: Type[Exception]) -> None: - """ - Terminate code execution in specific thread by setting exception. - - :return: None - """ + """Terminate code execution in specific thread by setting exception.""" ctypes.pythonapi.PyThreadState_SetAsyncExc( ctypes.c_long(thread_id), ctypes.py_object(exception_class) ) @@ -274,8 +249,6 @@ def _set_timeout_watch(self) -> None: Set task checking code execution time. ExecTimeoutThreadGuard.start is required at least once in project before usage! - - :return: None """ if not self._supervisor_thread: _default_logger.warning( @@ -293,8 +266,6 @@ def _remove_timeout_watch(self) -> None: Stop control over execution time. Cancel task checking code execution time. - - :return: None """ if self._future_guard_task and not self._future_guard_task.done(): self._future_guard_task.cancel() diff --git a/aea/helpers/file_io.py b/aea/helpers/file_io.py index 78cd42287e..d498fb9780 100644 --- a/aea/helpers/file_io.py +++ b/aea/helpers/file_io.py @@ -86,6 +86,8 @@ def lock_file( """Lock file in context manager. :param file_descriptor: file descriptor of file to lock. + :param logger: the logger. + :yield: generator """ with exception_log_and_reraise( logger.error, f"Couldn't acquire lock for file {file_descriptor.name}: {{}}", @@ -125,8 +127,10 @@ def envelope_from_bytes( """ Decode bytes to get the envelope. + :param bytes_: the encoded envelope + :param separator: the separator used + :param logger: the logger :return: Envelope - :raise: Exception """ logger.debug("processing: {!r}".format(bytes_)) envelope = None # type: Optional[Envelope] diff --git a/aea/helpers/install_dependency.py b/aea/helpers/install_dependency.py index a6736b1806..5acdf2df4a 100644 --- a/aea/helpers/install_dependency.py +++ b/aea/helpers/install_dependency.py @@ -34,8 +34,7 @@ def install_dependency( :param dependency_name: name of the python package :param dependency: Dependency specification - - :return: None + :param logger: the logger """ try: pip_args = dependency.get_pip_install_args() diff --git a/aea/helpers/ipfs/base.py b/aea/helpers/ipfs/base.py index dd83a1172c..633a5c5790 100644 --- a/aea/helpers/ipfs/base.py +++ b/aea/helpers/ipfs/base.py @@ -39,7 +39,7 @@ def _dos2unix(file_content: bytes) -> bytes: Replace occurrences of Windows line terminator CR/LF with only LF. :param file_content: the content of the file. - :return the same content but with the line terminator + :return: the same content but with the line terminator """ return re.sub(b"\r\n", b"\n", file_content, flags=re.M) @@ -73,6 +73,7 @@ def get(self, file_path: str) -> str: Get the IPFS hash for a single file. :param file_path: the file path + :return: the ipfs hash """ file_b = _read(file_path) file_pb = self._pb_serialize_file(file_b) diff --git a/aea/helpers/logging.py b/aea/helpers/logging.py index 7e4c4d6c1d..897d439caf 100644 --- a/aea/helpers/logging.py +++ b/aea/helpers/logging.py @@ -37,6 +37,7 @@ def __init__(self, logger: Logger, agent_name: str) -> None: """ Initialize the logger adapter. + :param logger: the logger. :param agent_name: the agent name. """ super().__init__(logger, dict(agent_name=agent_name)) diff --git a/aea/helpers/multiaddr/base.py b/aea/helpers/multiaddr/base.py index 30d9a0f50e..6d94aee9c5 100644 --- a/aea/helpers/multiaddr/base.py +++ b/aea/helpers/multiaddr/base.py @@ -58,7 +58,6 @@ def update(self, input_data: bytes) -> None: Update data to hash. :param input_data: the data - :return: None """ self._digest += input_data @@ -166,6 +165,7 @@ def from_string(cls, maddr: str) -> "MultiAddr": Construct a MultiAddr object from its string format :param maddr: multiaddress string + :return: multiaddress object """ parts = maddr.split("/") if len(parts) != 7 or not parts[4].isdigit(): diff --git a/aea/helpers/multiple_executor.py b/aea/helpers/multiple_executor.py index 4042b23f01..89dc98e878 100644 --- a/aea/helpers/multiple_executor.py +++ b/aea/helpers/multiple_executor.py @@ -134,7 +134,7 @@ def create_async_task( Raise error, cause async mode is not supported, cause this task for multiprocess executor only. :param loop: the event loop - :return: task to run in asyncio loop. + :raises ValueError: async task construction not possible """ raise ValueError( "This task was designed only for multiprocess executor, not for async!" @@ -203,6 +203,7 @@ async def _wait_tasks_complete( Wait tasks execution to complete. :param skip_exceptions: skip exceptions if raised in tasks + :param on_stop: bool, indicating if stopping """ if not on_stop: self._is_running = True @@ -238,7 +239,6 @@ async def _handle_exception( :param task: task exception handled in :param exc: Exception raised - :return: None """ _default_logger.exception(f"Exception raised during {task.id} running.") _default_logger.info(f"Exception raised during {task.id} running.") @@ -274,7 +274,6 @@ def _stop_task(task: AbstractExecutorTask) -> None: Stop particular task. :param task: AbstractExecutorTask instance to stop. - :return: None """ task.stop() @@ -383,7 +382,6 @@ def start(self, threaded: bool = False) -> None: Run agents. :param threaded: run in dedicated thread without blocking current thread. - :return: None """ if threaded: self._thread = Thread(target=self._executor.start, daemon=True) @@ -396,7 +394,6 @@ def stop(self, timeout: Optional[float] = None) -> None: Stop agents. :param timeout: timeout in seconds to wait thread stopped, only if started in thread mode. - :return: None """ self._executor.stop() if self._thread is not None: diff --git a/aea/helpers/pipe.py b/aea/helpers/pipe.py index 5e450da4cb..3775b48b8a 100644 --- a/aea/helpers/pipe.py +++ b/aea/helpers/pipe.py @@ -51,6 +51,7 @@ async def connect(self, timeout: float = PIPE_CONN_TIMEOUT) -> bool: Connect to communication channel :param timeout: timeout for other end to connect + :return: connection status """ @abstractmethod @@ -75,11 +76,7 @@ async def read(self) -> Optional[bytes]: @abstractmethod async def close(self) -> None: - """ - Close the communication channel. - - :return: None - """ + """Close the communication channel.""" class IPCChannel(IPCChannelClient): @@ -119,6 +116,8 @@ def __init__( :param in_path: rendezvous point for incoming data :param out_path: rendezvous point for outgoing data + :param logger: the logger + :param loop: the event loop """ self.logger = logger @@ -262,6 +261,8 @@ def __init__( :param reader: established asyncio reader :param writer: established asyncio writer + :param logger: the logger + :param loop: the event loop """ self.logger = logger @@ -345,6 +346,7 @@ async def connect(self, timeout: float = PIPE_CONN_TIMEOUT) -> bool: Setup communication channel and wait for other end to connect. :param timeout: timeout for the connection to be established + :return: connection status """ if self._loop is None: @@ -394,7 +396,7 @@ async def read(self) -> Optional[bytes]: """ Read from channel. - :param data: read bytes + :return: read bytes """ if self._sock is None: raise ValueError("Socket pipe not connected.") # pragma: nocover @@ -508,6 +510,8 @@ def __init__( # pylint: disable=unused-argument :param in_path: rendezvous point for incoming data :param out_path: rendezvous point for outgoing data + :param logger: the logger + :param loop: the event loop """ self.logger = logger self._loop = loop @@ -528,6 +532,7 @@ async def connect(self, timeout: float = PIPE_CONN_TIMEOUT) -> bool: Connect to the other end of the communication channel. :param timeout: timeout for connection to be established + :return: connection status """ if self._loop is None: self._loop = asyncio.get_event_loop() @@ -601,6 +606,8 @@ def __init__( :param in_path: rendezvous point for incoming data :param out_path: rendezvous point for outgoing data + :param logger: the logger + :param loop: the event loop """ self.logger = logger @@ -615,6 +622,7 @@ async def connect(self, timeout: float = PIPE_CONN_TIMEOUT) -> bool: Connect to the other end of the communication channel. :param timeout: timeout for connection to be established + :return: connection status """ if self._loop is None: diff --git a/aea/helpers/preference_representations/base.py b/aea/helpers/preference_representations/base.py index f15267b16b..a0cf3f0546 100644 --- a/aea/helpers/preference_representations/base.py +++ b/aea/helpers/preference_representations/base.py @@ -35,8 +35,7 @@ def logarithmic_utility( :param utility_params_by_good_id: utility params by good identifier :param quantities_by_good_id: quantities by good identifier - :param quantity_shift: a non-negative factor to shift the quantities in the utility function (to - ensure the natural logarithm can be used on the entire range of quantities) + :param quantity_shift: a non-negative factor to shift the quantities in the utility function (to ensure the natural logarithm can be used on the entire range of quantities) :return: utility value """ enforce( diff --git a/aea/helpers/profiling.py b/aea/helpers/profiling.py index 575fcd5dce..31ac404f06 100644 --- a/aea/helpers/profiling.py +++ b/aea/helpers/profiling.py @@ -89,6 +89,8 @@ def __init__( Init profiler. :param period: delay between profiling output in seconds. + :param objects_instances_to_count: object to count + :param objects_created_to_count: object created to count :param output_function: function to display output, one str argument. """ if period < 1: # pragma: nocover diff --git a/aea/helpers/search/models.py b/aea/helpers/search/models.py index aab95c8e68..7e9d1397cb 100644 --- a/aea/helpers/search/models.py +++ b/aea/helpers/search/models.py @@ -266,6 +266,7 @@ def __init__( :param name: the name of the data model. :param attributes: the attributes of the data model. + :param description: the data model description. """ self.name: str = name self.attributes = sorted( @@ -365,7 +366,7 @@ def __init__( :param values: the values in the description. :param data_model: the data model (optional) - :pram data_model_name: the data model name if a datamodel is created on the fly. + :param data_model_name: the data model name if a datamodel is created on the fly. """ _values = deepcopy(values) self._values = _values @@ -503,8 +504,6 @@ def encode(cls, description_pb: Any, description: "Description") -> None: :param description_pb: the protocol buffer object whose type corresponds with this class. :param description: an instance of this class to be encoded in the protocol buffer object. - - :return: None """ description_bytes_pb = description._encode() # pylint: disable=protected-access description_bytes_bytes = description_bytes_pb.SerializeToString() @@ -627,7 +626,7 @@ def check_validity(self) -> bool: """ Check the validity of the input provided. - :return: None + :return: boolean to indicate validity :raises ValueError: if the value is not valid wrt the constraint type. """ try: @@ -765,6 +764,7 @@ def get_data_type(self) -> Type[ATTRIBUTE_TYPES]: >>> c.get_data_type() + :return: data type """ if isinstance(self.value, (list, tuple, set)): value = next(iter(self.value)) @@ -1067,16 +1067,15 @@ def check_validity(self) -> None: # pylint: disable=no-self-use # pragma: noco """ Check whether a Constraint Expression satisfies some basic requirements. - :return ``None`` :raises ValueError: if the object does not satisfy some requirements. """ - return None @staticmethod def _encode(expression: Any) -> models_pb2.Query.ConstraintExpr: # type: ignore """ Encode an instance of this class into a protocol buffer object. + :param expression: an expression :return: the matching protocol buffer object """ constraint_expression_pb = models_pb2.Query.ConstraintExpr() # type: ignore @@ -1532,6 +1531,7 @@ def is_valid(self, data_model: Optional[DataModel]) -> bool: """ Given a data model, check whether the query is valid for that data model. + :param data_model: optional datamodel :return: ``True`` if the query is compliant with the data model, ``False`` otherwise. """ if data_model is None: @@ -1605,8 +1605,6 @@ def encode(cls, query_pb: Any, query: "Query") -> None: :param query_pb: the protocol buffer object wrapping an object that corresponds with this class. :param query: an instance of this class to be encoded in the protocol buffer object. - - :return: None """ query_bytes_pb = query._encode() # pylint: disable=protected-access query_bytes_bytes = query_bytes_pb.SerializeToString() diff --git a/aea/helpers/storage/backends/base.py b/aea/helpers/storage/backends/base.py index e441fa1003..cb0c46b20c 100644 --- a/aea/helpers/storage/backends/base.py +++ b/aea/helpers/storage/backends/base.py @@ -41,7 +41,8 @@ def _check_collection_name(self, collection_name: str) -> None: """ Check collection name is valid. - raises ValueError if bad collection name provided. + :param collection_name: the collection name. + :raises ValueError: if bad collection name provided. """ if not self.VALID_COL_NAME.match(collection_name): raise ValueError( diff --git a/aea/helpers/storage/backends/sqlite.py b/aea/helpers/storage/backends/sqlite.py index 13338ecfd7..75fdb1c182 100644 --- a/aea/helpers/storage/backends/sqlite.py +++ b/aea/helpers/storage/backends/sqlite.py @@ -117,8 +117,7 @@ async def ensure_collection(self, collection_name: str) -> None: """ Create collection if not exits. - :param collection_name: str. - :return: None + :param collection_name: name of the collection. """ self._check_collection_name(collection_name) sql = f"""CREATE TABLE IF NOT EXISTS {collection_name} ( @@ -136,7 +135,6 @@ async def put( :param collection_name: str. :param object_id: str object id :param object_body: python dict, json compatible. - :return: None """ self._check_collection_name(collection_name) sql = f"""INSERT OR REPLACE INTO {collection_name} (object_id, object_body) @@ -172,8 +170,6 @@ async def remove(self, collection_name: str, object_id: str) -> None: :param collection_name: str. :param object_id: str object id - - :return: None """ self._check_collection_name(collection_name) sql = f"""DELETE FROM {collection_name} WHERE object_id = ?;""" # nosec @@ -188,8 +184,7 @@ async def find( :param collection_name: str. :param field: field name to search: example "parent.field" :param equals: value field should be equal to - - :return: None + :return: list of object ids and body """ self._check_collection_name(collection_name) sql = f"""SELECT object_id, object_body FROM {collection_name} WHERE json_extract(object_body, ?) = ?;""" # nosec diff --git a/aea/helpers/storage/generic_storage.py b/aea/helpers/storage/generic_storage.py index fe2b3b32e9..0c59f16bdc 100644 --- a/aea/helpers/storage/generic_storage.py +++ b/aea/helpers/storage/generic_storage.py @@ -185,8 +185,6 @@ def __init__( :param storage_uri: configuration string for storage. :param loop: asyncio event loop to use. :param threaded: bool. start in thread if True. - - :return: None """ super().__init__(loop=loop, threaded=threaded) self._storage_uri = storage_uri diff --git a/aea/helpers/sym_link.py b/aea/helpers/sym_link.py index d9a5a0c784..52e330dd63 100644 --- a/aea/helpers/sym_link.py +++ b/aea/helpers/sym_link.py @@ -81,6 +81,10 @@ def create_symlink(link_path: Path, target_path: Path, root_path: Path) -> int: cd directory_1 && ln -s ../../directory_1/target_path symbolic_link + :param link_path: the source path + :param target_path: the target path + :param root_path: the root path + :return: exit code """ working_directory = link_path.parent target_relative_to_root = target_path.relative_to(root_path) diff --git a/aea/helpers/transaction/base.py b/aea/helpers/transaction/base.py index e03e6554c1..86d71ac2c3 100644 --- a/aea/helpers/transaction/base.py +++ b/aea/helpers/transaction/base.py @@ -69,7 +69,6 @@ def encode( :param raw_transaction_protobuf_object: the protocol buffer object whose type corresponds with this class. :param raw_transaction_object: an instance of this class to be encoded in the protocol buffer object. - :return: None """ raw_transaction_dict = { @@ -160,7 +159,6 @@ def encode( :param raw_message_protobuf_object: the protocol buffer object whose type corresponds with this class. :param raw_message_object: an instance of this class to be encoded in the protocol buffer object. - :return: None """ raw_message_dict = { "ledger_id": raw_message_object.ledger_id, @@ -245,7 +243,6 @@ def encode( :param signed_transaction_protobuf_object: the protocol buffer object whose type corresponds with this class. :param signed_transaction_object: an instance of this class to be encoded in the protocol buffer object. - :return: None """ signed_transaction_dict = { "ledger_id": signed_transaction_object.ledger_id, @@ -337,7 +334,6 @@ def encode( :param signed_message_protobuf_object: the protocol buffer object whose type corresponds with this class. :param signed_message_object: an instance of this class to be encoded in the protocol buffer object. - :return: None """ signed_message_dict = { "ledger_id": signed_message_object.ledger_id, @@ -419,7 +415,6 @@ def encode(state_protobuf_object: Any, state_object: "State") -> None: :param state_protobuf_object: the protocol buffer object whose type corresponds with this class. :param state_object: an instance of this class to be encoded in the protocol buffer object. - :return: None """ state_dict = { "ledger_id": state_object.ledger_id, @@ -496,10 +491,11 @@ def __init__( :param counterparty_address: the counterparty address of the transaction. :param amount_by_currency_id: the amount by the currency of the transaction. :param quantities_by_good_id: a map from good id to the quantity of that good involved in the transaction. - :param is_sender_payable_tx_fee: whether the sender or counterparty pays the tx fee. :param nonce: nonce to be included in transaction to discriminate otherwise identical transactions. + :param is_sender_payable_tx_fee: whether the sender or counterparty pays the tx fee. :param fee_by_currency_id: the fee associated with the transaction. :param is_strict: whether or not terms must have quantities and amounts of opposite signs. + :param kwargs: keyword arguments """ self._ledger_id = ledger_id self._sender_address = sender_address @@ -837,14 +833,15 @@ def get_hash( """ Generate a hash from transaction information. - :param sender_addr: the sender address - :param counterparty_addr: the counterparty address + :param ledger_id: the ledger id + :param sender_address: the sender address + :param counterparty_address: the counterparty address :param good_ids: the list of good ids :param sender_supplied_quantities: the quantities supplied by the sender (must all be positive) :param counterparty_supplied_quantities: the quantities supplied by the counterparty (must all be positive) :param sender_payable_amount: the amount payable by the sender :param counterparty_payable_amount: the amount payable by the counterparty - :param tx_nonce: the nonce of the transaction + :param nonce: the nonce of the transaction :return: the hash """ if len(good_ids) == 0: @@ -894,7 +891,6 @@ def encode(terms_protobuf_object: Any, terms_object: "Terms") -> None: :param terms_protobuf_object: the protocol buffer object whose type corresponds with this class. :param terms_object: an instance of this class to be encoded in the protocol buffer object. - :return: None """ terms_dict = { "ledger_id": terms_object.ledger_id, @@ -1005,7 +1001,6 @@ def encode( :param transaction_digest_protobuf_object: the protocol buffer object whose type corresponds with this class. :param transaction_digest_object: an instance of this class to be encoded in the protocol buffer object. - :return: None """ transaction_digest_dict = { "ledger_id": transaction_digest_object.ledger_id, @@ -1096,7 +1091,6 @@ def encode( :param transaction_receipt_protobuf_object: the protocol buffer object whose type corresponds with this class. :param transaction_receipt_object: an instance of this class to be encoded in the protocol buffer object. - :return: None """ transaction_receipt_dict = { "ledger_id": transaction_receipt_object.ledger_id, diff --git a/aea/helpers/yaml_utils.py b/aea/helpers/yaml_utils.py index dd319065d7..e776ec3f84 100644 --- a/aea/helpers/yaml_utils.py +++ b/aea/helpers/yaml_utils.py @@ -41,6 +41,9 @@ def __init__(self, *args: Any, **kwargs: Any) -> None: Initialize the AEAYamlLoader. It adds a YAML Loader constructor to use 'OderedDict' to load the files. + + :param args: the positional arguments. + :param kwargs: the keyword arguments. """ super().__init__(*args, **kwargs) _AEAYamlLoader.add_constructor( @@ -71,6 +74,9 @@ def __init__(self, *args: Any, **kwargs: Any) -> None: Initialize the AEAYamlDumper. It adds a YAML Dumper representer to use 'OderedDict' to dump the files. + + :param args: the positional arguments. + :param kwargs: the keyword arguments. """ super().__init__(*args, **kwargs) _AEAYamlDumper.add_representer(OrderedDict, self._dict_representer) @@ -110,7 +116,6 @@ def yaml_dump(data: Dict, stream: Optional[TextIO] = None) -> None: :param data: the data to write. :param stream: (optional) the file to write on. - :return: None """ yaml.dump(data, stream=stream, Dumper=_AEAYamlDumper) # nosec @@ -121,6 +126,5 @@ def yaml_dump_all(data: Sequence[Dict], stream: Optional[TextIO] = None) -> None :param data: the data to write. :param stream: (optional) the file to write on. - :return: None """ yaml.dump_all(data, stream=stream, Dumper=_AEAYamlDumper) # nosec diff --git a/aea/mail/base.py b/aea/mail/base.py index 7782dbaad7..4b6de700ae 100644 --- a/aea/mail/base.py +++ b/aea/mail/base.py @@ -45,7 +45,6 @@ def __init__(self, uri_raw: str) -> None: Must follow: https://tools.ietf.org/html/rfc3986.html :param uri_raw: the raw form uri - :raises ValueError: if uri_raw is not RFC3986 compliant """ self._uri_raw = uri_raw diff --git a/aea/manager/manager.py b/aea/manager/manager.py index ecc2e1754a..d2579f692d 100644 --- a/aea/manager/manager.py +++ b/aea/manager/manager.py @@ -164,8 +164,6 @@ def __init__( :param registry_path: str. path to the local packages registry :param auto_add_remove_project: bool. add/remove project on the first agent add/last agent remove :param password: the password to encrypt/decrypt the private key. - - :return: None """ self.working_dir = working_dir self._auto_add_remove_project = auto_add_remove_project @@ -273,6 +271,7 @@ def add_error_callback( ) -> None: """Add error callback to call on error raised.""" self._error_callbacks.append(error_callback) + return self def start_manager( self, local: bool = False, remote: bool = False @@ -381,10 +380,10 @@ def add_project( registry, and then from remote registry in case of failure). :param public_id: the public if of the agent project. - :param local: whether or not to fetch from local registry. :param remote: whether or not to fetch from remote registry. :param restore: bool flag for restoring already fetched agent. + :return: self """ if public_id.to_any() in self._versionless_projects_set: raise ValueError( @@ -463,13 +462,10 @@ def add_agent( :param agent_name: unique name for the agent :param agent_overrides: overrides for agent config. :param component_overrides: overrides for component section. - :param config: agent config (used for agent re-creation). - :param local: whether or not to fetch from local registry. :param remote: whether or not to fetch from remote registry. :param restore: bool flag for restoring already fetched agent. - - :return: manager + :return: self """ agent_name = agent_name or public_id.name @@ -556,8 +552,7 @@ def set_agent_overrides( :param agent_name: str :param agent_overides: optional dict of agent config overrides :param components_overrides: optional list of dict of components overrides - - :return: None + :return: self """ if agent_name not in self._agents: # pragma: nocover raise ValueError(f"Agent with name {agent_name} does not exist!") @@ -566,6 +561,7 @@ def set_agent_overrides( raise ValueError("Agent is running. stop it first!") self._agents[agent_name].set_overrides(agent_overides, components_overrides) + return self def list_agents_info(self) -> List[Dict[str, Any]]: """ @@ -686,7 +682,7 @@ def stop_agent(self, agent_name: str) -> "MultiAgentManager": :param agent_name: agent name to stop - :return: None + :return: self """ if not self._is_agent_running(agent_name) or not self._thread or not self._loop: raise ValueError(f"{agent_name} is not running!") @@ -721,7 +717,7 @@ def stop_all_agents(self) -> "MultiAgentManager": """ Stop all agents running. - :return: None + :return: self """ agents_list = self.list_agents(running_only=True) self.stop_agents(agents_list) @@ -732,7 +728,8 @@ def stop_agents(self, agent_names: List[str]) -> "MultiAgentManager": """ Stop specified agents. - :return: None + :param agent_names: names of agents + :return: self """ for agent_name in agent_names: if not self._is_agent_running(agent_name): @@ -747,7 +744,8 @@ def start_agents(self, agent_names: List[str]) -> "MultiAgentManager": """ Stop specified agents. - :return: None + :param agent_names: names of agents + :return: self """ for agent_name in agent_names: self.start_agent(agent_name) @@ -758,6 +756,7 @@ def get_agent_alias(self, agent_name: str) -> AgentAlias: """ Return details about agent alias definition. + :param agent_name: name of agent :return: AgentAlias """ if agent_name not in self._agents: # pragma: nocover @@ -792,7 +791,7 @@ def _load_state( :param local: whether or not to fetch from local registry. :param remote: whether or not to fetch from remote registry. - :return: None + :return: Tuple of bool indicating load success, settings of loaded, list of failed :raises: ValueError if failed to load state. """ if not os.path.exists(self._save_path): @@ -836,10 +835,6 @@ def _load_state( return True, loaded_ok, failed_to_load def _save_state(self) -> None: - """ - Save MultiAgentManager state. - - :return: None. - """ + """Save MultiAgentManager state.""" with open_file(self._save_path, "w") as f: json.dump(self.dict_state, f, indent=4, sort_keys=True) diff --git a/aea/manager/project.py b/aea/manager/project.py index 04d8aa2a04..f3041ea3ac 100644 --- a/aea/manager/project.py +++ b/aea/manager/project.py @@ -109,10 +109,12 @@ def load( :param working_dir: the working directory :param public_id: the public id :param is_local: whether to fetch from local - :param is_remote whether to fetch from remote - :param verbosity: the logging verbosity of the CLI + :param is_remote: whether to fetch from remote + :param is_restore: whether to restore or not + :param cli_verbosity: the logging verbosity of the CLI :param registry_path: the path to the registry locally :param skip_consistency_check: consistency checks flag + :return: project """ ctx = Context( cwd=working_dir, verbosity=cli_verbosity, registry_path=registry_path @@ -167,8 +169,6 @@ def set_agent_config_from_data(self, json_data: List[Dict]) -> None: Set agent config instance constructed from json data. :param json_data: agent config json data - - :return: None """ self._agent_config = AEABuilder.loader.load_agent_config_from_json(json_data) self._ensure_private_keys() @@ -225,6 +225,7 @@ def _create_private_key( :param ledger: the ledger id :param replace: whether or not to replace an existing key :param is_connection: whether or not it is a connection key + :return: file path to private key """ file_name = ( f"{ledger}_connection_private.key" diff --git a/aea/protocols/base.py b/aea/protocols/base.py index c76ffdad78..2217cae1e1 100644 --- a/aea/protocols/base.py +++ b/aea/protocols/base.py @@ -71,7 +71,7 @@ def __init__(self, _body: Optional[Dict] = None, **kwargs: Any) -> None: """ Initialize a Message object. - :param body: the dictionary of values to hold. + :param _body: the dictionary of values to hold. :param kwargs: any additional value to add to the body. It will overwrite the body values. """ self._slots = self._SlotsCls() @@ -122,11 +122,7 @@ def has_sender(self) -> bool: @property def sender(self) -> Address: - """ - Get the sender of the message in Address form. - - :return the address - """ + """Get the sender of the message in Address form.""" if self._sender is None: raise ValueError("Message's 'Sender' field must be set.") # pragma: nocover return self._sender @@ -177,7 +173,6 @@ def _body(self, body: Dict) -> None: Set the body of the message. :param body: the body. - :return: None """ self._slots = self._SlotsCls() # new instance to clean up all data self._update_slots_from_dict(body) @@ -216,7 +211,6 @@ def set(self, key: str, value: Any) -> None: :param key: the key. :param value: the value. - :return: None """ try: setattr(self._slots, key, value) @@ -344,6 +338,7 @@ def __init__( :param configuration: the protocol configurations. :param message_class: the message class. + :param kwargs: the keyword arguments. """ super().__init__(configuration, **kwargs) self._message_class = message_class @@ -359,6 +354,7 @@ def from_dir(cls, directory: str, **kwargs: Any) -> "Protocol": Load the protocol from a directory. :param directory: the directory to the skill package. + :param kwargs: the keyword arguments. :return: the protocol object. """ configuration = cast( @@ -374,6 +370,7 @@ def from_config(cls, configuration: ProtocolConfig, **kwargs: Any) -> "Protocol" Load the protocol from configuration. :param configuration: the protocol configuration. + :param kwargs: the keyword arguments. :return: the protocol object. """ if configuration.directory is None: # pragma: nocover diff --git a/aea/protocols/dialogue/base.py b/aea/protocols/dialogue/base.py index fd9b2ccc55..f5d9bd4983 100644 --- a/aea/protocols/dialogue/base.py +++ b/aea/protocols/dialogue/base.py @@ -96,8 +96,6 @@ def __init__( :param dialogue_reference: the reference of the dialogue. :param dialogue_opponent_addr: the addr of the agent with which the dialogue is kept. :param dialogue_starter_addr: the addr of the agent which started the dialogue. - - :return: None """ self._dialogue_reference = dialogue_reference self._dialogue_opponent_addr = dialogue_opponent_addr @@ -265,8 +263,6 @@ def __init__( :param initial_performatives: the set of all initial performatives. :param terminal_performatives: the set of all terminal performatives. :param valid_replies: the reply structure of speech-acts. - - :return: None """ self._initial_performatives = initial_performatives self._terminal_performatives = terminal_performatives @@ -343,10 +339,9 @@ def __init__( Initialize a dialogue. :param dialogue_label: the identifier of the dialogue + :param message_class: the message class used :param self_address: the address of the entity for whom this dialogue is maintained :param role: the role of the agent this dialogue is maintained for - - :return: None """ self._self_address = self_address self._dialogue_label = dialogue_label @@ -369,7 +364,6 @@ def add_terminal_state_callback(self, fn: Callable[["Dialogue"], None]) -> None: Add callback to be called on dialogue reach terminal state. :param fn: callable to be called with one argument: Dialogue - :return: None """ self._terminal_state_callbacks.add(fn) @@ -600,7 +594,6 @@ def _update(self, message: Message) -> None: Extend the list of incoming/outgoing messages with 'message', if 'message' belongs to dialogue and is valid. :param message: a message to be added - :return: None :raises: InvalidDialogueMessage: if message does not belong to this dialogue, or if message is invalid """ if not message.has_sender: @@ -1030,8 +1023,6 @@ def add_dialogue_endstate( :param end_state: the end state of the dialogue :param is_self_initiated: whether the dialogue is initiated by the agent or the opponent - - :return: None """ if is_self_initiated: enforce(end_state in self._self_initiated, "End state not present!") @@ -1120,7 +1111,6 @@ def add(self, dialogue: Dialogue) -> None: Add dialogue to storage. :param dialogue: dialogue to add. - :return: None """ dialogue.add_terminal_state_callback(self.dialogue_terminal_state_callback) self._dialogues_by_dialogue_label[dialogue.dialogue_label] = dialogue @@ -1133,7 +1123,6 @@ def _add_terminal_state_dialogue(self, dialogue: Dialogue) -> None: Add terminal state dialogue to storage. :param dialogue: dialogue to add. - :return: None """ self.add(dialogue) self._terminal_state_dialogues_labels.add(dialogue.dialogue_label) @@ -1143,7 +1132,6 @@ def remove(self, dialogue_label: DialogueLabel) -> None: Remove dialogue from storage by it's label. :param dialogue_label: label of the dialogue to remove - :return: None """ dialogue = self._dialogues_by_dialogue_label.pop(dialogue_label, None) @@ -1504,9 +1492,10 @@ def __init__( :param self_address: the address of the entity for whom dialogues are maintained :param end_states: the list of dialogue endstates + :param message_class: the message class used + :param dialogue_class: the dialogue class used + :param role_from_first_message: the callable determining role from first message :param keep_terminal_state_dialogues: specify do dialogues in terminal state should stay or not - - :return: None """ self._dialogues_storage = PersistDialoguesStorageWithOffloading(self) @@ -1804,7 +1793,6 @@ def _complete_dialogue_reference(self, message: Message) -> None: Update a self initiated dialogue label with a complete dialogue reference from counterparty's first message. :param message: A message in the dialogue (the first by the counterparty with a complete reference) - :return: None """ complete_dialogue_reference = message.dialogue_reference enforce( @@ -1882,7 +1870,7 @@ def _get_latest_label(self, dialogue_label: DialogueLabel) -> DialogueLabel: Retrieve the latest dialogue label if present otherwise return same label. :param dialogue_label: the dialogue label - :return dialogue_label: the dialogue label + :return: the dialogue label """ return self._dialogues_storage.get_latest_label(dialogue_label) @@ -1907,6 +1895,7 @@ def _create_self_initiated( Create a self initiated dialogue. :param dialogue_opponent_addr: the address of the agent with which the dialogue is kept. + :param dialogue_reference: the reference of the dialogue :param role: the agent's role :return: the created dialogue. diff --git a/aea/protocols/generator/base.py b/aea/protocols/generator/base.py index e0d19bfaf9..2e515dd3a5 100644 --- a/aea/protocols/generator/base.py +++ b/aea/protocols/generator/base.py @@ -156,8 +156,6 @@ def __init__( :raises FileNotFoundError if any prerequisite application is not installed :raises yaml.YAMLError if yaml parser encounters an error condition :raises ProtocolSpecificationParseError if specification fails generator's validation - - :return: None """ # Check the prerequisite applications are installed try: @@ -211,8 +209,6 @@ def _change_indent(self, number: int, mode: str = None) -> None: :param number: the number of indentation levels to set/increment/decrement :param mode: the mode of indentation change - - :return: None """ if mode and mode == "s": if number >= 0: @@ -2061,6 +2057,7 @@ def generate_full_mode(self, language: str) -> Optional[str]: e) applies black formatting f) applies isort formatting + :param language: the language for which to create protobuf files :return: optional warning message """ if language != PROTOCOL_LANGUAGE_PYTHON: diff --git a/aea/protocols/generator/common.py b/aea/protocols/generator/common.py index 704cb385fb..e56e376cdd 100644 --- a/aea/protocols/generator/common.py +++ b/aea/protocols/generator/common.py @@ -334,11 +334,7 @@ def base_protolint_command() -> str: def check_prerequisites() -> None: - """ - Check whether a programme is installed on the system. - - :return: None - """ + """Check whether a programme is installed on the system.""" # check black code formatter is installed if not is_installed("black"): raise FileNotFoundError( @@ -398,8 +394,6 @@ def _create_protocol_file( :param path_to_protocol_package: path to the file :param file_name: the name of the file :param file_content: the content of the file - - :return: None """ pathname = os.path.join(path_to_protocol_package, file_name) @@ -412,7 +406,6 @@ def try_run_black_formatting(path_to_protocol_package: str) -> None: Run Black code formatting via subprocess. :param path_to_protocol_package: a path where formatting should be applied. - :return: None """ subprocess.run( # nosec [sys.executable, "-m", "black", path_to_protocol_package, "--quiet"], @@ -425,7 +418,6 @@ def try_run_isort_formatting(path_to_protocol_package: str) -> None: Run Isort code formatting via subprocess. :param path_to_protocol_package: a path where formatting should be applied. - :return: None """ subprocess.run( # nosec [sys.executable, "-m", "isort", *ISORT_CLI_ARGS, path_to_protocol_package], @@ -444,8 +436,6 @@ def try_run_protoc( :param path_to_generated_protocol_package: path to the protocol buffer schema file. :param name: name of the protocol buffer schema file. :param language: the target language in which to compile the protobuf schema file - - :return: A completed process object. """ # for closure-styled imports for JS, comment the first line and uncomment the second js_commonjs_import_option = ( @@ -474,8 +464,6 @@ def try_run_protolint(path_to_generated_protocol_package: str, name: str) -> Non :param path_to_generated_protocol_package: path to the protocol buffer schema file. :param name: name of the protocol buffer schema file. - - :return: A completed process object. """ # path to proto file path_to_proto_file = os.path.join( diff --git a/aea/protocols/generator/extract_specification.py b/aea/protocols/generator/extract_specification.py index a4339bc039..0e06dfc33c 100644 --- a/aea/protocols/generator/extract_specification.py +++ b/aea/protocols/generator/extract_specification.py @@ -146,11 +146,7 @@ class PythonicProtocolSpecification: # pylint: disable=too-few-public-methods """This class represents a protocol specification in python.""" def __init__(self) -> None: - """ - Instantiate a Pythonic protocol specification. - - :return: None - """ + """Instantiate a Pythonic protocol specification.""" self.speech_acts = dict() # type: Dict[str, Dict[str, str]] self.all_performatives = list() # type: List[str] self.all_unique_contents = dict() # type: Dict[str, str] diff --git a/aea/protocols/generator/validate.py b/aea/protocols/generator/validate.py index b1a92310a2..4f418bca62 100644 --- a/aea/protocols/generator/validate.py +++ b/aea/protocols/generator/validate.py @@ -667,6 +667,7 @@ def _validate_termination( :param termination: List of terminal messages of a dialogue. :param performatives_set: set of all performatives in the dialogue. + :param terminal_performatives_from_reply: terminal performatives extracted from reply structure. :return: Boolean result, and associated message. """ diff --git a/aea/protocols/scaffold/message.py b/aea/protocols/scaffold/message.py index 07ef2d1ed2..5e3c071232 100644 --- a/aea/protocols/scaffold/message.py +++ b/aea/protocols/scaffold/message.py @@ -46,6 +46,7 @@ def __init__(self, performative: Performative, **kwargs: Any) -> None: Initialize. :param performative: the type of message. + :param kwargs: the keyword arguments. """ super().__init__(performative=performative, **kwargs) enforce( # pragma: no cover diff --git a/aea/registries/base.py b/aea/registries/base.py index f0ff9a0dc1..67bb732b08 100644 --- a/aea/registries/base.py +++ b/aea/registries/base.py @@ -196,8 +196,6 @@ def __init__(self, **kwargs: Any) -> None: Instantiate the registry. :param kwargs: kwargs - - :return: None """ super().__init__(**kwargs) self._components_by_type: Dict[ComponentType, Dict[PublicId, Component]] = {} @@ -234,7 +232,6 @@ def _register(self, component_id: ComponentId, component: Component) -> None: :param component_id: the component id :param component: the component to register - :return: None """ self._components_by_type.setdefault(component_id.component_type, {})[ component_id.public_id @@ -290,7 +287,7 @@ def fetch_all(self) -> List[Component]: """ Fetch all the components. - :return the list of registered components. + :return: the list of registered components. """ return [ component @@ -303,7 +300,7 @@ def fetch_by_type(self, component_type: ComponentType) -> List[Component]: Fetch all the components by a given type.. :param component_type: a component type - :return the list of registered components of a given type. + :return: the list of registered components of a given type. """ return list(self._components_by_type.get(component_type, {}).values()) @@ -312,18 +309,10 @@ def ids(self) -> Set[ComponentId]: return self._registered_keys def setup(self) -> None: - """ - Set up the registry. - - :return: None - """ + """Set up the registry.""" def teardown(self) -> None: - """ - Teardown the registry. - - :return: None - """ + """Teardown the registry.""" class ComponentRegistry( @@ -338,8 +327,6 @@ def __init__(self, **kwargs: Any) -> None: Instantiate the registry. :param kwargs: kwargs - - :return: None """ super().__init__(**kwargs) self._items: PublicIdRegistry[ @@ -359,7 +346,6 @@ def register( :param item_id: a pair (skill id, item name). :param item: the item to register. :param is_dynamically_added: whether or not the item is dynamically added. - :return: None :raises: ValueError if an item is already registered with that item id. """ skill_id = item_id[0] @@ -467,11 +453,7 @@ def ids(self) -> Set[Tuple[PublicId, str]]: return result def setup(self) -> None: - """ - Set up the items in the registry. - - :return: None - """ + """Set up the items in the registry.""" for item in self.fetch_all(): if item.context.is_active: self.logger.debug( @@ -493,11 +475,7 @@ def setup(self) -> None: ) def teardown(self) -> None: - """ - Teardown the registry. - - :return: None - """ + """Teardown the registry.""" for name_to_items in self._items.fetch_all(): for _, item in name_to_items.items(): self.logger.debug( @@ -528,8 +506,6 @@ def __init__(self, **kwargs: Any) -> None: Instantiate the registry. :param kwargs: kwargs - - :return: None """ super().__init__(**kwargs) # nested public id registries; one for protocol ids, one for skill ids @@ -549,7 +525,6 @@ def register( :param item_id: the item id. :param item: the handler. :param is_dynamically_added: whether or not the item is dynamically added. - :return: None :raises ValueError: if the protocol is None, or an item with pair (skill_id, protocol_id_ already exists. """ skill_id = item_id[0] @@ -587,7 +562,7 @@ def unregister(self, item_id: Tuple[PublicId, str]) -> Handler: Unregister a item. :param item_id: a pair (skill id, item name). - :return: None + :return: the unregistered handler :raises: ValueError if no item is registered with that item id. """ skill_id = item_id[0] diff --git a/aea/registries/filter.py b/aea/registries/filter.py index 743e02300a..435e47ca7f 100644 --- a/aea/registries/filter.py +++ b/aea/registries/filter.py @@ -90,11 +90,7 @@ def get_active_behaviours(self) -> List[Behaviour]: return active_behaviour def handle_new_handlers_and_behaviours(self) -> None: - """ - Handle the messages from the decision maker. - - :return: None - """ + """Handle the messages from the decision maker.""" self._handle_new_behaviours() self._handle_new_handlers() diff --git a/aea/registries/resources.py b/aea/registries/resources.py index 5d7b1195e9..3c07ecd5e5 100644 --- a/aea/registries/resources.py +++ b/aea/registries/resources.py @@ -52,7 +52,7 @@ def __init__(self, agent_name: str = "standalone") -> None: """ Instantiate the resources. - :return None + :param agent_name: the name of the agent """ self._agent_name = agent_name self._component_registry = AgentComponentRegistry(agent_name=agent_name) @@ -115,7 +115,6 @@ def add_protocol(self, protocol: Protocol) -> None: Add a protocol to the set of resources. :param protocol: a protocol - :return: None """ self._component_registry.register(protocol.component_id, protocol) self._specification_to_protocol_id[ @@ -166,7 +165,6 @@ def remove_protocol(self, protocol_id: PublicId) -> None: Remove a protocol from the set of resources. :param protocol_id: the protocol id for the protocol to be removed. - :return: None """ protocol = cast( Optional[Protocol], @@ -182,7 +180,6 @@ def add_contract(self, contract: Contract) -> None: Add a contract to the set of resources. :param contract: a contract - :return: None """ self._component_registry.register(contract.component_id, contract) @@ -212,7 +209,6 @@ def remove_contract(self, contract_id: PublicId) -> None: Remove a contract from the set of resources. :param contract_id: the contract id for the contract to be removed. - :return: None """ self._component_registry.unregister( ComponentId(ComponentType.CONTRACT, contract_id) @@ -223,7 +219,6 @@ def add_connection(self, connection: Connection) -> None: Add a connection to the set of resources. :param connection: a connection - :return: None """ self._component_registry.register(connection.component_id, connection) @@ -253,7 +248,6 @@ def remove_connection(self, connection_id: PublicId) -> None: Remove a connection from the set of resources. :param connection_id: the connection id for the connection to be removed. - :return: None """ self._component_registry.unregister( ComponentId(ComponentType.CONNECTION, connection_id) @@ -264,7 +258,6 @@ def add_skill(self, skill: Skill) -> None: Add a skill to the set of resources. :param skill: a skill - :return: None """ self._component_registry.register(skill.component_id, skill) if skill.handlers is not None: @@ -307,7 +300,6 @@ def remove_skill(self, skill_id: PublicId) -> None: Remove a skill from the set of resources. :param skill_id: the skill id for the skill to be removed. - :return: None """ self._component_registry.unregister(ComponentId(ComponentType.SKILL, skill_id)) with suppress(ValueError): @@ -390,8 +382,6 @@ def setup(self) -> None: Set up the resources. Calls setup on all resources. - - :return: None """ for r in self._registries: r.setup() @@ -401,8 +391,6 @@ def teardown(self) -> None: Teardown the resources. Calls teardown on all resources. - - :return: None """ for r in self._registries: r.teardown() diff --git a/aea/skills/base.py b/aea/skills/base.py index 7ff9bd6fc4..d71487ea2d 100644 --- a/aea/skills/base.py +++ b/aea/skills/base.py @@ -73,8 +73,8 @@ def __init__( """ Initialize a skill context. - :agent_context: the agent context. - :skill: the skill. + :param agent_context: the agent context. + :param skill: the skill. """ self._agent_context = agent_context # type: Optional[AgentContext] self._in_queue = Queue() # type: Queue @@ -147,7 +147,7 @@ def new_behaviours(self) -> "Queue[Behaviour]": This queue can be used to send messages to the framework to request the registration of a behaviour. - :return the queue of new behaviours. + :return: the queue of new behaviours. """ return self._new_behaviours_queue @@ -159,7 +159,7 @@ def new_handlers(self) -> "Queue[Handler]": This queue can be used to send messages to the framework to request the registration of a handler. - :return the queue of new handlers. + :return: the queue of new handlers. """ return self._new_handlers_queue @@ -263,10 +263,10 @@ def send_to_skill( """ Send message or envelope to another skill. - :param message_or_envelope: envelope to send to another skill. - if message passed it will be wrapped into envelope with optional envelope context. + If message passed it will be wrapped into envelope with optional envelope context. - :return: None + :param message_or_envelope: envelope to send to another skill. + :param context: the optional envelope context """ if self._agent_context is None: # pragma: nocover raise ValueError("agent context was not set!") @@ -289,6 +289,7 @@ def __init__( :param name: the name of the component. :param configuration: the configuration for the component. :param skill_context: the skill context. + :param kwargs: the keyword arguments. """ if name is None: raise ValueError("SkillComponent name is not provided.") @@ -334,22 +335,14 @@ def config(self) -> Dict[Any, Any]: @abstractmethod def setup(self) -> None: - """ - Implement the setup. - - :return: None - """ + """Implement the setup.""" super_obj = super() if hasattr(super_obj, "setup"): super_obj.setup() # type: ignore # pylint: disable=no-member @abstractmethod def teardown(self) -> None: - """ - Implement the teardown. - - :return: None - """ + """Implement the teardown.""" super_obj = super() if hasattr(super_obj, "teardown"): super_obj.teardown() # type: ignore # pylint: disable=no-member @@ -520,8 +513,7 @@ def __init__( :param configuration: the configuration for the component. :param skill_context: the skill context. :param keep_terminal_state_dialogues: specify do dialogues in terminal state should stay or not - - :return: None + :param kwargs: the keyword arguments. """ super().__init__(name, skill_context, configuration=configuration, **kwargs) @@ -581,6 +573,7 @@ def __init__( :param handlers: dictionary of handlers. :param behaviours: dictionary of behaviours. :param models: dictionary of models. + :param kwargs: the keyword arguments. """ if kwargs is not None: pass @@ -636,7 +629,8 @@ def from_dir( Load the skill from a directory. :param directory: the directory to the skill package. - :param agent_context: the skill context + :param agent_context: the skill context. + :param kwargs: the keyword arguments. :return: the skill object. """ configuration = cast( @@ -653,6 +647,8 @@ def logger(self) -> Logger: In the case of a skill, return the logger provided by the skill context. + + :return: the logger """ return self.skill_context.logger @@ -670,6 +666,7 @@ def from_config( :param configuration: a skill configuration. Must be associated with a directory. :param agent_context: the agent context. + :param kwargs: the keyword arguments. :return: the skill. """ @@ -926,9 +923,7 @@ def _load_component_classes( Load component classes from Python modules. :param module_paths: a set of paths to Python modules. - :return: a mapping from path to skill component classes in that module - (containing potential duplicates). Skill components in one path - are + :return: a mapping from path to skill component classes in that module (containing potential duplicates). Skill components in one path are """ module_to_classes: Dict[Path, Set[Tuple[str, Type[SkillComponent]]]] = {} for module_path in module_paths: @@ -952,7 +947,7 @@ def _get_declared_skill_component_configurations( """ Get all the declared skill component configurations. - :return: + :return: dictionary of declared skill component configurations """ handlers_by_id = dict(self.configuration.handlers.read_all()) behaviours_by_id = dict(self.configuration.behaviours.read_all()) @@ -1027,8 +1022,9 @@ def _match_class_and_configurations( In this function, the above criteria are applied in that order. - :param component_classes_by_path: - :return: None + :param component_classes_by_path: the component classes by path + :param declared_component_classes: the declared component classes + :return: list of skill component loading items """ result: List[_SkillComponentLoadingItem] = [] @@ -1138,7 +1134,6 @@ def _print_warning_message_for_unused_classes( :param component_classes_by_path: the component classes by path. :param used_classes: the classes used. - :return: None """ for path, set_of_class_name_pairs in component_classes_by_path.items(): # take only classes, not class names diff --git a/aea/skills/behaviours.py b/aea/skills/behaviours.py index 849ee3cb00..b1663a6d60 100644 --- a/aea/skills/behaviours.py +++ b/aea/skills/behaviours.py @@ -79,6 +79,7 @@ def is_done(self) -> bool: Return True if the behaviour is terminated, False otherwise. The user should implement it properly to determine the stopping condition. + :return: bool indicating status """ return False @@ -116,6 +117,7 @@ def __init__( :param tick_interval: interval of the behaviour in seconds. :param start_at: whether to start the behaviour with an offset. + :param kwargs: the keyword arguments. """ super().__init__(**kwargs) @@ -170,7 +172,7 @@ def __init__(self, behaviour_sequence: List[Behaviour], **kwargs: Any) -> None: Initialize the sequence behaviour. :param behaviour_sequence: the sequence of behaviour. - :param kwargs: + :param kwargs: the keyword arguments """ super().__init__(**kwargs) @@ -184,6 +186,8 @@ def current_behaviour(self) -> Optional[Behaviour]: Get the current behaviour. If None, the sequence behaviour can be considered done. + + :return: current behaviour or None """ return ( None @@ -273,8 +277,7 @@ def register_state(self, name: str, state: State, initial: bool = False) -> None :param name: the name of the state. :param state: the behaviour in that state. :param initial: whether the state is an initial state. - :return: None - :raise ValueError: if a state with the provided name already exists. + :raises ValueError: if a state with the provided name already exists. """ if name in self._name_to_state: raise ValueError("State name already existing.") @@ -289,8 +292,7 @@ def register_final_state(self, name: str, state: State) -> None: :param name: the name of the state. :param state: the state. - :return: None - :raise ValueError: if a state with the provided name already exists. + :raises ValueError: if a state with the provided name already exists. """ if name in self._name_to_state: raise ValueError("State name already existing.") @@ -302,8 +304,7 @@ def unregister_state(self, name: str) -> None: Unregister a state. :param name: the state name to unregister. - :return: None - :raise ValueError: if the state is not registered. + :raises ValueError: if the state is not registered. """ if name not in self._name_to_state: raise ValueError("State name not registered.") @@ -375,10 +376,9 @@ def register_transition( No sanity check is done. :param source: the source state name. - :param destination: the destination state name. + :param destination: the destination state name. :param event: the event. - :return: None - :raise ValueError: if a transition from source with event is already present. + :raises ValueError: if a transition from source with event is already present. """ if source in self.transitions and event in self.transitions.get(source, {}): raise ValueError("Transition already registered.") @@ -392,10 +392,9 @@ def unregister_transition( Unregister a transition. :param source: the source state name. - :param destination: the destination state name. + :param destination: the destination state name. :param event: the event. - :return: None - :raise ValueError: if a transition from source with event is not present. + :raises ValueError: if a transition from source with event is not present. """ if ( source not in self.transitions.keys() diff --git a/aea/skills/scaffold/behaviours.py b/aea/skills/scaffold/behaviours.py index 1df3678311..fd5247b44b 100644 --- a/aea/skills/scaffold/behaviours.py +++ b/aea/skills/scaffold/behaviours.py @@ -26,25 +26,13 @@ class MyScaffoldBehaviour(Behaviour): """This class scaffolds a behaviour.""" def setup(self) -> None: - """ - Implement the setup. - - :return: None - """ + """Implement the setup.""" raise NotImplementedError def act(self) -> None: - """ - Implement the act. - - :return: None - """ + """Implement the act.""" raise NotImplementedError def teardown(self) -> None: - """ - Implement the task teardown. - - :return: None - """ + """Implement the task teardown.""" raise NotImplementedError diff --git a/aea/skills/scaffold/handlers.py b/aea/skills/scaffold/handlers.py index d02a439c2b..fc796ac0aa 100644 --- a/aea/skills/scaffold/handlers.py +++ b/aea/skills/scaffold/handlers.py @@ -32,11 +32,7 @@ class MyScaffoldHandler(Handler): SUPPORTED_PROTOCOL = None # type: Optional[PublicId] def setup(self) -> None: - """ - Implement the setup. - - :return: None - """ + """Implement the setup.""" raise NotImplementedError def handle(self, message: Message) -> None: @@ -44,14 +40,9 @@ def handle(self, message: Message) -> None: Implement the reaction to an envelope. :param message: the message - :return: None """ raise NotImplementedError def teardown(self) -> None: - """ - Implement the handler teardown. - - :return: None - """ + """Implement the handler teardown.""" raise NotImplementedError diff --git a/aea/skills/tasks.py b/aea/skills/tasks.py index 4b904ac5ee..fae508586e 100644 --- a/aea/skills/tasks.py +++ b/aea/skills/tasks.py @@ -45,7 +45,7 @@ def __call__(self, *args: Any, **kwargs: Any) -> Any: :param args: positional arguments forwarded to the 'execute' method. :param kwargs: keyword arguments forwarded to the 'execute' method. - :return the task instance + :return: the task instance :raises ValueError: if the task has already been executed. """ if self._is_executed: @@ -75,7 +75,7 @@ def result(self) -> Any: """ Get the result. - :return the result from the execute method. + :return: the result from the execute method. :raises ValueError: if the task has not been executed yet. """ if not self._is_executed: @@ -83,26 +83,20 @@ def result(self) -> Any: return self._result def setup(self) -> None: - """ - Implement the task setup. - - :return: None - """ + """Implement the task setup.""" @abstractmethod def execute(self, *args: Any, **kwargs: Any) -> Any: """ Run the task logic. - :return: None + :param args: the positional arguments + :param kwargs: the keyword arguments + :return: any """ def teardown(self) -> None: - """ - Implement the task teardown. - - :return: None - """ + """Implement the task teardown.""" def init_worker() -> None: @@ -111,8 +105,6 @@ def init_worker() -> None: Disable the SIGINT handler of process pool is using. Related to a well-known bug: https://bugs.python.org/issue8296 - - :return: None """ if Pool.__class__.__name__ == "Pool": # pragma: nocover # Process worker @@ -139,6 +131,7 @@ def __init__( :param nb_workers: the number of worker processes. :param is_lazy_pool_start: option to postpone pool creation till the first enqueue_task called. + :param logger: the logger. :param pool_mode: str. multithread or multiprocess """ WithLogger.__init__(self, logger) @@ -182,7 +175,7 @@ def enqueue_task( :param func: the callable instance to be enqueued :param args: the positional arguments to be passed to the function. :param kwargs: the keyword arguments to be passed to the function. - :return the task id to get the the result. + :return: the task id to get the the result. :raises ValueError: if the task manager is not running. """ with self._lock: @@ -207,6 +200,7 @@ def get_task_result(self, task_id: int) -> AsyncResult: """ Get the result from a task. + :param task_id: the task id :return: async result for task_id """ task_result = self._results_by_task_id.get( @@ -218,11 +212,7 @@ def get_task_result(self, task_id: int) -> AsyncResult: return task_result def start(self) -> None: - """ - Start the task manager. - - :return: None - """ + """Start the task manager.""" with self._lock: if self._stopped is False: self.logger.debug("Task manager already running.") @@ -233,11 +223,7 @@ def start(self) -> None: self._start_pool() def stop(self) -> None: - """ - Stop the task manager. - - :return: None - """ + """Stop the task manager.""" with self._lock: if self._stopped is True: self.logger.debug("Task manager already stopped.") @@ -251,8 +237,6 @@ def _start_pool(self) -> None: Start internal task pool. Only one pool will be created. - - :return: None """ if self._pool: self.logger.debug("Pool was already started!") @@ -263,11 +247,7 @@ def _start_pool(self) -> None: self._pool = pool_cls(self._nb_workers, initializer=init_worker) def _stop_pool(self) -> None: - """ - Stop internal task pool. - - :return: None - """ + """Stop internal task pool.""" if not self._pool: self.logger.debug("Pool is not started!.") return diff --git a/aea/test_tools/generic.py b/aea/test_tools/generic.py index 8bb81c4405..feee92ffa0 100644 --- a/aea/test_tools/generic.py +++ b/aea/test_tools/generic.py @@ -46,8 +46,6 @@ def write_envelope_to_file(envelope: Envelope, file_path: str) -> None: :param envelope: Envelope. :param file_path: the file path - - :return: None """ with open(Path(file_path), "ab+") as f: write_envelope(envelope, f) @@ -57,7 +55,7 @@ def read_envelope_from_file(file_path: str) -> Envelope: """ Read an envelope from a file. - :param file_path the file path. + :param file_path: the file path. :return: envelope """ @@ -94,8 +92,6 @@ def _nested_set( :param configuration_obj: configuration object :param keys: list of keys. :param value: a value to set. - - :return: None. """ def get_nested_ordered_dict_from_dict(input_dict: Dict) -> Dict: @@ -188,8 +184,6 @@ def nested_set_config( :param dotted_path: dotted path to a setting. :param value: a value to assign. Must be of yaml serializable type. :param author: the author name, used to parse the dotted path. - - :return: None. """ settings_keys, config_file_path, config_loader, _ = handle_dotted_path( dotted_path, author diff --git a/aea/test_tools/test_cases.py b/aea/test_tools/test_cases.py index 3cbe5fa39b..0a25f4f73d 100644 --- a/aea/test_tools/test_cases.py +++ b/aea/test_tools/test_cases.py @@ -148,8 +148,6 @@ def disable_aea_logging(cls) -> None: Disable AEA logging of specific agent. Run from agent's directory. - - :return: None """ config_update_dict = { "agent.logging_config.disable_existing_loggers": "False", @@ -194,6 +192,7 @@ def _run_python_subprocess(cls, *args: str, cwd: str = ".") -> subprocess.Popen: Run python with args as subprocess. :param args: CLI args + :param cwd: the current working directory :return: subprocess object. """ @@ -217,6 +216,7 @@ def start_subprocess(cls, *args: str, cwd: str = ".") -> subprocess.Popen: Run python with args as subprocess. :param args: CLI args + :param cwd: the current working directory :return: subprocess object. """ @@ -231,9 +231,8 @@ def start_thread(cls, target: Callable, **kwargs: subprocess.Popen) -> Thread: Start python Thread. :param target: target method. - :param process: subprocess passed to thread args. - - :return: None. + :param kwargs: thread keyword arguments + :return: thread """ if "process" in kwargs: thread = Thread(target=target, args=(kwargs["process"],)) @@ -252,9 +251,7 @@ def create_agents( :param agents_names: str agent names. :param is_local: a flag for local folder add True by default. - :param empty: optional boolean flag for skip adding default dependencies. - - :return: None + :param is_empty: optional boolean flag for skip adding default dependencies. """ cli_args = ["create", "--local", "--empty"] if not is_local: # pragma: nocover @@ -273,10 +270,8 @@ def fetch_agent( Create agents in current working directory. :param public_id: str public id - :param agents_name: str agent name. + :param agent_name: str agent name. :param is_local: a flag for local folder add True by default. - - :return: None """ cli_args = ["fetch", "--local"] if not is_local: # pragma: nocover @@ -290,7 +285,7 @@ def difference_to_fetched_agent(cls, public_id: str, agent_name: str) -> List[st Compare agent against the one fetched from public id. :param public_id: str public id - :param agents_name: str agent name. + :param agent_name: str agent name. :return: list of files differing in the projects """ @@ -391,8 +386,6 @@ def delete_agents(cls, *agents_names: str) -> None: Delete agents in current working directory. :param agents_names: str agent names. - - :return: None """ for name in set(agents_names): cls.run_cli_command("delete", name) @@ -418,8 +411,6 @@ def run_interaction(cls) -> subprocess.Popen: Run from agent's directory. - :param args: CLI args - :return: subprocess object. """ return cls._start_cli_process("interact") @@ -471,11 +462,7 @@ def is_successfully_terminated(cls, *subprocesses: subprocess.Popen) -> bool: @classmethod def initialize_aea(cls, author: str) -> None: - """ - Initialize AEA locally with author name. - - :return: None - """ + """Initialize AEA locally with author name.""" cls.run_cli_command("init", "--local", "--author", author, cwd=cls._get_cwd()) @classmethod @@ -543,7 +530,7 @@ def fingerprint_item(cls, item_type: str, public_id: str) -> Result: Run from agent's directory. :param item_type: str item type. - :param name: public id of the item. + :param public_id: public id of the item. :return: Result """ @@ -617,6 +604,7 @@ def add_private_key( :param ledger_api_id: ledger API ID. :param private_key_filepath: private key filepath. :param connection: whether or not the private key filepath is for a connection. + :param password: the password to encrypt private keys. :return: Result """ @@ -664,8 +652,6 @@ def replace_private_key_in_file( :param private_key: the private key :param private_key_filepath: the filepath to the private key file - - :return: None :raises: exception if file does not exist """ with cd(cls._get_cwd()): # pragma: nocover @@ -718,7 +704,6 @@ def replace_file_content(cls, src: Path, dest: Path) -> None: # pragma: nocover :param src: the source file. :param dest: the destination file. - :return: None """ enforce( src.is_file() and dest.is_file(), "Source or destination is not a file." @@ -731,7 +716,6 @@ def change_directory(cls, path: Path) -> None: Change current working directory. :param path: path to the new working directory. - :return: None """ os.chdir(Path(path)) @@ -783,8 +767,6 @@ def _start_output_read_thread(cls, process: subprocess.Popen) -> None: Start an output reading thread. :param process: target process passed to a thread args. - - :return: None. """ cls.stdout[process.pid] = "" cls.start_thread(target=cls._read_out, process=process) @@ -795,8 +777,6 @@ def _start_error_read_thread(cls, process: subprocess.Popen) -> None: Start an error reading thread. :param process: target process passed to a thread args. - - :return: None. """ cls.stderr[process.pid] = "" cls.start_thread(target=cls._read_err, process=process) @@ -875,6 +855,7 @@ def is_running( :param process: agent subprocess. :param timeout: the timeout to wait for launch to complete + :return: bool indicating status """ missing_strings = cls.missing_from_output( process, (LAUNCH_SUCCEED_MESSAGE,), timeout, is_terminating=False diff --git a/aea/test_tools/test_skill.py b/aea/test_tools/test_skill.py index 75552c3f3b..657f97adfd 100644 --- a/aea/test_tools/test_skill.py +++ b/aea/test_tools/test_skill.py @@ -331,7 +331,7 @@ def _extract_message_fields( :param message: the dialogue message :param index: the index of this dialogue message in the sequence of messages - :param message: the is_incoming of the last message in the sequence + :param last_is_incoming: the is_incoming of the last message in the sequence :return: the performative, contents, message_id, is_incoming, target of the message """ From 75f11b95422abb7826984c48a204866c2bd03944 Mon Sep 17 00:00:00 2001 From: David Minarsch Date: Tue, 1 Jun 2021 14:05:04 +0100 Subject: [PATCH 128/147] chore: restrict darglint to aea for now --- Makefile | 2 +- tox.ini | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Makefile b/Makefile index 241d78c40a..fb6e6f317d 100644 --- a/Makefile +++ b/Makefile @@ -46,7 +46,7 @@ lint: isort aea benchmark examples packages plugins scripts tests flake8 aea benchmark examples packages plugins scripts tests vulture aea scripts/whitelist.py --exclude "*_pb2.py" - darglint aea benchmark examples packages + darglint aea .PHONY: pylint pylint: diff --git a/tox.ini b/tox.ini index 5f2015ce5e..2c861c9e9b 100644 --- a/tox.ini +++ b/tox.ini @@ -280,7 +280,7 @@ skipsdist = True skip_install = True deps = darglint==1.8.0 -commands = darglint aea benchmark examples packages +commands = darglint aea [testenv:check_doc_links] skipsdist = True From d5a477cf6bf0b1cbd3d9356701cc883cc203ff6e Mon Sep 17 00:00:00 2001 From: David Minarsch Date: Tue, 1 Jun 2021 15:25:40 +0100 Subject: [PATCH 129/147] chore: fix docstrings cli --- aea/cli/add.py | 3 --- aea/cli/add_key.py | 2 -- aea/cli/build.py | 1 - aea/cli/create.py | 6 ++--- aea/cli/delete.py | 4 ++-- aea/cli/eject.py | 4 ++-- aea/cli/fetch.py | 8 ++----- aea/cli/fingerprint.py | 9 ++------ aea/cli/generate_key.py | 2 -- aea/cli/generate_wealth.py | 1 - aea/cli/get_address.py | 2 +- aea/cli/get_multiaddress.py | 5 +++- aea/cli/get_public_key.py | 2 +- aea/cli/init.py | 6 +---- aea/cli/install.py | 5 ++-- aea/cli/interact.py | 2 -- aea/cli/issue_certificates.py | 1 - aea/cli/launch.py | 2 -- aea/cli/local_registry_sync.py | 8 +++---- aea/cli/login.py | 2 -- aea/cli/logout.py | 6 +---- aea/cli/plugin.py | 1 - aea/cli/publish.py | 11 +++------ aea/cli/push.py | 2 +- aea/cli/register.py | 2 -- aea/cli/registry/add.py | 1 + aea/cli/registry/fetch.py | 1 - aea/cli/registry/login.py | 2 -- aea/cli/registry/logout.py | 6 +---- aea/cli/registry/push.py | 8 ++++--- aea/cli/registry/utils.py | 5 ++-- aea/cli/remove.py | 42 +++++++++++++--------------------- aea/cli/remove_key.py | 2 -- aea/cli/reset_password.py | 2 -- aea/cli/run.py | 3 +-- aea/cli/scaffold.py | 2 -- aea/cli/search.py | 13 +++-------- aea/cli/transfer.py | 3 +-- aea/cli/upgrade.py | 13 ++++------- aea/cli/utils/click_utils.py | 7 ++++-- aea/cli/utils/config.py | 16 +------------ aea/cli/utils/context.py | 3 +-- aea/cli/utils/decorators.py | 11 ++++++--- aea/cli/utils/loggers.py | 5 ++++ aea/cli/utils/package_utils.py | 34 +++++++++++++-------------- aea/crypto/registries/base.py | 1 - 46 files changed, 98 insertions(+), 179 deletions(-) diff --git a/aea/cli/add.py b/aea/cli/add.py index 2a15571436..bcb2c79581 100644 --- a/aea/cli/add.py +++ b/aea/cli/add.py @@ -111,7 +111,6 @@ def add_item(ctx: Context, item_type: str, item_public_id: PublicId) -> None: :param ctx: Context object. :param item_type: the item type. :param item_public_id: the item public id. - :return: None """ click.echo(f"Adding {item_type} '{item_public_id}'...") if is_item_present(ctx.cwd, ctx.agent_config, item_type, item_public_id): @@ -161,8 +160,6 @@ def _add_item_deps( :param ctx: Context object. :param item_type: type of item. :param item_config: item configuration object. - - :return: None """ if item_type in {CONNECTION, SKILL}: item_config = cast(Union[SkillConfig, ConnectionConfig], item_config) diff --git a/aea/cli/add_key.py b/aea/cli/add_key.py index 6a45a0d8ae..167d65a00a 100644 --- a/aea/cli/add_key.py +++ b/aea/cli/add_key.py @@ -81,8 +81,6 @@ def _add_private_key( :param file: path to file. :param connection: whether or not it is a private key for a connection. :param password: the password to encrypt/decrypt the private key. - - :return: None """ ctx = cast(Context, click_context.obj) if file is None: diff --git a/aea/cli/build.py b/aea/cli/build.py index b2d56525a4..40480c0a5d 100644 --- a/aea/cli/build.py +++ b/aea/cli/build.py @@ -44,7 +44,6 @@ def build_aea(skip_consistency_check: bool) -> None: That is, run the 'build entrypoint' script of each AEA package of the project. :param skip_consistency_check: the skip consistency check boolean. - :return: None """ try: builder = AEABuilder.from_aea_project( diff --git a/aea/cli/create.py b/aea/cli/create.py index aa2f4bcccc..877a03a1da 100644 --- a/aea/cli/create.py +++ b/aea/cli/create.py @@ -102,8 +102,7 @@ def create_aea( :param author: optional author name (valid with local=True and remote=False only). :param empty: optional boolean flag for skip adding default dependencies. - :return: None - :raises: ClickException if an error occurred. + :raises ClickException: if an error occurred. """ enforce( not (local and remote), "'local' and 'remote' options are mutually exclusive." @@ -215,8 +214,7 @@ def _create_agent_config(ctx: Context, agent_name: str, set_author: str) -> Agen def _check_is_parent_folders_are_aea_projects_recursively() -> None: """Look for 'aea-config.yaml' in parent folders recursively up to the user home directory. - :return: None - :raise ValueError: if a parent folder has a file named 'aea-config.yaml'. + :raises ValueError: if a parent folder has a file named 'aea-config.yaml'. """ current = Path(".").resolve() root = Path("/").resolve() diff --git a/aea/cli/delete.py b/aea/cli/delete.py index c54a2877a5..1125d5fb8f 100644 --- a/aea/cli/delete.py +++ b/aea/cli/delete.py @@ -45,10 +45,10 @@ def delete_aea(ctx: Context, agent_name: str) -> None: """ Delete agent's directory. + :param ctx: click context :param agent_name: name of the agent (equal to folder name). - :return: None - :raises: ClickException if OSError occurred. + :raises ClickException: if OSError occurred. """ agent_path = os.path.join(ctx.cwd, agent_name) try: diff --git a/aea/cli/eject.py b/aea/cli/eject.py index a62530c087..1b3d175101 100644 --- a/aea/cli/eject.py +++ b/aea/cli/eject.py @@ -148,9 +148,9 @@ def _eject_item( :param item_type: item type. :param public_id: item public ID. :param quiet: if false, the function will ask the user in case of recursive eject. + :param with_symlinks: if eject should create symlinks. - :return: None - :raises: ClickException if item is absent at source path or present at destination path. + :raises ClickException: if item is absent at source path or present at destination path. """ # we know cli_author is set because of the above checks. cli_author: str = cast(str, ctx.config.get("cli_author")) diff --git a/aea/cli/fetch.py b/aea/cli/fetch.py index c9a91c0b89..f3f513f9cd 100644 --- a/aea/cli/fetch.py +++ b/aea/cli/fetch.py @@ -81,10 +81,9 @@ def do_fetch( :param ctx: the CLI context. :param public_id: the public id. :param local: whether to fetch from local - :param remote whether to fetch from remote + :param remote: whether to fetch from remote :param alias: the agent alias. :param target_dir: the target directory, in case fetching locally. - :return: None """ enforce( not (local and remote), "'local' and 'remote' options are mutually exclusive." @@ -135,7 +134,6 @@ def fetch_agent_locally( :param public_id: public ID of agent to be fetched. :param alias: an optional alias. :param target_dir: the target directory to which the agent is fetched. - :return: None """ try: registry_path = ctx.registry_path @@ -190,8 +188,7 @@ def _fetch_agent_deps(ctx: Context) -> None: :param ctx: context object. - :return: None - :raises: ClickException re-raises if occurs in add_item call. + :raises ClickException: re-raises if occurs in add_item call. """ for item_type in (PROTOCOL, CONTRACT, CONNECTION, SKILL): item_type_plural = "{}s".format(item_type) @@ -213,7 +210,6 @@ def fetch_mixed( :param public_id: the public id. :param alias: the alias to the agent. :param target_dir: the target directory. - :return: None """ try: fetch_agent_locally(ctx, public_id, alias=alias, target_dir=target_dir) diff --git a/aea/cli/fingerprint.py b/aea/cli/fingerprint.py index 5a9e7d6d80..676391ff03 100644 --- a/aea/cli/fingerprint.py +++ b/aea/cli/fingerprint.py @@ -104,7 +104,6 @@ def fingerprint_item(ctx: Context, item_type: str, item_public_id: PublicId) -> :param ctx: the context. :param item_type: the item type. :param item_public_id: the item public id. - :return: None """ item_type_plural = item_type + "s" @@ -125,8 +124,6 @@ def fingerprint_package_by_path(package_dir: Path) -> None: Fingerprint package placed in package_dir. :param package_dir: directory of the package - - :return: None """ package_type = determine_package_type_for_directory(package_dir) fingerprint_package(package_dir, package_type) @@ -167,10 +164,8 @@ def fingerprint_package( """ Fingerprint components of an item. - :param ctx: the context. - :param item_type: the item type. - :param item_public_id: the item public id. - :return: None + :param package_dir: the package directory. + :param package_type: the package type. """ package_type = PackageType(package_type) item_type = str(package_type) diff --git a/aea/cli/generate_key.py b/aea/cli/generate_key.py index 1a64d0f5a7..f8226960fa 100644 --- a/aea/cli/generate_key.py +++ b/aea/cli/generate_key.py @@ -57,8 +57,6 @@ def _generate_private_key( :param type_: type. :param file: path to file. :param password: the password to encrypt/decrypt the private key. - - :return: None """ if type_ == "all" and file is not None: raise click.ClickException("Type all cannot be used in combination with file.") diff --git a/aea/cli/generate_wealth.py b/aea/cli/generate_wealth.py index 32e6f5bdfc..3c011f90b0 100644 --- a/aea/cli/generate_wealth.py +++ b/aea/cli/generate_wealth.py @@ -70,7 +70,6 @@ def _try_generate_wealth( :param url: the url :param sync: whether to sync or not :param password: the password to encrypt/decrypt the private key. - :return: None """ wallet = get_wallet_from_context(ctx, password=password) try: diff --git a/aea/cli/get_address.py b/aea/cli/get_address.py index 8ef339ae02..3c575f2a55 100644 --- a/aea/cli/get_address.py +++ b/aea/cli/get_address.py @@ -53,7 +53,7 @@ def _try_get_address(ctx: Context, type_: str, password: Optional[str] = None) - """ Try to get address. - :param click_context: click context object. + :param ctx: click context object. :param type_: type. :param password: the password to encrypt/decrypt the private key. diff --git a/aea/cli/get_multiaddress.py b/aea/cli/get_multiaddress.py index 133f881a76..090a64e4fc 100644 --- a/aea/cli/get_multiaddress.py +++ b/aea/cli/get_multiaddress.py @@ -107,6 +107,7 @@ def _try_get_multiaddress( :param connection_id: the connection id. :param host_field: if connection_id specified, the config field to retrieve the host :param port_field: if connection_id specified, the config field to retrieve the port + :param uri_field: uri field :return: address. """ @@ -162,9 +163,11 @@ def _read_host_and_port_from_config( """ Read host and port from config connection. + :param connection_config: connection configuration. + :param connection_id: the connection id. + :param uri_field: the uri field. :param host_field: the host field. :param port_field: the port field. - :param uri_field: the uri field. :return: the host and the port. """ host_is_none = host_field is None diff --git a/aea/cli/get_public_key.py b/aea/cli/get_public_key.py index cac746d3d8..6b4a8f4f6e 100644 --- a/aea/cli/get_public_key.py +++ b/aea/cli/get_public_key.py @@ -53,7 +53,7 @@ def _try_get_public_key( """ Try to get public key. - :param click_context: click context object. + :param ctx: click context object. :param type_: type. :param password: the password to encrypt/decrypt the private key. diff --git a/aea/cli/init.py b/aea/cli/init.py index e8f9443e63..a057d43369 100644 --- a/aea/cli/init.py +++ b/aea/cli/init.py @@ -54,8 +54,6 @@ def do_init(author: str, reset: bool, registry: bool, no_subscribe: bool) -> Non :param reset: True, if resetting the author name :param registry: True, if registry is used :param no_subscribe: bool flag for developers subscription skip on register. - - :return: None. """ config = get_or_create_cli_config() if reset or config.get(AUTHOR_KEY, None) is None: @@ -80,10 +78,8 @@ def _registry_init(username: str, no_subscribe: bool) -> None: """ Create an author name on the registry. - :param author: the author name + :param username: the user name :param no_subscribe: bool flag for developers subscription skip on register. - - :return: None. """ if username is not None and is_auth_token_present(): check_is_author_logged_in(username) diff --git a/aea/cli/install.py b/aea/cli/install.py index d685e0df28..661bd7eb34 100644 --- a/aea/cli/install.py +++ b/aea/cli/install.py @@ -55,8 +55,7 @@ def do_install(ctx: Context, requirement: Optional[str] = None) -> None: :param ctx: context object. :param requirement: optional str requirement. - :return: None - :raises: ClickException if AEAException occurs. + :raises ClickException: if AEAException occurs. """ try: if requirement: @@ -79,7 +78,7 @@ def _install_from_requirement(file: str, install_timeout: float = 300) -> None: :param file: requirement.txt file path :param install_timeout: timeout to wait pip to install - :return: None + :raises AEAException: if an error occurs during installation. """ try: returncode = run_install_subprocess( diff --git a/aea/cli/interact.py b/aea/cli/interact.py index 0315a2b297..99a6617757 100644 --- a/aea/cli/interact.py +++ b/aea/cli/interact.py @@ -186,8 +186,6 @@ def _process_envelopes( :param outbox: an outbox object. :param dialogues: the dialogues object. :param message_class: the message class. - - :return: None. """ envelope = _try_construct_envelope(agent_name, dialogues, message_class) if envelope is None: diff --git a/aea/cli/issue_certificates.py b/aea/cli/issue_certificates.py index fd88d6078e..40695fd449 100644 --- a/aea/cli/issue_certificates.py +++ b/aea/cli/issue_certificates.py @@ -60,7 +60,6 @@ def issue_certificates_( :param agent_config_manager: the agent configuration manager. :param path_prefix: the path prefix for "save_path". Defaults to project directory. :param password: the password to encrypt/decrypt the private key. - :return: None """ path_prefix = path_prefix or project_directory for connection_id in agent_config_manager.agent_config.connections: diff --git a/aea/cli/launch.py b/aea/cli/launch.py index 0478feca33..af87233084 100644 --- a/aea/cli/launch.py +++ b/aea/cli/launch.py @@ -60,8 +60,6 @@ def _launch_agents( :param agents: agents names. :param multithreaded: bool flag to run as multithreads. :param password: the password to encrypt/decrypt the private key. - - :return: None. """ agents_directories = list(map(Path, list(OrderedDict.fromkeys(agents)))) mode = "threaded" if multithreaded else "multiprocess" diff --git a/aea/cli/local_registry_sync.py b/aea/cli/local_registry_sync.py index 357fbad13a..406302c7d7 100644 --- a/aea/cli/local_registry_sync.py +++ b/aea/cli/local_registry_sync.py @@ -53,8 +53,7 @@ def do_local_registry_update( Perform local registry update. :param base_dir: root directory of the local registry. - - :return: None + :param skip_consistency_check: whether or not to skip consistency checks. """ for package_id, package_dir in enlist_packages(base_dir, skip_consistency_check): current_public_id = package_id.public_id @@ -77,8 +76,6 @@ def replace_package( :param package_type: str. :param public_id: package public id to download :param: package_dir: target package dir - - :return: None """ with TemporaryDirectory() as tmp_dir: new_package_dir = os.path.join(tmp_dir, public_id.name) @@ -111,8 +108,9 @@ def enlist_packages( Generate list of the packages in local repo directory. :param base_dir: path or str of the local repo. + :param skip_consistency_check: whether or not to skip consistency checks. - :return: generator with Tuple of package_id and package directory. + :yield: generator with Tuple of package_id and package directory. """ for author in os.listdir(base_dir): author_dir = os.path.join(base_dir, author) diff --git a/aea/cli/login.py b/aea/cli/login.py index 3a492394e7..5843549b10 100644 --- a/aea/cli/login.py +++ b/aea/cli/login.py @@ -40,8 +40,6 @@ def do_login(username: str, password: str) -> None: :param username: str username. :param password: str password. - - :return: None """ click.echo("Signing in as {}...".format(username)) token = registry_login(username, password) diff --git a/aea/cli/logout.py b/aea/cli/logout.py index f6e1007b7e..d8c7ff0c78 100644 --- a/aea/cli/logout.py +++ b/aea/cli/logout.py @@ -35,10 +35,6 @@ def logout() -> None: def do_logout() -> None: - """ - Logout from Registry account. - - :return: None. - """ + """Logout from Registry account.""" registry_logout() update_cli_config({AUTH_TOKEN_KEY: None}) diff --git a/aea/cli/plugin.py b/aea/cli/plugin.py index 4c4e42d10d..e0478387a8 100644 --- a/aea/cli/plugin.py +++ b/aea/cli/plugin.py @@ -90,7 +90,6 @@ def invoke(self, ctx: click.Context) -> None: Print the traceback instead of doing nothing. :param ctx: the click.Context object. - :return: None """ click.echo(self.help, color=ctx.color) diff --git a/aea/cli/publish.py b/aea/cli/publish.py index fe46c72a53..ea93b0b17b 100644 --- a/aea/cli/publish.py +++ b/aea/cli/publish.py @@ -75,7 +75,6 @@ def _validate_config(ctx: Context) -> None: :param ctx: Context object. - :return: None :raises ClickException: if validation is failed. """ try: @@ -89,9 +88,7 @@ def _validate_pkp(private_key_paths: CRUDCollection) -> None: Prevent to publish agents with non-empty private_key_paths. :param private_key_paths: private_key_paths from agent config. - :raises: ClickException if private_key_paths is not empty. - - :return: None. + :raises ClickException: if private_key_paths is not empty. """ if private_key_paths.read_all() != []: raise click.ClickException( @@ -124,8 +121,7 @@ def _check_is_item_in_remote_registry( :param public_id: the public id. :param item_type_plural: the type of the item. - :return: None - :raises click.ClickException: if the item is not present. + :raises ClickException: if the item is not present. """ get_package_meta(item_type_plural[:-1], public_id) @@ -148,8 +144,7 @@ def _save_agent_locally(ctx: Context, is_mixed: bool = False) -> None: Save agent to local packages. :param ctx: the context - - :return: None + :param is_mixed: whether or not to fetch in mixed mode """ try: registry_path = ctx.registry_path diff --git a/aea/cli/push.py b/aea/cli/push.py index 6970ae2aa1..26aee4aafd 100644 --- a/aea/cli/push.py +++ b/aea/cli/push.py @@ -94,9 +94,9 @@ def _save_item_locally(ctx: Context, item_type: str, item_id: PublicId) -> None: """ Save item to local packages. + :param ctx: click context :param item_type: str type of item (connection/protocol/skill). :param item_id: the public id of the item. - :return: None """ item_type_plural = item_type + "s" try: diff --git a/aea/cli/register.py b/aea/cli/register.py index b3d1976b00..1331a46d2c 100644 --- a/aea/cli/register.py +++ b/aea/cli/register.py @@ -57,8 +57,6 @@ def do_register( :param password: str password. :param password_confirmation: str password confirmation. :param no_subscribe: bool flag for developers subscription skip on register. - - :return: None """ username = validate_author_name(username) token = register_new_account(username, email, password, password_confirmation) diff --git a/aea/cli/registry/add.py b/aea/cli/registry/add.py index d8ba0cebd5..a44a59bed9 100644 --- a/aea/cli/registry/add.py +++ b/aea/cli/registry/add.py @@ -35,6 +35,7 @@ def fetch_package(obj_type: str, public_id: PublicId, cwd: str, dest: str) -> Pa 'connection', 'protocol', 'skill' :param public_id: str public ID of object. :param cwd: str path to current working directory. + :param dest: destination where to save package. :return: package path """ diff --git a/aea/cli/registry/fetch.py b/aea/cli/registry/fetch.py index 5fdd02902a..ade9194ba1 100644 --- a/aea/cli/registry/fetch.py +++ b/aea/cli/registry/fetch.py @@ -56,7 +56,6 @@ def fetch_agent( :param public_id: str public ID of desirable agent. :param alias: an optional alias. :param target_dir: the target directory to which the agent is fetched. - :return: None """ author, name, version = public_id.author, public_id.name, public_id.version diff --git a/aea/cli/registry/login.py b/aea/cli/registry/login.py index 1b76f73270..8b650c75ee 100644 --- a/aea/cli/registry/login.py +++ b/aea/cli/registry/login.py @@ -49,7 +49,5 @@ def registry_reset_password(email: str) -> None: Request Registry to reset password. :param email: user email. - - :return: None. """ request_api("POST", "/rest-auth/password/reset/", data={"email": email}) diff --git a/aea/cli/registry/logout.py b/aea/cli/registry/logout.py index 0fc1fceb8d..de9dbb4093 100644 --- a/aea/cli/registry/logout.py +++ b/aea/cli/registry/logout.py @@ -22,9 +22,5 @@ def registry_logout() -> None: - """ - Logout from Registry account. - - :return: None - """ + """Logout from Registry account.""" request_api("POST", "/rest-auth/logout/") diff --git a/aea/cli/registry/push.py b/aea/cli/registry/push.py index 5706e0774b..4133f6126b 100644 --- a/aea/cli/registry/push.py +++ b/aea/cli/registry/push.py @@ -71,6 +71,9 @@ def check_package_public_id( """ Check component version is corresponds to specified version. + :param source_path: the source path + :param item_type: str type of item (connection/protocol/skill). + :param item_id: item public id. :return: actual package public id """ # we load only based on item_name, hence also check item_version and item_author match. @@ -98,10 +101,9 @@ def push_item(ctx: Context, item_type: str, item_id: PublicId) -> None: """ Push item to the Registry. + :param ctx: click context :param item_type: str type of item (connection/protocol/skill). - :param item_id: str item name. - - :return: None + :param item_id: item public id. """ item_type_plural = item_type + "s" diff --git a/aea/cli/registry/utils.py b/aea/cli/registry/utils.py index 056f4d6e33..d5b26c5f3c 100644 --- a/aea/cli/registry/utils.py +++ b/aea/cli/registry/utils.py @@ -69,6 +69,8 @@ def request_api( :param data: dict POST data. :param is_auth: bool is auth required (default False). :param files: optional dict {file_field_name: open(filepath, "rb")} (default None). + :param handle_400: whether or not to handle 400 response + :param return_code: whether or not to return return_code :return: dict response from Registry API or tuple (dict response, status code). """ @@ -156,8 +158,6 @@ def extract(source: str, target: str) -> None: :param source: str path to a source tarball file. :param target: str path to target directory. - - :return: None """ if source.endswith("tar.gz"): tar = tarfile.open(source, "r:gz") @@ -200,7 +200,6 @@ def check_is_author_logged_in(author_name: str) -> None: :param author_name: str item author username. :raise ClickException: if username and author's name are not equal. - :return: None. """ resp = cast(JSONLike, request_api("GET", "/rest-auth/user/", is_auth=True)) if not author_name == resp["username"]: diff --git a/aea/cli/remove.py b/aea/cli/remove.py index 0e880ecbd1..2d9802a086 100644 --- a/aea/cli/remove.py +++ b/aea/cli/remove.py @@ -76,11 +76,7 @@ def remove( @click.argument("connection_id", type=PublicIdParameter(), required=True) @pass_ctx def connection(ctx: Context, connection_id: PublicId) -> None: - """ - Remove a connection from the agent. - - It expects the public id of the connection to remove from the local registry. - """ + """Remove a connection from the agent.""" remove_item(ctx, CONNECTION, connection_id) @@ -88,11 +84,7 @@ def connection(ctx: Context, connection_id: PublicId) -> None: @click.argument("contract_id", type=PublicIdParameter(), required=True) @pass_ctx def contract(ctx: Context, contract_id: PublicId) -> None: - """ - Remove a contract from the agent. - - It expects the public id of the contract to remove from the local registry. - """ + """Remove a contract from the agent.""" remove_item(ctx, CONTRACT, contract_id) @@ -100,11 +92,7 @@ def contract(ctx: Context, contract_id: PublicId) -> None: @click.argument("protocol_id", type=PublicIdParameter(), required=True) @pass_ctx def protocol(ctx: Context, protocol_id: PublicId) -> None: - """ - Remove a protocol from the agent. - - It expects the public id of the protocol to remove from the local registry. - """ + """Remove a protocol from the agent.""" remove_item(ctx, PROTOCOL, protocol_id) @@ -112,11 +100,7 @@ def protocol(ctx: Context, protocol_id: PublicId) -> None: @click.argument("skill_id", type=PublicIdParameter(), required=True) @pass_ctx def skill(ctx: Context, skill_id: PublicId) -> None: - """ - Remove a skill from the agent. - - It expects the public id of the skill to remove from the local registry. - """ + """Remove a skill from the agent.""" remove_item(ctx, SKILL, skill_id) @@ -188,7 +172,9 @@ def _get_item_requirements( """ List all the requirements for item provided. - :return: generator with package ids: (type, public_id) + :param item: the item package configuration + :param ignore_non_vendor: whether or not to ignore vendor packages + :yield: generator with package ids: (type, public_id) """ for item_type in map(str, ComponentType): items = getattr(item, f"{item_type}s", set()) @@ -211,6 +197,8 @@ def get_item_dependencies_with_reverse_dependencies( It's recursive and provides all the sub dependencies. + :param item: the item package configuration + :param package_id: the package id. :return: dict with PackageId: and set of PackageIds that uses this package """ result: defaultdict = defaultdict(set) @@ -254,6 +242,8 @@ def check_remove( can be deleted - set of dependencies used only by component so can be deleted can not be deleted - dict - keys - packages can not be deleted, values are set of packages required by. + :param item_type: the item type. + :param item_public_id: the item public id. :return: Tuple[required by, can be deleted, can not be deleted.] """ package_id = PackageId(item_type, item_public_id) @@ -281,6 +271,9 @@ def remove_unused_component_configurations(ctx: Context) -> Generator: Context manager! Clean all configurations on enter, restore actual configurations and dump agent config. + + :param ctx: click context + :yield: generator """ saved_configuration = ctx.agent_config.component_configurations ctx.agent_config.component_configurations = {} @@ -322,11 +315,9 @@ def __init__( :param ctx: click context. :param item_type: str, package type :param item_id: PublicId of the item to remove. + :param with_dependencies: whether or not to remove dependencies. :param force: bool. if True remove even required by another package. - :param ignore_non_vendor: bool. if True, ignore non-vendor packages when computing - inverse dependencies. The effect of this flag is ignored if force = True - - :return: None + :param ignore_non_vendor: bool. if True, ignore non-vendor packages when computing inverse dependencies. The effect of this flag is ignored if force = True """ self.ctx = ctx self.force = force @@ -472,7 +463,6 @@ def remove_item(ctx: Context, item_type: str, item_id: PublicId) -> None: :param item_type: type of item. :param item_id: item public ID. - :return: None :raises ClickException: if some error occurs. """ with remove_unused_component_configurations(ctx): diff --git a/aea/cli/remove_key.py b/aea/cli/remove_key.py index d6fe28d082..e6feafe4c4 100644 --- a/aea/cli/remove_key.py +++ b/aea/cli/remove_key.py @@ -57,8 +57,6 @@ def _remove_private_key( :param click_context: click context object. :param type_: type. :param connection: whether or not it is a private key for a connection - - :return: None """ ctx = cast(Context, click_context.obj) _try_remove_key(ctx, type_, connection) diff --git a/aea/cli/reset_password.py b/aea/cli/reset_password.py index 9c49a5dd9b..6633ed3c88 100644 --- a/aea/cli/reset_password.py +++ b/aea/cli/reset_password.py @@ -38,8 +38,6 @@ def _do_password_reset(email: str) -> None: Request Registry to reset password. :param email: str email. - - :return: """ registry_reset_password(email) click.echo("An email with a password reset link was sent to {}".format(email)) diff --git a/aea/cli/run.py b/aea/cli/run.py index d560163fb5..bbf3785459 100644 --- a/aea/cli/run.py +++ b/aea/cli/run.py @@ -147,8 +147,7 @@ def run_aea( :param is_install_deps: bool flag is install dependencies. :param password: the password to encrypt/decrypt the private key. - :return: None - :raises: ClickException if any Exception occurs. + :raises ClickException: if any Exception occurs. """ skip_consistency_check = ctx.config["skip_consistency_check"] _prepare_environment(ctx, env_file, is_install_deps) diff --git a/aea/cli/scaffold.py b/aea/cli/scaffold.py index 7ef5150ab3..4cce0e54d9 100644 --- a/aea/cli/scaffold.py +++ b/aea/cli/scaffold.py @@ -131,7 +131,6 @@ def scaffold_item(ctx: Context, item_type: str, item_name: str) -> None: :param item_type: type of item. :param item_name: item name. - :return: None :raises ClickException: if some error occurs. """ validate_package_name(item_name) @@ -244,7 +243,6 @@ def _scaffold_non_package_item( :param type_name: the type name (e.g. "decision maker") :param class_name: the class name (e.g. "DecisionMakerHandler") :param aea_dir: the AEA directory that contains the scaffold module - :return: None """ existing_item = getattr(ctx.agent_config, item_type) if existing_item != {}: diff --git a/aea/cli/search.py b/aea/cli/search.py index 0422129515..75daf76cf9 100644 --- a/aea/cli/search.py +++ b/aea/cli/search.py @@ -54,15 +54,7 @@ @click.option("--local", is_flag=True, help="For local search.") @click.pass_context def search(click_context: click.Context, local: bool) -> None: - """Search for packages in the registry. - - If called from an agent directory, it will check - - E.g. - - aea search connections - aea search --local skills - """ + """Search for packages in the registry.""" ctx = cast(Context, click_context.obj) if local: ctx.set_config("is_local", True) @@ -197,6 +189,7 @@ def search_items( :param ctx: Context object. :param item_type: item type. :param query: query string. + :param page: page. :return: (List of items, int items total count). """ @@ -228,7 +221,7 @@ def _output_search_results( :param item_type: str item type. :param results: list of found items. :param count: items total count. - + :param page: page. """ item_type_plural = item_type + "s" len_results = len(results) diff --git a/aea/cli/transfer.py b/aea/cli/transfer.py index 17d10a244b..dbca1279d5 100644 --- a/aea/cli/transfer.py +++ b/aea/cli/transfer.py @@ -112,8 +112,7 @@ def wait_tx_settled( :param tx_digest: str, transaction digest :param timeout: int, timeout in seconds before timeout error raised - :return: None - raises TimeoutError on timeout + :raises TimeoutError: on timeout """ t = time.time() while True: diff --git a/aea/cli/upgrade.py b/aea/cli/upgrade.py index d746d8373a..bea6b09f65 100644 --- a/aea/cli/upgrade.py +++ b/aea/cli/upgrade.py @@ -128,7 +128,6 @@ def update_agent_config(ctx: Context) -> None: - update author name if it is different :param ctx: the context. - :return: None """ update_aea_version_range(ctx.agent_config) cli_author = ctx.config.get("cli_author") @@ -145,7 +144,6 @@ def update_aea_version_in_nonvendor_packages(cwd: str) -> None: Update aea_version in non-vendor packages. :param cwd: the current working directory. - :return: None """ for package_path in get_non_vendor_package_path(Path(cwd)): package_type = PackageType(package_path.parent.name[:-1]) @@ -313,11 +311,7 @@ def upgrade(self) -> bool: return True def _unpack_fetched_agent(self) -> None: - """ - Unpack fetched agent in current directory and remove temporary directory. - - :return: None - """ + """Unpack fetched agent in current directory and remove temporary directory.""" current_path = Path(self.ctx.cwd) fetched_agent_dir = current_path / self._TEMP_ALIAS for subpath in fetched_agent_dir.iterdir(): @@ -608,6 +602,10 @@ def _try_to_confirm(message: str, yes_by_default: bool) -> bool: In particular: - if "yes_by_default" is True, never prompt and return True. - if "yes_by_default" is False, ask to the user. + + :param message: the message + :param yes_by_default: bool to override confirm + :return: result """ return click.confirm(message) if not yes_by_default else True @@ -620,7 +618,6 @@ def upgrade_item(ctx: Context, item_type: str, item_public_id: PublicId) -> None :param ctx: Context object. :param item_type: the item type. :param item_public_id: the item public id. - :return: None """ try: with remove_unused_component_configurations(ctx): diff --git a/aea/cli/utils/click_utils.py b/aea/cli/utils/click_utils.py index 9b9974eb93..b05ffcb40a 100644 --- a/aea/cli/utils/click_utils.py +++ b/aea/cli/utils/click_utils.py @@ -45,7 +45,7 @@ def type_cast_value( :param ctx: the click context :param value: the list of connection names, as a string. - :return: + :return: list of public ids """ if value is None: return None @@ -78,6 +78,9 @@ def __init__( # pylint: disable=useless-super-delegation Initialize the Public Id parameter. Just forwards arguments to parent constructor. + + :param args: positional arguments + :param kwargs: keyword arguments """ super().__init__(*args, **kwargs) # type: ignore @@ -187,7 +190,7 @@ def handle_parse_result( :param ctx: the click context. :param opts: the options. :param args: the list of arguments (to be forwarded to the parent class). - :return: + :return: tuple of results """ if self.mutually_exclusive.intersection(opts) and self.name in opts: raise UsageError( diff --git a/aea/cli/utils/config.py b/aea/cli/utils/config.py index c60b4be96b..6c91db88c0 100644 --- a/aea/cli/utils/config.py +++ b/aea/cli/utils/config.py @@ -69,8 +69,6 @@ def try_to_load_agent_config( :param ctx: click command context object. :param is_exit_on_except: bool option to exit on exception (default = True). :param agent_src_path: path to an agent dir if needed to load a custom config. - - :return None """ if agent_src_path is None: agent_src_path = ctx.cwd @@ -103,11 +101,7 @@ def try_to_load_agent_config( def _init_cli_config() -> None: - """ - Create cli config folder and file. - - :return: None - """ + """Create cli config folder and file.""" conf_dir = os.path.dirname(CLI_CONFIG_PATH) if not os.path.exists(conf_dir): os.makedirs(conf_dir) @@ -120,8 +114,6 @@ def update_cli_config(dict_conf: Dict) -> None: Update CLI config and write to yaml file. :param dict_conf: dict config to write. - - :return: None """ config = get_or_create_cli_config() config.update(dict_conf) @@ -149,7 +141,6 @@ def set_cli_author(click_context: click.Context) -> None: The key of the new field is 'cli_author'. :param click_context: the Click context - :return: None. """ config = get_or_create_cli_config() cli_author = config.get(AUTHOR_KEY, None) @@ -191,8 +182,6 @@ def dump_item_config( :param package_configuration: the package configuration. :param package_path: path to package from which config should be dumped. - - :return: None """ configuration_file_name = _get_default_configuration_file_name_from_type( package_configuration.package_type @@ -212,8 +201,6 @@ def update_item_config(item_type: str, package_path: Path, **kwargs: Any) -> Non :param item_type: type of item. :param package_path: path to a package folder. :param kwargs: pairs of config key-value to update. - - :return: None """ item_config = load_item_config(item_type, package_path) for key, value in kwargs.items(): @@ -234,7 +221,6 @@ def validate_item_config(item_type: str, package_path: Path) -> None: :param item_type: type of item. :param package_path: path to a package folder. - :return: None :raises AEAConfigException: if something is wrong with item configuration. """ item_config = load_item_config(item_type, package_path) diff --git a/aea/cli/utils/context.py b/aea/cli/utils/context.py index 41f3c96a11..ee90920175 100644 --- a/aea/cli/utils/context.py +++ b/aea/cli/utils/context.py @@ -108,7 +108,6 @@ def set_config(self, key: str, value: Any) -> None: :param key: the key for the configuration. :param value: the value associated with the key. - :return: None """ self.config[key] = value logger.debug(" config[{}] = {}".format(key, value)) @@ -138,7 +137,7 @@ def _get_item_dependencies(item_type: str, public_id: PublicId) -> Dependencies: def get_dependencies(self) -> Dependencies: """Aggregate the dependencies from every component. - :return a list of dependency version specification. e.g. ["gym >= 1.0.0"] + :return: a list of dependency version specification. e.g. ["gym >= 1.0.0"] """ dependencies = {} # type: Dependencies diff --git a/aea/cli/utils/decorators.py b/aea/cli/utils/decorators.py index 5b1415e1c7..c9a188b984 100644 --- a/aea/cli/utils/decorators.py +++ b/aea/cli/utils/decorators.py @@ -53,7 +53,7 @@ def _validate_config_consistency(ctx: Context, check_aea_version: bool = True) - :param ctx: the context :param check_aea_version: whether it should check also the AEA version. - :raise ValueError: if there is a missing configuration file. + :raises ValueError: if there is a missing configuration file. or if the configuration file is not valid. or if the fingerprints do not match """ @@ -147,6 +147,11 @@ def check_aea_project( - try to load agent configuration file - iterate over all the agent packages and check for consistency. + + :param f: callable + :param check_aea_version: whether or not to check aea version + :param check_finger_prints: whether or not to check fingerprints + :return: callable """ def wrapper(*args: Any, **kwargs: Any) -> Callable: @@ -165,8 +170,6 @@ def _rmdirs(*paths: str) -> None: Remove directories. :param paths: paths to folders to remove. - - :return: None """ for path in paths: if os.path.exists(path): @@ -208,6 +211,8 @@ def wrapper( Call a source method, remove dirs listed in ctx.clean_paths if ClickException is raised. :param context: context object. + :param args: positional arguments. + :param kwargs: keyword arguments. :raises ClickException: if caught re-raises it. diff --git a/aea/cli/utils/loggers.py b/aea/cli/utils/loggers.py index 2f4608868e..bb904bbb4d 100644 --- a/aea/cli/utils/loggers.py +++ b/aea/cli/utils/loggers.py @@ -63,6 +63,11 @@ def simple_verbosity_option( Name can be configured through `*names`. Keyword arguments are passed to the underlying `click.option` decorator. + + :param logger_: the logger + :param names: list of names + :param kwargs: keyword arguments + :return: callable """ if not names: names = ("--verbosity", "-v") diff --git a/aea/cli/utils/package_utils.py b/aea/cli/utils/package_utils.py index 901c32dc84..6eed192b7c 100644 --- a/aea/cli/utils/package_utils.py +++ b/aea/cli/utils/package_utils.py @@ -84,7 +84,6 @@ def verify_private_keys_ctx( :param ctx: Context :param aea_project_path: the path to the aea project - :param exit_on_error: whether or not to exit on error :param password: the password to encrypt/decrypt the private key. """ try: @@ -111,6 +110,8 @@ def validate_package_name(package_name: str) -> None: Traceback (most recent call last): ... click.exceptions.BadParameter: this-is-not is not a valid package name. + + :param package_name: the package name """ if re.fullmatch(PublicId.PACKAGE_NAME_REGEX, package_name) is None: raise click.BadParameter("{} is not a valid package name.".format(package_name)) @@ -126,6 +127,9 @@ def _is_valid_author_handle(author: str) -> bool: >>> _is_valid_author_handle("this-is-not") ... False + + :param author: author name + :return: bool indicating whether author name is valid """ if re.fullmatch(PublicId.AUTHOR_REGEX, author) is None: return False @@ -174,7 +178,7 @@ def try_get_item_target_path( Get the item target path. :param path: the target path root - :param author_name the author name + :param author_name: the author name :param item_type_plural: the item type (plural) :param item_name: the item name @@ -229,6 +233,7 @@ def get_package_path_unified( - Otherwise, first look into local packages, then into vendor/. :param project_directory: directory to look for packages. + :param agent_config: agent configuration. :param item_type: item type. :param public_id: item public ID. @@ -270,7 +275,7 @@ def copy_package_directory(src: Path, dst: str) -> Path: :param dst: str package destination path. :return: copied folder target path. - :raises SystemExit: if the copy raises an exception. + :raises ClickException: if the copy raises an exception. """ # copy the item package into the agent's supported packages. src_path = str(src.absolute()) @@ -297,7 +302,7 @@ def find_item_locally( :return: tuple of path to the package directory (either in registry or in aea directory) and component configuration - :raises SystemExit: if the search fails. + :raises ClickException: if the search fails. """ item_type_plural = item_type + "s" item_name = item_public_id.name @@ -354,7 +359,7 @@ def find_item_in_distribution( # pylint: disable=unused-argument :param item_type: the type of the item to load. One of: protocols, connections, skills :param item_public_id: the public id of the item to find. :return: path to the package directory (either in registry or in aea directory). - :raises SystemExit: if the search fails. + :raises ClickException: if the search fails. """ item_type_plural = item_type + "s" item_name = item_public_id.name @@ -399,6 +404,7 @@ def validate_author_name(author: Optional[str] = None) -> str: Validate an author name. :param author: the author name (optional) + :return: validated author name """ is_acceptable_author = False if ( @@ -444,8 +450,7 @@ def is_fingerprint_correct( :param package_path: path to a package folder. :param item_config: item configuration. - - :return: None. + :return: bool indicating correctness of fingerprint. """ fingerprint = _compute_fingerprint( package_path, ignore_patterns=item_config.fingerprint_ignore_patterns @@ -460,8 +465,6 @@ def register_item(ctx: Context, item_type: str, item_public_id: PublicId) -> Non :param ctx: click context object. :param item_type: type of item. :param item_public_id: PublicId of item. - - :return: None. """ logger.debug( "Registering the {} into {}".format(item_type, DEFAULT_AEA_CONFIG_FILE) @@ -596,6 +599,9 @@ def is_distributed_item(item_public_id: PublicId) -> bool: If the provided item has version 'latest', only the prefixes are compared. Otherwise, the function will try to match the exact version occurrence among the distributed packages. + + :param item_public_id: public id of the item + :return: bool, indicating whether distributed or not """ if item_public_id.package_version.is_latest: return any(item_public_id.same_prefix(other) for other in DISTRIBUTED_PACKAGES) @@ -650,6 +656,7 @@ def get_wallet_from_context(ctx: Context, password: Optional[str] = None) -> Wal Get wallet from current click Context. :param ctx: click context + :param password: the password to encrypt/decrypt private keys :return: wallet """ @@ -667,8 +674,6 @@ def update_item_public_id_in_init( :param item_type: type of item. :param package_path: path to a package folder. :param item_id: public_id - - :return: None """ if item_type != SKILL: return @@ -698,7 +703,6 @@ def update_references( :param ctx: the context. :param replacements: mapping from old component ids to new component ids. - :return: None. """ # preprocess replacement so to index them by component type replacements_by_type: Dict[ComponentType, Dict[PublicId, PublicId]] = {} @@ -730,8 +734,6 @@ def create_symlink_vendor_to_local( :param ctx: click context :param item_type: item type :param public_id: public_id of the item - - :return: None """ vendor_path_str = get_package_path(ctx.cwd, item_type, public_id, is_vendor=True) local_path = get_package_path(ctx.cwd, item_type, public_id, is_vendor=False) @@ -746,8 +748,6 @@ def create_symlink_packages_to_vendor(ctx: Context) -> None: Creates a symlink from a local packages to the vendor folder. :param ctx: click context - - :return: None """ if not os.path.exists(PACKAGES): create_symlink(Path(PACKAGES), Path(VENDOR), Path(ctx.cwd)) @@ -770,7 +770,6 @@ def replace_all_import_statements( :param item_type: the item type. :param old_public_id: the old public id. :param new_public_id: the new public id. - :return: None """ old_formats = dict( author=old_public_id.author, type=item_type.to_plural(), name=old_public_id.name @@ -799,7 +798,6 @@ def fingerprint_all(ctx: Context) -> None: Fingerprint all non-vendor packages. :param ctx: the CLI context. - :return: None """ aea_project_path = Path(ctx.cwd) for package_path in get_non_vendor_package_path(aea_project_path): diff --git a/aea/crypto/registries/base.py b/aea/crypto/registries/base.py index 0f3185a6f0..3ed81f2ace 100644 --- a/aea/crypto/registries/base.py +++ b/aea/crypto/registries/base.py @@ -184,7 +184,6 @@ def register( :param entry_point: the entry point to load the crypto object. :param class_kwargs: keyword arguments to be attached on the class as class variables. :param kwargs: arguments to provide to the crypto class. - :return: None. """ item_id = ItemId(id_) entry_point = EntryPoint[ItemType](entry_point) From 3bcf4b0e5d68ec39e77f7a01261eed5053111863 Mon Sep 17 00:00:00 2001 From: David Minarsch Date: Tue, 1 Jun 2021 16:01:44 +0100 Subject: [PATCH 130/147] chore: finish darglint fixes for aea --- aea/aea_builder.py | 6 +++--- aea/cli/fetch.py | 2 -- aea/cli/publish.py | 1 - aea/cli/remove.py | 2 -- aea/cli/utils/decorators.py | 2 +- aea/components/loader.py | 4 ++-- aea/configurations/manager.py | 2 +- aea/connections/scaffold/connection.py | 2 +- aea/contracts/base.py | 6 +++--- aea/contracts/scaffold/contract.py | 6 +++--- aea/crypto/plugin.py | 2 +- aea/helpers/base.py | 4 ++-- aea/helpers/search/models.py | 6 +++--- aea/protocols/scaffold/serialization.py | 4 ++-- aea/registries/base.py | 2 +- setup.cfg | 2 +- 16 files changed, 24 insertions(+), 29 deletions(-) diff --git a/aea/aea_builder.py b/aea/aea_builder.py index 57d78e1c77..c678ca5641 100644 --- a/aea/aea_builder.py +++ b/aea/aea_builder.py @@ -924,8 +924,8 @@ def add_component( :param component_type: the component type. :param directory: the directory path. :param skip_consistency_check: if True, the consistency check are skipped. - :raises AEAException: if a component is already registered with the same component id. - | or if there's a missing dependency. + :raises AEAException: if a component is already registered with the same component id. # noqa: DAR402 + | or if there's a missing dependency. # noqa: DAR402 :return: the AEABuilder """ directory = Path(directory) @@ -1799,7 +1799,7 @@ def _find_import_order( - load the skill/connection configurations to find the import order - detect if there are cycles - import skills/connections from the leaves of the dependency graph, by finding a topological ordering. - + :param component_ids: component ids to check :param aea_project_path: project path to AEA :param skip_consistency_check: consistency check of AEA diff --git a/aea/cli/fetch.py b/aea/cli/fetch.py index f3f513f9cd..d94df2a4fc 100644 --- a/aea/cli/fetch.py +++ b/aea/cli/fetch.py @@ -187,8 +187,6 @@ def _fetch_agent_deps(ctx: Context) -> None: Fetch agent dependencies. :param ctx: context object. - - :raises ClickException: re-raises if occurs in add_item call. """ for item_type in (PROTOCOL, CONTRACT, CONNECTION, SKILL): item_type_plural = "{}s".format(item_type) diff --git a/aea/cli/publish.py b/aea/cli/publish.py index ea93b0b17b..af019a5db2 100644 --- a/aea/cli/publish.py +++ b/aea/cli/publish.py @@ -121,7 +121,6 @@ def _check_is_item_in_remote_registry( :param public_id: the public id. :param item_type_plural: the type of the item. - :raises ClickException: if the item is not present. """ get_package_meta(item_type_plural[:-1], public_id) diff --git a/aea/cli/remove.py b/aea/cli/remove.py index 2d9802a086..f9c5be550f 100644 --- a/aea/cli/remove.py +++ b/aea/cli/remove.py @@ -462,8 +462,6 @@ def remove_item(ctx: Context, item_type: str, item_id: PublicId) -> None: :param ctx: Context object. :param item_type: type of item. :param item_id: item public ID. - - :raises ClickException: if some error occurs. """ with remove_unused_component_configurations(ctx): RemoveItem( diff --git a/aea/cli/utils/decorators.py b/aea/cli/utils/decorators.py index c9a188b984..59098da02f 100644 --- a/aea/cli/utils/decorators.py +++ b/aea/cli/utils/decorators.py @@ -214,7 +214,7 @@ def wrapper( :param args: positional arguments. :param kwargs: keyword arguments. - :raises ClickException: if caught re-raises it. + :raises ClickException: if caught re-raises it. # noqa: DAR402 :return: source method output. """ diff --git a/aea/components/loader.py b/aea/components/loader.py index a497fcfda4..8e6e3c913d 100644 --- a/aea/components/loader.py +++ b/aea/components/loader.py @@ -102,7 +102,7 @@ def _handle_error_while_loading_component_module_not_found( :param configuration: the configuration :param e: the exception - :raises ModuleNotFoundError: if it is not + :raises ModuleNotFoundError: if it is not # noqa: DAR402 :raises AEAPackageLoadingError: the same exception, but prepending an informative message. """ error_message = str(e) @@ -171,7 +171,7 @@ def _handle_error_while_loading_component_generic_error( :param configuration: the configuration :param e: the exception - :raises Exception: the same exception, but prepending an informative message. + :raises AEAPackageLoadingError: the same exception, but prepending an informative message. """ e_str = parse_exception(e) raise AEAPackageLoadingError( diff --git a/aea/configurations/manager.py b/aea/configurations/manager.py index 9eccb48fe9..21c755476c 100644 --- a/aea/configurations/manager.py +++ b/aea/configurations/manager.py @@ -121,7 +121,7 @@ def _try_get_component_id_from_prefix( :param component_ids: the set of component id. :param component_prefix: the component prefix. :return: the component id that matches the prefix. - :raises ValueError: if there are more than two components as candidate results. + :raises AEAEnforceError: if there are more than two components as candidate results. # noqa: DAR402 """ type_, author, name = component_prefix results = list( diff --git a/aea/connections/scaffold/connection.py b/aea/connections/scaffold/connection.py index ab411dcb8c..9db19b8c33 100644 --- a/aea/connections/scaffold/connection.py +++ b/aea/connections/scaffold/connection.py @@ -87,7 +87,7 @@ async def receive(self, *args: Any, **kwargs: Any) -> Optional[Envelope]: :param args: arguments to receive :param kwargs: keyword arguments to receive - :return: the envelope received, if present. + :return: the envelope received, if present. # noqa: DAR202 """ raise NotImplementedError # pragma: no cover diff --git a/aea/contracts/base.py b/aea/contracts/base.py index aea5959548..b22d69a26b 100644 --- a/aea/contracts/base.py +++ b/aea/contracts/base.py @@ -164,7 +164,7 @@ def get_raw_transaction( :param ledger_api: the ledger apis. :param contract_address: the contract address. :param kwargs: the keyword arguments. - :return: the tx + :return: the tx # noqa: DAR202 """ raise NotImplementedError @@ -181,7 +181,7 @@ def get_raw_message( :param ledger_api: the ledger apis. :param contract_address: the contract address. :param kwargs: the keyword arguments. - :return: the tx + :return: the tx # noqa: DAR202 """ raise NotImplementedError @@ -198,7 +198,7 @@ def get_state( :param ledger_api: the ledger apis. :param contract_address: the contract address. :param kwargs: the keyword arguments. - :return: the tx + :return: the tx # noqa: DAR202 """ raise NotImplementedError diff --git a/aea/contracts/scaffold/contract.py b/aea/contracts/scaffold/contract.py index 6c73ff2e1f..39cffd238f 100644 --- a/aea/contracts/scaffold/contract.py +++ b/aea/contracts/scaffold/contract.py @@ -45,7 +45,7 @@ def get_raw_transaction( :param ledger_api: the ledger apis. :param contract_address: the contract address. :param kwargs: the keyword arguments. - :return: the tx + :return: the tx # noqa: DAR202 """ raise NotImplementedError @@ -62,7 +62,7 @@ def get_raw_message( :param ledger_api: the ledger apis. :param contract_address: the contract address. :param kwargs: the keyword arguments. - :return: the tx + :return: the tx # noqa: DAR202 """ raise NotImplementedError @@ -79,6 +79,6 @@ def get_state( :param ledger_api: the ledger apis. :param contract_address: the contract address. :param kwargs: the keyword arguments. - :return: the tx + :return: the tx # noqa: DAR202 """ raise NotImplementedError diff --git a/aea/crypto/plugin.py b/aea/crypto/plugin.py index c6273d0100..ed64665297 100644 --- a/aea/crypto/plugin.py +++ b/aea/crypto/plugin.py @@ -64,7 +64,7 @@ def _check_consistency(self) -> None: """ Check consistency of input. - :raises AEAPluginError: if some input is not correct. + :raises AEAPluginError: if some input is not correct. # noqa: DAR402 """ _error_message_prefix = f"Error with plugin '{self._entry_point.name}':" enforce( diff --git a/aea/helpers/base.py b/aea/helpers/base.py index d559e977c3..9090a81525 100644 --- a/aea/helpers/base.py +++ b/aea/helpers/base.py @@ -118,8 +118,8 @@ def load_module(dotted_path: str, filepath: Path) -> types.ModuleType: :param dotted_path: the dotted save_path of the package/module. :param filepath: the file to the package/module. :return: module type - :raises ValueError: if the filepath provided is not a module. - :raises Exception: if the execution of the module raises exception. + :raises ValueError: if the filepath provided is not a module. # noqa: DAR402 + :raises Exception: if the execution of the module raises exception. # noqa: DAR402 """ spec = importlib.util.spec_from_file_location(dotted_path, str(filepath)) module = importlib.util.module_from_spec(spec) diff --git a/aea/helpers/search/models.py b/aea/helpers/search/models.py index 7e9d1397cb..b79d9d88c4 100644 --- a/aea/helpers/search/models.py +++ b/aea/helpers/search/models.py @@ -616,7 +616,7 @@ def __init__(self, type_: Union[ConstraintTypes, str], value: Any) -> None: | Either an instance of the ConstraintTypes enum, | or a string representation associated with the type. :param value: the value that defines the constraint. - :raises ValueError: if the type of the constraint is not + :raises AEAEnforceError: if the type of the constraint is not # noqa: DAR402 """ self.type = ConstraintTypes(type_) self.value = value @@ -627,7 +627,7 @@ def check_validity(self) -> bool: Check the validity of the input provided. :return: boolean to indicate validity - :raises ValueError: if the value is not valid wrt the constraint type. + :raises AEAEnforceError: if the value is not valid wrt the constraint type. # noqa: DAR402 """ try: if self.type == ConstraintTypes.EQUAL: @@ -1067,7 +1067,7 @@ def check_validity(self) -> None: # pylint: disable=no-self-use # pragma: noco """ Check whether a Constraint Expression satisfies some basic requirements. - :raises ValueError: if the object does not satisfy some requirements. + :raises AEAEnforceError: if the object does not satisfy some requirements. # noqa: DAR402 """ @staticmethod diff --git a/aea/protocols/scaffold/serialization.py b/aea/protocols/scaffold/serialization.py index 82d9086e05..9c5256739d 100644 --- a/aea/protocols/scaffold/serialization.py +++ b/aea/protocols/scaffold/serialization.py @@ -32,7 +32,7 @@ def encode(msg: Message) -> bytes: Decode the message. :param msg: the message object - :return: the bytes + :return: the bytes # noqa: DAR202 """ raise NotImplementedError # pragma: no cover @@ -42,6 +42,6 @@ def decode(obj: bytes) -> Message: Decode the message. :param obj: the bytes object - :return: the message + :return: the message # noqa: DAR202 """ raise NotImplementedError # pragma: no cover diff --git a/aea/registries/base.py b/aea/registries/base.py index 67bb732b08..a8ae739aef 100644 --- a/aea/registries/base.py +++ b/aea/registries/base.py @@ -562,7 +562,7 @@ def unregister(self, item_id: Tuple[PublicId, str]) -> Handler: Unregister a item. :param item_id: a pair (skill id, item name). - :return: the unregistered handler + :return: the unregistered handler :raises: ValueError if no item is registered with that item id. """ skill_id = item_id[0] diff --git a/setup.cfg b/setup.cfg index cfcbc7f7fb..28e10053d1 100644 --- a/setup.cfg +++ b/setup.cfg @@ -13,7 +13,7 @@ exclude=.md, scripts/oef/launch.py max-line-length = 88 select = B,C,D,E,F,I,W, -ignore = E203,E501,W503,D202,B014,D400,D401 +ignore = E203,E501,W503,D202,B014,D400,D401,DAR application-import-names = aea,packages,tests,scripts # ignore as too restrictive for our needs: From 59044dd7ba21061f954cb99686da7e3fdd2fe409 Mon Sep 17 00:00:00 2001 From: David Minarsch Date: Tue, 1 Jun 2021 16:26:40 +0100 Subject: [PATCH 131/147] fix: address docstring issues identified in code review --- aea/cli/local_registry_sync.py | 2 +- aea/cli/remove.py | 4 ++-- aea/connections/scaffold/connection.yaml | 2 +- aea/contracts/scaffold/contract.yaml | 2 +- aea/manager/manager.py | 3 +-- aea/protocols/scaffold/protocol.yaml | 4 ++-- aea/skills/scaffold/skill.yaml | 4 ++-- aea/test_tools/test_cases.py | 1 + packages/hashes.csv | 8 ++++---- 9 files changed, 15 insertions(+), 15 deletions(-) diff --git a/aea/cli/local_registry_sync.py b/aea/cli/local_registry_sync.py index 406302c7d7..d113c28771 100644 --- a/aea/cli/local_registry_sync.py +++ b/aea/cli/local_registry_sync.py @@ -110,7 +110,7 @@ def enlist_packages( :param base_dir: path or str of the local repo. :param skip_consistency_check: whether or not to skip consistency checks. - :yield: generator with Tuple of package_id and package directory. + :yield: a Tuple of package_id and package directory. """ for author in os.listdir(base_dir): author_dir = os.path.join(base_dir, author) diff --git a/aea/cli/remove.py b/aea/cli/remove.py index f9c5be550f..8549ee342d 100644 --- a/aea/cli/remove.py +++ b/aea/cli/remove.py @@ -174,7 +174,7 @@ def _get_item_requirements( :param item: the item package configuration :param ignore_non_vendor: whether or not to ignore vendor packages - :yield: generator with package ids: (type, public_id) + :yield: package ids: (type, public_id) """ for item_type in map(str, ComponentType): items = getattr(item, f"{item_type}s", set()) @@ -273,7 +273,7 @@ def remove_unused_component_configurations(ctx: Context) -> Generator: Clean all configurations on enter, restore actual configurations and dump agent config. :param ctx: click context - :yield: generator + :yield: None """ saved_configuration = ctx.agent_config.component_configurations ctx.agent_config.component_configurations = {} diff --git a/aea/connections/scaffold/connection.yaml b/aea/connections/scaffold/connection.yaml index 7e545a8f46..0e20859d00 100644 --- a/aea/connections/scaffold/connection.yaml +++ b/aea/connections/scaffold/connection.yaml @@ -8,7 +8,7 @@ license: Apache-2.0 aea_version: '>=1.0.0, <2.0.0' fingerprint: __init__.py: QmZvYZ5ECcWwqiNGh8qNTg735wu51HqaLxTSifUxkQ4KGj - connection.py: QmWzHujCpkxBv8gDMreEohFLhrhWjpuq5Y871bDnNxffDU + connection.py: QmPSdUNVTdgcoS8VFXQPZyjX8DjywLiK4Gt5Z64ekVWcqh readme.md: Qmdt71SaCCwAG1c24VktXDm4pxgUBiPMg4bWfUTiqorypf fingerprint_ignore_patterns: [] connections: [] diff --git a/aea/contracts/scaffold/contract.yaml b/aea/contracts/scaffold/contract.yaml index 12bb6b1433..7715132bfa 100644 --- a/aea/contracts/scaffold/contract.yaml +++ b/aea/contracts/scaffold/contract.yaml @@ -7,7 +7,7 @@ license: Apache-2.0 aea_version: '>=1.0.0, <2.0.0' fingerprint: __init__.py: QmPBwWhEg3wcH1q9612srZYAYdANVdWLDFWKs7TviZmVj6 - contract.py: QmbaG1cJbzb1oNAce78n8UuLKqquHFLvHKKBfHL5F5Ci7G + contract.py: QmbZsDupLbK3dSN8H3nxPRvK7ReQcGzuvd4ZQmzz6LphK7 fingerprint_ignore_patterns: [] class_name: MyScaffoldContract contract_interface_paths: {} diff --git a/aea/manager/manager.py b/aea/manager/manager.py index 05aca4f7e5..55f352fb72 100644 --- a/aea/manager/manager.py +++ b/aea/manager/manager.py @@ -861,7 +861,6 @@ def _default_error_callback( :param agent_name: the agent name :param exception: the caught exception - :return None """ self._print_exception_occurred_but_no_error_callback(agent_name, exception) @@ -872,7 +871,7 @@ def _print_exception_occurred_but_no_error_callback( Print a warning message when an exception occurred but no error callback is registered. :param agent_name: the agent name. - :return: None + :param exception: the caught exception. """ if self._warning_message_printed_for_agent.get(agent_name, False): return diff --git a/aea/protocols/scaffold/protocol.yaml b/aea/protocols/scaffold/protocol.yaml index fb391a3b65..b115d47377 100644 --- a/aea/protocols/scaffold/protocol.yaml +++ b/aea/protocols/scaffold/protocol.yaml @@ -8,7 +8,7 @@ aea_version: '>=1.0.0, <2.0.0' protocol_specification_id: fetchai/scaffold:0.1.0 fingerprint: __init__.py: Qmc9Ln8THrWmwou4nr3Acag7vcZ1fv8v5oRSkCWtv1aH6t - message.py: QmReBFhGWJLt56LU9En8hmbkUvcH8ZxU4zLNQxu1TbUA68 - serialization.py: QmaAf5fppirUWe8JaeBbsqfbeofTHe8DDGHJooe2X389qo + message.py: QmSTvkiRCT9SEvKkNsMHABM1L2i88tFeKBdVaQDV8ntxBz + serialization.py: QmZbSDKy54EuhX8QN5BFP8pgZyUYMZWwPKRkdiqHudRcVL fingerprint_ignore_patterns: [] dependencies: {} diff --git a/aea/skills/scaffold/skill.yaml b/aea/skills/scaffold/skill.yaml index ddfbb71bce..febe33a3c0 100644 --- a/aea/skills/scaffold/skill.yaml +++ b/aea/skills/scaffold/skill.yaml @@ -7,8 +7,8 @@ license: Apache-2.0 aea_version: '>=1.0.0, <2.0.0' fingerprint: __init__.py: QmYRssFqDqb3uWDvfoXy93avisjKRx2yf9SbAQXnkRj1QB - behaviours.py: QmNgDDAmBzWBeBF7e5gUCny38kdqVVfpvHGaAZVZcMtm9Q - handlers.py: QmTHd2YX941T76eS39SSdfiafXBYehL2oibugnVmmp4Tvy + behaviours.py: QmW5Qngr5LFj5XBi9d71sNRG29G6NKgbq6swv5XQRt11va + handlers.py: QmPVNnZCXu2LgJZoMiSLj3qe7EAeJqDEM3CV75JvNGiwCM my_model.py: QmPaZ6G37Juk63mJj88nParaEp71XyURts8AmmX1axs24V fingerprint_ignore_patterns: [] connections: [] diff --git a/aea/test_tools/test_cases.py b/aea/test_tools/test_cases.py index af721bef93..20e2b9dc25 100644 --- a/aea/test_tools/test_cases.py +++ b/aea/test_tools/test_cases.py @@ -691,6 +691,7 @@ def get_wealth( Run from agent's directory. :param ledger_api_id: ledger API ID. + :param password: the password to encrypt/decrypt private keys. :return: command line output """ diff --git a/packages/hashes.csv b/packages/hashes.csv index ec334c096f..8f38eec7ad 100644 --- a/packages/hashes.csv +++ b/packages/hashes.csv @@ -45,7 +45,7 @@ fetchai/connections/p2p_libp2p,QmVNYmkpXEispwVdDjPjkrKpaZ5S4pPGtRhgZBz1QEKVTv fetchai/connections/p2p_libp2p_client,QmYq2U83xTTnv1WDy1quvEz47QaV2uw1ZmB6sFHoCL31GC fetchai/connections/p2p_stub,QmToCExj3ZpdxUu3vYSMo4jZRJiMTkXMbyhbh18Hq6Nc4b fetchai/connections/prometheus,QmeRmgrccxRh8UG4AfTMct6D5wSuK9BXYoLwnohut2LJaB -fetchai/connections/scaffold,QmSrZ99ccW1VDxo6kR8TWENzNXcFWmV7aje6RNcSEwqRyd +fetchai/connections/scaffold,QmXkrasghjzRmos9i2hmPDK8sJ419exdjaiNW6fQKA4uTx fetchai/connections/soef,QmXYKva5pyxunoDuw8EbMcRLLbZsLDvA22chGq4KMvoyys fetchai/connections/stub,QmQjSUgExNU6Wgks9rwBa1zYsjdPkzs7FZy3SS2Lo3bcqz fetchai/connections/tcp,QmceuewTDJ8eKeCkcHH1enwF7EEocajkmuHi7QptJB7r5j @@ -55,7 +55,7 @@ fetchai/contracts/erc1155,QmZGci8V8dbWZuQJZk1kX2Ziod2WwkriiKpVz8CFiH2p3h fetchai/contracts/fet_erc20,QmSWRCQAfvW8jWCohyGnuYewyLeAsdn9HCGNzEExLSNgXx fetchai/contracts/oracle,QmaGgr3vdgKdT5NHxYR4KQLtrGgorqJm7vkCmeVPVseBPi fetchai/contracts/oracle_client,QmWHkC13YjAYYbUmXiovy5qj9fYgmGL1E2DQBB4cvZdiw7 -fetchai/contracts/scaffold,QmZuYdqJtxhKQU4sDHjaarya9tF6nctRFNZqoiF7WsgbS9 +fetchai/contracts/scaffold,QmVpHToPRYPjBbjQd3fArdb1SWHqiQAvDnLickULehsNRL fetchai/contracts/staking_erc20,QmVJZpvNmgVYWmD11Br8uytKVvYNSm6zHyrHdBNvK5Ag7s fetchai/protocols/aggregation,Qmf1cCWdpFKGUp3jZubQbFxQd5iDTWNVX2BEBTCAwmGGoG fetchai/protocols/contract_api,QmYjgYKBM9ATJ9S2ReNVFy7GEQzBA6CGmea5giyzJVUV84 @@ -68,7 +68,7 @@ fetchai/protocols/ml_trade,QmSqZQBNVPQGbU9sirQVXx8hkkAYCFEBwb8XH4kpB7pKHQ fetchai/protocols/oef_search,QmXxdyom7byWhwU65mHeoKuMYaS1xZbmuiuQpVBv6i7omi fetchai/protocols/prometheus,QmNkFAa1c2K39CQvfsYNQ2cpDioWU6xS2ReLn4eHSZvtoG fetchai/protocols/register,QmY5fspiKidLz5dUQh6wZGvfxwzEbAUNydZHhMe1QX2D2M -fetchai/protocols/scaffold,Qme5dmt3JdcPggLQEv2kJTPBdauJBTw6z6cyhH7qpCCf9i +fetchai/protocols/scaffold,QmXAP9ynrTauMpHZeZNcqaACkVhb2kVuGucqiy6eDNBqwR fetchai/protocols/signing,QmTQ7eki7Bnc71DNgqqDg8abf4dqHnnuaShFQdJULHE6kA fetchai/protocols/state_update,QmcLkEEmAxaynioNobJvaMdWUJuG8AG2BzxCYwX6kJhhA5 fetchai/protocols/tac,QmZGEpFDwtUU7ykRmwr3Scjmw69CixH7ZFMNptYWSZ5eBe @@ -94,7 +94,7 @@ fetchai/skills/http_echo,QmUtHKVRkDh12UiuJoh2WMQ4FSBgWVEmT2AuXoTDpg76Cb fetchai/skills/ml_data_provider,Qmbz5a4YmZu9SWnnHuh7coTsNwaTBHr1gLhsEK9Nrc667Y fetchai/skills/ml_train,QmUCs5XVpisQpMC8j5Y46qFhp5ux2yD6DpehZqWiox8Arr fetchai/skills/registration_aw1,QmRkDTaeWzSSDSMQ6EGvKntU6fmfbfuC18jJumFdFfDPZK -fetchai/skills/scaffold,QmewidPpxgAAWn7hPtv6TkTotHCdG29VP7ECojzbz3H73Q +fetchai/skills/scaffold,QmUiNp6kSteawmGixMPDCxcEZmkpnAdw3osKSvu9xFR4VG fetchai/skills/simple_aggregation,QmSDvt7i6bjmfJYJD6rD32KrJYJxFsqorG7pfiY5ue9R5q fetchai/skills/simple_buyer,QmPAVhTzgUwvMBYZvxKbHpKs7chKv6BUEaWSMCTLVAD8ia fetchai/skills/simple_data_request,QmerBkpLG4P2LPvXTQ6VkZxZE4DGXuSs8RujvTZL9nYk2J From 573a602ac56d03692f45ab0a910b32c2007e39f3 Mon Sep 17 00:00:00 2001 From: David Minarsch Date: Tue, 1 Jun 2021 16:36:12 +0100 Subject: [PATCH 132/147] fix: add missing type annotations in manager --- aea/manager/manager.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/aea/manager/manager.py b/aea/manager/manager.py index 55f352fb72..ebedb62f3d 100644 --- a/aea/manager/manager.py +++ b/aea/manager/manager.py @@ -275,7 +275,7 @@ async def _manager_loop(self) -> None: def add_error_callback( self, error_callback: Callable[[str, BaseException], None] - ) -> None: + ) -> "MultiAgentManager": """Add error callback to call on error raised.""" if len(self._error_callbacks) == 1 and not self._custom_callback_added: # only default callback present, reset before adding new callback @@ -556,7 +556,7 @@ def set_agent_overrides( agent_name: str, agent_overides: Optional[Dict], components_overrides: Optional[List[Dict]], - ) -> None: + ) -> "MultiAgentManager": """ Set agent overrides. From 2bd1a67b98d343481b9e26805e375968f7e2127d Mon Sep 17 00:00:00 2001 From: David Minarsch Date: Tue, 1 Jun 2021 19:34:31 +0100 Subject: [PATCH 133/147] fix: broken config test --- aea/configurations/data_types.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/aea/configurations/data_types.py b/aea/configurations/data_types.py index 79f5ed6c6e..b88df1d748 100644 --- a/aea/configurations/data_types.py +++ b/aea/configurations/data_types.py @@ -429,9 +429,10 @@ def __lt__(self, other: Any) -> bool: >>> public_id_1 < public_id_3 Traceback (most recent call last): ... + ValueError: The public IDs author_1/name_1:0.1.0 and author_1/name_2:0.1.0 cannot be compared. Their author or name attributes are different. :param other: the object to compate to - :raises ValueError: The public IDs author_1/name_1:0.1.0 and author_1/name_2:0.1.0 cannot be compared. Their author or name attributes are different. + :raises ValueError: if the public ids cannot be confirmed :return: whether or not the inequality is satisfied """ if ( From 72267f5911ee4be6a4edbd77b00c43e82aee4759 Mon Sep 17 00:00:00 2001 From: David Minarsch Date: Tue, 1 Jun 2021 21:51:15 +0100 Subject: [PATCH 134/147] chore: bump aea version to 1.0.2 --- aea/__version__.py | 2 +- deploy-image/Dockerfile | 2 +- develop-image/docker-env.sh | 2 +- docs/quickstart.md | 4 ++-- examples/tac_deploy/Dockerfile | 2 +- scripts/install.ps1 | 2 +- scripts/install.sh | 2 +- tests/test_docs/test_bash_yaml/md_files/bash-quickstart.md | 4 ++-- user-image/docker-env.sh | 2 +- 9 files changed, 11 insertions(+), 11 deletions(-) diff --git a/aea/__version__.py b/aea/__version__.py index 460085a0b7..b3e4e1e223 100644 --- a/aea/__version__.py +++ b/aea/__version__.py @@ -22,7 +22,7 @@ __title__ = "aea" __description__ = "Autonomous Economic Agent framework" __url__ = "https://github.com/fetchai/agents-aea.git" -__version__ = "1.0.1" +__version__ = "1.0.2" __author__ = "Fetch.AI Limited" __license__ = "Apache-2.0" __copyright__ = "2019 Fetch.AI Limited" diff --git a/deploy-image/Dockerfile b/deploy-image/Dockerfile index d91c96d732..ad5898a430 100644 --- a/deploy-image/Dockerfile +++ b/deploy-image/Dockerfile @@ -16,7 +16,7 @@ RUN apk add --no-cache go # aea installation RUN pip install --upgrade pip -RUN pip install --upgrade --force-reinstall aea[all]==1.0.1 +RUN pip install --upgrade --force-reinstall aea[all]==1.0.2 # directories and aea cli config COPY /.aea /home/.aea diff --git a/develop-image/docker-env.sh b/develop-image/docker-env.sh index 45a055e6a9..6441dc97aa 100755 --- a/develop-image/docker-env.sh +++ b/develop-image/docker-env.sh @@ -1,7 +1,7 @@ #!/bin/bash # Swap the following lines if you want to work with 'latest' -DOCKER_IMAGE_TAG=fetchai/aea-develop:1.0.1 +DOCKER_IMAGE_TAG=fetchai/aea-develop:1.0.2 # DOCKER_IMAGE_TAG=aea-develop:latest DOCKER_BUILD_CONTEXT_DIR=.. diff --git a/docs/quickstart.md b/docs/quickstart.md index b4da74256d..68f27ab437 100644 --- a/docs/quickstart.md +++ b/docs/quickstart.md @@ -166,7 +166,7 @@ Confirm password: / ___ \ | |___ / ___ \ /_/ \_\|_____|/_/ \_\ -v1.0.1 +v1.0.2 AEA configurations successfully initialized: {'author': 'fetchai'} ``` @@ -279,7 +279,7 @@ You will see the echo skill running in the terminal window (an output similar to / ___ \ | |___ / ___ \ /_/ \_\|_____|/_/ \_\ -v1.0.1 +v1.0.2 Starting AEA 'my_first_aea' in 'async' mode ... info: Echo Handler: setup method called. diff --git a/examples/tac_deploy/Dockerfile b/examples/tac_deploy/Dockerfile index 74c0c73e1b..0882488d88 100644 --- a/examples/tac_deploy/Dockerfile +++ b/examples/tac_deploy/Dockerfile @@ -19,7 +19,7 @@ RUN apk add --no-cache go # aea installation RUN python -m pip install --upgrade pip -RUN pip install --upgrade --force-reinstall aea[all]==1.0.1 +RUN pip install --upgrade --force-reinstall aea[all]==1.0.2 # directories and aea cli config COPY /.aea /home/.aea diff --git a/scripts/install.ps1 b/scripts/install.ps1 index ec9954d98d..4e4adfa206 100644 --- a/scripts/install.ps1 +++ b/scripts/install.ps1 @@ -34,7 +34,7 @@ function instal_choco_golang_gcc { } function install_aea { echo "Install aea" - $output=pip install aea[all]==1.0.1 --force --no-cache-dir 2>&1 |out-string; + $output=pip install aea[all]==1.0.2 --force --no-cache-dir 2>&1 |out-string; if ($LastExitCode -ne 0) { echo $output echo "AEA install failed!" diff --git a/scripts/install.sh b/scripts/install.sh index 53007e96df..45e8122636 100755 --- a/scripts/install.sh +++ b/scripts/install.sh @@ -42,7 +42,7 @@ function is_python_version_ok() { function install_aea (){ echo "Install AEA" - output=$(pip3 install --user aea[all]==1.0.1 --force --no-cache-dir) + output=$(pip3 install --user aea[all]==1.0.2 --force --no-cache-dir) if [[ $? -ne 0 ]]; then echo "$output" diff --git a/tests/test_docs/test_bash_yaml/md_files/bash-quickstart.md b/tests/test_docs/test_bash_yaml/md_files/bash-quickstart.md index 8baf0d0aef..035180521f 100644 --- a/tests/test_docs/test_bash_yaml/md_files/bash-quickstart.md +++ b/tests/test_docs/test_bash_yaml/md_files/bash-quickstart.md @@ -56,7 +56,7 @@ Confirm password: / ___ \ | |___ / ___ \ /_/ \_\|_____|/_/ \_\ -v1.0.1 +v1.0.2 AEA configurations successfully initialized: {'author': 'fetchai'} ``` @@ -97,7 +97,7 @@ aea run / ___ \ | |___ / ___ \ /_/ \_\|_____|/_/ \_\ -v1.0.1 +v1.0.2 Starting AEA 'my_first_aea' in 'async' mode ... info: Echo Handler: setup method called. diff --git a/user-image/docker-env.sh b/user-image/docker-env.sh index 49aba80e0d..7a447219f4 100644 --- a/user-image/docker-env.sh +++ b/user-image/docker-env.sh @@ -1,7 +1,7 @@ #!/bin/bash # Swap the following lines if you want to work with 'latest' -DOCKER_IMAGE_TAG=fetchai/aea-user:1.0.1 +DOCKER_IMAGE_TAG=fetchai/aea-user:1.0.2 # DOCKER_IMAGE_TAG=fetchai/aea-user:latest DOCKER_BUILD_CONTEXT_DIR=.. From 8e0f4d08ada11a5486fb26c8c40f444c8fe8126c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 1 Jun 2021 20:56:00 +0000 Subject: [PATCH 135/147] chore(deps-dev): bump sqlalchemy from 1.3.17 to 1.4.17 Bumps [sqlalchemy](https://github.com/sqlalchemy/sqlalchemy) from 1.3.17 to 1.4.17. - [Release notes](https://github.com/sqlalchemy/sqlalchemy/releases) - [Changelog](https://github.com/sqlalchemy/sqlalchemy/blob/master/CHANGES) - [Commits](https://github.com/sqlalchemy/sqlalchemy/commits) --- updated-dependencies: - dependency-name: sqlalchemy dependency-type: direct:development update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- Pipfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Pipfile b/Pipfile index 3949763be2..0e0ca1ffee 100644 --- a/Pipfile +++ b/Pipfile @@ -61,7 +61,7 @@ pytest-rerunfailures = "==9.0" requests = ">=2.22.0" safety = "==1.10.3" scikit-image = ">=0.17.2" -sqlalchemy = "==1.3.17" +sqlalchemy = "==1.4.17" temper-py = "==0.0.3" tensorflow = "==2.4.0" tox = "==3.15.1" From a801a871b3060ac37ed96b82352ee8325b4fd552 Mon Sep 17 00:00:00 2001 From: David Minarsch Date: Tue, 1 Jun 2021 21:59:08 +0100 Subject: [PATCH 136/147] chore: update plugin versions --- plugins/aea-cli-ipfs/setup.py | 2 +- plugins/aea-ledger-cosmos/setup.py | 2 +- plugins/aea-ledger-ethereum/setup.py | 2 +- plugins/aea-ledger-fetchai/setup.py | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/plugins/aea-cli-ipfs/setup.py b/plugins/aea-cli-ipfs/setup.py index 7f9877ffeb..fc297bc0ce 100755 --- a/plugins/aea-cli-ipfs/setup.py +++ b/plugins/aea-cli-ipfs/setup.py @@ -27,7 +27,7 @@ setup( name="aea-cli-ipfs", - version="1.0.0", + version="1.0.1", author="Fetch.AI Limited", license="Apache-2.0", description="CLI extension for AEA framework wrapping IPFS functionality.", diff --git a/plugins/aea-ledger-cosmos/setup.py b/plugins/aea-ledger-cosmos/setup.py index 937c179a06..15ae10dfd2 100644 --- a/plugins/aea-ledger-cosmos/setup.py +++ b/plugins/aea-ledger-cosmos/setup.py @@ -25,7 +25,7 @@ setup( name="aea-ledger-cosmos", - version="1.0.0", + version="1.0.1", author="Fetch.AI Limited", license="Apache-2.0", description="Python package wrapping the public and private key cryptography and ledger api of Cosmos.", diff --git a/plugins/aea-ledger-ethereum/setup.py b/plugins/aea-ledger-ethereum/setup.py index 2f0776d322..12cb356faf 100644 --- a/plugins/aea-ledger-ethereum/setup.py +++ b/plugins/aea-ledger-ethereum/setup.py @@ -25,7 +25,7 @@ setup( name="aea-ledger-ethereum", - version="1.0.0", + version="1.0.1", author="Fetch.AI Limited", license="Apache-2.0", description="Python package wrapping the public and private key cryptography and ledger api of Ethereum.", diff --git a/plugins/aea-ledger-fetchai/setup.py b/plugins/aea-ledger-fetchai/setup.py index a4c69fd6a8..0298dab933 100644 --- a/plugins/aea-ledger-fetchai/setup.py +++ b/plugins/aea-ledger-fetchai/setup.py @@ -30,7 +30,7 @@ setup( name="aea-ledger-fetchai", - version="1.0.0", + version="1.0.1", author="Fetch.AI Limited", license="Apache-2.0", description="Python package wrapping the public and private key cryptography and ledger API of Fetch.AI.", From 4e59a5f8dd8e064efdbe0c82e1c8b27e7f126ac1 Mon Sep 17 00:00:00 2001 From: David Minarsch Date: Tue, 1 Jun 2021 22:04:51 +0100 Subject: [PATCH 137/147] chore: bump test protocols --- tests/data/generator/t_protocol/__init__.py | 2 +- tests/data/generator/t_protocol/protocol.yaml | 2 +- tests/data/generator/t_protocol_no_ct/__init__.py | 2 +- tests/data/generator/t_protocol_no_ct/protocol.yaml | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/data/generator/t_protocol/__init__.py b/tests/data/generator/t_protocol/__init__.py index e72adaf165..6bec8945f5 100644 --- a/tests/data/generator/t_protocol/__init__.py +++ b/tests/data/generator/t_protocol/__init__.py @@ -20,7 +20,7 @@ """ This module contains the support resources for the t_protocol protocol. -It was created with protocol buffer compiler version `libprotoc 3.11.4` and aea version `1.0.1`. +It was created with protocol buffer compiler version `libprotoc 3.11.4` and aea version `1.0.2`. """ from tests.data.generator.t_protocol.message import TProtocolMessage diff --git a/tests/data/generator/t_protocol/protocol.yaml b/tests/data/generator/t_protocol/protocol.yaml index fa8e8b32f2..9a44702e2f 100644 --- a/tests/data/generator/t_protocol/protocol.yaml +++ b/tests/data/generator/t_protocol/protocol.yaml @@ -7,7 +7,7 @@ description: A protocol for testing purposes. license: Apache-2.0 aea_version: '>=1.0.0, <2.0.0' fingerprint: - __init__.py: QmRFM25tWb2wzG4KsJjMQUiWBVq2Sv4x1U9bMtz7j14Mqh + __init__.py: QmXbS5WzdxtqC9daoTSuwQN4YezmMdJAGuHU9ds4mwLaJq custom_types.py: QmWg8HFav8w9tfZfMrTG5Uo7QpexvYKKkhpGPD18233pLw dialogues.py: QmWAdikDRJWTG7HUXsCsZRg4Wxnf8cMr5KujpyC4M75gnB message.py: QmTjeJythfhy4XyzXRCA4fY2eULHciHBRCowWpZkEH77z9 diff --git a/tests/data/generator/t_protocol_no_ct/__init__.py b/tests/data/generator/t_protocol_no_ct/__init__.py index b47c9a013d..3b643b5ad1 100644 --- a/tests/data/generator/t_protocol_no_ct/__init__.py +++ b/tests/data/generator/t_protocol_no_ct/__init__.py @@ -20,7 +20,7 @@ """ This module contains the support resources for the t_protocol_no_ct protocol. -It was created with protocol buffer compiler version `libprotoc 3.11.4` and aea version `1.0.1`. +It was created with protocol buffer compiler version `libprotoc 3.11.4` and aea version `1.0.2`. """ from tests.data.generator.t_protocol_no_ct.message import TProtocolNoCtMessage diff --git a/tests/data/generator/t_protocol_no_ct/protocol.yaml b/tests/data/generator/t_protocol_no_ct/protocol.yaml index cb4d6420c9..e8df65a8b8 100644 --- a/tests/data/generator/t_protocol_no_ct/protocol.yaml +++ b/tests/data/generator/t_protocol_no_ct/protocol.yaml @@ -7,7 +7,7 @@ description: A protocol for testing purposes. license: Apache-2.0 aea_version: '>=1.0.0, <2.0.0' fingerprint: - __init__.py: QmP56cVMkxKriXjZYRsjrN5NY1qPo9xdCQjhm5oBvXePN3 + __init__.py: QmYjByb2ZHf98pG8mAo5cMXW5WRCyAFuwLygd7fC4EN4c9 dialogues.py: Qmeq7m8vf1LW5WeehNG8qnoGoRstQrABw2vdQh5tmB3KxX message.py: QmWBJxk69r34YiRTpadyd6JbKxMEBGj8zwnpZsj6r2Wg5d serialization.py: QmSGoA2WjKU7F6oYfLwxG4uJVFuVhHNRSo7TNJ1EuYadEg From 143f7a26f0edb0ac2c68fb5102e2bf6cc9679f25 Mon Sep 17 00:00:00 2001 From: David Minarsch Date: Tue, 1 Jun 2021 22:06:47 +0100 Subject: [PATCH 138/147] chore: fix hashes test packages --- tests/data/hashes.csv | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/data/hashes.csv b/tests/data/hashes.csv index 4c642dd06a..fa6fb47498 100644 --- a/tests/data/hashes.csv +++ b/tests/data/hashes.csv @@ -2,7 +2,7 @@ dummy_author/agents/dummy_aea,QmVmz9HDijBdNmTmkCqCBBYkEviwcLj7ib37qSguvP25EN dummy_author/skills/dummy_skill,QmXHU1KwFNtJWrXv9TUqE1dLWwumky2HFuGKohGiD4HKiP fetchai/connections/dummy_connection,QmTLkQHXmZd8xF46Ds47pyTUuLQuosC4PNwh9waxtzQv1b fetchai/contracts/dummy_contract,QmP67brp7EU1kg6n2ckQP6A6jfxLJDeCBD5J6EzpDGb5Kb -fetchai/protocols/t_protocol,QmNqV6QvRuBrfXbNgqKhU9BWZ83Tz1CegbhXT8anf8RiQE -fetchai/protocols/t_protocol_no_ct,QmfNEK4uVktCWNmBzsb9xzsd256NdKADSH3hnJNqeKPJE9 +fetchai/protocols/t_protocol,QmeFdoMfgdD1BZ9K6avmhED7xac82y9oopqFnnHPpjzTGW +fetchai/protocols/t_protocol_no_ct,QmThxCWqMzp4o2KYE8MmF7PsMRRtYTo9WhGHyaiinm2Rwj fetchai/skills/dependencies_skill,QmaxnwbY9u3JPYfc2gnmiCjFd9mCfgXV8Yv5B9VbsDkg7K fetchai/skills/exception_skill,Qmcch6VUH2YELniNiaJxLNa19BRD8PAzb5HTzd7SQhEBgf From 26f592061d5771a58f325a2d974a8e3ee2a67f4f Mon Sep 17 00:00:00 2001 From: David Minarsch Date: Tue, 1 Jun 2021 22:15:51 +0100 Subject: [PATCH 139/147] chore: bump package versions --- docs/aggregation-demo.md | 10 +-- docs/aries-cloud-agent-demo.md | 18 ++-- docs/car-park-skills.md | 26 +++--- docs/cli-vs-programmatic-aeas.md | 2 +- docs/config.md | 2 +- docs/connect-a-frontend.md | 2 +- docs/contract.md | 2 +- docs/erc1155-skills.md | 26 +++--- docs/generic-skills-step-by-step.md | 18 ++-- docs/generic-skills.md | 22 ++--- docs/ml-skills.md | 26 +++--- docs/multi-agent-manager.md | 8 +- docs/oracle-demo.md | 12 +-- docs/orm-integration.md | 22 ++--- docs/p2p-connection.md | 20 ++--- docs/simple-oef-usage.md | 2 +- docs/skill-guide.md | 12 +-- docs/tac-skills-contract.md | 32 +++---- docs/tac-skills.md | 32 +++---- docs/thermometer-skills.md | 26 +++--- docs/weather-skills.md | 26 +++--- .../agents/aries_alice/aea-config.yaml | 12 +-- .../agents/aries_faber/aea-config.yaml | 12 +-- .../agents/car_data_buyer/aea-config.yaml | 10 +-- .../agents/car_detector/aea-config.yaml | 10 +-- .../agents/coin_price_oracle/aea-config.yaml | 14 +-- .../coin_price_oracle_client/aea-config.yaml | 8 +- .../confirmation_aea_aw1/aea-config.yaml | 14 +-- .../confirmation_aea_aw2/aea-config.yaml | 14 +-- .../confirmation_aea_aw3/aea-config.yaml | 14 +-- .../confirmation_aea_aw5/aea-config.yaml | 14 +-- .../agents/erc1155_client/aea-config.yaml | 16 ++-- .../agents/erc1155_deployer/aea-config.yaml | 16 ++-- .../agents/fipa_dummy_buyer/aea-config.yaml | 8 +- .../agents/generic_buyer/aea-config.yaml | 10 +-- .../agents/generic_seller/aea-config.yaml | 10 +-- .../agents/ml_data_provider/aea-config.yaml | 10 +-- .../agents/ml_model_trainer/aea-config.yaml | 12 +-- .../registration_aea_aw1/aea-config.yaml | 14 +-- .../agents/simple_aggregator/aea-config.yaml | 8 +- .../agents/simple_buyer_aw2/aea-config.yaml | 14 +-- .../agents/simple_buyer_aw5/aea-config.yaml | 14 +-- .../agents/simple_seller_aw2/aea-config.yaml | 14 +-- .../agents/simple_seller_aw5/aea-config.yaml | 14 +-- .../aea-config.yaml | 10 +-- .../simple_service_search/aea-config.yaml | 10 +-- .../agents/tac_controller/aea-config.yaml | 10 +-- .../tac_controller_contract/aea-config.yaml | 14 +-- .../agents/tac_participant/aea-config.yaml | 10 +-- .../tac_participant_contract/aea-config.yaml | 14 +-- .../agents/thermometer_aea/aea-config.yaml | 10 +-- .../agents/thermometer_client/aea-config.yaml | 10 +-- .../agents/weather_client/aea-config.yaml | 10 +-- .../agents/weather_station/aea-config.yaml | 10 +-- .../fetchai/connections/p2p_libp2p/README.md | 2 +- .../connections/p2p_libp2p/connection.py | 2 +- .../connections/p2p_libp2p/connection.yaml | 6 +- packages/fetchai/connections/soef/README.md | 2 +- .../fetchai/connections/soef/connection.py | 2 +- .../fetchai/connections/soef/connection.yaml | 6 +- packages/fetchai/contracts/oracle/contract.py | 2 +- .../fetchai/contracts/oracle/contract.yaml | 4 +- .../contracts/oracle_client/contract.py | 2 +- .../contracts/oracle_client/contract.yaml | 4 +- .../fetchai/skills/aries_alice/__init__.py | 2 +- .../fetchai/skills/aries_alice/skill.yaml | 4 +- .../fetchai/skills/aries_faber/__init__.py | 2 +- .../fetchai/skills/aries_faber/skill.yaml | 4 +- .../fetchai/skills/erc1155_client/__init__.py | 2 +- .../fetchai/skills/erc1155_client/skill.yaml | 4 +- .../fetchai/skills/erc1155_deploy/__init__.py | 2 +- .../fetchai/skills/erc1155_deploy/skill.yaml | 4 +- packages/fetchai/skills/ml_train/__init__.py | 2 +- packages/fetchai/skills/ml_train/skill.yaml | 4 +- .../fetchai/skills/simple_oracle/__init__.py | 2 +- .../fetchai/skills/simple_oracle/skill.yaml | 6 +- .../skills/simple_oracle_client/__init__.py | 2 +- .../skills/simple_oracle_client/skill.yaml | 6 +- packages/hashes.csv | 88 +++++++++---------- tests/data/dummy_aea/aea-config.yaml | 2 +- tests/data/hashes.csv | 2 +- tests/test_cli/test_upgrade.py | 6 +- .../md_files/bash-aggregation-demo.md | 10 +-- .../md_files/bash-aries-cloud-agent-demo.md | 16 ++-- .../md_files/bash-car-park-skills.md | 22 ++--- .../md_files/bash-cli-vs-programmatic-aeas.md | 2 +- .../test_bash_yaml/md_files/bash-config.md | 2 +- .../md_files/bash-erc1155-skills.md | 30 +++---- .../bash-generic-skills-step-by-step.md | 18 ++-- .../md_files/bash-generic-skills.md | 22 ++--- .../test_bash_yaml/md_files/bash-ml-skills.md | 24 ++--- .../md_files/bash-oracle-demo.md | 12 +-- .../md_files/bash-orm-integration.md | 22 ++--- .../md_files/bash-p2p-connection.md | 14 +-- .../md_files/bash-skill-guide.md | 10 +-- .../md_files/bash-tac-skills-contract.md | 30 +++---- .../md_files/bash-tac-skills.md | 34 +++---- .../md_files/bash-thermometer-skills.md | 22 ++--- .../md_files/bash-weather-skills.md | 22 ++--- .../test_cli_vs_programmatic_aea.py | 2 +- .../test_orm_integration.py | 14 +-- .../test_skill_guide/test_skill_guide.py | 10 +-- .../test_skills_integration/test_carpark.py | 32 +++---- .../test_skills_integration/test_erc1155.py | 22 ++--- .../test_skills_integration/test_generic.py | 32 +++---- .../test_skills_integration/test_ml_skills.py | 36 ++++---- .../test_simple_aggregation.py | 8 +- .../test_simple_oracle.py | 28 +++--- .../test_skills_integration/test_tac.py | 38 ++++---- .../test_thermometer.py | 32 +++---- .../test_skills_integration/test_weather.py | 34 +++---- 111 files changed, 756 insertions(+), 756 deletions(-) diff --git a/docs/aggregation-demo.md b/docs/aggregation-demo.md index 2473fad3b1..d7c77d4bff 100644 --- a/docs/aggregation-demo.md +++ b/docs/aggregation-demo.md @@ -19,7 +19,7 @@ Repeat the following process four times in four different terminals (for each {` Fetch the aggregator AEA: ``` bash agent_name="agg$i" -aea fetch fetchai/simple_aggregator:0.2.0 --alias $agent_name +aea fetch fetchai/simple_aggregator:0.3.0 --alias $agent_name cd $agent_name aea install aea build @@ -36,13 +36,13 @@ aea create agent_name cd agent_name aea add connection fetchai/http_client:0.22.0 aea add connection fetchai/http_server:0.21.0 -aea add connection fetchai/p2p_libp2p:0.23.0 -aea add connection fetchai/soef:0.24.0 +aea add connection fetchai/p2p_libp2p:0.24.0 +aea add connection fetchai/soef:0.25.0 aea add connection fetchai/prometheus:0.7.0 aea add skill fetchai/advanced_data_request:0.5.0 aea add skill fetchai/simple_aggregation:0.1.0 -aea config set agent.default_connection fetchai/p2p_libp2p:0.23.0 +aea config set agent.default_connection fetchai/p2p_libp2p:0.24.0 aea install aea build ``` @@ -127,7 +127,7 @@ aea config set vendor.fetchai.connections.http_server.config.port $((8000+i)) To publish the aggregated value to an oracle smart contract, add the ledger connection and simple oracle skill to one of the aggregators: ``` bash aea add connection fetchai/ledger:0.18.0 -aea add skill fetchai/simple_oracle:0.12.0 +aea add skill fetchai/simple_oracle:0.13.0 ``` Configure the simple oracle skill for the `fetchai` ledger: diff --git a/docs/aries-cloud-agent-demo.md b/docs/aries-cloud-agent-demo.md index 4ad22936fc..38bdf586bf 100644 --- a/docs/aries-cloud-agent-demo.md +++ b/docs/aries-cloud-agent-demo.md @@ -180,7 +180,7 @@ Now you can create **Alice_AEA** and **Faber_AEA** in terminals 3 and 4 respecti In the third terminal, fetch **Alice_AEA** and move into its project folder: ``` bash -aea fetch fetchai/aries_alice:0.29.0 +aea fetch fetchai/aries_alice:0.30.0 cd aries_alice ``` @@ -191,11 +191,11 @@ The following steps create Alice_AEA from scratch: ``` bash aea create aries_alice cd aries_alice -aea add connection fetchai/p2p_libp2p:0.23.0 -aea add connection fetchai/soef:0.24.0 +aea add connection fetchai/p2p_libp2p:0.24.0 +aea add connection fetchai/soef:0.25.0 aea add connection fetchai/http_client:0.22.0 aea add connection fetchai/webhook:0.18.0 -aea add skill fetchai/aries_alice:0.22.0 +aea add skill fetchai/aries_alice:0.23.0 ```

@@ -257,14 +257,14 @@ Finally run **Alice_AEA**: aea run ``` -Once you see a message of the form `To join its network use multiaddr 'SOME_ADDRESS'` take note of the address. (Alternatively, use `aea get-multiaddress fetchai -c -i fetchai/p2p_libp2p:0.23.0 -u public_uri` to retrieve the address.) We will refer to this as **Alice_AEA's P2P address**. +Once you see a message of the form `To join its network use multiaddr 'SOME_ADDRESS'` take note of the address. (Alternatively, use `aea get-multiaddress fetchai -c -i fetchai/p2p_libp2p:0.24.0 -u public_uri` to retrieve the address.) We will refer to this as **Alice_AEA's P2P address**. ### Faber_AEA In the fourth terminal, fetch **Faber_AEA** and move into its project folder: ``` bash -aea fetch fetchai/aries_faber:0.29.0 +aea fetch fetchai/aries_faber:0.30.0 cd aries_faber ``` @@ -275,11 +275,11 @@ The following steps create Faber_AEA from scratch: ``` bash aea create aries_faber cd aries_faber -aea add connection fetchai/p2p_libp2p:0.23.0 -aea add connection fetchai/soef:0.24.0 +aea add connection fetchai/p2p_libp2p:0.24.0 +aea add connection fetchai/soef:0.25.0 aea add connection fetchai/http_client:0.22.0 aea add connection fetchai/webhook:0.18.0 -aea add skill fetchai/aries_faber:0.20.0 +aea add skill fetchai/aries_faber:0.21.0 ```

diff --git a/docs/car-park-skills.md b/docs/car-park-skills.md index 5b2530b78e..c7b1f6ed85 100644 --- a/docs/car-park-skills.md +++ b/docs/car-park-skills.md @@ -57,9 +57,9 @@ Install the AEA ManagerAgentLand block explorer and request some test tokens via `Get Funds`. @@ -97,7 +97,7 @@ Follow the Preliminaries and =1.0.0"} }' -aea config set agent.default_connection fetchai/p2p_libp2p:0.23.0 +aea config set agent.default_connection fetchai/p2p_libp2p:0.24.0 aea config set --type dict agent.default_routing \ '{ "fetchai/ledger_api:1.0.0": "fetchai/ledger:0.18.0", - "fetchai/oef_search:1.0.0": "fetchai/soef:0.24.0" + "fetchai/oef_search:1.0.0": "fetchai/soef:0.25.0" }' aea install aea build @@ -135,7 +135,7 @@ aea build Then, fetch the car data client AEA: ``` bash -aea fetch fetchai/car_data_buyer:0.30.0 +aea fetch fetchai/car_data_buyer:0.31.0 cd car_data_buyer aea install aea build @@ -148,19 +148,19 @@ The following steps create the car data client from scratch: ``` bash aea create car_data_buyer cd car_data_buyer -aea add connection fetchai/p2p_libp2p:0.23.0 -aea add connection fetchai/soef:0.24.0 +aea add connection fetchai/p2p_libp2p:0.24.0 +aea add connection fetchai/soef:0.25.0 aea add connection fetchai/ledger:0.18.0 aea add skill fetchai/carpark_client:0.25.0 aea config set --type dict agent.dependencies \ '{ "aea-ledger-fetchai": {"version": "<2.0.0,>=1.0.0"} }' -aea config set agent.default_connection fetchai/p2p_libp2p:0.23.0 +aea config set agent.default_connection fetchai/p2p_libp2p:0.24.0 aea config set --type dict agent.default_routing \ '{ "fetchai/ledger_api:1.0.0": "fetchai/ledger:0.18.0", - "fetchai/oef_search:1.0.0": "fetchai/soef:0.24.0" + "fetchai/oef_search:1.0.0": "fetchai/soef:0.25.0" }' aea install aea build @@ -225,7 +225,7 @@ First, run the car data seller AEA: aea run ``` -Once you see a message of the form `To join its network use multiaddr 'SOME_ADDRESS'` take note of the address. (Alternatively, use `aea get-multiaddress fetchai -c -i fetchai/p2p_libp2p:0.23.0 -u public_uri` to retrieve the address.) +Once you see a message of the form `To join its network use multiaddr 'SOME_ADDRESS'` take note of the address. (Alternatively, use `aea get-multiaddress fetchai -c -i fetchai/p2p_libp2p:0.24.0 -u public_uri` to retrieve the address.) This is the entry peer address for the local agent communication network created by the car data seller. Then, in the car data buyer, run this command (replace `SOME_ADDRESS` with the correct value as described above): diff --git a/docs/cli-vs-programmatic-aeas.md b/docs/cli-vs-programmatic-aeas.md index cad4efe3b9..e66a649bb3 100644 --- a/docs/cli-vs-programmatic-aeas.md +++ b/docs/cli-vs-programmatic-aeas.md @@ -33,7 +33,7 @@ If you want to create the weather station AEA step by step you can follow this g Fetch the weather station AEA with the following command : ``` bash -aea fetch fetchai/weather_station:0.29.0 +aea fetch fetchai/weather_station:0.30.0 cd weather_station aea install aea build diff --git a/docs/config.md b/docs/config.md index 392026562a..49eea930f8 100644 --- a/docs/config.md +++ b/docs/config.md @@ -27,7 +27,7 @@ protocols: # The list of protocol public id - fetchai/default:1.0.0 skills: # The list of skill public ids the AEA project depends on (each public id must satisfy PUBLIC_ID_REGEX). - fetchai/error:0.16.0 -default_connection: fetchai/p2p_libp2p:0.23.0 # The default connection used for envelopes sent by the AEA (must satisfy PUBLIC_ID_REGEX). +default_connection: fetchai/p2p_libp2p:0.24.0 # The default connection used for envelopes sent by the AEA (must satisfy PUBLIC_ID_REGEX). default_ledger: fetchai # The default ledger identifier the AEA project uses (must satisfy LEDGER_ID_REGEX) required_ledgers: [fetchai] # the list of identifiers of ledgers that the AEA project requires key pairs for (each item must satisfy LEDGER_ID_REGEX) default_routing: {} # The default routing scheme applied to envelopes sent by the AEA, it maps from protocol public ids to connection public ids (both keys and values must satisfy PUBLIC_ID_REGEX) diff --git a/docs/connect-a-frontend.md b/docs/connect-a-frontend.md index 89880fe01c..e6e4436d7e 100644 --- a/docs/connect-a-frontend.md +++ b/docs/connect-a-frontend.md @@ -6,4 +6,4 @@ This page lays out two options for connecting a front-end to an AEA. The followi The first option is to create a `HTTP Server` connection that handles incoming requests from a REST API. In this scenario, the REST API communicates with the AEA and requests are handled by the `HTTP Server` connection package. The REST API should send CRUD requests to the `HTTP Server` connection (`fetchai/http_server:0.21.0`) which translates these into Envelopes to be consumed by the correct skill. ## Case 2 -The second option is to create a front-end comprising a stand-alone `Multiplexer` with a `P2P` connection (`fetchai/p2p_libp2p:0.23.0`). In this scenario the Agent Communication Network can be used to send Envelopes from the AEA to the front-end. \ No newline at end of file +The second option is to create a front-end comprising a stand-alone `Multiplexer` with a `P2P` connection (`fetchai/p2p_libp2p:0.24.0`). In this scenario the Agent Communication Network can be used to send Envelopes from the AEA to the front-end. \ No newline at end of file diff --git a/docs/contract.md b/docs/contract.md index 37abf56d76..0eba24ebb2 100644 --- a/docs/contract.md +++ b/docs/contract.md @@ -182,4 +182,4 @@ class MyContract(Contract): ``` Above, we implement a method to create a transaction, in this case a transaction to create a batch of tokens. The method will be called by the framework, specifically the `fetchai/ledger:0.18.0` connection once it receives a message (see bullet point 2 above). The method first gets the latest transaction nonce of the `deployer_address`, then constructs the contract instance, then uses the instance to build the transaction and finally updates the gas on the transaction. -It helps to look at existing contract packages, like `fetchai/erc1155:0.21.0`, and skills using them, like `fetchai/erc1155_client:0.11.0` and `fetchai/erc1155_deploy:0.28.0`, for inspiration and guidance. +It helps to look at existing contract packages, like `fetchai/erc1155:0.21.0`, and skills using them, like `fetchai/erc1155_client:0.11.0` and `fetchai/erc1155_deploy:0.29.0`, for inspiration and guidance. diff --git a/docs/erc1155-skills.md b/docs/erc1155-skills.md index aecd50a6ca..da4a21210f 100644 --- a/docs/erc1155-skills.md +++ b/docs/erc1155-skills.md @@ -25,7 +25,7 @@ The scope of this guide is demonstrating how you can deploy a smart contract and Fetch the AEA that will deploy the contract: ``` bash -aea fetch fetchai/erc1155_deployer:0.31.0 +aea fetch fetchai/erc1155_deployer:0.32.0 cd erc1155_deployer aea install aea build @@ -39,22 +39,22 @@ Create the AEA that will deploy the contract. ``` bash aea create erc1155_deployer cd erc1155_deployer -aea add connection fetchai/p2p_libp2p:0.23.0 -aea add connection fetchai/soef:0.24.0 +aea add connection fetchai/p2p_libp2p:0.24.0 +aea add connection fetchai/soef:0.25.0 aea add connection fetchai/ledger:0.18.0 -aea add skill fetchai/erc1155_deploy:0.28.0 +aea add skill fetchai/erc1155_deploy:0.29.0 aea config set --type dict agent.dependencies \ '{ "aea-ledger-fetchai": {"version": "<2.0.0,>=1.0.0"}, "aea-ledger-ethereum": {"version": "<2.0.0,>=1.0.0"}, "aea-ledger-cosmos": {"version": "<2.0.0,>=1.0.0"} }' -aea config set agent.default_connection fetchai/p2p_libp2p:0.23.0 +aea config set agent.default_connection fetchai/p2p_libp2p:0.24.0 aea config set --type dict agent.default_routing \ '{ "fetchai/contract_api:1.0.0": "fetchai/ledger:0.18.0", "fetchai/ledger_api:1.0.0": "fetchai/ledger:0.18.0", - "fetchai/oef_search:1.0.0": "fetchai/soef:0.24.0" + "fetchai/oef_search:1.0.0": "fetchai/soef:0.25.0" }' aea config set --type list vendor.fetchai.connections.p2p_libp2p.cert_requests \ '[{"identifier": "acn", "ledger_id": "ethereum", "not_after": "2022-01-01", "not_before": "2021-01-01", "public_key": "fetchai", "save_path": ".certs/conn_cert.txt"}]' @@ -95,7 +95,7 @@ aea issue-certificates In another terminal, fetch the client AEA which will receive some tokens from the deployer. ``` bash -aea fetch fetchai/erc1155_client:0.31.0 +aea fetch fetchai/erc1155_client:0.32.0 cd erc1155_client aea install aea build @@ -109,22 +109,22 @@ Create the AEA that will get some tokens from the deployer. ``` bash aea create erc1155_client cd erc1155_client -aea add connection fetchai/p2p_libp2p:0.23.0 -aea add connection fetchai/soef:0.24.0 +aea add connection fetchai/p2p_libp2p:0.24.0 +aea add connection fetchai/soef:0.25.0 aea add connection fetchai/ledger:0.18.0 -aea add skill fetchai/erc1155_client:0.26.0 +aea add skill fetchai/erc1155_client:0.27.0 aea config set --type dict agent.dependencies \ '{ "aea-ledger-fetchai": {"version": "<2.0.0,>=1.0.0"}, "aea-ledger-ethereum": {"version": "<2.0.0,>=1.0.0"}, "aea-ledger-cosmos": {"version": "<2.0.0,>=1.0.0"} }' -aea config set agent.default_connection fetchai/p2p_libp2p:0.23.0 +aea config set agent.default_connection fetchai/p2p_libp2p:0.24.0 aea config set --type dict agent.default_routing \ '{ "fetchai/contract_api:1.0.0": "fetchai/ledger:0.18.0", "fetchai/ledger_api:1.0.0": "fetchai/ledger:0.18.0", - "fetchai/oef_search:1.0.0": "fetchai/soef:0.24.0" + "fetchai/oef_search:1.0.0": "fetchai/soef:0.25.0" }' aea config set --type list vendor.fetchai.connections.p2p_libp2p.cert_requests \ '[{"identifier": "acn", "ledger_id": "ethereum", "not_after": "2022-01-01", "not_before": "2021-01-01", "public_key": "fetchai", "save_path": ".certs/conn_cert.txt"}]' @@ -199,7 +199,7 @@ aea run Once you see a message of the form `To join its network use multiaddr 'SOME_ADDRESS'` take note of this address. -Alternatively, use `aea get-multiaddress fetchai -c -i fetchai/p2p_libp2p:0.23.0 -u public_uri` to retrieve the address. The output will be something like `/dns4/127.0.0.1/tcp/9000/p2p/16Uiu2HAm2JPsUX1Su59YVDXJQizYkNSe8JCusqRpLeeTbvY76fE5`. +Alternatively, use `aea get-multiaddress fetchai -c -i fetchai/p2p_libp2p:0.24.0 -u public_uri` to retrieve the address. The output will be something like `/dns4/127.0.0.1/tcp/9000/p2p/16Uiu2HAm2JPsUX1Su59YVDXJQizYkNSe8JCusqRpLeeTbvY76fE5`. This is the entry peer address for the local agent communication network created by the deployer. diff --git a/docs/generic-skills-step-by-step.md b/docs/generic-skills-step-by-step.md index edea186f98..d9fde83ce5 100644 --- a/docs/generic-skills-step-by-step.md +++ b/docs/generic-skills-step-by-step.md @@ -11,14 +11,14 @@ Follow the Preliminaries and Preliminaries and =1.0.0"} }' -aea config set agent.default_connection fetchai/p2p_libp2p:0.23.0 +aea config set agent.default_connection fetchai/p2p_libp2p:0.24.0 aea config set --type dict agent.default_routing \ '{ "fetchai/ledger_api:1.0.0": "fetchai/ledger:0.18.0", - "fetchai/oef_search:1.0.0": "fetchai/soef:0.24.0" + "fetchai/oef_search:1.0.0": "fetchai/soef:0.25.0" }' aea install aea build @@ -96,7 +96,7 @@ aea build Then, in another terminal fetch the buyer AEA: ``` bash -aea fetch fetchai/generic_buyer:0.27.0 --alias my_buyer_aea +aea fetch fetchai/generic_buyer:0.28.0 --alias my_buyer_aea cd my_buyer_aea aea install aea build @@ -109,19 +109,19 @@ The following steps create the buyer from scratch: ``` bash aea create my_buyer_aea cd my_buyer_aea -aea add connection fetchai/p2p_libp2p:0.23.0 -aea add connection fetchai/soef:0.24.0 +aea add connection fetchai/p2p_libp2p:0.24.0 +aea add connection fetchai/soef:0.25.0 aea add connection fetchai/ledger:0.18.0 aea add skill fetchai/generic_buyer:0.25.0 aea config set --type dict agent.dependencies \ '{ "aea-ledger-fetchai": {"version": "<2.0.0,>=1.0.0"} }' -aea config set agent.default_connection fetchai/p2p_libp2p:0.23.0 +aea config set agent.default_connection fetchai/p2p_libp2p:0.24.0 aea config set --type dict agent.default_routing \ '{ "fetchai/ledger_api:1.0.0": "fetchai/ledger:0.18.0", - "fetchai/oef_search:1.0.0": "fetchai/soef:0.24.0" + "fetchai/oef_search:1.0.0": "fetchai/soef:0.25.0" }' aea install aea build @@ -252,7 +252,7 @@ First, run the seller AEA: aea run ``` -Once you see a message of the form `To join its network use multiaddr 'SOME_ADDRESS'` take note of this address. (Alternatively, use `aea get-multiaddress fetchai -c -i fetchai/p2p_libp2p:0.23.0 -u public_uri` to retrieve the address.) +Once you see a message of the form `To join its network use multiaddr 'SOME_ADDRESS'` take note of this address. (Alternatively, use `aea get-multiaddress fetchai -c -i fetchai/p2p_libp2p:0.24.0 -u public_uri` to retrieve the address.) This is the entry peer address for the local agent communication network created by the seller. Then, configure the buyer to connect to this same local ACN by running the following command in the buyer terminal, replacing `SOME_ADDRESS` with the value you noted above: diff --git a/docs/ml-skills.md b/docs/ml-skills.md index 2259670bcb..a790d8c173 100644 --- a/docs/ml-skills.md +++ b/docs/ml-skills.md @@ -104,7 +104,7 @@ Follow the Preliminaries and =1.0.0"} }' -aea config set agent.default_connection fetchai/p2p_libp2p:0.23.0 +aea config set agent.default_connection fetchai/p2p_libp2p:0.24.0 aea config set --type dict agent.default_routing \ '{ "fetchai/ledger_api:1.0.0": "fetchai/ledger:0.18.0", - "fetchai/oef_search:1.0.0": "fetchai/soef:0.24.0" + "fetchai/oef_search:1.0.0": "fetchai/soef:0.25.0" }' aea install aea build @@ -142,7 +142,7 @@ aea build Then, fetch the model trainer AEA: ``` bash -aea fetch fetchai/ml_model_trainer:0.30.0 +aea fetch fetchai/ml_model_trainer:0.31.0 cd ml_model_trainer aea install aea build @@ -155,19 +155,19 @@ The following steps create the model trainer from scratch: ``` bash aea create ml_model_trainer cd ml_model_trainer -aea add connection fetchai/p2p_libp2p:0.23.0 -aea add connection fetchai/soef:0.24.0 +aea add connection fetchai/p2p_libp2p:0.24.0 +aea add connection fetchai/soef:0.25.0 aea add connection fetchai/ledger:0.18.0 -aea add skill fetchai/ml_train:0.26.0 +aea add skill fetchai/ml_train:0.27.0 aea config set --type dict agent.dependencies \ '{ "aea-ledger-fetchai": {"version": "<2.0.0,>=1.0.0"} }' -aea config set agent.default_connection fetchai/p2p_libp2p:0.23.0 +aea config set agent.default_connection fetchai/p2p_libp2p:0.24.0 aea config set --type dict agent.default_routing \ '{ "fetchai/ledger_api:1.0.0": "fetchai/ledger:0.18.0", - "fetchai/oef_search:1.0.0": "fetchai/soef:0.24.0" + "fetchai/oef_search:1.0.0": "fetchai/soef:0.25.0" }' aea install aea build @@ -232,7 +232,7 @@ First, run the data provider AEA: aea run ``` -Once you see a message of the form `To join its network use multiaddr 'SOME_ADDRESS'` take note of the address. (Alternatively, use `aea get-multiaddress fetchai -c -i fetchai/p2p_libp2p:0.23.0 -u public_uri` to retrieve the address.) +Once you see a message of the form `To join its network use multiaddr 'SOME_ADDRESS'` take note of the address. (Alternatively, use `aea get-multiaddress fetchai -c -i fetchai/p2p_libp2p:0.24.0 -u public_uri` to retrieve the address.) This is the entry peer address for the local agent communication network created by the ML data provider.