diff --git a/dapr/serializers/util.py b/dapr/serializers/util.py index 522ad03d3..deb4f34d2 100644 --- a/dapr/serializers/util.py +++ b/dapr/serializers/util.py @@ -74,4 +74,7 @@ def convert_to_dapr_duration(td: timedelta) -> str: milliseconds, microseconds = divmod(td.microseconds, 1000.0) hours, mins = divmod(total_minutes, 60.0) - return f'{hours:.0f}h{mins:.0f}m{seconds:.0f}s{milliseconds:.0f}ms{microseconds:.0f}μs' + # `seconds` still carries the sub-second fraction, which is also emitted via + # the ms/μs fields below; truncate it so the fraction is not double-counted + # (and so a fraction >= 0.5 is not rounded up into an extra whole second). + return f'{hours:.0f}h{mins:.0f}m{int(seconds)}s{milliseconds:.0f}ms{microseconds:.0f}μs' diff --git a/tests/serializers/test_util.py b/tests/serializers/test_util.py index 25124fdf6..07ce8a2ae 100644 --- a/tests/serializers/test_util.py +++ b/tests/serializers/test_util.py @@ -63,6 +63,13 @@ def test_convert_timedelta_to_dapr_duration(self): ) self.assertEqual(duration, '4h15m40s123ms35μs') + def test_convert_timedelta_to_dapr_duration_subsecond(self): + # A sub-second fraction >= 0.5 must not be rounded up into an extra whole + # second nor double-counted with the ms/μs fields; the round-trip must be exact. + duration = convert_to_dapr_duration(timedelta(milliseconds=1500)) + self.assertEqual(duration, '0h0m1s500ms0μs') + self.assertEqual(convert_from_dapr_duration(duration), timedelta(milliseconds=1500)) + def test_convert_invalid_duration_string(self): TESTSTRING = '4h15m40s123ms35μshello' with self.assertRaises(ValueError) as exeception_context: