diff --git a/heat/common/timeutils.py b/heat/common/timeutils.py index e31e9be04b..eca6291b05 100644 --- a/heat/common/timeutils.py +++ b/heat/common/timeutils.py @@ -15,6 +15,7 @@ Utilities for handling ISO 8601 duration format. """ +import datetime import random import re import time @@ -80,3 +81,12 @@ def retry_backoff_delay(attempt, scale_factor=1.0, jitter_max=0.0): if jitter_max == 0.0: return exp return exp + random.random() * jitter_max + + +def round_to_seconds(dt): + """Round a datetime to the nearest second.""" + rounding = 0 + if dt.microsecond >= 500000: + rounding = 1 + return dt + datetime.timedelta(0, rounding, + -dt.microsecond) diff --git a/heat/engine/stack.py b/heat/engine/stack.py index e9d3c36601..a2c35acc32 100644 --- a/heat/engine/stack.py +++ b/heat/engine/stack.py @@ -33,6 +33,7 @@ from heat.common.i18n import _LI from heat.common.i18n import _LW from heat.common import identifier from heat.common import lifecycle_plugin_utils +from heat.common import timeutils from heat.engine import dependencies from heat.engine import environment from heat.engine import event @@ -1688,8 +1689,10 @@ class Stack(collections.Mapping): ''' Time elapsed in seconds since the stack operation started. ''' - start_time = self.updated_time or self.created_time - return (datetime.datetime.utcnow() - start_time).seconds + start_time = timeutils.round_to_seconds(self.updated_time or + self.created_time) + nowish = timeutils.round_to_seconds(datetime.datetime.utcnow()) + return (nowish - start_time).seconds def time_remaining(self): ''' diff --git a/heat/tests/test_stack.py b/heat/tests/test_stack.py index eee61e20dd..b8aceaf31d 100644 --- a/heat/tests/test_stack.py +++ b/heat/tests/test_stack.py @@ -111,6 +111,17 @@ class StackTest(common.HeatTestCase): 10, 10, 0) self.assertEqual(600, self.stack.time_elapsed()) + @mock.patch.object(stack, 'datetime') + def test_time_elapsed_ms(self, mock_dt): + self.stack = stack.Stack(self.ctx, 'test_stack', self.tmpl) + # dummy create time 10:00:00 + self.stack.created_time = datetime.datetime(2015, 7, 27, 10, 5, 0) + # mock utcnow set to microsecond offset + mock_dt.datetime.utcnow.return_value = datetime.datetime(2015, 7, 27, + 10, 4, 59, + 750000) + self.assertEqual(0, self.stack.time_elapsed()) + @mock.patch.object(stack, 'datetime') def test_time_elapsed_with_updated_time(self, mock_dt): self.stack = stack.Stack(self.ctx, 'test_stack', self.tmpl) diff --git a/heat/tests/test_timeutils.py b/heat/tests/test_timeutils.py index 567a502e8d..28ac125c24 100644 --- a/heat/tests/test_timeutils.py +++ b/heat/tests/test_timeutils.py @@ -11,6 +11,7 @@ # License for the specific language governing permissions and limitations # under the License. +import datetime from testtools import matchers from heat.common import timeutils as util @@ -157,3 +158,17 @@ class RetryBackoffJitterTest(common.HeatTestCase): self.attempt, self.scale_factor, self.jitter_max) self.assertThat(delay, matchers.GreaterThan(self.delay_from)) self.assertThat(delay, matchers.LessThan(self.delay_to)) + + +class RoundToSecondsTest(common.HeatTestCase): + + scenarios = [('down', dict(in_secs=5, in_ms=12345, out_secs=5)), + ('up', dict(in_secs=5, in_ms=501000, out_secs=6)), + ('same', dict(in_secs=5, in_ms=0, out_secs=5))] + + def test_rounding(self): + inp = datetime.datetime(2015, 7, 27, 4, + 34, self.in_secs, self.in_ms) + exp = datetime.datetime(2015, 7, 27, 4, + 34, self.out_secs, 0) + self.assertEqual(exp, util.round_to_seconds(inp))