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

Tests: Improve coverage #65

Merged
merged 15 commits into from
Feb 9, 2022
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
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ jobs:

- name: Run tests
run: |
pytest --cov=aiidalab_launch -v --slow
pytest -v --slow
coverage json

- name: Upload coverage to Codecov
Expand Down
12 changes: 2 additions & 10 deletions aiidalab_launch/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,14 +63,6 @@ def _get_host_ports(container: Container) -> Generator[int, None, None]:
pass


def _get_system_user(container: Container) -> str:
return get_docker_env(container, "SYSTEM_USER")


def _get_jupyter_token(container: Container) -> str:
return get_docker_env(container, "JUPYTER_TOKEN")


def _get_aiidalab_default_apps(container: Container) -> list:
try:
return get_docker_env(container, "AIIDALAB_DEFAULT_APPS").split()
Expand Down Expand Up @@ -128,7 +120,7 @@ def from_container(cls, container: Container) -> Profile:
f"Container {container.id} does not appear to be an AiiDAlab container."
)

system_user = _get_system_user(container)
system_user = get_docker_env(container, "SYSTEM_USER")
return Profile(
name=profile_name,
port=_get_configured_host_port(container),
Expand Down Expand Up @@ -492,7 +484,7 @@ def url(self) -> str:
self.container.reload()
host_ports = list(_get_host_ports(self.container))
if len(host_ports) > 0:
jupyter_token = _get_jupyter_token(self.container)
jupyter_token = get_docker_env(self.container, "JUPYTER_TOKEN")
return f"http://localhost:{host_ports[0]}/?token={jupyter_token}"
else:
raise NoHostPortAssigned(self.container.id)
3 changes: 2 additions & 1 deletion setup.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ ignore =
W503 # Line break before binary operator, preferred formatting for black.
E203 # Whitespace before ':', preferred formatting for black.
per-file-ignores =
tests/*: U100
tests/*: U100, U101

[mypy]
warn_unused_configs = True
Expand All @@ -78,6 +78,7 @@ aiidalab_launch/version.py =
__version__ = "{version}"

[tool:pytest]
addopts = --cov=aiidalab_launch --cov-fail-under=80
asyncio_mode = auto
markers =
slow: marks tests as slow
Expand Down
135 changes: 114 additions & 21 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,13 @@

Provide fixtures for all tests.
"""
import asyncio
import random
import string
import sys
from functools import partial
from pathlib import Path
from typing import Iterator

import click
import docker
Expand All @@ -25,54 +27,108 @@
)


@pytest.fixture(scope="session")
def docker_client():
try:
yield docker.from_env()
except docker.errors.DockerException:
pytest.skip("docker not available")


@pytest.fixture
def random_token():
return "".join(random.choices(string.ascii_lowercase + string.digits, k=8))


@pytest.fixture(autouse=True)
def default_port(monkeypatch):
monkeypatch.setattr(aiidalab_launch.core, "DEFAULT_PORT", None)
yield None
@pytest.fixture(scope="session")
def session_token():
return "".join(random.choices(string.ascii_lowercase + string.digits, k=8))


@pytest.fixture(autouse=True)
def app_config(tmp_path, monkeypatch):
app_config_dir = tmp_path.joinpath("app_dirs")
monkeypatch.setattr(
click, "get_app_dir", lambda app_id: str(app_config_dir.joinpath(app_id))
)
yield app_config_dir
# Redefine event_loop fixture to be session-scoped.
# See: https://github.com/pytest-dev/pytest-asyncio#async-fixtures
@pytest.fixture(scope="session")
def event_loop(request: "pytest.FixtureRequest") -> Iterator[asyncio.AbstractEventLoop]:
"""Create an instance of the default event loop for the whole session."""
loop = asyncio.get_event_loop_policy().new_event_loop()
yield loop
loop.close()


@pytest.fixture(autouse=True)
def container_prefix(random_token, monkeypatch):
# Adapted from https://github.com/pytest-dev/pytest/issues/1872#issuecomment-375108891:
@pytest.fixture(scope="session")
def monkeypatch_session():
from _pytest.monkeypatch import MonkeyPatch

m = MonkeyPatch()
yield m
m.undo()


# Avoid interfering with used ports on the host system.
@pytest.fixture(scope="session", autouse=True)
def _default_port(monkeypatch_session):
monkeypatch_session.setattr(aiidalab_launch.core, "DEFAULT_PORT", None)
yield None


@pytest.fixture
def _container_prefix(random_token, monkeypatch):
container_prefix = f"aiidalab-launch_tests_{random_token}_"
monkeypatch.setattr(aiidalab_launch.core, "CONTAINER_PREFIX", container_prefix)
yield container_prefix


@pytest.fixture(scope="session")
def _shared_container_prefix(monkeypatch_session, session_token):
container_prefix = f"aiidalab-launch_tests_session_{session_token}_"
monkeypatch_session.setattr(
aiidalab_launch.core, "CONTAINER_PREFIX", container_prefix
)
yield container_prefix


@pytest.fixture(autouse=True)
def home_path(tmp_path, monkeypatch):
monkeypatch.setattr(Path, "home", lambda: tmp_path.joinpath("home"))


@pytest.fixture
def profile():
return Profile()
def app_config(tmp_path, monkeypatch):
app_config_dir = tmp_path.joinpath("app_dirs")
monkeypatch.setattr(
click, "get_app_dir", lambda app_id: str(app_config_dir.joinpath(app_id))
)
yield app_config_dir


@pytest.fixture(scope="session")
def _shared_app_config(tmp_path_factory, monkeypatch_session):
app_config_dir = tmp_path_factory.mktemp("app_dirs")
monkeypatch_session.setattr(
click, "get_app_dir", lambda app_id: str(app_config_dir.joinpath(app_id))
)
yield app_config_dir


@pytest.fixture
def config():
def config(app_config):
return Config()


@pytest.fixture(scope="session")
def _shared_config(_shared_app_config):
return Config()


@pytest.fixture
def docker_client():
try:
yield docker.from_env()
except docker.errors.DockerException:
pytest.skip("docker not available")
def profile(config, _container_prefix):
return Profile()


@pytest.fixture(scope="session")
def _shared_profile(_shared_config, _shared_container_prefix):
return Profile()


@pytest.fixture
Expand All @@ -91,6 +147,43 @@ def instance(docker_client, profile):
)


@pytest.fixture(scope="session")
def _shared_instance(docker_client, _shared_profile):
instance = AiidaLabInstance(client=docker_client, profile=_shared_profile)
yield instance
for op in (instance.stop, partial(instance.remove, data=True)):
try:
op()
except (docker.errors.NotFound, RequiresContainerInstance):
continue
except (RuntimeError, docker.errors.APIError) as error:
print(
f"WARNING: Issue while stopping/removing instance: {error}",
file=sys.stderr,
)


@pytest.fixture(scope="session")
async def started_instance(_shared_instance):
_shared_instance.create()
assert _shared_instance.container is not None
assert (
await _shared_instance.status()
is _shared_instance.AiidaLabInstanceStatus.CREATED
)
_shared_instance.start()
assert (
await asyncio.wait_for(_shared_instance.status(), timeout=20)
is _shared_instance.AiidaLabInstanceStatus.STARTING
)
await asyncio.wait_for(_shared_instance.wait_for_services(), timeout=300)
assert (
await asyncio.wait_for(_shared_instance.status(), timeout=20)
is _shared_instance.AiidaLabInstanceStatus.UP
)
yield _shared_instance


def pytest_addoption(parser):
parser.addoption(
"--slow",
Expand Down
111 changes: 106 additions & 5 deletions tests/test_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,44 +40,145 @@ def test_version_displays_expected_message():
assert "AiiDAlab Launch" in result.output.strip()


def test_list_profiles():
def test_version_verbose_logging():
runner: CliRunner = CliRunner()
result: Result = runner.invoke(cli.cli, ["-vvv", "version"])
assert "AiiDAlab Launch" in result.output.strip()
assert "Verbose logging is enabled." in result.output.strip()


def test_list_profiles(app_config):
runner: CliRunner = CliRunner()
result: Result = runner.invoke(cli.cli, ["profiles", "list"])
assert "default" in result.output.strip()


def test_show_profile():
def test_show_profile(app_config):
runner: CliRunner = CliRunner()
result: Result = runner.invoke(cli.cli, ["profiles", "show", "default"])
assert Profile.loads("default", result.output) == Profile()


def test_add_remove_profile():
def test_change_default_profile(app_config):
runner: CliRunner = CliRunner()
result: Result = runner.invoke(cli.cli, ["profiles", "set-default", "default"])
assert result.exit_code == 0
result: Result = runner.invoke(
cli.cli, ["profiles", "set-default", "does-not-exist"]
)
assert result.exit_code == 1
assert "does not exist" in result.output


def test_add_remove_profile(app_config):
runner: CliRunner = CliRunner()

# Add new-profile
result: Result = runner.invoke(
cli.cli, ["profiles", "add", "new-profile"], input="n\n"
)
assert result.exit_code == 0
assert "Added profile 'new-profile'." in result.output

# Check that new-profile exists
result: Result = runner.invoke(cli.cli, ["profiles", "list"])
assert "new-profile" in result.output
result: Result = runner.invoke(cli.cli, ["profiles", "show", "new-profile"])
assert result.exit_code == 0

# Try add another profile with the same name (should fail)
result: Result = runner.invoke(
cli.cli, ["profiles", "add", "new-profile"], input="n\n"
)
assert result.exit_code == 1
assert "Profile with name 'new-profile' already exists." in result.output

# Try make new profile default
result: Result = runner.invoke(cli.cli, ["profiles", "set-default", "new-profile"])
assert result.exit_code == 0
assert "Set default profile to 'new-profile'." in result.output
# Reset default profile
result: Result = runner.invoke(cli.cli, ["profiles", "set-default", "default"])
assert result.exit_code == 0
assert "Set default profile to 'default'." in result.output

# Remove new-profile
result: Result = runner.invoke(
cli.cli, ["profiles", "remove", "new-profile"], input="y\n"
)
assert result.exit_code == 0
result: Result = runner.invoke(cli.cli, ["profiles", "list"])
assert "new-profile" not in result.output

# Remove new-profile (again – should fail)
result: Result = runner.invoke(
cli.cli, ["profiles", "remove", "new-profile"], input="y\n"
)
assert result.exit_code == 1
assert "Profile with name 'new-profile' does not exist." in result.output


def test_add_profile_invalid_name(app_config):
runner: CliRunner = CliRunner()
# underscores are not allowed
result: Result = runner.invoke(cli.cli, ["profiles", "add", "new_profile"])
assert result.exit_code == 1
assert "Invalid profile name 'new_profile'." in result.output


@pytest.mark.slow
@pytest.mark.trylast
def test_status(started_instance):
runner: CliRunner = CliRunner()
result: Result = runner.invoke(cli.cli, ["status"])
assert result.exit_code == 0
assert started_instance.profile.name in result.output
assert started_instance.profile.container_name() in result.output
assert "up" in result.output
assert started_instance.profile.home_mount in result.output
assert started_instance.url() in result.output


@pytest.mark.slow
@pytest.mark.trylast
def test_exec(started_instance):
runner: CliRunner = CliRunner()
result: Result = runner.invoke(cli.cli, ["exec", "--", "whoami"])
assert result.exit_code == 0
assert "aiida" in result.output


@pytest.mark.slow
@pytest.mark.trylast
def test_logs(started_instance):
runner: CliRunner = CliRunner()
result: Result = runner.invoke(cli.cli, ["logs"])
assert result.exit_code == 0
assert len(result.output.strip().splitlines()) > 100


@pytest.mark.slow
@pytest.mark.trylast
def test_remove_running_profile(started_instance):
runner: CliRunner = CliRunner()
result: Result = runner.invoke(cli.cli, ["profiles", "remove", "default"])
assert result.exit_code == 1
assert "is still running" in result.output


@pytest.mark.slow
@pytest.mark.trylast
def test_start_stop(instance):
runner: CliRunner = CliRunner()
result: Result = runner.invoke(cli.cli, ["start"])
result: Result = runner.invoke(
cli.cli, ["-vvv", "start", "--no-browser", "--wait=300"]
)
assert result.exit_code == 0
result: Result = runner.invoke(cli.cli, ["status"])
assert result.exit_code == 0
result: Result = runner.invoke(cli.cli, ["stop"])
result: Result = runner.invoke(cli.cli, ["stop", "--remove"])
assert result.exit_code == 0
result: Result = runner.invoke(cli.cli, ["status"])
assert result.exit_code == 0
assert instance.profile.container_name() in result.output
assert "down" in result.output
Loading