30538b7262
Preserve legacy profiler contracts and record expected cancellation below error level.
290 lines
11 KiB
C#
290 lines
11 KiB
C#
namespace HrynCo.Common.Tests;
|
|
|
|
using System.Collections.Concurrent;
|
|
using HrynCo.Common;
|
|
using FluentAssertions;
|
|
using Serilog;
|
|
using Serilog.Core;
|
|
using Serilog.Events;
|
|
using Xunit;
|
|
|
|
public sealed class ProfilerTests
|
|
{
|
|
[Fact]
|
|
public async Task MeasureExecutionAsync_ShouldReturnResultAndWriteStartAndEndEvents()
|
|
{
|
|
var sink = new CollectingSink();
|
|
ILogger logger = new LoggerConfiguration()
|
|
.MinimumLevel.Verbose()
|
|
.WriteTo.Sink(sink)
|
|
.CreateLogger();
|
|
|
|
var profiler = new Profiler(logger);
|
|
|
|
int result = await profiler.MeasureExecutionAsync(async () =>
|
|
{
|
|
await Task.Delay(1);
|
|
return 42;
|
|
}, "LoadItems");
|
|
|
|
result.Should().Be(42);
|
|
sink.Events.Count.Should().BeGreaterThan(1);
|
|
sink.Events.Should().ContainSingle(e =>
|
|
e.Level == LogEventLevel.Information
|
|
&& e.MessageTemplate.Text.Contains("Start", StringComparison.Ordinal)
|
|
&& e.Properties["BlockName"].ToString().Contains("LoadItems", StringComparison.Ordinal));
|
|
sink.Events.Should().ContainSingle(e =>
|
|
e.Level == LogEventLevel.Information
|
|
&& e.MessageTemplate.Text.Contains("End", StringComparison.Ordinal)
|
|
&& e.Properties["Measurements"].ToString().Contains("True", StringComparison.Ordinal));
|
|
}
|
|
|
|
[Fact]
|
|
public async Task MeasureExecutionAsync_ActionOverload_ShouldInvokeAction()
|
|
{
|
|
var sink = new CollectingSink();
|
|
ILogger logger = new LoggerConfiguration()
|
|
.MinimumLevel.Verbose()
|
|
.WriteTo.Sink(sink)
|
|
.CreateLogger();
|
|
|
|
var profiler = new Profiler(logger);
|
|
bool invoked = false;
|
|
|
|
await profiler.MeasureExecutionAsync(async () =>
|
|
{
|
|
invoked = true;
|
|
await Task.CompletedTask;
|
|
}, "ActionBlock");
|
|
|
|
invoked.Should().BeTrue();
|
|
}
|
|
|
|
[Fact]
|
|
public async Task MeasureExecutionAsync_ShouldLogErrorAndRethrow()
|
|
{
|
|
var sink = new CollectingSink();
|
|
ILogger logger = new LoggerConfiguration()
|
|
.MinimumLevel.Verbose()
|
|
.WriteTo.Sink(sink)
|
|
.CreateLogger();
|
|
|
|
var profiler = new Profiler(logger);
|
|
|
|
Func<Task> act = async () =>
|
|
{
|
|
await profiler.MeasureExecutionAsync<int>(() => throw new InvalidOperationException("boom"), "FailBlock");
|
|
};
|
|
|
|
await act.Should().ThrowAsync<InvalidOperationException>();
|
|
sink.Events.Should().Contain(e =>
|
|
e.Level == LogEventLevel.Error
|
|
&& e.MessageTemplate.Text.Contains("An error occurred", StringComparison.Ordinal));
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData(false)]
|
|
[InlineData(true)]
|
|
public async Task MeasureExecutionAsync_WithCanceledToken_ShouldLogCancellationWithoutExceptionAndRethrow(bool actionOverload)
|
|
{
|
|
var sink = new CollectingSink();
|
|
IProfiler profiler = CreateProfiler(sink);
|
|
using var cancellation = new CancellationTokenSource();
|
|
cancellation.Cancel();
|
|
var exception = new OperationCanceledException(cancellation.Token);
|
|
|
|
Func<Task> act = () => MeasureFailureAsync(profiler, actionOverload, exception, cancellation.Token);
|
|
|
|
var assertion = await act.Should().ThrowAsync<OperationCanceledException>();
|
|
assertion.Which.Should().BeSameAs(exception);
|
|
sink.Events.Should().ContainSingle(e =>
|
|
e.Level == LogEventLevel.Debug
|
|
&& e.Exception == null
|
|
&& e.MessageTemplate.Text == "{BlockName} - Canceled"
|
|
&& e.Properties["BlockName"].ToString() == "\"CanceledBlock\""
|
|
&& e.Properties["PerformanceLog"].ToString() == "True");
|
|
sink.Events.Should().NotContain(e => e.Level == LogEventLevel.Error || e.Properties.ContainsKey("Measurements"));
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData(false)]
|
|
[InlineData(true)]
|
|
public async Task MeasureExecutionAsync_WithCanceledToken_ShouldRecognizeTaskCanceledException(bool actionOverload)
|
|
{
|
|
var sink = new CollectingSink();
|
|
IProfiler profiler = CreateProfiler(sink);
|
|
using var cancellation = new CancellationTokenSource();
|
|
cancellation.Cancel();
|
|
var exception = new TaskCanceledException();
|
|
|
|
Func<Task> act = () => MeasureFailureAsync(profiler, actionOverload, exception, cancellation.Token);
|
|
|
|
var assertion = await act.Should().ThrowAsync<TaskCanceledException>();
|
|
assertion.Which.Should().BeSameAs(exception);
|
|
sink.Events.Should().ContainSingle(e => e.Level == LogEventLevel.Debug && e.Exception == null);
|
|
sink.Events.Should().NotContain(e => e.Level == LogEventLevel.Error || e.Properties.ContainsKey("Measurements"));
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData(false, false)]
|
|
[InlineData(false, true)]
|
|
[InlineData(true, false)]
|
|
[InlineData(true, true)]
|
|
public async Task MeasureExecutionAsync_WithUncanceledToken_ShouldLogOperationCanceledExceptionAsError(bool actionOverload, bool tokenCanBeCanceled)
|
|
{
|
|
var sink = new CollectingSink();
|
|
IProfiler profiler = CreateProfiler(sink);
|
|
using var cancellation = new CancellationTokenSource();
|
|
CancellationToken token = tokenCanBeCanceled ? cancellation.Token : CancellationToken.None;
|
|
var exception = new OperationCanceledException();
|
|
|
|
Func<Task> act = () => MeasureFailureAsync(profiler, actionOverload, exception, token);
|
|
|
|
var assertion = await act.Should().ThrowAsync<OperationCanceledException>();
|
|
assertion.Which.Should().BeSameAs(exception);
|
|
sink.Events.Should().ContainSingle(e => e.Level == LogEventLevel.Error && ReferenceEquals(e.Exception, exception));
|
|
sink.Events.Should().NotContain(e => e.Level == LogEventLevel.Debug || e.Properties.ContainsKey("Measurements"));
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData(false)]
|
|
[InlineData(true)]
|
|
public async Task MeasureExecutionAsync_WithCanceledToken_ShouldKeepOtherExceptionsAsErrors(bool actionOverload)
|
|
{
|
|
var sink = new CollectingSink();
|
|
IProfiler profiler = CreateProfiler(sink);
|
|
using var cancellation = new CancellationTokenSource();
|
|
cancellation.Cancel();
|
|
var exception = new InvalidOperationException("boom");
|
|
|
|
Func<Task> act = () => MeasureFailureAsync(profiler, actionOverload, exception, cancellation.Token);
|
|
|
|
var assertion = await act.Should().ThrowAsync<InvalidOperationException>();
|
|
assertion.Which.Should().BeSameAs(exception);
|
|
sink.Events.Should().ContainSingle(e => e.Level == LogEventLevel.Error && ReferenceEquals(e.Exception, exception));
|
|
sink.Events.Should().NotContain(e => e.Level == LogEventLevel.Debug || e.Properties.ContainsKey("Measurements"));
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData(false)]
|
|
[InlineData(true)]
|
|
public async Task MeasureExecutionAsync_WithoutToken_ShouldKeepCancellationAsError(bool actionOverload)
|
|
{
|
|
var sink = new CollectingSink();
|
|
IProfiler profiler = CreateProfiler(sink);
|
|
using var cancellation = new CancellationTokenSource();
|
|
cancellation.Cancel();
|
|
var exception = new OperationCanceledException(cancellation.Token);
|
|
|
|
Func<Task> act = () => actionOverload
|
|
? profiler.MeasureExecutionAsync(() => Task.FromException(exception), "LegacyBlock")
|
|
: profiler.MeasureExecutionAsync(() => Task.FromException<int>(exception), "LegacyBlock");
|
|
|
|
var assertion = await act.Should().ThrowAsync<OperationCanceledException>();
|
|
assertion.Which.Should().BeSameAs(exception);
|
|
sink.Events.Should().ContainSingle(e => e.Level == LogEventLevel.Error && ReferenceEquals(e.Exception, exception));
|
|
sink.Events.Should().NotContain(e => e.Level == LogEventLevel.Debug || e.Properties.ContainsKey("Measurements"));
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData(false)]
|
|
[InlineData(true)]
|
|
public async Task MeasureExecutionAsync_WithCanceledToken_ShouldAllowSuccessfulWork(bool actionOverload)
|
|
{
|
|
var sink = new CollectingSink();
|
|
IProfiler profiler = CreateProfiler(sink);
|
|
using var cancellation = new CancellationTokenSource();
|
|
cancellation.Cancel();
|
|
bool invoked = false;
|
|
|
|
if (actionOverload)
|
|
{
|
|
await profiler.MeasureExecutionAsync(() =>
|
|
{
|
|
invoked = true;
|
|
return Task.CompletedTask;
|
|
}, "SuccessBlock", cancellation.Token);
|
|
invoked.Should().BeTrue();
|
|
}
|
|
else
|
|
{
|
|
int result = await profiler.MeasureExecutionAsync(() => Task.FromResult(42), "SuccessBlock", cancellation.Token);
|
|
result.Should().Be(42);
|
|
}
|
|
|
|
sink.Events.Should().ContainSingle(e => e.Level == LogEventLevel.Information && e.Properties.ContainsKey("Measurements"));
|
|
sink.Events.Should().NotContain(e => e.Level == LogEventLevel.Error || e.Level == LogEventLevel.Debug);
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData(false)]
|
|
[InlineData(true)]
|
|
public async Task MeasureExecutionAsync_WithToken_ShouldSupportLegacyInterfaceImplementations(bool actionOverload)
|
|
{
|
|
var legacyProfiler = new LegacyProfiler();
|
|
IProfiler profiler = legacyProfiler;
|
|
using var cancellation = new CancellationTokenSource();
|
|
cancellation.Cancel();
|
|
bool invoked = false;
|
|
|
|
if (actionOverload)
|
|
{
|
|
await profiler.MeasureExecutionAsync(() =>
|
|
{
|
|
invoked = true;
|
|
return Task.CompletedTask;
|
|
}, "LegacyBlock", cancellation.Token);
|
|
invoked.Should().BeTrue();
|
|
}
|
|
else
|
|
{
|
|
int result = await profiler.MeasureExecutionAsync(() => Task.FromResult(42), "LegacyBlock", cancellation.Token);
|
|
result.Should().Be(42);
|
|
}
|
|
|
|
legacyProfiler.BlockName.Should().Be("LegacyBlock");
|
|
}
|
|
|
|
private sealed class LegacyProfiler : IProfiler
|
|
{
|
|
public string? BlockName { get; private set; }
|
|
|
|
public Task<T> MeasureExecutionAsync<T>(Func<Task<T>> function, string blockName = "")
|
|
{
|
|
BlockName = blockName;
|
|
return function();
|
|
}
|
|
|
|
public Task MeasureExecutionAsync(Func<Task> action, string blockName = "")
|
|
{
|
|
BlockName = blockName;
|
|
return action();
|
|
}
|
|
}
|
|
|
|
private static IProfiler CreateProfiler(CollectingSink sink)
|
|
{
|
|
return new Profiler(new LoggerConfiguration()
|
|
.MinimumLevel.Verbose()
|
|
.WriteTo.Sink(sink)
|
|
.CreateLogger());
|
|
}
|
|
|
|
private static Task MeasureFailureAsync(IProfiler profiler, bool actionOverload, Exception exception, CancellationToken cancellationToken)
|
|
{
|
|
return actionOverload
|
|
? profiler.MeasureExecutionAsync(() => Task.FromException(exception), "CanceledBlock", cancellationToken)
|
|
: profiler.MeasureExecutionAsync(() => Task.FromException<int>(exception), "CanceledBlock", cancellationToken);
|
|
}
|
|
|
|
private sealed class CollectingSink : ILogEventSink
|
|
{
|
|
public ConcurrentQueue<LogEvent> Events { get; } = new();
|
|
|
|
public void Emit(LogEvent logEvent)
|
|
{
|
|
Events.Enqueue(logEvent);
|
|
}
|
|
}
|
|
}
|