Git 客户端,采用 C# 编写。
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 

129 lines
5.1 KiB

// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Collections.Concurrent;
using System.Diagnostics;
namespace System
{
/// <summary>
/// Use (within a using) to eat asserts.
/// </summary>
public sealed class NoAssertContext : IDisposable
{
// For any given thread we don't need to lock to decide how to route messages, as any messages for that
// given thread will not happen while we're in the constructor or dispose method on that thread. That
// means we can safely check to see if we've hooked our thread without locking (outside of using a
// concurrent collection to make sure the collection is in a known state).
//
// We do, however need to lock around hooking/unhooking our custom listener to make sure that we
// are rerouting correctly if multiple threads are creating/disposing this class concurrently.
#pragma warning disable SA1308 // Variable names should not be prefixed
private static readonly object s_lock = new();
private static bool s_hooked;
private static readonly ConcurrentDictionary<int, int> s_suppressedThreads = new();
// "Default" is the listener that terminates the process when debug assertions fail.
private static readonly TraceListener s_defaultListener = Trace.Listeners["Default"];
private static readonly NoAssertListener s_noAssertListener = new();
#pragma warning restore SA1308 // Variable names should not be prefixed
public NoAssertContext()
{
s_suppressedThreads.AddOrUpdate(Thread.CurrentThread.ManagedThreadId, 1, (key, oldValue) => oldValue + 1);
// Lock to make sure we are hooked properly if two threads come into the constructor/dispose at the same time.
lock (s_lock)
{
if (!s_hooked)
{
// Hook our custom listener first so we don't lose assertions from other threads when
// we disconnect the default listener.
Trace.Listeners.Add(s_noAssertListener);
Trace.Listeners.Remove(s_defaultListener);
s_hooked = true;
}
}
}
public void Dispose()
{
GC.SuppressFinalize(this);
int currentThread = Thread.CurrentThread.ManagedThreadId;
if (s_suppressedThreads.TryRemove(currentThread, out int count))
{
if (count > 1)
{
// We're in a nested assert context on a given thread, re-add with a decremented count.
// This doesn't need to be atomic as we're currently on the thread that would care about
// being rerouted.
s_suppressedThreads.TryAdd(currentThread, --count);
}
}
lock (s_lock)
{
if (s_hooked && s_suppressedThreads.Count == 0)
{
// We're the first to hit the need to unhook. Add the default listener back first to
// ensure we don't lose any asserts from other threads.
Trace.Listeners.Add(s_defaultListener);
Trace.Listeners.Remove(s_noAssertListener);
s_hooked = false;
}
}
}
~NoAssertContext()
{
// We need this class to be used in a using to effectively rationalize about a test.
throw new InvalidOperationException($"Did not dispose {nameof(NoAssertContext)}");
}
private class NoAssertListener : TraceListener
{
public NoAssertListener()
: base(typeof(NoAssertListener).FullName)
{
}
public override void Fail(string message)
{
if (!s_suppressedThreads.TryGetValue(Thread.CurrentThread.ManagedThreadId, out _))
{
s_defaultListener.Fail(message);
}
}
public override void Fail(string message, string detailMessage)
{
if (!s_suppressedThreads.TryGetValue(Thread.CurrentThread.ManagedThreadId, out _))
{
s_defaultListener.Fail(message, detailMessage);
}
}
// Write and WriteLine are virtual
public override void Write(string message)
{
if (!s_suppressedThreads.TryGetValue(Thread.CurrentThread.ManagedThreadId, out _))
{
s_defaultListener.Write(message);
}
}
public override void WriteLine(string message)
{
if (!s_suppressedThreads.TryGetValue(Thread.CurrentThread.ManagedThreadId, out _))
{
s_defaultListener.WriteLine(message);
}
}
}
}
}