From e11b4589460b6be9ff9ca1f5f70a1d0890fa7bb3 Mon Sep 17 00:00:00 2001 From: David J Peacock Date: Wed, 9 Jun 2021 10:49:21 -0400 Subject: [PATCH] add spinner to highlight progress Implementing a Spinner class and utilizing it around a run action. This will add a moving icon that animates in place until output is reached, letting the operator know that things are happening. The interactive TTY test is included to ensure that during some unknown usage such as logging to disk this feature doesn't spew unnecessary clutter. Change-Id: Ieef256a5c12b238008e9250c0ee182d80a2b6dfb --- validations_libs/cli/common.py | 38 +++++++++++ .../tests/test_validation_actions.py | 52 +++++++++++++++ validations_libs/validation_actions.py | 65 +++++++++++++------ 3 files changed, 135 insertions(+), 20 deletions(-) diff --git a/validations_libs/cli/common.py b/validations_libs/cli/common.py index 5701729c..889c6ba9 100644 --- a/validations_libs/cli/common.py +++ b/validations_libs/cli/common.py @@ -18,6 +18,9 @@ import json import logging from prettytable import PrettyTable import re +import sys +import time +import threading import yaml try: @@ -110,3 +113,38 @@ def read_extra_vars_file(extra_vars_file): "The extra_vars file must be properly formatted YAML/JSON." "Details: {}.").format(error) raise RuntimeError(error_msg) + + +class Spinner(object): + """Animated spinner to indicate activity during processing""" + busy = False + delay = 0.1 + + @staticmethod + def spinning_cursor(): + while 1: + for cursor in '|/-\\': + yield cursor + + def __init__(self, delay=None): + self.spinner_generator = self.spinning_cursor() + if delay and float(delay): + self.delay = delay + + def spinner_task(self): + while self.busy: + sys.stdout.write(next(self.spinner_generator)) + sys.stdout.flush() + time.sleep(self.delay) + sys.stdout.write('\b') + sys.stdout.flush() + + def __enter__(self): + self.busy = True + threading.Thread(target=self.spinner_task).start() + + def __exit__(self, exception, value, tb): + self.busy = False + time.sleep(self.delay) + if exception is not None: + return False diff --git a/validations_libs/tests/test_validation_actions.py b/validations_libs/tests/test_validation_actions.py index 49604d73..22d9f801 100644 --- a/validations_libs/tests/test_validation_actions.py +++ b/validations_libs/tests/test_validation_actions.py @@ -273,6 +273,58 @@ class TestValidationActions(TestCase): validations_dir='/tmp/foo') self.assertEqual(run_return, expected_run_return) + @mock.patch('validations_libs.ansible.Ansible._playbook_check', + side_effect=RuntimeError) + @mock.patch('validations_libs.utils.os.makedirs') + @mock.patch('validations_libs.utils.os.access', return_value=True) + @mock.patch('validations_libs.utils.os.path.exists', return_value=True) + @mock.patch('validations_libs.utils.parse_all_validations_on_disk') + def test_spinner_exception_failure_condition(self, mock_validation_dir, + mock_exists, mock_access, + mock_makedirs, + mock_playbook_check): + + mock_validation_dir.return_value = [{ + 'description': 'My Validation One Description', + 'groups': ['prep', 'pre-deployment'], + 'id': 'foo', + 'name': 'My Validition One Name', + 'parameters': {}}] + playbook = ['fake.yaml'] + inventory = 'tmp/inventory.yaml' + + run = ValidationActions() + + self.assertRaises(RuntimeError, run.run_validations, playbook, + inventory, group=fakes.GROUPS_LIST, + validations_dir='/tmp/foo') + + @mock.patch('validations_libs.ansible.Ansible._playbook_check', + side_effect=RuntimeError) + @mock.patch('validations_libs.utils.os.makedirs') + @mock.patch('validations_libs.utils.os.access', return_value=True) + @mock.patch('validations_libs.utils.os.path.exists', return_value=True) + @mock.patch('validations_libs.utils.parse_all_validations_on_disk') + @mock.patch('sys.__stdin__.isatty', return_value=True) + def test_spinner_forced_run(self, mock_stdin_isatty, mock_validation_dir, + mock_exists, mock_access, mock_makedirs, + mock_playbook_check): + + mock_validation_dir.return_value = [{ + 'description': 'My Validation One Description', + 'groups': ['prep', 'pre-deployment'], + 'id': 'foo', + 'name': 'My Validition One Name', + 'parameters': {}}] + playbook = ['fake.yaml'] + inventory = 'tmp/inventory.yaml' + + run = ValidationActions() + + self.assertRaises(RuntimeError, run.run_validations, playbook, + inventory, group=fakes.GROUPS_LIST, + validations_dir='/tmp/foo') + @mock.patch('validations_libs.utils.get_validations_playbook', return_value=[]) def test_validation_run_no_validation(self, mock_get_val): diff --git a/validations_libs/validation_actions.py b/validations_libs/validation_actions.py index c13e70ee..7f3836d1 100644 --- a/validations_libs/validation_actions.py +++ b/validations_libs/validation_actions.py @@ -14,11 +14,13 @@ # import logging import os +import sys import json import yaml from validations_libs.ansible import Ansible as v_ansible from validations_libs.group import Group +from validations_libs.cli.common import Spinner from validations_libs.validation_logs import ValidationLogs, ValidationLog from validations_libs import constants from validations_libs import utils as v_utils @@ -374,26 +376,49 @@ class ValidationActions(object): validation_uuid, artifacts_dir = v_utils.create_artifacts_dir( log_path=log_path, prefix=os.path.basename(playbook)) run_ansible = v_ansible(validation_uuid) - _playbook, _rc, _status = run_ansible.run( - workdir=artifacts_dir, - playbook=playbook, - base_dir=base_dir, - playbook_dir=validations_dir, - parallel_run=True, - inventory=inventory, - output_callback=output_callback, - callback_whitelist=callback_whitelist, - quiet=quiet, - extra_vars=extra_vars, - limit_hosts=_hosts, - extra_env_variables=extra_env_vars, - ansible_cfg=ansible_cfg, - gathering_policy='explicit', - ansible_artifact_path=artifacts_dir, - log_path=log_path, - run_async=run_async, - python_interpreter=python_interpreter, - ssh_user=ssh_user) + if sys.__stdin__.isatty(): + with Spinner(): + _playbook, _rc, _status = run_ansible.run( + workdir=artifacts_dir, + playbook=playbook, + base_dir=base_dir, + playbook_dir=validations_dir, + parallel_run=True, + inventory=inventory, + output_callback=output_callback, + callback_whitelist=callback_whitelist, + quiet=quiet, + extra_vars=extra_vars, + limit_hosts=_hosts, + extra_env_variables=extra_env_vars, + ansible_cfg=ansible_cfg, + gathering_policy='explicit', + ansible_artifact_path=artifacts_dir, + log_path=log_path, + run_async=run_async, + python_interpreter=python_interpreter, + ssh_user=ssh_user) + else: + _playbook, _rc, _status = run_ansible.run( + workdir=artifacts_dir, + playbook=playbook, + base_dir=base_dir, + playbook_dir=validations_dir, + parallel_run=True, + inventory=inventory, + output_callback=output_callback, + callback_whitelist=callback_whitelist, + quiet=quiet, + extra_vars=extra_vars, + limit_hosts=_hosts, + extra_env_variables=extra_env_vars, + ansible_cfg=ansible_cfg, + gathering_policy='explicit', + ansible_artifact_path=artifacts_dir, + log_path=log_path, + run_async=run_async, + python_interpreter=python_interpreter, + ssh_user=ssh_user) results.append({'playbook': _playbook, 'rc_code': _rc, 'status': _status,