Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Output interactive mode env to a file #520

Merged
merged 9 commits into from
Jul 28, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
75 changes: 75 additions & 0 deletions .github/workflows/integration.yml
Original file line number Diff line number Diff line change
@@ -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

12 changes: 8 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
azawlocki marked this conversation as resolved.
Show resolved Hide resolved

Press Ctrl+C at any moment to stop the test harness.
source /tmp/goth_interactive.env
azawlocki marked this conversation as resolved.
Show resolved Hide resolved

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.
Expand Down
29 changes: 22 additions & 7 deletions goth/interactive.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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,
Expand All @@ -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)
28 changes: 16 additions & 12 deletions goth/runner/probe/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -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()
Expand Down
3 changes: 2 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"]
Expand Down
23 changes: 23 additions & 0 deletions test/integration/conftest.py
Original file line number Diff line number Diff line change
@@ -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"
34 changes: 34 additions & 0 deletions test/integration/test_interactive.py
Original file line number Diff line number Diff line change
@@ -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
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.