Add "duration" to workflow executions printed by CLI commands
* It's convenient to see duration of an execution right away w/o having to calculate it ourselves. * Minor style changes according to the Mistral Coding Guidelines. Change-Id: Ibfe806d1f1fcebb9ca0459c82daded308677de44
This commit is contained in:
parent
5171cdd63a
commit
084b6d57ce
|
@ -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.
|
||||
|
|
|
@ -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:
|
||||
# (<field name>, <field title>, <optional synthetic flag>)
|
||||
# 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)
|
||||
|
|
|
@ -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 '<none>',
|
||||
execution.root_execution_id or '<none>',
|
||||
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 '<none>',
|
||||
wf_ex.root_execution_id or '<none>',
|
||||
wf_ex.state,
|
||||
state_info,
|
||||
execution.created_at,
|
||||
execution.updated_at or '<none>'
|
||||
wf_ex.created_at,
|
||||
wf_ex.updated_at or '<none>',
|
||||
duration
|
||||
)
|
||||
else:
|
||||
data = (tuple('' for _ in
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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 = (
|
|||
'',
|
||||
'<none>',
|
||||
'<none>',
|
||||
'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'])
|
||||
|
|
|
@ -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"}
|
||||
|
|
Loading…
Reference in New Issue