From 6570a28a4314a14e8a569a8547da66bb674045a4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Armando=20Garc=C3=ADa=20Sancio?= Date: Thu, 29 Jan 2015 21:34:13 +0000 Subject: [PATCH] Add integration tests for the dcos-marathon cli --- bin/start_integration_test.sh | 3 +- dcos/api/marathon.py | 14 +- dcos/cli/marathon/main.py | 31 ++-- integrations/cli/common.py | 15 +- integrations/cli/test_config.py | 46 ++--- integrations/cli/test_dcos.py | 35 ++-- integrations/cli/test_help.py | 20 +-- integrations/cli/test_marathon.py | 168 +++++++++++++++++++ integrations/cli/test_package.py | 20 +-- tests/data/Dcos.toml | 11 +- tests/data/marathon/zero_instance_sleep.json | 11 ++ 11 files changed, 273 insertions(+), 101 deletions(-) create mode 100644 integrations/cli/test_marathon.py create mode 100644 tests/data/marathon/zero_instance_sleep.json diff --git a/bin/start_integration_test.sh b/bin/start_integration_test.sh index 1cafd42..4ef856d 100755 --- a/bin/start_integration_test.sh +++ b/bin/start_integration_test.sh @@ -13,4 +13,5 @@ sleep 2 # Run the tox integration tests -tox -c /dcos-cli/tox.ini +cd /dcos-cli +make clean all diff --git a/dcos/api/marathon.py b/dcos/api/marathon.py index 30871c6..6db5cbc 100644 --- a/dcos/api/marathon.py +++ b/dcos/api/marathon.py @@ -1,9 +1,14 @@ import json -import urllib +import logging import requests from dcos.api import errors +try: + from urllib import urlencode +except ImportError: + from urllib.parse import urlencode + class Client(object): """Class for talking to the Marathon server. """ @@ -38,7 +43,7 @@ class Client(object): path=path) if query_params is not None: - query_string = urllib.urlencode(query_params) + query_string = urlencode(query_params) url = (url + '?{}').format(query_string) return url @@ -64,6 +69,11 @@ class Client(object): :rtype: Error """ + message = response.json().get('message') + if message is None: + logging.error('Missing message from json: %s', response.json()) + return Error('Unknown error from Marathon') + return Error('Error: {}'.format(response.json()['message'])) def get_app(self, app_id): diff --git a/dcos/cli/marathon/main.py b/dcos/cli/marathon/main.py index 8820635..42624dd 100644 --- a/dcos/cli/marathon/main.py +++ b/dcos/cli/marathon/main.py @@ -1,24 +1,26 @@ """ Usage: + dcos marathon describe [--json] dcos marathon info dcos marathon list - dcos marathon describe + dcos marathon remove [--force] + dcos marathon scale [--force] dcos marathon start - dcos marathon scale [--force] - dcos marathon suspend [--force] - dcos marathon remove [--force] - dcos marathon --help - dcos marathon --version + dcos marathon suspend [--force] Options: -h, --help Show this screen --version Show version + --force This flag disable checks in Marathon during update + operations. + --json Outputs JSON format instead of default (TOML) format """ import json import os import docopt +import toml from dcos.api import config, constants, marathon, options @@ -35,7 +37,7 @@ def main(): return _list(toml_config) elif args['marathon'] and args['describe']: toml_config = config.load_from_path(config_path) - return _describe(args[''], toml_config) + return _describe(args[''], args['--json'], toml_config) elif args['marathon'] and args['start']: toml_config = config.load_from_path(config_path) return _start(args[''], toml_config) @@ -94,7 +96,7 @@ def _list(config): return 1 if not apps: - print("No apps to list.") + print("No applications to list.") for app in apps: print(app['id']) @@ -102,11 +104,13 @@ def _list(config): return 0 -def _describe(app_id, config): - """Show details of a Marathon applications. +def _describe(app_id, is_json, config): + """Show details of a Marathon application. :param app_id: ID of the app to suspend :type app_id: str + :param is_json: Whether to print in JSON format or TOML + :type is_json: bool :param config: Configuration dictionary :type config: config.Toml :returns: Process status @@ -119,9 +123,10 @@ def _describe(app_id, config): print(err.error()) return 1 - print(json.dumps(app, - sort_keys=True, - indent=2)) + if is_json: + print(json.dumps(app, sort_keys=True, indent=2)) + else: + print(toml.dumps(app)) return 0 diff --git a/integrations/cli/common.py b/integrations/cli/common.py index 05d93e5..f64cdbf 100644 --- a/integrations/cli/common.py +++ b/integrations/cli/common.py @@ -1,3 +1,4 @@ +import logging import subprocess @@ -8,12 +9,20 @@ def exec_command(cmd, env=None): :type cmd: list of str :param env: Environment variables :type env: dict of str to str - :returns: Object to the running process - :rtype: subprocess.Popen + :returns: A tuple with the returncode, stdout and stderr + :rtype: (int, bytes, bytes) """ - return subprocess.Popen( + process = subprocess.Popen( cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, env=env) + + stdout, stderr = process.communicate() + + # We should always print the stdout and stderr + logging.error('STDOUT: %s', stdout.decode('utf-8')) + logging.error('STDERR: %s', stderr.decode('utf-8')) + + return (process.returncode, stdout, stderr) diff --git a/integrations/cli/test_config.py b/integrations/cli/test_config.py index 79087de..533286c 100644 --- a/integrations/cli/test_config.py +++ b/integrations/cli/test_config.py @@ -2,10 +2,9 @@ from common import exec_command def test_help(): - process = exec_command(['dcos', 'config', '--help']) - stdout, stderr = process.communicate() + returncode, stdout, stderr = exec_command(['dcos', 'config', '--help']) - assert process.returncode == 0 + assert returncode == 0 assert stdout == b"""Usage: dcos config info dcos config [] @@ -21,29 +20,25 @@ Options: def test_info(): - process = exec_command(['dcos', 'config', 'info']) - stdout, stderr = process.communicate() + returncode, stdout, stderr = exec_command(['dcos', 'config', 'info']) - assert process.returncode == 0 + assert returncode == 0 assert stdout == b'Get and set DCOS command line options\n' assert stderr == b'' def test_version(): - process = exec_command(['dcos', 'config', '--version']) - stdout, stderr = process.communicate() + returncode, stdout, stderr = exec_command(['dcos', 'config', '--version']) - assert process.returncode == 0 + assert returncode == 0 assert stdout == b'dcos-config version 0.1.0\n' assert stderr == b'' def test_list_property(): - process = exec_command(['dcos', 'config', '--list']) + returncode, stdout, stderr = exec_command(['dcos', 'config', '--list']) - stdout, stderr = process.communicate() - - assert process.returncode == 0 + assert returncode == 0 assert stdout == b"""marathon.host=localhost marathon.port=8080 package.cache=tmp/cache @@ -81,40 +76,33 @@ def test_set_missing_property(): def _set_value(key, value): - process = exec_command(['dcos', 'config', key, value]) + returncode, stdout, stderr = exec_command(['dcos', 'config', key, value]) - stdout, stderr = process.communicate() - - assert process.returncode == 0 + assert returncode == 0 assert stdout == b'' assert stderr == b'' def _get_value(key, value): - process = exec_command(['dcos', 'config', key]) + returncode, stdout, stderr = exec_command(['dcos', 'config', key]) - stdout, stderr = process.communicate() - - assert process.returncode == 0 + assert returncode == 0 assert stdout == '{}\n'.format(value).encode('utf-8') assert stderr == b'' def _unset_value(key): - process = exec_command(['dcos', 'config', '--unset', key]) + returncode, stdout, stderr = exec_command( + ['dcos', 'config', '--unset', key]) - stdout, stderr = process.communicate() - - assert process.returncode == 0 + assert returncode == 0 assert stdout == b'' assert stderr == b'' def _get_missing_value(key): - process = exec_command(['dcos', 'config', key]) + returncode, stdout, stderr = exec_command(['dcos', 'config', key]) - stdout, stderr = process.communicate() - - assert process.returncode == 1 + assert returncode == 1 assert stdout == b'' assert stderr == b'' diff --git a/integrations/cli/test_dcos.py b/integrations/cli/test_dcos.py index 1cb6f28..b758613 100644 --- a/integrations/cli/test_dcos.py +++ b/integrations/cli/test_dcos.py @@ -5,10 +5,9 @@ from dcos.cli.main import _which def test_help(): - process = exec_command(['dcos', '--help']) - stdout, stderr = process.communicate() + returncode, stdout, stderr = exec_command(['dcos', '--help']) - assert process.returncode == 0 + assert returncode == 0 assert stdout == b"""Usage: dcos [...] @@ -23,10 +22,9 @@ read about a specific subcommand. def test_version(): - process = exec_command(['dcos', '--version']) - stdout, stderr = process.communicate() + returncode, stdout, stderr = exec_command(['dcos', '--version']) - assert process.returncode == 0 + assert returncode == 0 assert stdout == b'dcos version 0.1.0\n' assert stderr == b'' @@ -37,10 +35,9 @@ def test_missing_dcos_config(): 'DCOS_PATH': os.environ['DCOS_PATH'], } - process = exec_command(['dcos'], env=env) - stdout, stderr = process.communicate() + returncode, stdout, stderr = exec_command(['dcos'], env=env) - assert process.returncode == 1 + assert returncode == 1 assert stdout == (b"Environment variable 'DCOS_CONFIG' must be set " b"to the DCOS config file.\n") assert stderr == b'' @@ -53,10 +50,9 @@ def test_dcos_config_not_a_file(): 'DCOS_CONFIG': 'missing/file', } - process = exec_command(['dcos'], env=env) - stdout, stderr = process.communicate() + returncode, stdout, stderr = exec_command(['dcos'], env=env) - assert process.returncode == 1 + assert returncode == 1 assert stdout == (b"Environment variable 'DCOS_CONFIG' maps to " b"'missing/file' and it is not a file.\n") assert stderr == b'' @@ -68,10 +64,9 @@ def test_missing_dcos_path(): 'DCOS_CONFIG': os.environ['DCOS_CONFIG'], } - process = exec_command(['dcos'], env=env) - stdout, stderr = process.communicate() + returncode, stdout, stderr = exec_command(['dcos'], env=env) - assert process.returncode == 1 + assert returncode == 1 assert stdout == (b"Environment variable 'DCOS_PATH' not set to the DCOS " b"CLI path.\n") assert stderr == b'' @@ -84,10 +79,9 @@ def test_dcos_path_not_a_dir(): 'DCOS_PATH': 'missing/dir', } - process = exec_command(['dcos'], env=env) - stdout, stderr = process.communicate() + returncode, stdout, stderr = exec_command(['dcos'], env=env) - assert process.returncode == 1 + assert returncode == 1 assert stdout == (b"Environment variable 'DCOS_PATH' maps to " b"'missing/dir' which is not a directory.\n") assert stderr == b'' @@ -99,9 +93,8 @@ def test_missing_path(): 'DCOS_PATH': os.environ['DCOS_PATH'], } - process = exec_command([_which('dcos')], env=env) - stdout, stderr = process.communicate() + returncode, stdout, stderr = exec_command([_which('dcos')], env=env) - assert process.returncode == 1 + assert returncode == 1 assert stdout == b"Environment variable 'PATH' not set.\n" assert stderr == b'' diff --git a/integrations/cli/test_help.py b/integrations/cli/test_help.py index 6d05163..f8a790f 100644 --- a/integrations/cli/test_help.py +++ b/integrations/cli/test_help.py @@ -3,10 +3,9 @@ from common import exec_command def test_help(): - process = exec_command(['dcos', 'help', '--help']) - stdout, stderr = process.communicate() + returncode, stdout, stderr = exec_command(['dcos', 'help', '--help']) - assert process.returncode == 0 + assert returncode == 0 assert stdout == b"""Usage: dcos help info dcos help --all @@ -20,28 +19,25 @@ Options: def test_info(): - process = exec_command(['dcos', 'help', 'info']) - stdout, stderr = process.communicate() + returncode, stdout, stderr = exec_command(['dcos', 'help', 'info']) - assert process.returncode == 0 + assert returncode == 0 assert stdout == b'Display help information about DCOS\n' assert stderr == b'' def test_version(): - process = exec_command(['dcos', 'help', '--version']) - stdout, stderr = process.communicate() + returncode, stdout, stderr = exec_command(['dcos', 'help', '--version']) - assert process.returncode == 0 + assert returncode == 0 assert stdout == b'dcos-help version 0.1.0\n' assert stderr == b'' def test_list_all(): - process = exec_command(['dcos', 'help', '--all']) - stdout, stderr = process.communicate() + returncode, stdout, stderr = exec_command(['dcos', 'help', '--all']) - assert process.returncode == 0 + assert returncode == 0 assert stdout == """Available DCOS command in '{}': \tconfig \tGet and set DCOS command line options diff --git a/integrations/cli/test_marathon.py b/integrations/cli/test_marathon.py new file mode 100644 index 0000000..d31705b --- /dev/null +++ b/integrations/cli/test_marathon.py @@ -0,0 +1,168 @@ +import json +from common import exec_command + + +def test_help(): + returncode, stdout, stderr = exec_command(['dcos', 'marathon', '--help']) + + assert returncode == 0 + assert stdout == b"""Usage: + dcos marathon describe [--json] + dcos marathon info + dcos marathon list + dcos marathon remove [--force] + dcos marathon scale [--force] + dcos marathon start + dcos marathon suspend [--force] + +Options: + -h, --help Show this screen + --version Show version + --force This flag disable checks in Marathon during update + operations. + --json Outputs JSON format instead of default (TOML) format +""" + + +def test_version(): + returncode, stdout, stderr = exec_command( + ['dcos', 'marathon', '--version']) + + assert returncode == 0 + assert stdout == b'dcos-marathon version 0.1.0\n' + assert stderr == b'' + + +def test_info(): + returncode, stdout, stderr = exec_command(['dcos', 'marathon', 'info']) + + assert returncode == 0 + assert stdout == b'Deploy and manage applications on Apache Mesos\n' + assert stderr == b'' + + +def test_empty_list(): + _list_apps() + + +def test_start_app(): + _start_app('tests/data/marathon/sleep.json') + _list_apps('test-app') + _remove_app('test-app') + + +def test_remove_app(): + _start_app('tests/data/marathon/sleep.json') + _remove_app('test-app') + _list_apps() + + +# TODO: Let's improve this once we have a fixed version of toml +def test_describe_app(): + _start_app('tests/data/marathon/sleep.json') + + returncode, stdout, stderr = exec_command( + ['dcos', 'marathon', 'describe', 'test-app']) + + assert returncode == 0 + assert stdout != b'' + assert stderr == b'' + + _remove_app('test-app') + + +def test_describe_app_in_json(): + _start_app('tests/data/marathon/sleep.json') + + returncode, stdout, stderr = exec_command( + ['dcos', 'marathon', 'describe', '--json', 'test-app']) + + result = json.loads(stdout.decode('utf-8')) + + assert returncode == 0 + assert isinstance(result, dict) + assert result['id'] == '/test-app' + assert stderr == b'' + + _remove_app('test-app') + + +def test_scale_app(): + _start_app('tests/data/marathon/zero_instance_sleep.json') + + returncode, stdout, stderr = exec_command( + ['dcos', 'marathon', 'scale', 'zero-instance-app', '2']) + + assert returncode == 0 + assert stdout.decode('utf-8').startswith('Created deployment ') + assert stderr == b'' + + _remove_app('zero-instance-app') + + +def test_force_scale_appp(): + _start_app('tests/data/marathon/sleep.json') + + returncode, stdout, stderr = exec_command( + ['dcos', 'marathon', 'scale', '--force', 'test-app', '2']) + + assert returncode == 0 + assert stdout.decode('utf-8').startswith('Created deployment ') + assert stderr == b'' + + _remove_app('test-app') + + +def test_suspend_app(): + _start_app('tests/data/marathon/zero_instance_sleep.json') + + returncode, stdout, stderr = exec_command( + ['dcos', 'marathon', 'suspend', 'zero-instance-app']) + + assert returncode == 0 + assert stdout.decode('utf-8').startswith('Created deployment ') + assert stderr == b'' + + _remove_app('zero-instance-app') + + +def test_remove_missing_app(): + returncode, stdout, stderr = exec_command( + ['dcos', 'marathon', 'remove', 'missing-id']) + + assert returncode == 1 + assert stdout == b"Error: App '/missing-id' does not exist\n" + assert stderr == b'' + + +def _start_app(file_path): + returncode, stdout, stderr = exec_command( + ['dcos', 'marathon', 'start', file_path]) + + assert returncode == 0 + assert stdout == b'' + assert stderr == b'' + + +def _list_apps(app_id=None): + returncode, stdout, stderr = exec_command(['dcos', 'marathon', 'list']) + + if app_id is None: + result = b'No applications to list.\n' + elif isinstance(app_id, str): + result = '/{}\n'.format(app_id).encode('utf-8') + else: + assert False + + assert returncode == 0 + assert stdout == result + assert stderr == b'' + + +def _remove_app(app_id): + returncode, stdout, stderr = exec_command( + ['dcos', 'marathon', 'remove', app_id]) + + assert returncode == 0 + assert stdout == b'' + assert stderr == b'' diff --git a/integrations/cli/test_package.py b/integrations/cli/test_package.py index 5a0db67..d0b3838 100644 --- a/integrations/cli/test_package.py +++ b/integrations/cli/test_package.py @@ -2,10 +2,9 @@ from common import exec_command def test_package(): - process = exec_command(['dcos', 'package', '--help']) - stdout, stderr = process.communicate() + returncode, stdout, stderr = exec_command(['dcos', 'package', '--help']) - assert process.returncode == 0 + assert returncode == 0 assert stdout == b"""Usage: dcos package configure dcos package info @@ -41,28 +40,25 @@ Configuration: def test_info(): - process = exec_command(['dcos', 'package', 'info']) - stdout, stderr = process.communicate() + returncode, stdout, stderr = exec_command(['dcos', 'package', 'info']) - assert process.returncode == 0 + assert returncode == 0 assert stdout == b'Install and manage DCOS software packages.\n' assert stderr == b'' def test_version(): - process = exec_command(['dcos', 'package', '--version']) - stdout, stderr = process.communicate() + returncode, stdout, stderr = exec_command(['dcos', 'package', '--version']) - assert process.returncode == 0 + assert returncode == 0 assert stdout == b'dcos-package version 0.1.0\n' assert stderr == b'' def test_sources_list(): - process = exec_command(['dcos', 'package', 'sources']) - stdout, stderr = process.communicate() + returncode, stdout, stderr = exec_command(['dcos', 'package', 'sources']) - assert process.returncode == 0 + assert returncode == 0 assert stdout == b"""cc5af1bcaec7323400a95e1c38caf61378f6f081 \ file:///Users/me/test-registry 0c854fa7f2ede3dcc3122bf2b7db160491cf9f33 \ diff --git a/tests/data/Dcos.toml b/tests/data/Dcos.toml index 7eea8d6..45f8481 100644 --- a/tests/data/Dcos.toml +++ b/tests/data/Dcos.toml @@ -1,11 +1,6 @@ [package] -cache = "tmp/cache" -sources = [ - "file:///Users/me/test-registry", - "https://my.org/registry", - "git://github.com/mesosphere/universe.git" -] - +cache = "tmp\/cache" +sources = [ "file:\/\/\/Users\/me\/test-registry", "https:\/\/my.org\/registry", "git:\/\/github.com\/mesosphere\/universe.git",] [marathon] -port = 8080 host = "localhost" +port = 8080 diff --git a/tests/data/marathon/zero_instance_sleep.json b/tests/data/marathon/zero_instance_sleep.json new file mode 100644 index 0000000..9cbcf7c --- /dev/null +++ b/tests/data/marathon/zero_instance_sleep.json @@ -0,0 +1,11 @@ +{ + "id": "zero-instance-app", + "cmd": "sleep 1000", + "cpus": 0.1, + "mem": 16, + "instances": 0, + "labels": { + "PACKAGE_ID": "zero-instance-app", + "PACKAGE_VERSION": "1.2.3" + } +}