From 8929c9615220cee1455a2e6cd306e68ddff7f456 Mon Sep 17 00:00:00 2001 From: "Md. Amdadul Bari Imad" Date: Mon, 12 Oct 2026 21:18:01 +0000 Subject: [PATCH] Fix convert_to_dapr_duration double-counting sub-second time (#1192) * Fix convert_to_dapr_duration double-counting sub-second time convert_to_dapr_duration() derived the seconds field from divmod(td.total_seconds(), 60), so it still carried the sub-second fraction that is also emitted via the ms/us fields, and formatting it with {:.0f} rounded a fraction >= 0.5 up into an extra whole second. As a result timedelta(milliseconds=1500) serialized to '0h0m2s500ms0us' (2.5s) and did not round-trip. Truncate the whole seconds with int() so the sub-second fraction is only counted once (via ms/us). Add a regression test. Signed-off-by: Md. Amdadul Bari Imad * test: cover 1999ms and 1001ms in dapr duration round-trip Add boundary cases requested in review to test_convert_timedelta_to_dapr_duration_subsecond: 1999ms verifies a fraction just under a second is not rounded up to the next whole second, and 1001ms verifies a tiny fraction is not dropped from the ms field. Signed-off-by: Md. Amdadul Bari Imad --------- Signed-off-by: Md. Amdadul Bari Imad Co-authored-by: Sam (cherry picked from commit 0943287160a2ea25f0b95b6c37b0c02f5f6d46d9) Signed-off-by: dapr-bot --- dapr/serializers/util.py | 5 ++++- tests/serializers/test_util.py | 17 +++++++++++++++++ 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/dapr/serializers/util.py b/dapr/serializers/util.py index e4e5c8569..d6e59c294 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 b09fe78ff..482799a13 100644 --- a/tests/serializers/test_util.py +++ b/tests/serializers/test_util.py @@ -73,6 +73,23 @@ 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)) + + # A fraction just under a second must not round up to the next whole second. + duration = convert_to_dapr_duration(timedelta(milliseconds=1999)) + self.assertEqual(duration, '0h0m1s999ms0μs') + self.assertEqual(convert_from_dapr_duration(duration), timedelta(milliseconds=1999)) + + # A tiny fraction must not be dropped from the ms field. + duration = convert_to_dapr_duration(timedelta(milliseconds=1001)) + self.assertEqual(duration, '0h0m1s1ms0μs') + self.assertEqual(convert_from_dapr_duration(duration), timedelta(milliseconds=1001)) + def test_convert_invalid_duration_string(self): TESTSTRING = '4h15m40s123ms35μshello' with self.assertRaises(ValueError) as exeception_context: