diff --git a/reana_client/api/client.py b/reana_client/api/client.py index 0bb15599..5cffd90c 100644 --- a/reana_client/api/client.py +++ b/reana_client/api/client.py @@ -49,7 +49,6 @@ def ping(self): def get_workflows(self, access_token): """List all existing workflows.""" try: - response, http_response = self._client.api.\ get_workflows(access_token=access_token).result() if http_response.status_code == 200: diff --git a/reana_client/cli/__init__.py b/reana_client/cli/__init__.py index d9c40446..f58e256e 100644 --- a/reana_client/cli/__init__.py +++ b/reana_client/cli/__init__.py @@ -25,9 +25,12 @@ class Config(object): """Configuration object to share across commands.""" - def __init__(self): - """Initialize config variables.""" - self.client = None + def __init__(self, client=None): + """Initialize config variables. + + :param client: :reana_commons:`reana_commons.api_client.BaseAPIClient`. + """ + self.client = client @click.group() @@ -38,13 +41,14 @@ def __init__(self): type=click.Choice(['DEBUG', 'INFO', 'WARNING']), default='WARNING') @click.pass_context -def cli(ctx, loglevel): +@click.pass_obj +def cli(obj, ctx, loglevel): """REANA client for interacting with REANA server.""" logging.basicConfig( format=DEBUG_LOG_FORMAT if loglevel == 'DEBUG' else LOG_FORMAT, stream=sys.stderr, level=loglevel) - ctx.obj = Config() + ctx.obj = obj or Config() commands = [] commands.extend(workflow.workflow.commands.values()) diff --git a/reana_client/cli/utils.py b/reana_client/cli/utils.py index 87a21902..49d74be0 100644 --- a/reana_client/cli/utils.py +++ b/reana_client/cli/utils.py @@ -16,7 +16,7 @@ def add_access_token_options(func): """Adds access token related options to click commands.""" @click.option('-at', '--access-token', - default=os.environ.get('REANA_ACCESS_TOKEN', None), + default=os.getenv('REANA_ACCESS_TOKEN', None), help='Access token of the current user.') @functools.wraps(func) def wrapper(*args, **kwargs): diff --git a/reana_client/decorators.py b/reana_client/decorators.py index d7f7a13f..22764c68 100644 --- a/reana_client/decorators.py +++ b/reana_client/decorators.py @@ -11,6 +11,7 @@ import os import sys +import click from click.core import Context from reana_client.api import Client @@ -31,17 +32,20 @@ def wrapper(*args, **kwargs): server_url = os.environ.get('REANA_SERVER_URL', None) if not server_url: - logging.error( + click.secho( 'REANA client is not connected to any REANA cluster.\n' 'Please set REANA_SERVER_URL environment variable to ' 'the remote REANA cluster you would like to connect to.\n' 'For example: export ' - 'REANA_SERVER_URL=https://reana.cern.ch/') + 'REANA_SERVER_URL=https://reana.cern.ch/', + fg='red', + err=True) sys.exit(1) logging.info('REANA server URL ($REANA_SERVER_URL) is: {}' .format(server_url)) - ctx.obj.client = Client('reana-server') + if not ctx.obj.client: + ctx.obj.client = Client('reana-server') else: raise Exception( 'This decorator should be used after click.pass_context.') diff --git a/setup.py b/setup.py index 3434bfa9..6a925568 100644 --- a/setup.py +++ b/setup.py @@ -24,10 +24,11 @@ 'docutils>=0.14', 'isort>=4.2.2', 'pydocstyle>=1.0.0', + 'pytest>=3.8.0,<4.0.0', 'pytest-cache>=1.0', 'pytest-cov>=1.8.0', 'pytest-pep8>=1.0.6', - 'pytest>=2.8.0,<3.0.0' + 'pytest-reana>=0.4.0.dev20181105,<0.5.0', ] extras_require = { @@ -53,7 +54,7 @@ 'click>=7,<8', 'cwltool==1.0.20180912090223', 'pyOpenSSL==17.3.0', # FIXME remove once yadage-schemas solves deps. - 'reana-commons>=0.4.0.dev20181017,<0.5.0', + 'reana-commons>=0.4.0.dev20181105,<0.5.0', 'rfc3987==1.3.7', # FIXME remove once yadage-schemas solves deps. 'strict-rfc3339==0.7', # FIXME remove once yadage-schemas solves deps. 'tablib>=0.12.1,<0.13', diff --git a/tests/conftest.py b/tests/conftest.py index 37e0cf59..83e7a4bc 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -10,6 +10,56 @@ from __future__ import absolute_import, print_function -import os - import pytest +from mock import Mock +from pytest_reana.test_utils import make_mock_api_client + +from reana_client.api.client import Client + + +@pytest.fixture() +def mock_base_api_client(): + """Create mocked api client.""" + def _make_mock_api_client(status_code=200, + response=None, + component='reana-server'): + mock_http_response, mock_response = Mock(), Mock() + mock_http_response.status_code = status_code + mock_http_response.raw_bytes = str(response).encode() + mock_response = response + reana_server_client = make_mock_api_client( + component)(mock_response, mock_http_response) + reana_client_server_api = Client(component) + reana_client_server_api._client = reana_server_client + return reana_client_server_api + return _make_mock_api_client + + +@pytest.fixture() +def create_yaml_workflow_schema(): + """Return dummy yaml workflow schema.""" + reana_yaml_schema = \ + ''' + version: 0.3.0 + inputs: + files: + - code/helloworld.py + - inputs/names.txt + parameters: + sleeptime: 2 + inputfile: inputs/names.txt + helloworld: code/helloworld.py + outputfile: outputs/greetings.txt + outputs: + files: + - outputs/greetings.txt + workflow: + type: serial + specification: + steps: + - environment: 'python:2.7' + commands: + - python "${helloworld}" --sleeptime ${sleeptime} \ + --inputfile "${inputfile}" --outputfile "${outputfile}" + ''' + return reana_yaml_schema diff --git a/tests/test_cli_files.py b/tests/test_cli_files.py new file mode 100644 index 00000000..ca064984 --- /dev/null +++ b/tests/test_cli_files.py @@ -0,0 +1,118 @@ +# -*- coding: utf-8 -*- +# +# This file is part of REANA. +# Copyright (C) 2017, 2018 CERN. +# +# REANA is free software; you can redistribute it and/or modify it +# under the terms of the MIT License; see LICENSE file for more details. + +"""REANA client files tests.""" + +import hashlib +import json +import os + +from click.testing import CliRunner + +from reana_client.cli import Config, cli + + +def test_list_files_server_not_reachable(): + """Test list workflow workspace files when not connected to any cluster.""" + message = 'REANA client is not connected to any REANA cluster.' + runner = CliRunner() + result = runner.invoke(cli, ['list']) + assert result.exit_code == 1 + assert message in result.output + + +def test_list_files_server_no_token(): + """Test list workflow workspace files when access token is not set.""" + message = 'Please provide your access token' + env = {'REANA_SERVER_URL': 'localhost'} + runner = CliRunner(env=env) + result = runner.invoke(cli, ['list']) + assert result.exit_code == 1 + assert message in result.output + + +def test_list_files_ok(mock_base_api_client): + """Test list workflow workspace files successfull.""" + status_code = 200 + response = [ + { + "last-modified": "string", + "name": "string", + "size": 0 + } + ] + reana_token = '000000' + env = {'REANA_SERVER_URL': 'localhost'} + mocked_api_client = mock_base_api_client(status_code, + response, + 'reana-server') + config = Config(mocked_api_client) + runner = CliRunner(env=env) + result = runner.invoke( + cli, + ['list', '-at', reana_token, '--workflow', 'mytest.1', '--json'], + obj=config + ) + json_response = json.loads(result.output) + assert result.exit_code == 0 + assert isinstance(json_response, list) + assert len(json_response) == 1 + assert json_response[0]['name'] in response[0]['name'] + + +def test_download_file(mock_base_api_client): + """Test file downloading.""" + status_code = 200 + reana_token = '000000' + env = {'REANA_SERVER_URL': 'localhost'} + response = 'Content of file to download' + response_md5 = hashlib.md5(response.encode('utf-8')).hexdigest() + file = 'dummy_file.txt' + message = 'File {0} downloaded to'.format(file) + mocked_api_client = mock_base_api_client(status_code, + response, + 'reana-server') + config = Config(mocked_api_client) + runner = CliRunner(env=env) + result = runner.invoke( + cli, + ['download', '-at', reana_token, '--workflow', 'mytest.1', file], + obj=config + ) + assert result.exit_code == 0 + assert os.path.isfile(file) is True + file_md5 = hashlib.md5(open(file, 'rb').read()).hexdigest() + assert file_md5 == response_md5 + assert message in result.output + + +def test_upload_file(mock_base_api_client, create_yaml_workflow_schema): + """Test upload file.""" + status_code = 200 + reana_token = '000000' + env = {'REANA_SERVER_URL': 'localhost'} + file = 'file.txt' + response = [file] + message = 'was successfully uploaded.' + mocked_api_client = mock_base_api_client(status_code, + response, + 'reana-server') + config = Config(mocked_api_client) + runner = CliRunner(env=env) + with runner.isolated_filesystem(): + with open(file, 'w') as f: + f.write('test') + with open('reana.yaml', 'w') as reana_schema: + reana_schema.write(create_yaml_workflow_schema) + result = runner.invoke( + cli, + ['upload', '-at', reana_token, '--workflow', 'mytest.1', file], + obj=config + ) + assert result.exit_code == 0 + assert message in result.output diff --git a/tests/test_cli_ping.py b/tests/test_cli_ping.py index ed6dca10..c5caaa65 100644 --- a/tests/test_cli_ping.py +++ b/tests/test_cli_ping.py @@ -10,9 +10,36 @@ from click.testing import CliRunner -from reana_client.cli import cli +from reana_client.cli import Config, cli -def test_ping(): - """Test ping command.""" - assert True +def test_ping_server_not_set(): + """Test ping when server is not set.""" + runner = CliRunner() + result = runner.invoke(cli, ['ping']) + message = 'REANA client is not connected to any REANA cluster.' + assert message in result.output + + +def test_ping_server_not_reachable(): + """Test ping when server is set, but unreachable.""" + env = {'REANA_SERVER_URL': 'localhot'} + runner = CliRunner(env=env) + result = runner.invoke(cli, ['ping']) + message = 'Could not connect to the selected' + assert message in result.output + + +def test_ping_ok(mock_base_api_client): + """Test ping server is set and reachable.""" + env = {'REANA_SERVER_URL': 'localhost'} + status_code = 200 + response = {"status": 200, "message": "OK"} + mocked_api_client = mock_base_api_client(status_code, + response, + 'reana-server') + config = Config(mocked_api_client) + runner = CliRunner(env=env) + result = runner.invoke(cli, ['ping'], obj=config) + message = 'Server is running' + assert message in result.output diff --git a/tests/test_cli_workflows.py b/tests/test_cli_workflows.py new file mode 100644 index 00000000..5d997aa0 --- /dev/null +++ b/tests/test_cli_workflows.py @@ -0,0 +1,211 @@ +# -*- coding: utf-8 -*- +# +# This file is part of REANA. +# Copyright (C) 2017, 2018 CERN. +# +# REANA is free software; you can redistribute it and/or modify it +# under the terms of the MIT License; see LICENSE file for more details. + +"""REANA client workflow tests.""" + +import json + +from click.testing import CliRunner + +from reana_client.cli import Config, cli + + +def test_workflows_server_not_connected(): + """Test workflows command when server is not connected.""" + runner = CliRunner() + result = runner.invoke(cli, ['workflows']) + message = 'REANA client is not connected to any REANA cluster.' + assert message in result.output + assert result.exit_code == 1 + + +def test_workflows_no_token(): + """Test workflows command when token is not set.""" + env = {'REANA_SERVER_URL': 'localhost'} + runner = CliRunner(env=env) + result = runner.invoke(cli, ['workflows']) + message = 'Please provide your access token' + assert result.exit_code == 1 + assert message in result.output + + +def test_workflows_server_ok(mock_base_api_client): + """Test workflows command when server is reachable.""" + response = [ + { + "status": "running", + "created": "2018-06-13T09:47:35.66097", + "user": "00000000-0000-0000-0000-000000000000", + "name": "mytest.1", + "id": "256b25f4-4cfb-4684-b7a8-73872ef455a1", + } + ] + status_code = 200 + mocked_api_client = mock_base_api_client(status_code, + response, + 'reana-server') + config = Config(mocked_api_client) + env = {'REANA_SERVER_URL': 'localhost', 'REANA_WORKON': 'mytest.1'} + reana_token = '000000' + runner = CliRunner(env=env) + result = runner.invoke(cli, ['workflows', '-at', reana_token], obj=config) + message = 'RUN_NUMBER' + assert result.exit_code == 0 + assert message in result.output + + +def test_workflows_valid_json(mock_base_api_client): + """Test workflows command with --json and -v flags.""" + response = [ + { + "status": "running", + "created": "2018-06-13T09:47:35.66097", + "user": "00000000-0000-0000-0000-000000000000", + "name": "mytest.1", + "id": "256b25f4-4cfb-4684-b7a8-73872ef455a1", + } + ] + status_code = 200 + mocked_api_client = mock_base_api_client(status_code, + response, + 'reana-server') + config = Config(mocked_api_client) + env = {'REANA_SERVER_URL': 'localhost'} + reana_token = '000000' + runner = CliRunner(env=env) + result = runner.invoke(cli, + ['workflows', '-v', '-at', reana_token, '--json'], + obj=config) + json_response = json.loads(result.output) + assert result.exit_code == 0 + assert isinstance(json_response, list) + assert len(json_response) == 1 + assert 'name' in json_response[0] + assert 'run_number' in json_response[0] + assert 'created' in json_response[0] + assert 'status' in json_response[0] + assert 'id' in json_response[0] + assert 'user' in json_response[0] + + +def test_workflow_create_failed(): + """Test workflow create when creation fails.""" + runner = CliRunner() + result = runner.invoke(cli, ['create']) + message = 'Error: Invalid value for "-f"' + assert message in result.output + assert result.exit_code == 2 + + +def test_workflow_create_successful(mock_base_api_client, + create_yaml_workflow_schema): + """Test workflow create when creation is successfull.""" + status_code = 201 + response = { + "message": "The workflow has been successfully created.", + "workflow_id": "cdcf48b1-c2f3-4693-8230-b066e088c6ac", + "workflow_name": "mytest.1" + } + env = {'REANA_SERVER_URL': 'localhost'} + reana_token = '000000' + mocked_api_client = mock_base_api_client(status_code, + response, + 'reana-server') + config = Config(mocked_api_client) + runner = CliRunner(env=env) + with runner.isolated_filesystem(): + with open('reana.yaml', 'w') as f: + f.write(create_yaml_workflow_schema) + result = runner.invoke( + cli, + ['create', '-at', reana_token, '--skip-validation'], + obj=config + ) + assert result.exit_code == 0 + assert response["workflow_name"] in result.output + + +def test_workflow_start_successful(mock_base_api_client): + """Test workflow start when creation is successfull.""" + response = { + "status": "created", + "message": "Workflow successfully launched", + "id": "256b25f4-4cfb-4684-b7a8-73872ef455a1", + "workflow_name": "mytest.1", + "user": "00000000-0000-0000-0000-000000000000" + } + status_code = 200 + mocked_api_client = mock_base_api_client(status_code, + response, + 'reana-server') + config = Config(mocked_api_client) + env = {'REANA_SERVER_URL': 'localhost'} + reana_token = '000000' + message = 'mytest.1 has been started' + runner = CliRunner(env=env) + result = runner.invoke( + cli, + ['start', '-at', reana_token, '-w', response["workflow_name"]], + obj=config + ) + assert result.exit_code == 0 + assert message in result.output + + +def test_workflows_validate(create_yaml_workflow_schema): + """Test validation of REANA specifications file.""" + message = "is a valid REANA specification file" + runner = CliRunner() + with runner.isolated_filesystem(): + with open('reana.yaml', 'w') as f: + f.write(create_yaml_workflow_schema) + result = runner.invoke( + cli, + ['validate', '--file', 'reana.yaml'], + ) + assert result.exit_code == 0 + assert message in result.output + + +def test_get_workflow_status_ok(mock_base_api_client): + """Test workflow status.""" + status_code = 200 + response = { + 'created': '2018-10-29T12:50:12', + 'id': '4e576cf9-a946-4346-9cde-7712f8dcbb3f', + 'logs': '', + 'name': 'workflow.5', + 'progress': { + 'current_command': None, + 'current_step_name': None, + 'failed': {'job_ids': [], 'total': 0}, + 'finished': {'job_ids': [], 'total': 0}, + 'run_started_at': '2018-10-29T12:51:04', + 'running': {'job_ids': [], 'total': 0}, + 'total': {'job_ids': [], 'total': 1} + }, + 'status': 'running', + 'user': '00000000-0000-0000-0000-000000000000' + } + mocked_api_client = mock_base_api_client(status_code, + response, + 'reana-server') + config = Config(mocked_api_client) + env = {'REANA_SERVER_URL': 'localhost'} + reana_token = '000000' + runner = CliRunner(env=env) + result = runner.invoke( + cli, + ['status', '-at', reana_token, '--json', '-v', '-w', response['name']], + obj=config + ) + json_response = json.loads(result.output) + assert result.exit_code == 0 + assert isinstance(json_response, list) + assert len(json_response) == 1 + assert json_response[0]['name'] in response['name']