Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
c36f285
wip
Feb 12, 2025
c248a7c
Merge remote-tracking branch 'orig-dotnet-extensions/main' into twies…
Mar 21, 2025
1b4ccdf
refactor
Mar 24, 2025
2ae8401
Merge remote-tracking branch 'orig-dotnet-extensions/main' into twies…
Mar 24, 2025
721ce3e
we might actually need timeout too
Mar 24, 2025
233e23a
WIP
Mar 24, 2025
38c1d9a
timeout capability
Mar 31, 2025
84a5a3c
separate test cases into separate tests
Mar 31, 2025
32d426e
cleanup
Mar 31, 2025
27142fb
Merge remote-tracking branch 'orig-dotnet-extensions/main' into twies…
Mar 31, 2025
1445427
Merge remote-tracking branch 'orig-dotnet-extensions/main'
Mar 31, 2025
26c6852
Merge branch 'main' into twiesner/5752_FakeLogCollector_waiting_diffe…
Mar 31, 2025
562db95
cleanup
Mar 31, 2025
aa9be3f
stackallock threshold + test run infrastructure timeout
May 15, 2025
9332616
fake time provider based tests instead of real time
May 16, 2025
45d9eaf
enriching test cases by logs before awaiting and between interesting …
May 20, 2025
4dec848
Merge remote-tracking branch 'dotnet-extensions/main' into twiesner/5…
Aug 18, 2025
985ed46
continue with enumeration
Aug 20, 2025
5ece729
wip
Aug 20, 2025
64a2775
wip
Aug 20, 2025
da4dc79
wip
Aug 21, 2025
efc4324
Merge branch 'twiesner/5752_refactors' into twiesner/5752_FakeLogColl…
Aug 21, 2025
367bae6
removing previous implementation of easier review
Aug 21, 2025
eb342f3
old waiting logic removal
Aug 21, 2025
7c4b5d6
minor test cleanup
Aug 21, 2025
a92bf7a
using retrieved index in test
Aug 21, 2025
a6f4f2e
guarding against concurrent movenextasync calls
Aug 21, 2025
1cc522a
minor tweaks
Aug 21, 2025
9c79cda
wip
Aug 21, 2025
b2fe7ee
adjust enumerator index on clear
Aug 21, 2025
1c4c5a9
demo tests
Aug 21, 2025
3c9c668
extend demo test to better illustrate
Aug 22, 2025
2d95552
remove timeout param
Aug 26, 2025
75b5153
remove startingIndex param support
Aug 26, 2025
77d6993
discarding the Clear(count) functionality
Aug 26, 2025
fd74ce6
count param
Aug 28, 2025
9286930
issue with concurrent Clear call and maxItems
Aug 28, 2025
c1baae9
count removed
Sep 8, 2025
9a358ce
minor changes
Sep 8, 2025
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,189 @@
// 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.Threading;
using System.Threading.Tasks;

namespace Microsoft.Extensions.Logging.Testing;

public partial class FakeLogCollector
{
private int _recordCollectionVersion;

private TaskCompletionSource<object?> _logEnumerationSharedWaiter =
new(TaskCreationOptions.RunContinuationsAsynchronously);

private int _waitingEnumeratorCount;

public IAsyncEnumerable<FakeLogRecord> GetLogsAsync(CancellationToken cancellationToken = default)

Check failure on line 20 in src/Libraries/Microsoft.Extensions.Diagnostics.Testing/Logging/FakeLogCollector.LogEnumeration.cs

View check run for this annotation

Azure Pipelines / extensions-ci (Correctness WarningsCheck)

src/Libraries/Microsoft.Extensions.Diagnostics.Testing/Logging/FakeLogCollector.LogEnumeration.cs#L20

src/Libraries/Microsoft.Extensions.Diagnostics.Testing/Logging/FakeLogCollector.LogEnumeration.cs(20,44): error CS1591: (NETCORE_ENGINEERING_TELEMETRY=Build) Missing XML comment for publicly visible type or member 'FakeLogCollector.GetLogsAsync(CancellationToken)'

Check failure on line 20 in src/Libraries/Microsoft.Extensions.Diagnostics.Testing/Logging/FakeLogCollector.LogEnumeration.cs

View check run for this annotation

Azure Pipelines / extensions-ci (Correctness WarningsCheck)

src/Libraries/Microsoft.Extensions.Diagnostics.Testing/Logging/FakeLogCollector.LogEnumeration.cs#L20

src/Libraries/Microsoft.Extensions.Diagnostics.Testing/Logging/FakeLogCollector.LogEnumeration.cs(20,44): error SA1600: (NETCORE_ENGINEERING_TELEMETRY=Build) Elements should be documented (https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1600.md)

Check failure on line 20 in src/Libraries/Microsoft.Extensions.Diagnostics.Testing/Logging/FakeLogCollector.LogEnumeration.cs

View check run for this annotation

Azure Pipelines / extensions-ci

src/Libraries/Microsoft.Extensions.Diagnostics.Testing/Logging/FakeLogCollector.LogEnumeration.cs#L20

src/Libraries/Microsoft.Extensions.Diagnostics.Testing/Logging/FakeLogCollector.LogEnumeration.cs(20,44): error CS1591: (NETCORE_ENGINEERING_TELEMETRY=Build) Missing XML comment for publicly visible type or member 'FakeLogCollector.GetLogsAsync(CancellationToken)'

Check failure on line 20 in src/Libraries/Microsoft.Extensions.Diagnostics.Testing/Logging/FakeLogCollector.LogEnumeration.cs

View check run for this annotation

Azure Pipelines / extensions-ci

src/Libraries/Microsoft.Extensions.Diagnostics.Testing/Logging/FakeLogCollector.LogEnumeration.cs#L20

src/Libraries/Microsoft.Extensions.Diagnostics.Testing/Logging/FakeLogCollector.LogEnumeration.cs(20,44): error SA1600: (NETCORE_ENGINEERING_TELEMETRY=Build) Elements should be documented (https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1600.md)
=> new LogAsyncEnumerable(this, cancellationToken);

private class LogAsyncEnumerable : IAsyncEnumerable<FakeLogRecord>
{
private readonly FakeLogCollector _collector;
private readonly CancellationToken _enumerableCancellationToken;

internal LogAsyncEnumerable(
FakeLogCollector collector,
CancellationToken enumerableCancellationToken)
{
_collector = collector;
_enumerableCancellationToken = enumerableCancellationToken;
}

public IAsyncEnumerator<FakeLogRecord> GetAsyncEnumerator(
CancellationToken enumeratorCancellationToken = default)
=> new StreamEnumerator(_collector, _enumerableCancellationToken, enumeratorCancellationToken);
}

private sealed class StreamEnumerator : IAsyncEnumerator<FakeLogRecord>
{
private readonly FakeLogCollector _collector;
private readonly CancellationTokenSource _mainCts;

private FakeLogRecord? _current;
private int _index;
private bool _disposed;
private int _observedRecordCollectionVersion;

// Concurrent MoveNextAsync guard
private int _moveNextActive; // 0 = inactive, 1 = active (int type used for net462 compatibility)

public StreamEnumerator(
FakeLogCollector collector,
CancellationToken enumerableCancellationToken,
CancellationToken enumeratorCancellationToken)
{
_collector = collector;
_mainCts = enumerableCancellationToken.CanBeCanceled || enumeratorCancellationToken.CanBeCanceled
? CancellationTokenSource.CreateLinkedTokenSource([enumerableCancellationToken, enumeratorCancellationToken])
: new CancellationTokenSource();
_observedRecordCollectionVersion = collector._recordCollectionVersion;
}

public FakeLogRecord Current => _current ?? throw new InvalidOperationException("Enumeration not started.");

public async ValueTask<bool> MoveNextAsync()
{
if (Interlocked.CompareExchange(ref _moveNextActive, 1, 0) == 1)
{
throw new InvalidOperationException("MoveNextAsync is already in progress. Concurrent calls are not allowed.");
}

try
{
ThrowIfDisposed();

var masterCancellationToken = _mainCts.Token;

masterCancellationToken.ThrowIfCancellationRequested();

while (true)
{
TaskCompletionSource<object?>? waiter = null;

try
{
masterCancellationToken.ThrowIfCancellationRequested();

lock (_collector._records)
{
if (_observedRecordCollectionVersion != _collector._recordCollectionVersion)
{
_index = 0; // based on assumption that version changed on full collection clear
_observedRecordCollectionVersion = _collector._recordCollectionVersion;
}

if (_index < _collector._records.Count)
{
_current = _collector._records[_index++];
return true;
}

// waiter needs to be subscribed within records lock
// if not: more records could be added in the meantime and the waiter could be stuck waiting even though the index is behind the actual count
waiter = _collector._logEnumerationSharedWaiter;
_collector._waitingEnumeratorCount++;
}

// Compatibility path for net462: emulate Task.WaitAsync(cancellationToken).
// After the wait is complete in normal flow, no need to decrement because the shared waiter will be swapped and counter reset.
await AwaitWithCancellationAsync(waiter.Task, masterCancellationToken).ConfigureAwait(false);

Check failure on line 113 in src/Libraries/Microsoft.Extensions.Diagnostics.Testing/Logging/FakeLogCollector.LogEnumeration.cs

View check run for this annotation

Azure Pipelines / extensions-ci (Correctness WarningsCheck)

src/Libraries/Microsoft.Extensions.Diagnostics.Testing/Logging/FakeLogCollector.LogEnumeration.cs#L113

src/Libraries/Microsoft.Extensions.Diagnostics.Testing/Logging/FakeLogCollector.LogEnumeration.cs(113,58): error VSTHRD003: (NETCORE_ENGINEERING_TELEMETRY=Build) Avoid awaiting or returning a Task representing work that was not started within your context as that can lead to deadlocks.

Check failure on line 113 in src/Libraries/Microsoft.Extensions.Diagnostics.Testing/Logging/FakeLogCollector.LogEnumeration.cs

View check run for this annotation

Azure Pipelines / extensions-ci

src/Libraries/Microsoft.Extensions.Diagnostics.Testing/Logging/FakeLogCollector.LogEnumeration.cs#L113

src/Libraries/Microsoft.Extensions.Diagnostics.Testing/Logging/FakeLogCollector.LogEnumeration.cs(113,58): error VSTHRD003: (NETCORE_ENGINEERING_TELEMETRY=Build) Avoid awaiting or returning a Task representing work that was not started within your context as that can lead to deadlocks.
}
catch (OperationCanceledException)
{
if (waiter is not null)
{
lock (_collector._records)
{
if (
_collector._waitingEnumeratorCount > 0 // counter can be zero during the cancellation path
&& waiter == _collector._logEnumerationSharedWaiter // makes sure we adjust the counter for the same shared waiting session
)
{
_collector._waitingEnumeratorCount--;
}
}
}

throw;
}
}

}
finally
{
Volatile.Write(ref _moveNextActive, 0);
}
}

public ValueTask DisposeAsync()
{
if (_disposed)
{
return default;
}

_disposed = true;

_mainCts.Cancel();

Check failure on line 151 in src/Libraries/Microsoft.Extensions.Diagnostics.Testing/Logging/FakeLogCollector.LogEnumeration.cs

View check run for this annotation

Azure Pipelines / extensions-ci (Correctness WarningsCheck)

src/Libraries/Microsoft.Extensions.Diagnostics.Testing/Logging/FakeLogCollector.LogEnumeration.cs#L151

src/Libraries/Microsoft.Extensions.Diagnostics.Testing/Logging/FakeLogCollector.LogEnumeration.cs(151,13): error CA1849: (NETCORE_ENGINEERING_TELEMETRY=Build) 'CancellationTokenSource.Cancel()' synchronously blocks. Await 'CancellationTokenSource.CancelAsync()' instead. (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1849)

Check failure on line 151 in src/Libraries/Microsoft.Extensions.Diagnostics.Testing/Logging/FakeLogCollector.LogEnumeration.cs

View check run for this annotation

Azure Pipelines / extensions-ci

src/Libraries/Microsoft.Extensions.Diagnostics.Testing/Logging/FakeLogCollector.LogEnumeration.cs#L151

src/Libraries/Microsoft.Extensions.Diagnostics.Testing/Logging/FakeLogCollector.LogEnumeration.cs(151,13): error CA1849: (NETCORE_ENGINEERING_TELEMETRY=Build) 'CancellationTokenSource.Cancel()' synchronously blocks. Await 'CancellationTokenSource.CancelAsync()' instead. (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1849)
_mainCts.Dispose();

return default;
}

private static async Task AwaitWithCancellationAsync(Task task, CancellationToken cancellationToken)
{
if (!cancellationToken.CanBeCanceled || task.IsCompleted)
{
await task.ConfigureAwait(false);

Check failure on line 161 in src/Libraries/Microsoft.Extensions.Diagnostics.Testing/Logging/FakeLogCollector.LogEnumeration.cs

View check run for this annotation

Azure Pipelines / extensions-ci (Correctness WarningsCheck)

src/Libraries/Microsoft.Extensions.Diagnostics.Testing/Logging/FakeLogCollector.LogEnumeration.cs#L161

src/Libraries/Microsoft.Extensions.Diagnostics.Testing/Logging/FakeLogCollector.LogEnumeration.cs(161,23): error VSTHRD003: (NETCORE_ENGINEERING_TELEMETRY=Build) Avoid awaiting or returning a Task representing work that was not started within your context as that can lead to deadlocks.
return;
}

var cancelTcs = new TaskCompletionSource<bool>(TaskCreationOptions.RunContinuationsAsynchronously);
CancellationTokenRegistration ctr = default;
try
{
ctr = cancellationToken.Register(static s =>
((TaskCompletionSource<bool>)s!).TrySetCanceled(), cancelTcs);

var completed = await Task.WhenAny(task, cancelTcs.Task).ConfigureAwait(false);
await completed.ConfigureAwait(false);
}
finally
{
ctr.Dispose();
}
}

private void ThrowIfDisposed()
{
if (_disposed)
{
throw new ObjectDisposedException(nameof(StreamEnumerator));
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Threading.Tasks;
using Microsoft.Extensions.Options;
using Microsoft.Shared.Diagnostics;

Expand All @@ -14,7 +15,7 @@ namespace Microsoft.Extensions.Logging.Testing;
/// </summary>
[DebuggerDisplay("Count = {Count}, LatestRecord = {LatestRecord}")]
[DebuggerTypeProxy(typeof(FakeLogCollectorDebugView))]
public class FakeLogCollector
public partial class FakeLogCollector
{
private readonly List<FakeLogRecord> _records = [];
private readonly FakeLogCollectorOptions _options;
Expand Down Expand Up @@ -54,6 +55,7 @@ public void Clear()
lock (_records)
{
_records.Clear();
_recordCollectionVersion++;
}
}

Expand Down Expand Up @@ -136,11 +138,23 @@ internal void AddRecord(FakeLogRecord record)
return;
}

TaskCompletionSource<object?>? logEnumerationSharedWaiterToWake = null;

lock (_records)
{
_records.Add(record);

if (_waitingEnumeratorCount > 0)
{
logEnumerationSharedWaiterToWake = _logEnumerationSharedWaiter;
_logEnumerationSharedWaiter = new TaskCompletionSource<object?>(TaskCreationOptions.RunContinuationsAsynchronously);
_waitingEnumeratorCount = 0;
}
}

// it is possible the task was already completed, but it does not matter and we can avoid locking
_ = logEnumerationSharedWaiterToWake?.TrySetResult(null);

_options.OutputSink?.Invoke(_options.OutputFormatter(record));
}

Expand Down
Loading
Loading