Skip to content

Commit 1a8c356

Browse files
committed
Improve LevelTokenRenderer handling and add tests
Enhanced LevelTokenRenderer to gracefully handle invalid log level indices and expanded test coverage for various level formatting scenarios. Also cleaned up unused usings and removed redundant tests in TokenRendererTests.
1 parent 2f2a980 commit 1a8c356

File tree

5 files changed

+235
-63
lines changed

5 files changed

+235
-63
lines changed

.github/FUNDING.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
11
github: vonhoff
2+
ko-fi: vonhoff

.github/PULL_REQUEST_TEMPLATE.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
<!-- Summarize your changes in the title -->
22

3-
**Motivation and Context**
3+
**Context**
44

55
<!-- Why is this change needed? Link any relevant issues. -->
66

Serilog.Sinks.RichTextBox.WinForms.Colored.Test/Integration/JsonFormattingTests.cs

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
1-
using System;
21
using Serilog.Events;
32
using Serilog.Sinks.RichTextBoxForms;
4-
using Serilog.Sinks.RichTextBoxForms.Themes;
53
using Xunit;
64

75
namespace Serilog.Tests.Integration
@@ -260,7 +258,7 @@ public void ScalarValue_IFormattableButNotNumericValueType()
260258
var enumValue = System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Instance;
261259
var prop = new LogEventProperty("EnumProp", new ScalarValue(enumValue));
262260
var logEvent = new LogEvent(DateTimeOffset.Now, LogEventLevel.Information, null, _parser.Parse("{EnumProp:j}"), new[] { prop });
263-
261+
264262
var result = RenderAndGetText(logEvent, "{Message:j}");
265263
Assert.NotNull(result);
266264
Assert.NotEmpty(result);
@@ -272,7 +270,7 @@ public void ScalarValue_NonFormattableObject()
272270
var plainObject = new object();
273271
var prop = new LogEventProperty("ObjectProp", new ScalarValue(plainObject));
274272
var logEvent = new LogEvent(DateTimeOffset.Now, LogEventLevel.Information, null, _parser.Parse("{ObjectProp:j}"), new[] { prop });
275-
273+
276274
var result = RenderAndGetText(logEvent, "{Message:j}");
277275
Assert.NotNull(result);
278276
Assert.NotEmpty(result);

Serilog.Sinks.RichTextBox.WinForms.Colored.Test/Integration/TokenRendererTests.cs

Lines changed: 223 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,6 @@
11
using Serilog.Events;
22
using Serilog.Parsing;
33
using Serilog.Sinks.RichTextBoxForms.Rendering;
4-
using Serilog.Sinks.RichTextBoxForms.Themes;
5-
using System;
6-
using System.Collections.Generic;
7-
using System.Linq;
84
using Xunit;
95

106
namespace Serilog.Tests.Integration
@@ -37,37 +33,9 @@ public void ExceptionTokenRenderer_RendersExceptionWithStackFrames()
3733
var result = _richTextBox.Text;
3834
Assert.Contains("InvalidOperationException", result);
3935
Assert.Contains("Test exception", result);
40-
41-
var lines = result.Split(new[] { '\n', '\r' }, StringSplitOptions.RemoveEmptyEntries);
42-
Assert.True(lines.Length > 1, $"Exception should have multiple lines including stack trace. Got: {lines.Length} lines. Text: {result}");
43-
}
44-
45-
[Fact]
46-
public void ExceptionTokenRenderer_RendersMultipleLines()
47-
{
48-
Exception? exception = null;
49-
try
50-
{
51-
throw new InvalidOperationException("Test exception");
52-
}
53-
catch (Exception ex)
54-
{
55-
exception = ex;
56-
}
57-
58-
var logEvent = new LogEvent(
59-
DateTimeOffset.Now,
60-
LogEventLevel.Error,
61-
exception,
62-
_parser.Parse("Error occurred"),
63-
Array.Empty<LogEventProperty>());
64-
65-
var renderer = new ExceptionTokenRenderer(_defaultTheme);
66-
renderer.Render(logEvent, _canvas);
6736

68-
var result = _richTextBox.Text;
6937
var lines = result.Split(new[] { '\n', '\r' }, StringSplitOptions.RemoveEmptyEntries);
70-
Assert.True(lines.Length > 1, $"Exception should have multiple lines. Got: {lines.Length} lines. Text: {result}");
38+
Assert.True(lines.Length > 1, $"Exception should have multiple lines including stack trace. Got: {lines.Length} lines. Text: {result}");
7139
}
7240

7341
[Fact]
@@ -107,26 +75,6 @@ public void EventPropertyTokenRenderer_RendersNonStringScalarValue()
10775
Assert.Contains("42", result);
10876
}
10977

110-
[Fact]
111-
public void EventPropertyTokenRenderer_RendersBooleanValue()
112-
{
113-
var template = _parser.Parse("IsValid: {IsValid}");
114-
var propertyToken = template.Tokens.OfType<PropertyToken>().Single(t => t.PropertyName == "IsValid");
115-
var renderer = new EventPropertyTokenRenderer(_defaultTheme, propertyToken, null);
116-
117-
var logEvent = new LogEvent(
118-
DateTimeOffset.Now,
119-
LogEventLevel.Information,
120-
null,
121-
template,
122-
new[] { new LogEventProperty("IsValid", new ScalarValue(true)) });
123-
124-
renderer.Render(logEvent, _canvas);
125-
126-
var result = _richTextBox.Text;
127-
Assert.Contains("True", result);
128-
}
129-
13078
[Fact]
13179
public void EventPropertyTokenRenderer_RendersStructureValue()
13280
{
@@ -254,6 +202,228 @@ public void EventPropertyTokenRenderer_RendersStringValue()
254202
var result = _richTextBox.Text;
255203
Assert.Contains("Hello World", result);
256204
}
205+
206+
[Fact]
207+
public void LevelTokenRenderer_WithLowercaseFormat_RendersCorrectly()
208+
{
209+
var template = _parser.Parse("Level: {Level:w3}");
210+
var levelToken = template.Tokens.OfType<PropertyToken>().Single(t => t.PropertyName == "Level");
211+
var renderer = new LevelTokenRenderer(_defaultTheme, levelToken);
212+
213+
var logEvent = new LogEvent(
214+
DateTimeOffset.Now,
215+
LogEventLevel.Information,
216+
null,
217+
_parser.Parse("Test"),
218+
Array.Empty<LogEventProperty>());
219+
220+
renderer.Render(logEvent, _canvas);
221+
222+
var result = _richTextBox.Text;
223+
Assert.Contains("inf", result);
224+
}
225+
226+
[Fact]
227+
public void LevelTokenRenderer_WithTitleCaseFormat_RendersCorrectly()
228+
{
229+
var template = _parser.Parse("Level: {Level:t3}");
230+
var levelToken = template.Tokens.OfType<PropertyToken>().Single(t => t.PropertyName == "Level");
231+
var renderer = new LevelTokenRenderer(_defaultTheme, levelToken);
232+
233+
var logEvent = new LogEvent(
234+
DateTimeOffset.Now,
235+
LogEventLevel.Warning,
236+
null,
237+
_parser.Parse("Test"),
238+
Array.Empty<LogEventProperty>());
239+
240+
renderer.Render(logEvent, _canvas);
241+
242+
var result = _richTextBox.Text;
243+
Assert.Contains("Wrn", result);
244+
}
245+
246+
[Fact]
247+
public void LevelTokenRenderer_WithUppercaseFormat_RendersCorrectly()
248+
{
249+
var template = _parser.Parse("Level: {Level:u3}");
250+
var levelToken = template.Tokens.OfType<PropertyToken>().Single(t => t.PropertyName == "Level");
251+
var renderer = new LevelTokenRenderer(_defaultTheme, levelToken);
252+
253+
var logEvent = new LogEvent(
254+
DateTimeOffset.Now,
255+
LogEventLevel.Error,
256+
null,
257+
_parser.Parse("Test"),
258+
Array.Empty<LogEventProperty>());
259+
260+
renderer.Render(logEvent, _canvas);
261+
262+
var result = _richTextBox.Text;
263+
Assert.Contains("ERR", result);
264+
}
265+
266+
[Fact]
267+
public void LevelTokenRenderer_WithThreeDigitWidth_RendersCorrectly()
268+
{
269+
var template = _parser.Parse("Level: {Level:u10}");
270+
var levelToken = template.Tokens.OfType<PropertyToken>().Single(t => t.PropertyName == "Level");
271+
var renderer = new LevelTokenRenderer(_defaultTheme, levelToken);
272+
273+
var logEvent = new LogEvent(
274+
DateTimeOffset.Now,
275+
LogEventLevel.Information,
276+
null,
277+
_parser.Parse("Test"),
278+
Array.Empty<LogEventProperty>());
279+
280+
renderer.Render(logEvent, _canvas);
281+
282+
var result = _richTextBox.Text;
283+
Assert.Contains("INFORMATIO", result);
284+
}
285+
286+
[Fact]
287+
public void LevelTokenRenderer_WithWidthLessThanOne_ReturnsEmpty()
288+
{
289+
var template = _parser.Parse("Level: {Level:u0}");
290+
var levelToken = template.Tokens.OfType<PropertyToken>().Single(t => t.PropertyName == "Level");
291+
var renderer = new LevelTokenRenderer(_defaultTheme, levelToken);
292+
293+
var logEvent = new LogEvent(
294+
DateTimeOffset.Now,
295+
LogEventLevel.Information,
296+
null,
297+
_parser.Parse("Test"),
298+
Array.Empty<LogEventProperty>());
299+
300+
renderer.Render(logEvent, _canvas);
301+
302+
var result = _richTextBox.Text;
303+
Assert.Empty(result);
304+
}
305+
306+
[Fact]
307+
public void LevelTokenRenderer_WithWidthGreaterThanFour_TruncatesAndFormats()
308+
{
309+
var template = _parser.Parse("Level: {Level:u5}");
310+
var levelToken = template.Tokens.OfType<PropertyToken>().Single(t => t.PropertyName == "Level");
311+
var renderer = new LevelTokenRenderer(_defaultTheme, levelToken);
312+
313+
var logEvent = new LogEvent(
314+
DateTimeOffset.Now,
315+
LogEventLevel.Information,
316+
null,
317+
_parser.Parse("Test"),
318+
Array.Empty<LogEventProperty>());
319+
320+
renderer.Render(logEvent, _canvas);
321+
322+
var result = _richTextBox.Text;
323+
Assert.Contains("INFOR", result);
324+
}
325+
326+
[Fact]
327+
public void LevelTokenRenderer_WithInvalidFormatSpecifier_UsesTextFormatter()
328+
{
329+
var template = _parser.Parse("Level: {Level:x3}");
330+
var levelToken = template.Tokens.OfType<PropertyToken>().Single(t => t.PropertyName == "Level");
331+
var renderer = new LevelTokenRenderer(_defaultTheme, levelToken);
332+
333+
var logEvent = new LogEvent(
334+
DateTimeOffset.Now,
335+
LogEventLevel.Information,
336+
null,
337+
_parser.Parse("Test"),
338+
Array.Empty<LogEventProperty>());
339+
340+
renderer.Render(logEvent, _canvas);
341+
342+
var result = _richTextBox.Text;
343+
Assert.Contains("Information", result);
344+
}
345+
346+
[Fact]
347+
public void LevelTokenRenderer_WithNonStandardFormatLength_UsesTextFormatter()
348+
{
349+
var template = _parser.Parse("Level: {Level:u}");
350+
var levelToken = template.Tokens.OfType<PropertyToken>().Single(t => t.PropertyName == "Level");
351+
var renderer = new LevelTokenRenderer(_defaultTheme, levelToken);
352+
353+
var logEvent = new LogEvent(
354+
DateTimeOffset.Now,
355+
LogEventLevel.Information,
356+
null,
357+
_parser.Parse("Test"),
358+
Array.Empty<LogEventProperty>());
359+
360+
renderer.Render(logEvent, _canvas);
361+
362+
var result = _richTextBox.Text;
363+
Assert.Contains("INFORMATION", result);
364+
}
365+
366+
[Fact]
367+
public void LevelTokenRenderer_WithInvalidLevelIndex_HandlesGracefully()
368+
{
369+
var template = _parser.Parse("Level: {Level:u3}");
370+
var levelToken = template.Tokens.OfType<PropertyToken>().Single(t => t.PropertyName == "Level");
371+
var renderer = new LevelTokenRenderer(_defaultTheme, levelToken);
372+
373+
var invalidLevel = (LogEventLevel)999;
374+
var logEvent = new LogEvent(
375+
DateTimeOffset.Now,
376+
invalidLevel,
377+
null,
378+
_parser.Parse("Test"),
379+
Array.Empty<LogEventProperty>());
380+
381+
renderer.Render(logEvent, _canvas);
382+
383+
var result = _richTextBox.Text;
384+
Assert.Contains("999", result);
385+
}
386+
387+
[Fact]
388+
public void LevelTokenRenderer_WithNegativeLevelIndex_HandlesGracefully()
389+
{
390+
var template = _parser.Parse("Level: {Level:u3}");
391+
var levelToken = template.Tokens.OfType<PropertyToken>().Single(t => t.PropertyName == "Level");
392+
var renderer = new LevelTokenRenderer(_defaultTheme, levelToken);
393+
394+
var invalidLevel = (LogEventLevel)(-1);
395+
var logEvent = new LogEvent(
396+
DateTimeOffset.Now,
397+
invalidLevel,
398+
null,
399+
_parser.Parse("Test"),
400+
Array.Empty<LogEventProperty>());
401+
402+
renderer.Render(logEvent, _canvas);
403+
404+
var result = _richTextBox.Text;
405+
Assert.Contains("-1", result);
406+
}
407+
408+
[Fact]
409+
public void LevelTokenRenderer_WithWidthGreaterThanStringLength_DoesNotTruncate()
410+
{
411+
var template = _parser.Parse("Level: {Level:u20}");
412+
var levelToken = template.Tokens.OfType<PropertyToken>().Single(t => t.PropertyName == "Level");
413+
var renderer = new LevelTokenRenderer(_defaultTheme, levelToken);
414+
415+
var logEvent = new LogEvent(
416+
DateTimeOffset.Now,
417+
LogEventLevel.Verbose,
418+
null,
419+
_parser.Parse("Test"),
420+
Array.Empty<LogEventProperty>());
421+
422+
renderer.Render(logEvent, _canvas);
423+
424+
var result = _richTextBox.Text;
425+
Assert.Contains("VERBOSE", result);
426+
}
257427
}
258428
}
259429

Serilog.Sinks.RichTextBox.WinForms.Colored/Sinks/RichTextBoxForms/Rendering/LevelTokenRenderer.cs

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,14 @@ public LevelTokenRenderer(Theme theme, PropertyToken levelToken)
8282
public void Render(LogEvent logEvent, IRtfCanvas canvas)
8383
{
8484
var levelIndex = (int)logEvent.Level;
85+
if (levelIndex < 0 || levelIndex >= _monikers.Length)
86+
{
87+
var format = string.Empty;
88+
var fallbackMoniker = TextFormatter.Format(logEvent.Level.ToString(), format);
89+
_theme.Render(canvas, StyleToken.Text, fallbackMoniker);
90+
return;
91+
}
92+
8593
var moniker = _monikers[levelIndex];
8694
var levelStyle = LevelStyles[levelIndex];
8795
_theme.Render(canvas, levelStyle, moniker);
@@ -119,11 +127,6 @@ private static string GetLevelMoniker(LogEventLevel value, string format = "")
119127
}
120128

121129
var index = (int)value;
122-
if (index is < 0 or > (int)LogEventLevel.Fatal)
123-
{
124-
return TextFormatter.Format(value.ToString(), format);
125-
}
126-
127130
return format[0] switch
128131
{
129132
'w' => LowercaseLevelMap[index][width - 1],

0 commit comments

Comments
 (0)