Skip to content

Commit 233fc78

Browse files
[release/10.0] Add test helper and test cases to validate EventSource IDs (#65427)
* Add test helper and test cases to validate EventSource IDs * Fix markdown lint error * Check for null reference, add generic overload for convenience * Update testing null check for netstandard/netframework targets * Fix tests that can't use generic overload * Remove error string check from test --------- Co-authored-by: David Negstad <David.Negstad@microsoft.com>
1 parent 3317390 commit 233fc78

File tree

7 files changed

+296
-1
lines changed

7 files changed

+296
-1
lines changed

docs/EventSourceAndCounters.md

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -169,7 +169,34 @@ namespace Microsoft.AspNetCore.Authentication.Internal
169169

170170
## Automated Testing of EventSources
171171

172-
EventSources can be tested using the `EventSourceTestBase` base class in `Microsoft.AspNetCore.InternalTesting`. An example test is below:
172+
### Validating Event ID Consistency
173+
174+
All `EventSource` subclasses should have a test that validates the `[Event(N)]` attribute IDs match the `WriteEvent(N, ...)` call arguments. This catches drift caused by bad merge resolution or missed updates that would otherwise surface only as runtime errors.
175+
176+
Use the `EventSourceValidator` utility in `Microsoft.AspNetCore.InternalTesting.Tracing`:
177+
178+
```csharp
179+
using Microsoft.AspNetCore.InternalTesting.Tracing;
180+
181+
public class MyEventSourceTests
182+
{
183+
[Fact]
184+
public void EventIdsAreConsistent()
185+
{
186+
EventSourceValidator.ValidateEventSourceIds<MyEventSource>();
187+
}
188+
}
189+
```
190+
191+
The validator:
192+
* Uses `EventSource.GenerateManifest` with `EventManifestOptions.Strict` to perform IL-level validation that each `WriteEvent(id, ...)` call argument matches the `[Event(id)]` attribute — the same validation the .NET runtime uses internally
193+
* Checks for duplicate event IDs across methods
194+
195+
> **Important:** Every new `EventSource` class should include this one-line validation test.
196+
197+
### Functional Testing with EventSourceTestBase
198+
199+
EventSources can also be functionally tested using the `EventSourceTestBase` base class in `Microsoft.AspNetCore.InternalTesting`. An example test is below:
173200

174201
```csharp
175202
// The base class MUST be used for EventSource testing because EventSources are global and parallel tests can cause issues.

src/Hosting/Hosting/test/Internal/HostingEventSourceTests.cs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,19 @@
66
using Microsoft.AspNetCore.Http;
77
using Microsoft.AspNetCore.Internal;
88
using Microsoft.AspNetCore.InternalTesting;
9+
using Microsoft.AspNetCore.InternalTesting.Tracing;
910
using Microsoft.Extensions.Logging;
1011

1112
namespace Microsoft.AspNetCore.Hosting;
1213

1314
public class HostingEventSourceTests : LoggedTest
1415
{
16+
[Fact]
17+
public void EventIdsAreConsistent()
18+
{
19+
EventSourceValidator.ValidateEventSourceIds(typeof(HostingEventSource));
20+
}
21+
1522
[Fact]
1623
public void MatchesNameAndGuid()
1724
{

src/Servers/Kestrel/Core/test/KestrelEventSourceTests.cs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
using System.Diagnostics.Tracing;
66
using System.Globalization;
77
using System.Reflection;
8+
using Microsoft.AspNetCore.InternalTesting.Tracing;
89
using Xunit;
910

1011
namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests;
@@ -26,4 +27,16 @@ public void ExistsWithCorrectId()
2627
Assert.Equal(Guid.Parse("bdeb4676-a36e-5442-db99-4764e2326c7d", CultureInfo.InvariantCulture), EventSource.GetGuid(esType));
2728
Assert.NotEmpty(EventSource.GenerateManifest(esType, "assemblyPathToIncludeInManifest"));
2829
}
30+
31+
[Fact]
32+
public void EventIdsAreConsistent()
33+
{
34+
var esType = typeof(KestrelServer).Assembly.GetType(
35+
"Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure.KestrelEventSource",
36+
throwOnError: true,
37+
ignoreCase: false
38+
);
39+
40+
EventSourceValidator.ValidateEventSourceIds(esType);
41+
}
2942
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using Microsoft.AspNetCore.Certificates.Generation;
5+
using Microsoft.AspNetCore.InternalTesting.Tracing;
6+
7+
namespace Microsoft.AspNetCore.Internal.Tests;
8+
9+
public class CertificateManagerEventSourceTests
10+
{
11+
[Fact]
12+
public void EventIdsAreConsistent()
13+
{
14+
EventSourceValidator.ValidateEventSourceIds<CertificateManager.CertificateManagerEventSource>();
15+
}
16+
}

src/SignalR/common/Http.Connections/test/Internal/HttpConnectionsEventSourceTests.cs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,20 @@
66
using System.Diagnostics.Tracing;
77
using System.Globalization;
88
using Microsoft.AspNetCore.Internal;
9+
using Microsoft.AspNetCore.InternalTesting.Tracing;
910
using Microsoft.Extensions.Internal;
1011
using Xunit;
1112

1213
namespace Microsoft.AspNetCore.Http.Connections.Internal;
1314

1415
public class HttpConnectionsEventSourceTests
1516
{
17+
[Fact]
18+
public void EventIdsAreConsistent()
19+
{
20+
EventSourceValidator.ValidateEventSourceIds(typeof(HttpConnectionsEventSource));
21+
}
22+
1623
[Fact]
1724
public void MatchesNameAndGuid()
1825
{
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using System;
5+
using System.Collections.Generic;
6+
using System.Diagnostics.Tracing;
7+
using System.Reflection;
8+
using Xunit;
9+
10+
namespace Microsoft.AspNetCore.InternalTesting.Tracing;
11+
12+
/// <summary>
13+
/// Validates that <see cref="EventSource"/>-derived classes have consistent
14+
/// <see cref="EventAttribute.EventId"/> values and <c>WriteEvent</c> call arguments.
15+
/// This catches drift caused by bad merge resolution or missed updates that would
16+
/// otherwise surface only as runtime errors.
17+
/// </summary>
18+
public static class EventSourceValidator
19+
{
20+
/// <summary>
21+
/// Validates all <c>[Event]</c>-attributed methods on <typeparamref name="T"/>.
22+
/// </summary>
23+
/// <typeparam name="T">A type that derives from <see cref="EventSource"/>.</typeparam>
24+
public static void ValidateEventSourceIds<T>() where T : EventSource
25+
=> ValidateEventSourceIds(typeof(T));
26+
27+
/// <summary>
28+
/// Validates all <c>[Event]</c>-attributed methods on the given <see cref="EventSource"/>-derived type.
29+
/// <para>
30+
/// Uses <see cref="EventSource.GenerateManifest(Type, string, EventManifestOptions)"/> with
31+
/// <see cref="EventManifestOptions.Strict"/> to perform IL-level validation that the integer
32+
/// argument passed to each <c>WriteEvent</c> call matches the <c>[Event(id)]</c> attribute
33+
/// on the calling method. This is the same validation the .NET runtime itself uses.
34+
/// </para>
35+
/// <para>
36+
/// Additionally checks for duplicate <see cref="EventAttribute.EventId"/> values across methods.
37+
/// </para>
38+
/// </summary>
39+
/// <param name="eventSourceType">A type that derives from <see cref="EventSource"/>.</param>
40+
public static void ValidateEventSourceIds(Type eventSourceType)
41+
{
42+
if (eventSourceType is null)
43+
{
44+
throw new ArgumentNullException(nameof(eventSourceType));
45+
}
46+
47+
Assert.True(
48+
typeof(EventSource).IsAssignableFrom(eventSourceType),
49+
$"Type '{eventSourceType.FullName}' does not derive from EventSource.");
50+
51+
var errors = new List<string>();
52+
53+
// Check for duplicate Event IDs across methods.
54+
var seenIds = new Dictionary<int, string>();
55+
var methods = eventSourceType.GetMethods(
56+
BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly);
57+
58+
foreach (var method in methods)
59+
{
60+
var eventAttr = method.GetCustomAttribute<EventAttribute>();
61+
if (eventAttr is null)
62+
{
63+
continue;
64+
}
65+
66+
if (seenIds.TryGetValue(eventAttr.EventId, out var existingMethod))
67+
{
68+
errors.Add(
69+
$"Duplicate EventId {eventAttr.EventId}: methods '{existingMethod}' and '{method.Name}' share the same ID.");
70+
}
71+
else
72+
{
73+
seenIds[eventAttr.EventId] = method.Name;
74+
}
75+
}
76+
77+
// Use GenerateManifest with Strict mode to validate that each method's
78+
// WriteEvent(id, ...) call uses an ID that matches its [Event(id)] attribute.
79+
// Internally this uses GetHelperCallFirstArg to IL-inspect the method body
80+
// and extract the integer constant passed to WriteEvent — the same validation
81+
// the .NET runtime performs when constructing an EventSource.
82+
try
83+
{
84+
var manifest = EventSource.GenerateManifest(
85+
eventSourceType,
86+
assemblyPathToIncludeInManifest: "assemblyPathForValidation",
87+
flags: EventManifestOptions.Strict);
88+
89+
if (manifest is null)
90+
{
91+
errors.Add("GenerateManifest returned null, indicating the type is not a valid EventSource.");
92+
}
93+
}
94+
catch (ArgumentException ex)
95+
{
96+
errors.Add(ex.Message);
97+
}
98+
99+
if (errors.Count > 0)
100+
{
101+
Assert.Fail(
102+
$"EventSource '{eventSourceType.FullName}' has event ID validation error(s):" +
103+
Environment.NewLine + string.Join(Environment.NewLine, errors));
104+
}
105+
}
106+
}
Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using System;
5+
using System.Diagnostics.Tracing;
6+
using Microsoft.AspNetCore.InternalTesting.Tracing;
7+
using Xunit;
8+
9+
namespace Microsoft.AspNetCore.InternalTesting;
10+
11+
public class EventSourceValidatorTests
12+
{
13+
[Fact]
14+
public void ValidateEventSourceIds_PassesForCorrectEventSource()
15+
{
16+
EventSourceValidator.ValidateEventSourceIds<CorrectEventSource>();
17+
}
18+
19+
[Fact]
20+
public void ValidateEventSourceIds_FailsForMismatchedWriteEventId()
21+
{
22+
// GenerateManifest(Strict) detects the mismatch via IL inspection
23+
// and our validator surfaces it through Assert.Fail.
24+
// The exact runtime error message varies by .NET version, so we
25+
// only verify the validator rejects the bad source.
26+
Assert.ThrowsAny<Exception>(
27+
() => EventSourceValidator.ValidateEventSourceIds<MismatchedIdEventSource>());
28+
}
29+
30+
[Fact]
31+
public void ValidateEventSourceIds_FailsForDuplicateEventIds()
32+
{
33+
// The duplicate ID message is produced by our validator code.
34+
var ex = Assert.ThrowsAny<Exception>(
35+
() => EventSourceValidator.ValidateEventSourceIds<DuplicateIdEventSource>());
36+
37+
Assert.Contains("Duplicate EventId 1", ex.Message);
38+
Assert.Contains("EventAlpha", ex.Message);
39+
Assert.Contains("EventBeta", ex.Message);
40+
}
41+
42+
[Fact]
43+
public void ValidateEventSourceIds_FailsForNonEventSourceType()
44+
{
45+
// The guard clause message is produced by our validator code.
46+
var ex = Assert.ThrowsAny<Exception>(
47+
() => EventSourceValidator.ValidateEventSourceIds(typeof(string)));
48+
49+
Assert.Contains("does not derive from EventSource", ex.Message);
50+
}
51+
52+
[Fact]
53+
public void ValidateEventSourceIds_PassesForEventSourceWithNoEvents()
54+
{
55+
EventSourceValidator.ValidateEventSourceIds<EmptyEventSource>();
56+
}
57+
58+
[Fact]
59+
public void ValidateEventSourceIds_PassesForEventSourceWithMultipleParameterTypes()
60+
{
61+
EventSourceValidator.ValidateEventSourceIds<MultiParamEventSource>();
62+
}
63+
64+
// -- Test-only EventSource implementations --
65+
66+
[EventSource(Name = "Test-Correct")]
67+
private sealed class CorrectEventSource : EventSource
68+
{
69+
[Event(1, Level = EventLevel.Informational)]
70+
public void EventOne(string message) => WriteEvent(1, message);
71+
72+
[Event(2, Level = EventLevel.Verbose)]
73+
public void EventTwo(int count) => WriteEvent(2, count);
74+
75+
[Event(3, Level = EventLevel.Warning)]
76+
public void EventThree() => WriteEvent(3);
77+
}
78+
79+
[EventSource(Name = "Test-MismatchedId")]
80+
private sealed class MismatchedIdEventSource : EventSource
81+
{
82+
[Event(1, Level = EventLevel.Informational)]
83+
public void EventOne(int value) => WriteEvent(99, value);
84+
}
85+
86+
[EventSource(Name = "Test-DuplicateId")]
87+
private sealed class DuplicateIdEventSource : EventSource
88+
{
89+
[Event(1, Level = EventLevel.Informational)]
90+
public void EventAlpha(string message) => WriteEvent(1, message);
91+
92+
[Event(1, Level = EventLevel.Informational)]
93+
public void EventBeta(int count) => WriteEvent(1, count);
94+
}
95+
96+
[EventSource(Name = "Test-Empty")]
97+
private sealed class EmptyEventSource : EventSource
98+
{
99+
}
100+
101+
[EventSource(Name = "Test-MultiParam")]
102+
private sealed class MultiParamEventSource : EventSource
103+
{
104+
[Event(1, Level = EventLevel.Informational)]
105+
public void EventWithString(string value) => WriteEvent(1, value);
106+
107+
[Event(2, Level = EventLevel.Informational)]
108+
public void EventWithInt(int value) => WriteEvent(2, value);
109+
110+
[Event(3, Level = EventLevel.Informational)]
111+
public void EventWithLong(long value) => WriteEvent(3, value);
112+
113+
[Event(4, Level = EventLevel.Informational)]
114+
public void EventWithMultiple(string name, int count) => WriteEvent(4, name, count);
115+
116+
[Event(5, Level = EventLevel.Informational)]
117+
public void EventWithNoArgs() => WriteEvent(5);
118+
}
119+
}

0 commit comments

Comments
 (0)