Skip to content

Commit 04ca838

Browse files
SagaAudit Timespan parsing fallback to "g" format in cases where "c" is not enough to stay compatible with previous versions (#4199) (#4203)
* CustomTimeSpanConverterTests * Align the lower range to what I have contributed to .NET runtime * More test cases and grouping * Fallback to parsing with "g" format --------- Co-authored-by: danielmarbach <danielmarbach@users.noreply.github.com> (cherry picked from commit 99fe799)
1 parent 9b7221e commit 04ca838

File tree

2 files changed

+113
-4
lines changed

2 files changed

+113
-4
lines changed
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
namespace ServiceControl.Audit.UnitTests.SagaAudit
2+
{
3+
using NUnit.Framework;
4+
using ServiceControl.SagaAudit;
5+
using JsonSerializer = System.Text.Json.JsonSerializer;
6+
7+
[TestFixture]
8+
public class CustomTimeSpanConverterTests
9+
{
10+
[Test]
11+
// Days, Hours, Minutes, and Seconds
12+
[TestCase("1:0:00:00")] // 1 day, 0 hours, 0 minutes, 0 seconds
13+
[TestCase("2:0:00:00")] // 2 days, 0 hours, 0 minutes, 0 seconds
14+
[TestCase("3:5:6:7.890")] // 3 days, 5 hours, 6 minutes, 7 seconds, with milliseconds
15+
[TestCase("1:02:03")] // 1 hour, 2 minutes, 3 seconds
16+
[TestCase("0:00:00")] // Zero TimeSpan
17+
[TestCase("1.0:00:00")] // 1 day, 0 hours, 0 minutes, 0 seconds
18+
[TestCase("1.23:59:59")] // 1 day, 23 hours, 59 minutes, 59 seconds
19+
[TestCase("10.12:30:45")] // 10 days, 12 hours, 30 minutes, 45 seconds
20+
21+
// Milliseconds and Ticks
22+
[TestCase("0:00:00.123")] // 123 milliseconds
23+
[TestCase("0:00:00.9999999")] // 9999999 ticks
24+
[TestCase("0:00:00.1")] // 1 tenth of a second
25+
[TestCase("1.0:00:00.1234567")] // 1 day, 0 hours, 0 minutes, 0 seconds, with fractional seconds
26+
27+
// Single Time Components
28+
[TestCase("1:00:00")] // 1 hour, 0 minutes, 0 seconds
29+
[TestCase("1:2:3")] // 1 hour, 2 minutes, 3 seconds without leading zeros
30+
[TestCase("0:1:1")] // Zero hours with 1 minute and 1 second
31+
[TestCase("1:1:1.123")] // 1 hour, 1 minute, 1 second with milliseconds
32+
[TestCase("0:0:0.1")] // 1 tenth of a second
33+
[TestCase("1:2:3.456")] // 1 hour, 2 minutes, 3 seconds with milliseconds
34+
[TestCase("12:34:56.789")] // 12 hours, 34 minutes, 56 seconds with milliseconds
35+
[TestCase("0:59:59")] // 59 minutes, 59 seconds
36+
[TestCase("0:0:0.999")] // 999 milliseconds
37+
[TestCase("6:30:0")] // 6 hours, 30 minutes
38+
39+
// Whole Days
40+
[TestCase("1")] // 1 day
41+
[TestCase("10")] // 10 days
42+
43+
// Minutes and Seconds
44+
[TestCase("00:01")] // 1 minute
45+
[TestCase("0:00:02")] // 2 seconds
46+
47+
// Small Fractions of a Second
48+
[TestCase("0:00:00.0000001")] // 1 tick
49+
[TestCase("0:00:00.0000010")] // 10 ticks
50+
[TestCase("0:00:00.0000100")] // 100 ticks
51+
[TestCase("0:00:00.0001000")] // 1000 ticks
52+
[TestCase("0:00:00.0010000")] // 10000 ticks (1 millisecond)
53+
[TestCase("0:00:00.0100000")] // 100000 ticks (10 milliseconds)
54+
[TestCase("0:00:00.1000000")] // 1000000 ticks (100 milliseconds)
55+
56+
// Large Time Values & Special chars
57+
[TestCase("23:59:59")] // 23 hours, 59 minutes, 59 seconds
58+
[TestCase("\\u002D23:59:59")] // -23 hours, 59 minutes, 59 seconds (Unicode escape for minus sign)
59+
[TestCase(
60+
"\\u0032\\u0033\\u003A\\u0035\\u0039\\u003A\\u0035\\u0039")] // "23:59:59" with Unicode escape sequences for digits and colon
61+
[TestCase("23:59:59.9")] // 23 hours, 59 minutes, 59 seconds, with 900 milliseconds
62+
[TestCase("23:59:59.9999999")] // 23 hours, 59 minutes, 59 seconds, with 9999999 ticks
63+
[TestCase("9999999.23:59:59.9999999")] // 9999999 days, 23 hours, 59 minutes, 59 seconds, with 9999999 ticks
64+
[TestCase("-9999999.23:59:59.9999999")] // -9999999 days, 23 hours, 59 minutes, 59 seconds, with 9999999 ticks
65+
66+
// Max and Min TimeSpan Values
67+
[TestCase("10675199.02:48:05.4775807")] // TimeSpan.MaxValue
68+
[TestCase("-10675199.02:48:05.4775808")] // TimeSpan.MinValue
69+
public void Should_not_throw_on_valid_timespans(string timespan)
70+
{
71+
string json = $$"""
72+
{
73+
"ResultingMessages": [
74+
{
75+
"DeliveryAt": null,
76+
"DeliveryDelay": "{{timespan}}"
77+
}
78+
]
79+
}
80+
""";
81+
82+
Assert.DoesNotThrow(() =>
83+
{
84+
JsonSerializer.Deserialize(json, SagaAuditMessagesSerializationContext.Default.SagaUpdatedMessage);
85+
});
86+
}
87+
88+
[Test]
89+
public void Should_not_throw_on_null()
90+
{
91+
string json = """
92+
{
93+
"ResultingMessages": [
94+
{
95+
"DeliveryAt": null,
96+
"DeliveryDelay":null
97+
}
98+
]
99+
}
100+
""";
101+
102+
Assert.DoesNotThrow(() =>
103+
{
104+
JsonSerializer.Deserialize(json, SagaAuditMessagesSerializationContext.Default.SagaUpdatedMessage);
105+
});
106+
}
107+
}
108+
}

src/ServiceControl.SagaAudit/CustomTimeSpanConverter.cs

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,7 @@ namespace ServiceControl.SagaAudit
1818
/// <remarks>Using this code outside this specific use case here is probably a very bad idea. Be warned.</remarks>
1919
sealed class CustomTimeSpanConverter : JsonConverter<TimeSpan>
2020
{
21-
// we allow the short format "g" too which has a minimum of 7 chars. .NET Requires min 8 chars
22-
const int MinimumTimeSpanFormatLength = 7; // hh:mm:ss or h:mm:ss
21+
const int MinimumTimeSpanFormatLength = 1; // d
2322
const int MaximumTimeSpanFormatLength = 26; // -dddddddd.hh:mm:ss.fffffff
2423
const int MaxExpansionFactorWhileEscaping = 6;
2524

@@ -57,8 +56,10 @@ public override TimeSpan Read(ref Utf8JsonReader reader, Type typeToConvert, Jso
5756
ThrowFormatException();
5857
}
5958

60-
// Ut8Parser.TryParse also handles short format "g" which has a minimum of 7 chars independent of the format identifier
61-
if (!Utf8Parser.TryParse(source, out TimeSpan parsedTimeSpan, out int bytesConsumed, 'c') || source.Length != bytesConsumed)
59+
// Ut8Parser.TryParse also handles some short format "g" cases which has a minimum of 1 chars independent of the format identifier
60+
if ((!Utf8Parser.TryParse(source, out TimeSpan parsedTimeSpan, out int bytesConsumed, 'c') || source.Length != bytesConsumed) &&
61+
// Otherwise we fall back to read with the short format "g" directly since that is what the SagaAudit plugin used to stay backward compatible
62+
(!Utf8Parser.TryParse(source, out parsedTimeSpan, out bytesConsumed, 'g') || source.Length != bytesConsumed))
6263
{
6364
ThrowFormatException();
6465
}

0 commit comments

Comments
 (0)