diff --git a/mistralclient/api/v2/executions.py b/mistralclient/api/v2/executions.py index 6ea9563d..c3e9dc1e 100644 --- a/mistralclient/api/v2/executions.py +++ b/mistralclient/api/v2/executions.py @@ -1,5 +1,6 @@ # Copyright 2014 - Mirantis, Inc. # Copyright 2015 - StackStorm, Inc. +# Copyright 2020 - Nokia Software. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/mistralclient/commands/v2/base.py b/mistralclient/commands/v2/base.py index 48428375..a27fd26a 100644 --- a/mistralclient/commands/v2/base.py +++ b/mistralclient/commands/v2/base.py @@ -1,5 +1,5 @@ # Copyright 2014 - Mirantis, Inc. -# All Rights Reserved +# Copyright 2020 - Nokia Software. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain @@ -15,6 +15,7 @@ # import abc +import datetime as dt import textwrap from osc_lib.command import command @@ -30,7 +31,12 @@ class MistralFormatter(object): @classmethod def fields(cls): - return [c[0] for c in cls.COLUMNS] + # Column should be a tuple: + # (, , ) + # If the 3rd value is specified and it's True then + # the field is synthetic (calculated) and should not be requested + # from the API client. + return [c[0] for c in cls.COLUMNS if len(c) == 2 or not c[2]] @classmethod def headings(cls): @@ -188,3 +194,34 @@ def get_filters(parsed_args): filters[arr[0]] = arr[1] return filters + + +def get_duration_str(start_dt_str, end_dt_str): + """Builds a human friendly duration string. + + :param start_dt_str: Start date time as an ISO string. Must not be empty. + :param end_dt_str: End date time as an ISO string. If empty, duration is + calculated from the current time. + :return: Duration(delta) string. + """ + start_dt = dt.datetime.strptime(start_dt_str, '%Y-%m-%d %H:%M:%S') + + if end_dt_str: + end_dt = dt.datetime.strptime(end_dt_str, '%Y-%m-%d %H:%M:%S') + + return str(end_dt - start_dt) + + delta_from_now = dt.datetime.utcnow() - start_dt + + # If delta is too small then we won't show any value. It means that + # the corresponding process (e.g. an execution) just started. + if delta_from_now < dt.timedelta(seconds=2): + return '...' + + # Drop microseconds to decrease verbosity. + delta = ( + delta_from_now + - dt.timedelta(microseconds=delta_from_now.microseconds) + ) + + return "{}...".format(delta) diff --git a/mistralclient/commands/v2/executions.py b/mistralclient/commands/v2/executions.py index d9c1f190..c012d62b 100644 --- a/mistralclient/commands/v2/executions.py +++ b/mistralclient/commands/v2/executions.py @@ -44,28 +44,35 @@ class ExecutionFormatter(base.MistralFormatter): ('state_info', 'State info'), ('created_at', 'Created at'), ('updated_at', 'Updated at'), + ('duration', 'Duration', True), ] @staticmethod - def format(execution=None, lister=False): - # TODO(nmakhotkin) Add parent task id when it's implemented in API. + def format(wf_ex=None, lister=False): + if wf_ex: + state_info = ( + wf_ex.state_info if not lister + else base.cut(wf_ex.state_info) + ) - if execution: - state_info = (execution.state_info if not lister - else base.cut(execution.state_info)) + duration = base.get_duration_str( + wf_ex.created_at, + wf_ex.updated_at if wf_ex.state in ['ERROR', 'SUCCESS'] else '' + ) data = ( - execution.id, - execution.workflow_id, - execution.workflow_name, - execution.workflow_namespace, - execution.description, - execution.task_execution_id or '', - execution.root_execution_id or '', - execution.state, + wf_ex.id, + wf_ex.workflow_id, + wf_ex.workflow_name, + wf_ex.workflow_namespace, + wf_ex.description, + wf_ex.task_execution_id or '', + wf_ex.root_execution_id or '', + wf_ex.state, state_info, - execution.created_at, - execution.updated_at or '' + wf_ex.created_at, + wf_ex.updated_at or '', + duration ) else: data = (tuple('' for _ in diff --git a/mistralclient/tests/unit/base.py b/mistralclient/tests/unit/base.py index b4f734ae..4fdb3dc6 100644 --- a/mistralclient/tests/unit/base.py +++ b/mistralclient/tests/unit/base.py @@ -22,12 +22,14 @@ class BaseClientTest(base.BaseTestCase): def setUp(self): super(BaseClientTest, self).setUp() + self.requests_mock = self.useFixture(fixture.Fixture()) class BaseCommandTest(base.BaseTestCase): def setUp(self): super(BaseCommandTest, self).setUp() + self.app = mock.Mock() self.client = self.app.client_manager.workflow_engine diff --git a/mistralclient/tests/unit/v2/test_cli_executions.py b/mistralclient/tests/unit/v2/test_cli_executions.py index 869d2b14..9af5c237 100644 --- a/mistralclient/tests/unit/v2/test_cli_executions.py +++ b/mistralclient/tests/unit/v2/test_cli_executions.py @@ -1,8 +1,7 @@ # Copyright 2014 - Mirantis, Inc. # Copyright 2015 - StackStorm, Inc. # Copyright 2016 - Brocade Communications Systems, Inc. -# -# All Rights Reserved +# Copyright 2020 - Nokia Software. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain @@ -19,8 +18,6 @@ import mock import pkg_resources as pkg -import six -import sys from oslo_serialization import jsonutils @@ -35,10 +32,10 @@ EXEC_DICT = { 'workflow_namespace': '', 'root_execution_id': '', 'description': '', - 'state': 'RUNNING', + 'state': 'SUCCESS', 'state_info': None, - 'created_at': '1', - 'updated_at': '1', + 'created_at': '2020-02-07 08:10:32', + 'updated_at': '2020-02-07 08:10:41', 'task_execution_id': None } @@ -55,8 +52,8 @@ SUB_WF_EXEC = executions.Execution( 'description': '', 'state': 'ERROR', 'state_info': None, - 'created_at': '1', - 'updated_at': '1', + 'created_at': '2020-02-07 08:10:32', + 'updated_at': '2020-02-07 08:10:41', 'task_execution_id': 'abc' } ) @@ -69,10 +66,11 @@ EX_RESULT = ( '', '', '', - 'RUNNING', + 'SUCCESS', None, - '1', - '1' + '2020-02-07 08:10:32', + '2020-02-07 08:10:41', + '0:00:09' ) SUB_WF_EX_RESULT = ( @@ -85,8 +83,9 @@ SUB_WF_EX_RESULT = ( 'ROOT_EXECUTION_ID', 'ERROR', None, - '1', - '1' + '2020-02-07 08:10:32', + '2020-02-07 08:10:41', + '0:00:09' ) EXECS_LIST = [EXEC, SUB_WF_EXEC] @@ -98,24 +97,12 @@ EXEC_WITH_PUBLISHED = executions.Execution(mock, EXEC_WITH_PUBLISHED_DICT) class TestCLIExecutionsV2(base.BaseCommandTest): - - stdout = six.moves.StringIO() - stderr = six.moves.StringIO() - def setUp(self): super(TestCLIExecutionsV2, self).setUp() - # Redirect stdout and stderr so it doesn't pollute the test result. - sys.stdout = self.stdout - sys.stderr = self.stderr - def tearDown(self): super(TestCLIExecutionsV2, self).tearDown() - # Reset to original stdout and stderr. - sys.stdout = sys.__stdout__ - sys.stderr = sys.__stderr__ - def test_create_wf_input_string(self): self.client.executions.create.return_value = EXEC @@ -124,10 +111,7 @@ class TestCLIExecutionsV2(base.BaseCommandTest): app_args=['id', '{ "context": true }'] ) - self.assertEqual( - EX_RESULT, - result[1] - ) + self.assertEqual(EX_RESULT, result[1]) def test_create_wf_input_file(self): self.client.executions.create.return_value = EXEC @@ -142,10 +126,7 @@ class TestCLIExecutionsV2(base.BaseCommandTest): app_args=['id', path] ) - self.assertEqual( - EX_RESULT, - result[1] - ) + self.assertEqual(EX_RESULT, result[1]) def test_create_with_description(self): self.client.executions.create.return_value = EXEC @@ -155,10 +136,7 @@ class TestCLIExecutionsV2(base.BaseCommandTest): app_args=['id', '{ "context": true }', '-d', ''] ) - self.assertEqual( - EX_RESULT, - result[1] - ) + self.assertEqual(EX_RESULT, result[1]) def test_update_state(self): states = ['RUNNING', 'SUCCESS', 'PAUSED', 'ERROR', 'CANCELLED'] @@ -175,14 +153,18 @@ class TestCLIExecutionsV2(base.BaseCommandTest): 'description': '', 'state': state, 'state_info': None, - 'created_at': '1', - 'updated_at': '1', + 'created_at': '2020-02-07 08:10:32', + 'updated_at': '2020-02-07 08:10:41', 'task_execution_id': None } ) ex_result = list(EX_RESULT) ex_result[7] = state + + # We'll ignore "duration" since for not terminal states + # it is unpredictable. + del ex_result[11] ex_result = tuple(ex_result) result = self.call( @@ -190,10 +172,11 @@ class TestCLIExecutionsV2(base.BaseCommandTest): app_args=['id', '-s', state] ) - self.assertEqual( - ex_result, - result[1] - ) + result_ex = list(result[1]) + del result_ex[11] + result_ex = tuple(result_ex) + + self.assertEqual(ex_result, result_ex) def test_update_invalid_state(self): states = ['IDLE', 'WAITING', 'DELAYED'] @@ -214,10 +197,7 @@ class TestCLIExecutionsV2(base.BaseCommandTest): app_args=['id', '-s', 'RUNNING', '--env', '{"k1": "foobar"}'] ) - self.assertEqual( - EX_RESULT, - result[1] - ) + self.assertEqual(EX_RESULT, result[1]) def test_update_description(self): self.client.executions.update.return_value = EXEC @@ -227,10 +207,7 @@ class TestCLIExecutionsV2(base.BaseCommandTest): app_args=['id', '-d', 'foobar'] ) - self.assertEqual( - EX_RESULT, - result[1] - ) + self.assertEqual(EX_RESULT, result[1]) def test_list(self): self.client.executions.list.return_value = [SUB_WF_EXEC, EXEC] @@ -353,20 +330,14 @@ class TestCLIExecutionsV2(base.BaseCommandTest): result = self.call(execution_cmd.Get, app_args=['id']) - self.assertEqual( - EX_RESULT, - result[1] - ) + self.assertEqual(EX_RESULT, result[1]) def test_get_sub_wf_ex(self): self.client.executions.get.return_value = SUB_WF_EXEC result = self.call(execution_cmd.Get, app_args=['id']) - self.assertEqual( - SUB_WF_EX_RESULT, - result[1] - ) + self.assertEqual(SUB_WF_EX_RESULT, result[1]) def test_delete(self): self.call(execution_cmd.Delete, app_args=['id']) diff --git a/mistralclient/tests/unit/v2/test_cli_tasks.py b/mistralclient/tests/unit/v2/test_cli_tasks.py index 91bc2f69..6b5aaef4 100644 --- a/mistralclient/tests/unit/v2/test_cli_tasks.py +++ b/mistralclient/tests/unit/v2/test_cli_tasks.py @@ -47,8 +47,8 @@ TASK_SUB_WF_EXEC = Execution( 'description': '', 'state': 'ERROR', 'state_info': None, - 'created_at': '1', - 'updated_at': '1', + 'created_at': '2020-02-07 08:10:32', + 'updated_at': '2020-02-07 08:10:41', 'task_execution_id': '123' } ) @@ -63,8 +63,9 @@ TASK_SUB_WF_EX_RESULT = ( 'ROOT_EXECUTION_ID', 'ERROR', None, - '1', - '1' + '2020-02-07 08:10:32', + '2020-02-07 08:10:41', + '0:00:09' ) TASK_RESULT = {"test": "is", "passed": "successfully"}