Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 28 additions & 1 deletion docs/EventSourceAndCounters.md
Original file line number Diff line number Diff line change
Expand Up @@ -169,7 +169,34 @@ namespace Microsoft.AspNetCore.Authentication.Internal

## Automated Testing of EventSources

EventSources can be tested using the `EventSourceTestBase` base class in `Microsoft.AspNetCore.InternalTesting`. An example test is below:
### Validating Event ID Consistency

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.

Use the `EventSourceValidator` utility in `Microsoft.AspNetCore.InternalTesting.Tracing`:

```csharp
using Microsoft.AspNetCore.InternalTesting.Tracing;

public class MyEventSourceTests
{
[Fact]
public void EventIdsAreConsistent()
{
EventSourceValidator.ValidateEventSourceIds<MyEventSource>();
}
}
```

The validator:
* 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
* Checks for duplicate event IDs across methods

> **Important:** Every new `EventSource` class should include this one-line validation test.
### Functional Testing with EventSourceTestBase

EventSources can also be functionally tested using the `EventSourceTestBase` base class in `Microsoft.AspNetCore.InternalTesting`. An example test is below:

```csharp
// The base class MUST be used for EventSource testing because EventSources are global and parallel tests can cause issues.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,19 @@
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Internal;
using Microsoft.AspNetCore.InternalTesting;
using Microsoft.AspNetCore.InternalTesting.Tracing;
using Microsoft.Extensions.Logging;

namespace Microsoft.AspNetCore.Hosting;

public class HostingEventSourceTests : LoggedTest
{
[Fact]
public void EventIdsAreConsistent()
{
EventSourceValidator.ValidateEventSourceIds(typeof(HostingEventSource));
}

[Fact]
public void MatchesNameAndGuid()
{
Expand Down
13 changes: 13 additions & 0 deletions src/Servers/Kestrel/Core/test/KestrelEventSourceTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
using System.Diagnostics.Tracing;
using System.Globalization;
using System.Reflection;
using Microsoft.AspNetCore.InternalTesting.Tracing;
using Xunit;

namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests;
Expand All @@ -26,4 +27,16 @@ public void ExistsWithCorrectId()
Assert.Equal(Guid.Parse("bdeb4676-a36e-5442-db99-4764e2326c7d", CultureInfo.InvariantCulture), EventSource.GetGuid(esType));
Assert.NotEmpty(EventSource.GenerateManifest(esType, "assemblyPathToIncludeInManifest"));
}

[Fact]
public void EventIdsAreConsistent()
{
var esType = typeof(KestrelServer).Assembly.GetType(
"Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure.KestrelEventSource",
throwOnError: true,
ignoreCase: false
);

EventSourceValidator.ValidateEventSourceIds(esType);
}
}
16 changes: 16 additions & 0 deletions src/Shared/test/Shared.Tests/CertificateManagerEventSourceTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using Microsoft.AspNetCore.Certificates.Generation;
using Microsoft.AspNetCore.InternalTesting.Tracing;

namespace Microsoft.AspNetCore.Internal.Tests;

public class CertificateManagerEventSourceTests
{
[Fact]
public void EventIdsAreConsistent()
{
EventSourceValidator.ValidateEventSourceIds<CertificateManager.CertificateManagerEventSource>();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,20 @@
using System.Diagnostics.Tracing;
using System.Globalization;
using Microsoft.AspNetCore.Internal;
using Microsoft.AspNetCore.InternalTesting.Tracing;
using Microsoft.Extensions.Internal;
using Xunit;

namespace Microsoft.AspNetCore.Http.Connections.Internal;

public class HttpConnectionsEventSourceTests
{
[Fact]
public void EventIdsAreConsistent()
{
EventSourceValidator.ValidateEventSourceIds(typeof(HttpConnectionsEventSource));
}

[Fact]
public void MatchesNameAndGuid()
{
Expand Down
106 changes: 106 additions & 0 deletions src/Testing/src/Tracing/EventSourceValidator.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System;
using System.Collections.Generic;
using System.Diagnostics.Tracing;
using System.Reflection;
using Xunit;

namespace Microsoft.AspNetCore.InternalTesting.Tracing;

/// <summary>
/// Validates that <see cref="EventSource"/>-derived classes have consistent
/// <see cref="EventAttribute.EventId"/> values and <c>WriteEvent</c> call arguments.
/// This catches drift caused by bad merge resolution or missed updates that would
/// otherwise surface only as runtime errors.
/// </summary>
public static class EventSourceValidator
{
/// <summary>
/// Validates all <c>[Event]</c>-attributed methods on <typeparamref name="T"/>.
/// </summary>
/// <typeparam name="T">A type that derives from <see cref="EventSource"/>.</typeparam>
public static void ValidateEventSourceIds<T>() where T : EventSource
=> ValidateEventSourceIds(typeof(T));

/// <summary>
/// Validates all <c>[Event]</c>-attributed methods on the given <see cref="EventSource"/>-derived type.
/// <para>
/// Uses <see cref="EventSource.GenerateManifest(Type, string, EventManifestOptions)"/> with
/// <see cref="EventManifestOptions.Strict"/> to perform IL-level validation that the integer
/// argument passed to each <c>WriteEvent</c> call matches the <c>[Event(id)]</c> attribute
/// on the calling method. This is the same validation the .NET runtime itself uses.
/// </para>
/// <para>
/// Additionally checks for duplicate <see cref="EventAttribute.EventId"/> values across methods.
/// </para>
/// </summary>
/// <param name="eventSourceType">A type that derives from <see cref="EventSource"/>.</param>
public static void ValidateEventSourceIds(Type eventSourceType)
{
if (eventSourceType is null)
{
throw new ArgumentNullException(nameof(eventSourceType));
}

Assert.True(
typeof(EventSource).IsAssignableFrom(eventSourceType),
$"Type '{eventSourceType.FullName}' does not derive from EventSource.");

var errors = new List<string>();

// Check for duplicate Event IDs across methods.
var seenIds = new Dictionary<int, string>();
var methods = eventSourceType.GetMethods(
BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly);

foreach (var method in methods)
{
var eventAttr = method.GetCustomAttribute<EventAttribute>();
if (eventAttr is null)
{
continue;
}

if (seenIds.TryGetValue(eventAttr.EventId, out var existingMethod))
{
errors.Add(
$"Duplicate EventId {eventAttr.EventId}: methods '{existingMethod}' and '{method.Name}' share the same ID.");
}
else
{
seenIds[eventAttr.EventId] = method.Name;
}
}

// Use GenerateManifest with Strict mode to validate that each method's
// WriteEvent(id, ...) call uses an ID that matches its [Event(id)] attribute.
// Internally this uses GetHelperCallFirstArg to IL-inspect the method body
// and extract the integer constant passed to WriteEvent — the same validation
// the .NET runtime performs when constructing an EventSource.
try
{
var manifest = EventSource.GenerateManifest(
eventSourceType,
assemblyPathToIncludeInManifest: "assemblyPathForValidation",
flags: EventManifestOptions.Strict);

if (manifest is null)
{
errors.Add("GenerateManifest returned null, indicating the type is not a valid EventSource.");
}
}
catch (ArgumentException ex)
{
errors.Add(ex.Message);
}

if (errors.Count > 0)
{
Assert.Fail(
$"EventSource '{eventSourceType.FullName}' has event ID validation error(s):" +
Environment.NewLine + string.Join(Environment.NewLine, errors));
}
}
}
119 changes: 119 additions & 0 deletions src/Testing/test/EventSourceValidatorTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System;
using System.Diagnostics.Tracing;
using Microsoft.AspNetCore.InternalTesting.Tracing;
using Xunit;

namespace Microsoft.AspNetCore.InternalTesting;

public class EventSourceValidatorTests
{
[Fact]
public void ValidateEventSourceIds_PassesForCorrectEventSource()
{
EventSourceValidator.ValidateEventSourceIds<CorrectEventSource>();
}

[Fact]
public void ValidateEventSourceIds_FailsForMismatchedWriteEventId()
{
// GenerateManifest(Strict) detects the mismatch via IL inspection
// and our validator surfaces it through Assert.Fail.
// The exact runtime error message varies by .NET version, so we
// only verify the validator rejects the bad source.
Assert.ThrowsAny<Exception>(
() => EventSourceValidator.ValidateEventSourceIds<MismatchedIdEventSource>());
}

[Fact]
public void ValidateEventSourceIds_FailsForDuplicateEventIds()
{
// The duplicate ID message is produced by our validator code.
var ex = Assert.ThrowsAny<Exception>(
() => EventSourceValidator.ValidateEventSourceIds<DuplicateIdEventSource>());

Assert.Contains("Duplicate EventId 1", ex.Message);
Assert.Contains("EventAlpha", ex.Message);
Assert.Contains("EventBeta", ex.Message);
}

[Fact]
public void ValidateEventSourceIds_FailsForNonEventSourceType()
{
// The guard clause message is produced by our validator code.
var ex = Assert.ThrowsAny<Exception>(
() => EventSourceValidator.ValidateEventSourceIds(typeof(string)));

Assert.Contains("does not derive from EventSource", ex.Message);
}

[Fact]
public void ValidateEventSourceIds_PassesForEventSourceWithNoEvents()
{
EventSourceValidator.ValidateEventSourceIds<EmptyEventSource>();
}

[Fact]
public void ValidateEventSourceIds_PassesForEventSourceWithMultipleParameterTypes()
{
EventSourceValidator.ValidateEventSourceIds<MultiParamEventSource>();
}

// -- Test-only EventSource implementations --

[EventSource(Name = "Test-Correct")]
private sealed class CorrectEventSource : EventSource
{
[Event(1, Level = EventLevel.Informational)]
public void EventOne(string message) => WriteEvent(1, message);

[Event(2, Level = EventLevel.Verbose)]
public void EventTwo(int count) => WriteEvent(2, count);

[Event(3, Level = EventLevel.Warning)]
public void EventThree() => WriteEvent(3);
}

[EventSource(Name = "Test-MismatchedId")]
private sealed class MismatchedIdEventSource : EventSource
{
[Event(1, Level = EventLevel.Informational)]
public void EventOne(int value) => WriteEvent(99, value);
}

[EventSource(Name = "Test-DuplicateId")]
private sealed class DuplicateIdEventSource : EventSource
{
[Event(1, Level = EventLevel.Informational)]
public void EventAlpha(string message) => WriteEvent(1, message);

[Event(1, Level = EventLevel.Informational)]
public void EventBeta(int count) => WriteEvent(1, count);
}

[EventSource(Name = "Test-Empty")]
private sealed class EmptyEventSource : EventSource
{
}

[EventSource(Name = "Test-MultiParam")]
private sealed class MultiParamEventSource : EventSource
{
[Event(1, Level = EventLevel.Informational)]
public void EventWithString(string value) => WriteEvent(1, value);

[Event(2, Level = EventLevel.Informational)]
public void EventWithInt(int value) => WriteEvent(2, value);

[Event(3, Level = EventLevel.Informational)]
public void EventWithLong(long value) => WriteEvent(3, value);

[Event(4, Level = EventLevel.Informational)]
public void EventWithMultiple(string name, int count) => WriteEvent(4, name, count);

[Event(5, Level = EventLevel.Informational)]
public void EventWithNoArgs() => WriteEvent(5);
}
}
Loading