Skip to content

Commit a344c77

Browse files
committed
Thread safety issue with SmartFormatter
1 parent 9c622ea commit a344c77

6 files changed

Lines changed: 753 additions & 4 deletions

File tree

AssemblyInfo.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,6 @@
2929
// You can specify all the values or you can default the Build and Revision Numbers
3030
// by using the '*' as shown below:
3131
// [assembly: AssemblyVersion("1.0.*")]
32-
[assembly: AssemblyVersion("2.1.3.0")]
33-
[assembly: AssemblyFileVersion("2.1.3.0")]
32+
[assembly: AssemblyVersion("2.1.4.0")]
33+
[assembly: AssemblyFileVersion("2.1.4.0")]
3434

XESmartTarget.Core/Utils/SmartFormatHelper.cs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,12 @@ public static class SmartFormatHelper
77
{
88
private static Logger logger = LogManager.GetCurrentClassLogger();
99

10-
private static SmartFormatter fmt = Smart.CreateDefaultSmartFormat();
10+
1111
public static string Format(string format, Dictionary<string, string> args)
1212
{
1313
try
1414
{
15+
SmartFormatter fmt = Smart.CreateDefaultSmartFormat();
1516
fmt.Settings.Parser.ConvertCharacterStringLiterals = false;
1617
return fmt.Format(format, args);
1718
}
@@ -26,6 +27,7 @@ public static string Format(string format, Dictionary<string, object> args)
2627
{
2728
try
2829
{
30+
SmartFormatter fmt = Smart.CreateDefaultSmartFormat();
2931
fmt.Settings.Parser.ConvertCharacterStringLiterals = false;
3032
return fmt.Format(format, args);
3133
}
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
using Xunit;
2+
using Xunit.Abstractions;
3+
using XESmartTarget.Core.Utils;
4+
using System.Collections.Generic;
5+
using System.Diagnostics;
6+
7+
namespace XESmartTarget.Tests.Utils
8+
{
9+
public class SmartFormatHelperBenchmark
10+
{
11+
private readonly ITestOutputHelper _output;
12+
13+
public SmartFormatHelperBenchmark(ITestOutputHelper output)
14+
{
15+
_output = output;
16+
}
17+
18+
[Fact]
19+
public void Benchmark_Format_Performance()
20+
{
21+
// Arrange
22+
var format = "Event: {name}, Time: {timestamp}, Duration: {duration}";
23+
var iterations = 100_000;
24+
25+
// Warmup
26+
for (int i = 0; i < 1000; i++)
27+
{
28+
SmartFormatHelper.Format(format, new Dictionary<string, object>
29+
{
30+
["name"] = "test_event",
31+
["timestamp"] = DateTime.Now,
32+
["duration"] = 123
33+
});
34+
}
35+
36+
// Benchmark
37+
var sw = Stopwatch.StartNew();
38+
for (int i = 0; i < iterations; i++)
39+
{
40+
var args = new Dictionary<string, object>
41+
{
42+
["name"] = $"event_{i}",
43+
["timestamp"] = DateTime.Now,
44+
["duration"] = i
45+
};
46+
SmartFormatHelper.Format(format, args);
47+
}
48+
sw.Stop();
49+
50+
// Report
51+
var totalMs = sw.ElapsedMilliseconds;
52+
var avgMicroseconds = (totalMs * 1000.0) / iterations;
53+
var throughput = iterations / (totalMs / 1000.0);
54+
55+
_output.WriteLine($"Total time: {totalMs} ms");
56+
_output.WriteLine($"Iterations: {iterations:N0}");
57+
_output.WriteLine($"Average: {avgMicroseconds:F2} μs per call");
58+
_output.WriteLine($"Throughput: {throughput:N0} calls/second");
59+
60+
// Assert reasonable performance
61+
// Should be able to do at least 10,000 formats per second
62+
Xunit.Assert.True(throughput > 10_000, $"Performance too slow: {throughput:N0} calls/sec");
63+
}
64+
}
65+
}
Lines changed: 268 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,268 @@
1+
using Xunit;
2+
using XESmartTarget.Core.Utils;
3+
using System.Collections.Generic;
4+
using System.Threading.Tasks;
5+
using System.Linq;
6+
7+
namespace XESmartTarget.Tests.Utils
8+
{
9+
public class SmartFormatHelperTests
10+
{
11+
[Fact]
12+
public void Format_WithStringDictionary_ReturnsFormattedString()
13+
{
14+
// Arrange
15+
var format = "Hello {name}, welcome to {place}!";
16+
var args = new Dictionary<string, string>
17+
{
18+
["name"] = "Alice",
19+
["place"] = "Wonderland"
20+
};
21+
22+
// Act
23+
var result = SmartFormatHelper.Format(format, args);
24+
25+
// Assert
26+
Xunit.Assert.Equal("Hello Alice, welcome to Wonderland!", result);
27+
}
28+
29+
[Fact]
30+
public void Format_WithObjectDictionary_ReturnsFormattedString()
31+
{
32+
// Arrange
33+
var format = "Count: {count}, Price: {price}";
34+
var args = new Dictionary<string, object>
35+
{
36+
["count"] = 42,
37+
["price"] = 99.99
38+
};
39+
40+
// Act
41+
var result = SmartFormatHelper.Format(format, args);
42+
43+
// Assert
44+
Xunit.Assert.Equal("Count: 42, Price: 99.99", result);
45+
}
46+
47+
[Fact]
48+
public void Format_WithMissingPlaceholder_ReturnsOriginalFormat()
49+
{
50+
// Arrange
51+
var format = "Hello {name}";
52+
var args = new Dictionary<string, string>
53+
{
54+
["other"] = "value"
55+
};
56+
57+
// Act
58+
var result = SmartFormatHelper.Format(format, args);
59+
60+
// Assert
61+
Xunit.Assert.Equal(format, result);
62+
}
63+
64+
[Fact]
65+
public void Format_WithEmptyDictionary_HandlesGracefully()
66+
{
67+
// Arrange
68+
var format = "No placeholders here";
69+
var args = new Dictionary<string, string>();
70+
71+
// Act
72+
var result = SmartFormatHelper.Format(format, args);
73+
74+
// Assert
75+
Xunit.Assert.Equal("No placeholders here", result);
76+
}
77+
78+
[Fact]
79+
public void Format_WithNumberFormatting_WorksCorrectly()
80+
{
81+
// Arrange
82+
var format = "{count:0000}";
83+
var args = new Dictionary<string, object>
84+
{
85+
["count"] = 42
86+
};
87+
88+
// Act
89+
var result = SmartFormatHelper.Format(format, args);
90+
91+
// Assert
92+
Xunit.Assert.Contains("0042", result);
93+
}
94+
95+
[Fact]
96+
public void Format_WithSpecialCharacters_HandlesCorrectly()
97+
{
98+
// Arrange
99+
var format = "Value: {value}";
100+
var args = new Dictionary<string, string>
101+
{
102+
["value"] = "Test\\Value"
103+
};
104+
105+
// Act
106+
var result = SmartFormatHelper.Format(format, args);
107+
108+
// Assert
109+
Xunit.Assert.Equal("Value: Test\\Value", result);
110+
}
111+
112+
[Fact]
113+
public void Format_WithNullValue_HandlesGracefully()
114+
{
115+
// Arrange
116+
var format = "Value: {value}";
117+
var args = new Dictionary<string, object>
118+
{
119+
["value"] = null!
120+
};
121+
122+
// Act
123+
var result = SmartFormatHelper.Format(format, args);
124+
125+
// Assert
126+
Xunit.Assert.NotNull(result);
127+
}
128+
129+
[Fact]
130+
public async Task Format_ConcurrentCalls_ProduceCorrectResults()
131+
{
132+
// Arrange
133+
var format = "Thread: {thread}, Value: {value}";
134+
var tasks = new List<Task<(int thread, string result)>>();
135+
136+
// Act - simulate concurrent formatting from multiple threads
137+
for (int i = 0; i < 100; i++)
138+
{
139+
int threadId = i;
140+
var task = Task.Run(() =>
141+
{
142+
var args = new Dictionary<string, object>
143+
{
144+
["thread"] = threadId,
145+
["value"] = $"Value_{threadId}"
146+
};
147+
var result = SmartFormatHelper.Format(format, args);
148+
return (threadId, result);
149+
});
150+
tasks.Add(task);
151+
}
152+
153+
var results = await Task.WhenAll(tasks);
154+
155+
// Assert - verify each result is exactly what we expect
156+
foreach (var (threadId, result) in results)
157+
{
158+
var expectedResult = $"Thread: {threadId}, Value: Value_{threadId}";
159+
Xunit.Assert.Equal(expectedResult, result);
160+
}
161+
}
162+
163+
[Fact]
164+
public void Format_SequentialCallsSameThread_NoStateLeakage()
165+
{
166+
// Arrange & Act
167+
var result1 = SmartFormatHelper.Format("Hello {name}", new Dictionary<string, string> { ["name"] = "Alice" });
168+
var result2 = SmartFormatHelper.Format("Goodbye {name}", new Dictionary<string, string> { ["name"] = "Bob" });
169+
var result3 = SmartFormatHelper.Format("Hi {name}", new Dictionary<string, string> { ["name"] = "Charlie" });
170+
171+
// Assert - each call should be completely independent
172+
Xunit.Assert.Equal("Hello Alice", result1);
173+
Xunit.Assert.Equal("Goodbye Bob", result2);
174+
Xunit.Assert.Equal("Hi Charlie", result3);
175+
176+
// Verify no cross-contamination
177+
Xunit.Assert.DoesNotContain("Bob", result1);
178+
Xunit.Assert.DoesNotContain("Alice", result2);
179+
Xunit.Assert.DoesNotContain("Charlie", result1);
180+
}
181+
182+
[Fact]
183+
public void Format_RapidSequentialCalls_NoMemoryLeak()
184+
{
185+
// Arrange & Act - call many times to check for memory issues
186+
for (int i = 0; i < 10000; i++)
187+
{
188+
var args = new Dictionary<string, object>
189+
{
190+
["iteration"] = i,
191+
["value"] = $"Value_{i}"
192+
};
193+
var result = SmartFormatHelper.Format("Iteration: {iteration}, {value}", args);
194+
195+
// Assert each result is correct
196+
Xunit.Assert.Contains($"Iteration: {i}", result);
197+
Xunit.Assert.Contains($"Value_{i}", result);
198+
}
199+
200+
// If we got here without OutOfMemoryException, the test passes
201+
Xunit.Assert.True(true);
202+
}
203+
204+
[Fact]
205+
public void Format_WithCharacterLiterals_DoesNotConvert()
206+
{
207+
// Arrange - test that ConvertCharacterStringLiterals = false works
208+
var format = "Value: {value}\\n";
209+
var args = new Dictionary<string, string>
210+
{
211+
["value"] = "test"
212+
};
213+
214+
// Act
215+
var result = SmartFormatHelper.Format(format, args);
216+
217+
// Assert - should keep \\n as literal, not convert to newline
218+
Xunit.Assert.Contains("\\n", result);
219+
Xunit.Assert.DoesNotContain("\n", result);
220+
}
221+
222+
[Fact]
223+
public void Format_ExceptionInFormatting_ReturnsOriginalFormat()
224+
{
225+
// Arrange - intentionally malformed format
226+
var format = "Bad format: {unclosed";
227+
var args = new Dictionary<string, string>
228+
{
229+
["test"] = "value"
230+
};
231+
232+
// Act
233+
var result = SmartFormatHelper.Format(format, args);
234+
235+
// Assert - should return original format on exception
236+
Xunit.Assert.Equal(format, result);
237+
}
238+
239+
[Theory]
240+
[InlineData("", "")]
241+
[InlineData("No placeholders", "No placeholders")]
242+
[InlineData("{name}", "Alice")]
243+
[InlineData("{first} {last}", "Alice Smith")]
244+
public void Format_VariousInputs_ProducesExpectedOutput(string format, string expected)
245+
{
246+
// Arrange
247+
var args = new Dictionary<string, string>
248+
{
249+
["name"] = "Alice",
250+
["first"] = "Alice",
251+
["last"] = "Smith"
252+
};
253+
254+
// Act
255+
var result = SmartFormatHelper.Format(format, args);
256+
257+
// Assert
258+
if (expected == "")
259+
{
260+
Xunit.Assert.Equal(format, result);
261+
}
262+
else
263+
{
264+
Xunit.Assert.Contains(expected, result);
265+
}
266+
}
267+
}
268+
}

0 commit comments

Comments
 (0)