Skip to content

Commit

Permalink
New tests for CLI
Browse files Browse the repository at this point in the history
Use multiprocessing to run CLI.
This is to ensure we can use monkeypatch and are going through the
Python API and not invoking the CLI through a separate Python process.

Extend CI to take `server` extra into account.

Fix and clean CLI according to tests.
Remove cli/options.py.
It only contained a single variable definition. This has been moved to
cli/run.py.

Avoid importing optimade-client package in CLI.
Define package version manually in CLI.
Add new manual package version number to `update-version` invoke task.
Add optimade_client/cli/run.py to Publish on PyPI workflow.

Create Jupyter config dir if it doesn't exist.
  • Loading branch information
CasperWA committed Oct 14, 2020
1 parent ddd7961 commit a6fdbfb
Show file tree
Hide file tree
Showing 9 changed files with 191 additions and 50 deletions.
1 change: 1 addition & 0 deletions .github/static/update_version.sh
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,5 @@ echo "\n### Commit updated files ###"
git add setup.py
git add optimade_client/__init__.py
git add optimade_client/informational.py
git add optimade_client/cli/run.py
git commit -m "Release ${GITHUB_REF#refs/tags/}"
10 changes: 8 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -59,14 +59,20 @@ jobs:
with:
python-version: ${{ matrix.python-version}}

- name: Install dependencies
- name: Install initial dependencies
run: |
python -m pip install -U pip
pip install -U setuptools
pip install -e .[testing]
- name: PyTest
run: pytest -vvv --cov=optimade_client --cov-report=xml tests/
run: pytest -vvv --cov=optimade_client --cov-report=xml --cov-append tests/

- name: Install server dependencies
run: pip install -e .[server]

- name: PyTest (with 'server' extra)
run: pytest --cov=optimade_client --cov-report=xml --cov-append tests/cli/

- name: Upload coverage to Codecov
if: matrix.python-version == 3.8 && github.repository == 'CasperWA/voila-optimade-client'
Expand Down
4 changes: 0 additions & 4 deletions optimade_client/cli/options.py

This file was deleted.

27 changes: 15 additions & 12 deletions optimade_client/cli/run.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import argparse
import logging
import os
from pathlib import Path
from shutil import copyfile
Expand All @@ -10,11 +11,12 @@
except ImportError:
voila = None

from optimade_client import __version__
from optimade_client.cli.options import LOGGING_LEVELS

LOGGING_LEVELS = [logging.getLevelName(level).lower() for level in range(0, 51, 10)]
VERSION = "2020.10.2" # Avoid importing optimade-client package

def main():

def main(args: list = None):
"""Run the OPTIMADE Client."""
parser = argparse.ArgumentParser(
description=main.__doc__,
Expand All @@ -24,7 +26,7 @@ def main():
"--version",
action="version",
help="Show the version and exit.",
version=f"OPTIMADE Client version {__version__}",
version=f"OPTIMADE Client version {VERSION}",
)
parser.add_argument(
"--log-level",
Expand All @@ -36,16 +38,15 @@ def main():
parser.add_argument(
"--debug",
action="store_true",
help="Will set the log-level to DEBUG. Note, parameter log-level takes precedence "
"if not 'info'!",
help="Will overrule log-level option and set the log-level to 'debug'.",
)
parser.add_argument(
"--open-browser",
action="store_true",
help="Attempt to open a browser upon starting the Voilà tornado server.",
)

args = parser.parse_args()
args = parser.parse_args(args)
log_level = args.log_level
debug = args.debug
open_browser = args.open_browser
Expand All @@ -60,6 +61,7 @@ def main():

# Rename jupyter_config.json to voila.json and copy it Jupyter's config dir
jupyter_config_dir = subprocess.getoutput("jupyter --config-dir")
Path(jupyter_config_dir).mkdir(parents=True, exist_ok=True)
copyfile(
Path(__file__).parent.parent.parent.joinpath("jupyter_config.json").resolve(),
f"{jupyter_config_dir}/voila.json",
Expand All @@ -72,13 +74,11 @@ def main():
check=False,
)

log_level = log_level.lower()
if debug and log_level == "info":
log_level = "debug"

argv = ["OPTIMADE Client.ipynb"]

if log_level == "debug":
if debug:
if log_level not in ("debug", "info"):
print("[OPTIMADE-Client] Overwriting requested log-level to: 'debug'")
os.environ["OPTIMADE_CLIENT_DEBUG"] = "True"
argv.append("--debug")
else:
Expand All @@ -87,4 +87,7 @@ def main():
if not open_browser:
argv.append("--no-browser")

if "--debug" not in argv:
argv.append(f"--Voila.log_level={getattr(logging, log_level.upper())}")

voila(argv)
4 changes: 4 additions & 0 deletions tasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,5 +47,9 @@ def update_version(_, version=""):
TOP_DIR.joinpath("optimade_client/informational.py"),
(r"Client version.*</code>", f"Client version</b>: <code>{version}</code>"),
)
update_file(
TOP_DIR.joinpath("optimade_client/cli/run.py"),
(r'VERSION = ".+"', f'VERSION = "{version}"'),
)

print(f"Bumped version to {version} !")
50 changes: 50 additions & 0 deletions tests/cli/conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
from multiprocessing import Process
import os
import signal
from time import sleep
from typing import List

import pytest


@pytest.fixture
def run_cli(capfd):
"""Run a command in the `optimade-client` CLI (through the Python API)."""

def _run_cli(options: List[str] = None, raises: bool = False) -> str:
"""Run a command in the `optimade-client` CLI (through the Python API)."""
from optimade_client.cli import run

if options is None:
options = []

try:
cli = Process(target=run.main, args=(options,))
cli.start()
sleep(5) # Startup time
output = capfd.readouterr()
finally:
os.kill(cli.pid, signal.SIGINT)
timeout = 10 # seconds
while cli.is_alive() and timeout:
sleep(1)
timeout -= 1
if cli.is_alive():
cli.kill()
cli.join()
sleep(1)

assert not cli.is_alive(), f"Could not stop CLI subprocess <PID={cli.pid}>"

if raises:
assert (
cli.exitcode != 0
), f"\nstdout:\n{output.out}\n\nstderr:\n{output.err}"
else:
assert (
cli.exitcode == 0
), f"\nstdout:\n{output.out}\n\nstderr:\n{output.err}"

return output.out + output.err

return _run_cli
25 changes: 25 additions & 0 deletions tests/cli/test_no_voila_run.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import pytest

try:
import voila as _ # noqa: F401
except ImportError:
VOILA_INSTALLED = False
else:
VOILA_INSTALLED = True

pytestmark = pytest.mark.skipif(
VOILA_INSTALLED,
reason="Voilà is installed. Tests in this module are rendered invalid.",
)


def test_voila_not_installed(run_cli):
"""Ensure the CLI can handle Voilà not being installed."""
output = run_cli(raises=True)
exit_text = (
"Voilà is not installed.\nPlease run:\n\n pip install optimade-client[server]\n\n"
"Or the equivalent, matching the installation in your environment, to install Voilà "
"(and ASE for a larger download format selection)."
)
assert exit_text in output
assert "[Voila]" not in output
101 changes: 76 additions & 25 deletions tests/cli/test_run.py
Original file line number Diff line number Diff line change
@@ -1,33 +1,84 @@
import pytest

try:
import voila as _
except ImportError:
VOILA_PACKAGE_EXISTS = False
else:
VOILA_PACKAGE_EXISTS = True


@pytest.mark.skipif(
not VOILA_PACKAGE_EXISTS,
reason="Voilà is not installed. This test is rendered invalid.",
)

_ = pytest.importorskip("voila")


def test_default(run_cli):
"""Run `optimade-client` with default settings"""
output = run_cli()
assert "[Voila] Voilà is running at:" in output, f"output:\n{output}"


@pytest.mark.skipif(
VOILA_PACKAGE_EXISTS, reason="Voilà is installed. This test is rendered invalid."
)
def test_voila_not_installed(run_cli):
"""Ensure the CLI can handle Voilà not being installed."""
output = run_cli(raises=True)
exit_text = (
"Voilà is not installed.\nPlease run:\n\n pip install optimade-client[server]\n\n"
"Or the equivalent, matching the installation in your environment, to install Voilà "
"(and ASE for a larger download format selection)."
)
assert exit_text in output
assert "[Voila]" not in output
def test_version(run_cli):
"""Check `--version` flag"""
from optimade_client import __version__

output = run_cli(["--version"])
assert output == f"OPTIMADE Client version {__version__}\n"


def test_log_level(run_cli):
"""Check `--log-level` option
Levels:
- `warning`: Above normal setting (INFO). There shouldn't be any output.
- `info`: Normal setting. There should be minimal output.
- `debug`: Below normal setting (INFO). There should be maximum output.
"""
signed_text = "Notebook already signed: OPTIMADE Client.ipynb\n"

log_level = "warning"

output = run_cli(["--log-level", log_level])
assert output == signed_text

log_level = "info"

output = run_cli(["--log-level", log_level])
assert output != signed_text
assert signed_text in output
assert "[Voila]" in output
assert "[Voila] template paths:" not in output

log_level = "debug"
output = run_cli(["--log-level", log_level])
assert output != signed_text
assert signed_text in output
assert "[Voila] template paths:" in output


def test_debug(run_cli):
"""Check `--debug` flag
This forcefully sets the log-level to `debug`.
"""
output = run_cli(["--debug"])
assert "[Voila] template paths:" in output

log_level = "info"
output = run_cli(["--log-level", log_level, "--debug"])
assert "[OPTIMADE-Client] Overwriting requested log-level to: 'debug'" not in output
assert "[Voila] template paths:" in output

log_level = "warning"
output = run_cli(["--log-level", log_level, "--debug"])
assert "[OPTIMADE-Client] Overwriting requested log-level to: 'debug'" in output
assert "[Voila] template paths:" in output

log_level = "debug"
output = run_cli(["--log-level", log_level, "--debug"])
assert "[OPTIMADE-Client] Overwriting requested log-level to: 'debug'" not in output
assert "[Voila] template paths:" in output


def test_open_browser(run_cli, monkeypatch):
"""Check `--open-browser` flag"""
check_text = "Testing: webbrowser.get has been overwritten"
monkeypatch.setattr("webbrowser.get", lambda *args, **kwargs: print(check_text))

output = run_cli(["--open-browser"])
assert check_text in output, f"output:\n{output}"

output = run_cli()
assert check_text not in output, f"output:\n{output}"
19 changes: 12 additions & 7 deletions tests/test_utils.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,12 @@
from optimade_client import utils


def test_fetch_providers_wrong_url():
"""Test when fetch_providers is provided a wrong URL
It should now return at the very least the cached list of providers
"""
import json

from optimade_client import utils

wrong_url = "https://this.is.a.wrong.url"

providers = utils.fetch_providers(providers_urls=wrong_url)
Expand All @@ -21,6 +20,8 @@ def test_fetch_providers_wrong_url():

def test_fetch_providers_content():
"""Test known content in dict of database providers"""
from optimade_client.utils import fetch_providers

exmpl = {
"type": "links",
"id": "exmpl",
Expand All @@ -33,13 +34,15 @@ def test_fetch_providers_content():
},
}

assert exmpl in utils.fetch_providers()
assert exmpl in fetch_providers()


def test_exmpl_not_in_list():
"""Make sure the 'exmpl' database provider is not in the final list"""
from optimade.models import LinksResourceAttributes

from optimade_client.utils import get_list_of_valid_providers

exmpl = (
"Example provider",
LinksResourceAttributes(
Expand Down Expand Up @@ -82,7 +85,7 @@ def test_exmpl_not_in_list():
),
)

list_of_database_providers = utils.get_list_of_valid_providers()
list_of_database_providers = get_list_of_valid_providers()

assert exmpl not in list_of_database_providers
assert mcloud in list_of_database_providers or odbx in list_of_database_providers
Expand All @@ -93,6 +96,8 @@ def test_ordered_query_url():
Testing already sorted URLs, making sure they come out exactly the same as when they came in.
"""
from optimade_client.utils import ordered_query_url

normal_url = (
"https://optimade.materialsproject.org/v1.0.0/structures?filter=%28+nelements%3E%3D1+AND+"
"nelements%3C%3D9+AND+nsites%3E%3D1+AND+nsites%3C%3D444+%29+AND+%28+NOT+structure_features"
Expand All @@ -106,8 +111,8 @@ def test_ordered_query_url():
"=json&response_format=xml"
)

ordered_url = utils.ordered_query_url(normal_url)
ordered_url = ordered_query_url(normal_url)
assert ordered_url == normal_url

ordered_url = utils.ordered_query_url(multi_query_param_url)
ordered_url = ordered_query_url(multi_query_param_url)
assert ordered_url == multi_query_param_url

0 comments on commit a6fdbfb

Please sign in to comment.