Skip to content

Commit

Permalink
test: restructure and complete the test suite
Browse files Browse the repository at this point in the history
  • Loading branch information
qdelamea-aneo committed Nov 15, 2024
1 parent 92b6720 commit ac2134e
Show file tree
Hide file tree
Showing 11 changed files with 380 additions and 65 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/publish.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ jobs:
packages-dir: dist/
repository-url: https://test.pypi.org/legacy/

- name: Publish to PyPiTest
- name: Publish to PyPi
if: github.event_name == 'release' # Publish on releases
uses: pypa/gh-action-pypi-publish@release/v1
with:
Expand Down
7 changes: 5 additions & 2 deletions src/armonik_cli/commands/sessions.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,8 @@ def list(endpoint: str, output: str, debug: bool) -> None:
sessions = [_clean_up_status(s) for s in sessions]
console.formatted_print(sessions, format=output, table_cols=SESSION_TABLE_COLS)

console.print(f"\n{total} sessions found.")
# TODO: Use logger to display this information
# console.print(f"\n{total} sessions found.")


@sessions.command()
Expand Down Expand Up @@ -255,7 +256,9 @@ def stop_submission(
session = sessions_client.stop_submission_session(
session_id=session_id, client=clients_only, worker=workers_only
)
console.formatted_print(session, format=output, table_cols=SESSION_TABLE_COLS)
console.formatted_print(
_clean_up_status(session), format=output, table_cols=SESSION_TABLE_COLS
)


def _clean_up_status(session: Session) -> Session:
Expand Down
6 changes: 3 additions & 3 deletions src/armonik_cli/core/decorators.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@

def error_handler(func=None):
"""Decorator to ensure correct display of errors.
Args:
func: The command function to be decorated. If None, a partial function is returned,
allowing the decorator to be used with parentheses.
Expand Down Expand Up @@ -46,7 +46,7 @@ def wrapper(*args, **kwargs):

def base_command(func=None):
"""Decorator to add common CLI options to a Click command function, including
'endpoint', 'output', and 'debug'. These options are automatically passed
'endpoint', 'output', and 'debug'. These options are automatically passed
as arguments to the decorated function.
The following options are added to the command:
Expand All @@ -55,7 +55,7 @@ def base_command(func=None):
- `--debug`: Enables debug mode, printing additional logs if set.
Warning:
If the decorated function has parameters with the same names as the options added by
If the decorated function has parameters with the same names as the options added by
this decorator, this can lead to conflicts and unpredictable behavior.
Args:
Expand Down
219 changes: 203 additions & 16 deletions tests/commands/test_sessions.py
Original file line number Diff line number Diff line change
@@ -1,26 +1,213 @@
import pytest

from datetime import datetime, timedelta
from copy import deepcopy

from armonik.client import ArmoniKSessions
from armonik.common import Session
from click.testing import CliRunner
from armonik.common import Session, TaskOptions, SessionStatus
from conftest import run_cmd_and_assert_exit_code, reformat_cmd_output

ENDPOINT = "172.17.119.85:5001"

raw_session = Session(
session_id="id",
status=SessionStatus.RUNNING,
client_submission=True,
worker_submission=True,
partition_ids=["default"],
options=TaskOptions(
max_duration=timedelta(hours=1),
priority=1,
max_retries=2,
partition_id="default",
application_name="",
application_version="",
application_namespace="",
application_service="",
engine_type="",
options={},
),
created_at=datetime(year=2024, month=11, day=11),
cancelled_at=None,
closed_at=None,
purged_at=None,
deleted_at=None,
duration=timedelta(hours=0),
)
serialized_session = {
"SessionId": "id",
"Status": "Running",
"ClientSubmission": True,
"WorkerSubmission": True,
"PartitionIds": ["default"],
"Options": {
"MaxDuration": "1:00:00",
"Priority": 1,
"MaxRetries": 2,
"PartitionId": "default",
"ApplicationName": "",
"ApplicationVersion": "",
"ApplicationNamespace": "",
"ApplicationService": "",
"EngineType": "",
"Options": {},
},
"CreatedAt": "2024-11-11 00:00:00",
"CancelledAt": None,
"ClosedAt": None,
"PurgedAt": None,
"DeletedAt": None,
"Duration": "0:00:00",
}


@pytest.mark.parametrize(
"cmd",
[
f"session list --endpoint {ENDPOINT}",
],
)
def test_session_list(mocker, cmd):
mocker.patch.object(ArmoniKSessions, "list_sessions", return_value=(1, [deepcopy(raw_session)]))
result = run_cmd_and_assert_exit_code(cmd)
assert reformat_cmd_output(result.output, deserialize=True) == [serialized_session]


@pytest.mark.parametrize(
"cmd",
[
f"session get --endpoint {ENDPOINT} id",
],
)
def test_session_get(mocker, cmd):
mocker.patch.object(ArmoniKSessions, "get_session", return_value=deepcopy(raw_session))
result = run_cmd_and_assert_exit_code(cmd)
assert reformat_cmd_output(result.output, deserialize=True) == serialized_session


@pytest.mark.parametrize(
"cmd",
[
f"session create --priority 1 --max-duration 01:00:0 --max-retries 2 --endpoint {ENDPOINT}",
f"session create --priority 1 --max-duration 01:00:0 --max-retries 2 --endpoint {ENDPOINT} "
"--default-partition bench --partition bench --partition htcmock --option op1=val1 --option opt2=val2 "
"--application-name app --application-version v1 --application-namespace ns --application-service svc --engine-type eng",
],
)
def test_session_create(mocker, cmd):
mocker.patch.object(ArmoniKSessions, "create_session", return_value="id")
mocker.patch.object(ArmoniKSessions, "get_session", return_value=deepcopy(raw_session))
result = run_cmd_and_assert_exit_code(cmd)
assert reformat_cmd_output(result.output, deserialize=True) == serialized_session


@pytest.mark.parametrize(
("cmd", "prompt"),
[
(f"session cancel --confirm --endpoint {ENDPOINT} id", None),
(f"session cancel --endpoint {ENDPOINT} id", "y"),
],
)
def test_session_cancel(mocker, cmd, prompt):
mocker.patch.object(ArmoniKSessions, "cancel_session", return_value=deepcopy(raw_session))
result = run_cmd_and_assert_exit_code(cmd, input=prompt)
assert (
reformat_cmd_output(
result.output, deserialize=True, first_line_out=True if prompt else False
)
== serialized_session
)


from armonik_cli.commands.sessions import list
@pytest.mark.parametrize(
"cmd",
[
f"session pause --endpoint {ENDPOINT} id",
],
)
def test_session_pause(mocker, cmd):
mocker.patch.object(ArmoniKSessions, "pause_session", return_value=deepcopy(raw_session))
result = run_cmd_and_assert_exit_code(cmd)
assert reformat_cmd_output(result.output, deserialize=True) == serialized_session


@pytest.mark.parametrize(
"cmd",
[
f"session resume --endpoint {ENDPOINT} id",
],
)
def test_session_resume(mocker, cmd):
mocker.patch.object(ArmoniKSessions, "resume_session", return_value=deepcopy(raw_session))
result = run_cmd_and_assert_exit_code(cmd)
assert reformat_cmd_output(result.output, deserialize=True) == serialized_session


@pytest.mark.parametrize(
("cmd", "prompt"),
[
(f"session close --confirm --endpoint {ENDPOINT} id", None),
(f"session close --endpoint {ENDPOINT} id", "y"),
],
)
def test_session_close(mocker, cmd, prompt):
mocker.patch.object(ArmoniKSessions, "close_session", return_value=deepcopy(raw_session))
result = run_cmd_and_assert_exit_code(cmd, input=prompt)
assert (
reformat_cmd_output(
result.output, deserialize=True, first_line_out=True if prompt else False
)
== serialized_session
)


@pytest.mark.parametrize(
("cmd", "prompt"),
[
(f"session purge --confirm --endpoint {ENDPOINT} id", None),
(f"session purge --endpoint {ENDPOINT} id", "y"),
],
)
def test_session_purge(mocker, cmd, prompt):
mocker.patch.object(ArmoniKSessions, "purge_session", return_value=deepcopy(raw_session))
result = run_cmd_and_assert_exit_code(cmd, input=prompt)
assert (
reformat_cmd_output(
result.output, deserialize=True, first_line_out=True if prompt else False
)
== serialized_session
)


@pytest.mark.parametrize(
("cmd", "prompt"),
[
(f"session delete --confirm --endpoint {ENDPOINT} id", None),
(f"session delete --endpoint {ENDPOINT} id", "y"),
],
)
def test_session_delete(mocker, cmd, prompt):
mocker.patch.object(ArmoniKSessions, "delete_session", return_value=deepcopy(raw_session))
result = run_cmd_and_assert_exit_code(cmd, input=prompt)
assert (
reformat_cmd_output(
result.output, deserialize=True, first_line_out=True if prompt else False
)
== serialized_session
)


@pytest.mark.parametrize(
("args", "mock_return", "output_id"),
"cmd",
[
(["--endpoint", "endpoint"], (0, []), "sessions_list_empty"),
(
["--endpoint", "endpoint", "-o", "json"],
(1, [Session(session_id="id")]),
"sessions_list",
),
f"session stop-submission --endpoint {ENDPOINT} id",
f"session stop-submission --clients-only --endpoint {ENDPOINT} id",
f"session stop-submission --workers-only --endpoint {ENDPOINT} id",
],
)
def test_armonik_sessions_list(mocker, cmd_outputs, args, mock_return, output_id):
mocker.patch.object(ArmoniKSessions, "list_sessions", return_value=mock_return)
runner = CliRunner()
result = runner.invoke(list, args)
assert result.exit_code == 0
assert result.output == cmd_outputs[output_id]
def test_session_stop_submission(mocker, cmd):
mocker.patch.object(
ArmoniKSessions, "stop_submission_session", return_value=deepcopy(raw_session)
)
result = run_cmd_and_assert_exit_code(cmd)
assert reformat_cmd_output(result.output, deserialize=True) == serialized_session
39 changes: 27 additions & 12 deletions tests/conftest.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,30 @@
import pytest
import json

from pathlib import Path
from typing import Dict, Optional

from click.testing import CliRunner, Result

@pytest.fixture
def cmd_outputs():
"""Read command output files located in tests/outputs and return a dictionnary which keys are
file names and values file contents."""
output_files = [
d
for d in (Path(__file__).parent / "outputs").iterdir()
if d.is_file() and d.suffix == ".txt"
]
return {f.name.removesuffix(".txt"): f.open("r").read() for f in output_files}
from armonik_cli.cli import cli


def run_cmd_and_assert_exit_code(
cmd: str, exit_code: int = 0, input: Optional[str] = None, env: Optional[Dict[str, str]] = None
) -> Result:
cmd = cmd.split()
runner = CliRunner()
with runner.isolated_filesystem():
result = runner.invoke(cli, cmd, input=input, env=env)
assert result.exit_code == exit_code
return result


def reformat_cmd_output(
output: str, deserialize: bool = False, first_line_out: bool = False
) -> str:
if first_line_out:
output = "\n".join(output.split("\n")[1:])
output = output.replace("\n", "")
output = " ".join(output.split())
if deserialize:
return json.loads(output)
return output
25 changes: 20 additions & 5 deletions tests/test_errors.py → tests/core/test_decorators.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

from grpc import RpcError, StatusCode

from armonik_cli.core import error_handler
from armonik_cli.core.decorators import error_handler, base_command
from armonik_cli.exceptions import NotFoundError, InternalError


Expand Down Expand Up @@ -31,18 +31,33 @@ def raise_error(code, details):
raise_error(code, "")


def test_error_handler_other_no_debug():
@error_handler
@pytest.mark.parametrize("decorator", [error_handler, error_handler()])
def test_error_handler_other_no_debug(decorator):
@decorator
def raise_error():
raise ValueError()

with pytest.raises(InternalError):
raise_error()


def test_error_handler_other_debug():
@error_handler
@pytest.mark.parametrize("decorator", [error_handler, error_handler()])
def test_error_handler_other_debug(decorator):
@decorator
def raise_error(debug=None):
raise ValueError()

raise_error(debug=True)


@pytest.mark.parametrize("decorator", [base_command, base_command()])
def test_base_command(decorator):
@decorator
def test_func():
pass

assert test_func.__name__ == "test_func"
assert len(test_func.__click_params__) == 3
assert test_func.__click_params__[0].name == "debug"
assert test_func.__click_params__[1].name == "output"
assert test_func.__click_params__[2].name == "endpoint"
Loading

0 comments on commit ac2134e

Please sign in to comment.