diff --git a/.github/workflows/integration.yml b/.github/workflows/integration.yml new file mode 100644 index 000000000..285045287 --- /dev/null +++ b/.github/workflows/integration.yml @@ -0,0 +1,75 @@ +name: Integration tests + +on: + push: + branches: + - master + pull_request: + branches: + - master + +jobs: + integration-test: + name: Run integration tests + runs-on: goth + steps: + - name: Checkout + uses: actions/checkout@v2 + + - name: Configure python + uses: actions/setup-python@v2 + with: + python-version: '3.8.0' + + - name: Configure poetry + uses: Gr1N/setup-poetry@v4 + with: + poetry-version: 1.1.4 + + - name: Install dependencies + run: poetry install + + - name: Disconnect Docker containers from default network + continue-on-error: true + # related to this issue: https://github.com/moby/moby/issues/23302 + run: | + docker network inspect docker_default + sudo apt-get install -y jq + docker network inspect docker_default | jq ".[0].Containers | map(.Name)[]" | tee /dev/stderr | xargs --max-args 1 -- docker network disconnect -f docker_default + + - name: Remove Docker containers + continue-on-error: true + run: docker rm -f $(docker ps -a -q) + + - name: Restart Docker daemon + # related to this issue: https://github.com/moby/moby/issues/23302 + run: sudo systemctl restart docker + + - name: Log in to GitHub Docker repository + run: echo ${{ secrets.GITHUB_TOKEN }} | docker login docker.pkg.github.com -u ${{github.actor}} --password-stdin + + - name: Run unit tests + env: + GITHUB_API_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: poetry run poe integration_test + + - name: Upload test logs + uses: actions/upload-artifact@v2 + if: always() + with: + name: goth-logs + path: /tmp/goth-tests + + # Only relevant for self-hosted runners + - name: Remove test logs + if: always() + run: rm -rf /tmp/goth-tests + + # Only relevant for self-hosted runners + - name: Remove poetry virtual env + if: always() + # Python version below should agree with the version set up by this job. + # In future we'll be able to use the `--all` flag here to remove envs for + # all Python versions (https://github.com/python-poetry/poetry/issues/3208). + run: poetry env remove python3.8 + diff --git a/README.md b/README.md index 1b7b8d3a7..daa8b158d 100644 --- a/README.md +++ b/README.md @@ -85,14 +85,18 @@ python -m goth start your/output/dir/goth-config.yml If everything went well you should see the following output: ``` -Now run your requestor agent as follows: +Local goth network ready! -$ PATH=/tmp/...:$PATH YAGNA_APPKEY=3438...7901 YAGNA_API_URL=http://172.19.0.6:6000 GSB_URL=tcp://172.19.0.6:6010 examples/blender/blender.py --subnet goth +You can now load the requestor configuration variables to your shell: -Press Ctrl+C at any moment to stop the test harness. +source /tmp/goth_interactive.env + +And then run your requestor agent from that same shell. + +Press Ctrl+C at any moment to stop the local network. ``` -This is a special case of `goth`'s usage. Running this command does not execute a test, but rather sets up a local Golem network which can be used for debugging purposes. Therefore, you are presented with the parameters required to connect to the `yagna` requestor running inside of this network. +This is a special case of `goth`'s usage. Running this command does not execute a test, but rather sets up a local Golem network which can be used for debugging purposes. The parameters required to connect to the requestor `yagna` node running in this network are output to the file `/tmp/goth_interactive.env` and can be `source`d from your shell. ### Creating and running test cases Take a look at the `yagna` integration tests [`README`](https://github.com/golemfactory/yagna/blob/master/goth_tests/README.md) to learn more about writing and launching your own test cases. diff --git a/goth/interactive.py b/goth/interactive.py index 50d181235..9764b1d57 100644 --- a/goth/interactive.py +++ b/goth/interactive.py @@ -2,7 +2,8 @@ import asyncio import logging from pathlib import Path -from typing import Optional +import tempfile +from typing import Dict, Optional from goth.configuration import Configuration from goth.runner import Runner @@ -11,6 +12,14 @@ logger = logging.getLogger(__name__) +env_file: Path = Path(tempfile.gettempdir()) / "goth_interactive.env" + + +def _write_env_file(env: Dict[str, str]) -> None: + with env_file.open("w") as f: + for key, val in env.items(): + f.write(f"export {key}={val}\n") + async def start_network( configuration: Configuration, @@ -36,14 +45,20 @@ async def start_network( for provider in providers: await provider.provider_agent.wait_for_log("Subscribed offer") - print("\n\033[33;1mNow run your requestor agent as follows:\n") - env = {"PATH": "$PATH"} - requestor.set_agent_env_vars(env) - env_vars = " ".join([f"{key}={val}" for key, val in env.items()]) + requestor_env = requestor.get_agent_env_vars() subnet = providers[0].provider_agent.subnet - print(f"$ {env_vars} examples/blender/blender.py --subnet {subnet}") + requestor_env["YAGNA_SUBNET"] = subnet + + _write_env_file(requestor_env) + env_vars = " ".join([f"{key}={val}" for key, val in requestor_env.items()]) - print("\nPress Ctrl+C at any moment to stop the test harness.\033[0m\n") + print("\n\033[33;1mLocal goth network ready!") + print("You can now load the requestor configuration variables to your shell:\n") + print(f"source {str(env_file)}\n") + print("And then run your requestor agent from that same shell.") + print("You can also use the variables directly like so:\n") + print(f"{env_vars} your/requestor/agent\n") + print("Press Ctrl+C at any moment to stop the local network.\033[0m\n") while True: await asyncio.sleep(5) diff --git a/goth/runner/probe/__init__.py b/goth/runner/probe/__init__.py index 644ed01b6..b44676521 100644 --- a/goth/runner/probe/__init__.py +++ b/goth/runner/probe/__init__.py @@ -261,20 +261,24 @@ async def create_app_key(self, key_name: str = "test_key") -> str: key = app_key.key return key - def set_agent_env_vars(self, env: Dict[str, str]) -> None: - """Add vars needed to talk to the daemon in this probe's container to `env`.""" + def get_agent_env_vars(self, path_var: str = "$PATH") -> Dict[str, str]: + """Get env vars needed to talk to the daemon in this probe's container. + + The returned vars include the `PATH` variable as it needs to include the + directory which contains the gftp proxy script. + By default, the `PATH` value is: `{gftp_script_dir}:$PATH`, allowing for shell + substitution of `$PATH`. This part can be overridden by setting the `path_var` + argument. + """ if not self.app_key: raise AttributeError("Yagna application key is not set yet") - path_var = env.get("PATH") - env.update( - { - "YAGNA_APPKEY": self.app_key, - "YAGNA_API_URL": YAGNA_REST_URL.substitute(host=self.ip_address), - "GSB_URL": YAGNA_BUS_URL.substitute(host=self.ip_address), - "PATH": f"{self._gftp_script_dir}:{path_var}", - } - ) + return { + "YAGNA_APPKEY": self.app_key, + "YAGNA_API_URL": YAGNA_REST_URL.substitute(host=self.ip_address), + "GSB_URL": YAGNA_BUS_URL.substitute(host=self.ip_address), + "PATH": f"{self._gftp_script_dir}:{path_var}", + } @contextlib.asynccontextmanager async def run_command_on_host( @@ -299,7 +303,7 @@ async def run_command_on_host( The monitor can be used for asserting properties of the command's output. """ cmd_env = {**env} if env is not None else {} - self.set_agent_env_vars(cmd_env) + cmd_env.update(self.get_agent_env_vars()) cmd_monitor = PatternMatchingEventMonitor(name="command output") cmd_monitor.start() diff --git a/pyproject.toml b/pyproject.toml index 679ad56db..48583bc2b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -50,7 +50,8 @@ pytest = "^6.2" codeformat = "black -v --check --diff ." codestyle = "flake8" interactive = "python -m goth start goth/default-assets/goth-config.yml" -unit_test = "pytest -svx test/goth" +unit_test = "pytest -svx test/unit" +integration_test = "pytest -svx test/integration" [build-system] requires = ["poetry-core>=1.0.0"] diff --git a/test/integration/conftest.py b/test/integration/conftest.py new file mode 100644 index 000000000..9ac3299fd --- /dev/null +++ b/test/integration/conftest.py @@ -0,0 +1,23 @@ +"""Fixtures providing common values for integration tests.""" + +from datetime import datetime, timezone +from pathlib import Path +import pytest + +from goth.project import PROJECT_ROOT + + +@pytest.fixture +def log_dir() -> Path: + """Return path to dir where goth test session logs should be placed.""" + base_dir = Path("/", "tmp", "goth-tests") + date_str = datetime.now(tz=timezone.utc).strftime("%Y%m%d_%H%M%S%z") + log_dir = base_dir / f"goth_{date_str}" + log_dir.mkdir(parents=True) + return log_dir + + +@pytest.fixture +def default_goth_config() -> Path: + """Return path to default `goth-config.yml` file.""" + return PROJECT_ROOT / "goth" / "default-assets" / "goth-config.yml" diff --git a/test/integration/test_interactive.py b/test/integration/test_interactive.py new file mode 100644 index 000000000..ba80fbb64 --- /dev/null +++ b/test/integration/test_interactive.py @@ -0,0 +1,34 @@ +"""Integration tests for the goth interactive mode.""" + +import asyncio +from pathlib import Path +import pytest + +from goth.configuration import load_yaml +from goth.interactive import start_network, env_file + + +@pytest.mark.asyncio +async def test_interactive( + capsys: pytest.CaptureFixture, default_goth_config: Path, log_dir: Path +) -> None: + """Test if goth interactive mode launches correctly.""" + goth_config = load_yaml(default_goth_config, []) + interactive_task = asyncio.create_task(start_network(goth_config, log_dir)) + + async def _scan_stdout(): + expected_msg = "Local goth network ready" + while True: + stdout, _stderr = capsys.readouterr() + if expected_msg in stdout: + break + await asyncio.sleep(0.1) + + try: + await asyncio.wait_for(_scan_stdout(), timeout=90) + assert env_file.exists() + except asyncio.TimeoutError: + pytest.fail("Timeout while waiting for interactive mode to start") + finally: + interactive_task.cancel() + await interactive_task diff --git a/test/goth/assertions/test_assertions.py b/test/unit/assertions/test_assertions.py similarity index 100% rename from test/goth/assertions/test_assertions.py rename to test/unit/assertions/test_assertions.py diff --git a/test/goth/assertions/test_monitor.py b/test/unit/assertions/test_monitor.py similarity index 100% rename from test/goth/assertions/test_monitor.py rename to test/unit/assertions/test_monitor.py diff --git a/test/goth/configuration/test-assets/goth-config.yml b/test/unit/configuration/test-assets/goth-config.yml similarity index 100% rename from test/goth/configuration/test-assets/goth-config.yml rename to test/unit/configuration/test-assets/goth-config.yml diff --git a/test/goth/configuration/test-assets/keys/001.json b/test/unit/configuration/test-assets/keys/001.json similarity index 100% rename from test/goth/configuration/test-assets/keys/001.json rename to test/unit/configuration/test-assets/keys/001.json diff --git a/test/goth/configuration/test_configuration.py b/test/unit/configuration/test_configuration.py similarity index 100% rename from test/goth/configuration/test_configuration.py rename to test/unit/configuration/test_configuration.py diff --git a/test/goth/runner/cli/__init__.py b/test/unit/runner/cli/__init__.py similarity index 100% rename from test/goth/runner/cli/__init__.py rename to test/unit/runner/cli/__init__.py diff --git a/test/goth/runner/cli/conftest.py b/test/unit/runner/cli/conftest.py similarity index 100% rename from test/goth/runner/cli/conftest.py rename to test/unit/runner/cli/conftest.py diff --git a/test/goth/runner/cli/mock.py b/test/unit/runner/cli/mock.py similarity index 100% rename from test/goth/runner/cli/mock.py rename to test/unit/runner/cli/mock.py diff --git a/test/goth/runner/cli/test_command_runner.py b/test/unit/runner/cli/test_command_runner.py similarity index 100% rename from test/goth/runner/cli/test_command_runner.py rename to test/unit/runner/cli/test_command_runner.py diff --git a/test/goth/runner/cli/test_yagna_app_key_cmd.py b/test/unit/runner/cli/test_yagna_app_key_cmd.py similarity index 100% rename from test/goth/runner/cli/test_yagna_app_key_cmd.py rename to test/unit/runner/cli/test_yagna_app_key_cmd.py diff --git a/test/goth/runner/cli/test_yagna_id_cmd.py b/test/unit/runner/cli/test_yagna_id_cmd.py similarity index 100% rename from test/goth/runner/cli/test_yagna_id_cmd.py rename to test/unit/runner/cli/test_yagna_id_cmd.py diff --git a/test/goth/runner/cli/test_yagna_payment_cmd.py b/test/unit/runner/cli/test_yagna_payment_cmd.py similarity index 100% rename from test/goth/runner/cli/test_yagna_payment_cmd.py rename to test/unit/runner/cli/test_yagna_payment_cmd.py diff --git a/test/goth/runner/container/test_container.py b/test/unit/runner/container/test_container.py similarity index 100% rename from test/goth/runner/container/test_container.py rename to test/unit/runner/container/test_container.py diff --git a/test/goth/runner/container/test_payment.py b/test/unit/runner/container/test_payment.py similarity index 100% rename from test/goth/runner/container/test_payment.py rename to test/unit/runner/container/test_payment.py diff --git a/test/goth/runner/container/test_utils.py b/test/unit/runner/container/test_utils.py similarity index 100% rename from test/goth/runner/container/test_utils.py rename to test/unit/runner/container/test_utils.py diff --git a/test/goth/runner/probe/test_run_command_on_host.py b/test/unit/runner/probe/test_run_command_on_host.py similarity index 100% rename from test/goth/runner/probe/test_run_command_on_host.py rename to test/unit/runner/probe/test_run_command_on_host.py diff --git a/test/goth/runner/test_assertion_monitors.py b/test/unit/runner/test_assertion_monitors.py similarity index 100% rename from test/goth/runner/test_assertion_monitors.py rename to test/unit/runner/test_assertion_monitors.py diff --git a/test/goth/runner/test_shutdown.py b/test/unit/runner/test_shutdown.py similarity index 100% rename from test/goth/runner/test_shutdown.py rename to test/unit/runner/test_shutdown.py