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:
Renat Akhmerov 2020-02-07 15:13:17 +07:00
parent 5171cdd63a
commit 084b6d57ce
6 changed files with 100 additions and 81 deletions

View File

@ -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.

View File

@ -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)

View File

@ -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

View File

@ -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

View File

@ -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'])

View File

@ -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"}