diff --git a/src/Moq/AmbientObserver.cs b/src/Moq/AmbientObserver.cs index 99ce144b9..3cd6424b5 100644 --- a/src/Moq/AmbientObserver.cs +++ b/src/Moq/AmbientObserver.cs @@ -31,21 +31,36 @@ namespace Moq internal sealed class AmbientObserver : IDisposable { [ThreadStatic] - private static AmbientObserver current; + private static Stack activations; public static AmbientObserver Activate() { - Debug.Assert(current == null); + var activation = new AmbientObserver(); - return current = new AmbientObserver(); + var activations = AmbientObserver.activations; + if (activations == null) + { + AmbientObserver.activations = activations = new Stack(); + } + activations.Push(activation); + + return activation; } public static bool IsActive(out AmbientObserver observer) { - var current = AmbientObserver.current; + var activations = AmbientObserver.activations; - observer = current; - return current != null; + if (activations != null && activations.Count > 0) + { + observer = activations.Peek(); + return true; + } + else + { + observer = null; + return false; + } } private List observations; @@ -64,7 +79,9 @@ public void Dispose() } } - current = null; + var activations = AmbientObserver.activations; + Debug.Assert(activations != null && activations.Count > 0); + activations.Pop(); } /// diff --git a/tests/Moq.Tests/AmbientObserverFixture.cs b/tests/Moq.Tests/AmbientObserverFixture.cs index 871927381..034757d3a 100644 --- a/tests/Moq.Tests/AmbientObserverFixture.cs +++ b/tests/Moq.Tests/AmbientObserverFixture.cs @@ -7,6 +7,63 @@ namespace Moq.Tests { public class AmbientObserverFixture { + [Fact] + public void Activations_can_be_nested() + { + Assert.False(AmbientObserver.IsActive(out var active)); + using (var outer = AmbientObserver.Activate()) + { + Assert.True(AmbientObserver.IsActive(out active)); + Assert.Same(outer, active); + using (var inner = AmbientObserver.Activate()) + { + Assert.True(AmbientObserver.IsActive(out active)); + Assert.Same(inner, active); + } + Assert.True(AmbientObserver.IsActive(out active)); + Assert.Same(outer, active); + } + Assert.False(AmbientObserver.IsActive(out active)); + } + + [Fact] + public void Nested_observers_do_not_share_their_observations() + { + using (var outer = AmbientObserver.Activate()) + { + _ = CreateMatch(); + Assert.True(outer.LastIsMatch(out var outerLastMatchBeforeInner)); + using (var inner = AmbientObserver.Activate()) + { + // The inner observer should not see the outer observer's match: + Assert.False(inner.LastIsMatch(out _)); + _ = CreateMatch(); + } + // And the outer observer should not see the (disposed) inner observer's match. + // Instead, it should still see the same match as the last one as before `inner`: + Assert.True(outer.LastIsMatch(out var outerLastMatchAfterInnerDisposed)); + Assert.Same(outerLastMatchBeforeInner, outerLastMatchAfterInnerDisposed); + } + } + + [Fact] + public void Nested_observers_when_disposed_dont_interrupt_outer_observers() + { + using (var outer = AmbientObserver.Activate()) + { + using (var inner = AmbientObserver.Activate()) + { + } + _ = CreateMatch(); + Assert.True(outer.LastIsMatch(out var match)); + } + } + + private int CreateMatch() + { + return Match.Create(i => i != 0, () => It.Is(i => i != 0)); + } + [Fact] public void IsActive_returns_false_when_no_AmbientObserver_instantiated() {