File: WorkspaceServiceTests\ReferenceCountedDisposableTests.cs
Web Access
Project: src\src\Workspaces\CoreTest\Microsoft.CodeAnalysis.Workspaces.UnitTests.csproj (Microsoft.CodeAnalysis.Workspaces.UnitTests)
// 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.
 
#nullable disable
 
using System;
using System.Reflection;
using Microsoft.CodeAnalysis.Test.Utilities;
using Roslyn.Utilities;
using Xunit;
 
namespace Microsoft.CodeAnalysis.UnitTests
{
    [Trait(Traits.Feature, Traits.Features.Workspace)]
    public class ReferenceCountedDisposableTests
    {
        [Fact]
        public void TestArgumentValidation()
            => Assert.Throws<ArgumentNullException>("instance", () => new ReferenceCountedDisposable<IDisposable>(null));
 
        [Theory]
        [InlineData(1)]
        [InlineData(3)]
        public void TestSingleReferenceDispose(int disposeCount)
        {
            var target = new DisposableObject();
 
            var reference = new ReferenceCountedDisposable<DisposableObject>(target);
            Assert.Same(target, reference.Target);
            Assert.False(target.IsDisposed);
            Assert.Equal(0, target.DisposeCount);
 
            for (var i = 0; i < disposeCount; i++)
            {
                reference.Dispose();
            }
 
            Assert.Throws<ObjectDisposedException>(() => reference.Target);
            Assert.True(target.IsDisposed);
            Assert.Equal(1, target.DisposeCount);
        }
 
        [Fact]
        public void TestTryAddReferenceFailsAfterDispose()
        {
            var target = new DisposableObject();
 
            var reference = new ReferenceCountedDisposable<DisposableObject>(target);
            reference.Dispose();
 
            Assert.Null(reference.TryAddReference());
        }
 
        [Fact]
        public void TestTryAddReferenceFailsAfterDispose2()
        {
            var target = new DisposableObject();
 
            var reference = new ReferenceCountedDisposable<DisposableObject>(target);
 
            // TryAddReference succeeds before dispose
            var reference2 = reference.TryAddReference();
            Assert.NotNull(reference2);
 
            reference.Dispose();
 
            // TryAddReference fails after dispose, even if another instance is alive
            Assert.Null(reference.TryAddReference());
            Assert.NotNull(reference2.Target);
            Assert.False(target.IsDisposed);
        }
 
        [Fact]
        public void TestOutOfOrderDispose()
        {
            var target = new DisposableObject();
 
            var reference = new ReferenceCountedDisposable<DisposableObject>(target);
            var reference2 = reference.TryAddReference();
            var reference3 = reference2.TryAddReference();
 
            reference2.Dispose();
            Assert.False(target.IsDisposed);
 
            reference3.Dispose();
            Assert.False(target.IsDisposed);
 
            reference.Dispose();
            Assert.True(target.IsDisposed);
            Assert.Equal(1, target.DisposeCount);
        }
 
        [Fact]
        public void TestWeakReferenceLifetime()
        {
            var target = new DisposableObject();
 
            var reference = new ReferenceCountedDisposable<DisposableObject>(target);
            var weakReference = new ReferenceCountedDisposable<DisposableObject>.WeakReference(reference);
 
            var reference2 = reference.TryAddReference();
            Assert.NotNull(reference2);
 
            reference.Dispose();
 
            // TryAddReference fails after dispose for a counted reference
            Assert.Null(reference.TryAddReference());
            Assert.NotNull(reference2.Target);
            Assert.False(target.IsDisposed);
 
            // However, a WeakReference created from the disposed reference can still add a reference
            var reference3 = weakReference.TryAddReference();
            Assert.NotNull(reference3);
 
            reference2.Dispose();
            Assert.False(target.IsDisposed);
 
            reference3.Dispose();
            Assert.True(target.IsDisposed);
        }
 
        [Fact]
        public void TestWeakReferenceArgumentValidation()
            => Assert.Throws<ArgumentNullException>("reference", () => new ReferenceCountedDisposable<IDisposable>.WeakReference(null));
 
        [Fact]
        public void TestDefaultWeakReference()
            => Assert.Null(default(ReferenceCountedDisposable<IDisposable>.WeakReference).TryAddReference());
 
        /// <summary>
        /// This test verifies that a weak reference cannot be created from a disposed reference, even if another strong
        /// reference to the same object is still alive. It specifically covers the case where a weak reference HAS NOT
        /// been created prior to the assertion.
        /// </summary>
        [Fact]
        public void TestWeakReferenceCannotBeCreatedFromDisposedReference_NoPriorWeakReference()
        {
            var target = new DisposableObject();
            var reference = new ReferenceCountedDisposable<DisposableObject>(target);
 
            var secondReference = reference.TryAddReference();
            Assert.NotNull(secondReference);
 
            reference.Dispose();
 
            var weakReference = new ReferenceCountedDisposable<DisposableObject>.WeakReference(reference);
            Assert.Null(weakReference.TryAddReference());
        }
 
        /// <summary>
        /// This test verifies that a weak reference cannot be created from a disposed reference, even if another strong
        /// reference to the same object is still alive. It specifically covers the case where a weak reference HAS been
        /// created prior to the assertion.
        /// </summary>
        [Fact]
        public void TestWeakReferenceCannotBeCreatedFromDisposedReference_WithPriorWeakReference()
        {
            var target = new DisposableObject();
            var reference = new ReferenceCountedDisposable<DisposableObject>(target);
 
            // Create an initial weak reference at a point where the reference is alive. This ensures the internal
            // shared WeakReference<T> is initialized.
            var weakReference = new ReferenceCountedDisposable<DisposableObject>.WeakReference(reference);
            Assert.NotNull(weakReference.TryAddReference());
 
            var secondReference = reference.TryAddReference();
            Assert.NotNull(secondReference);
 
            reference.Dispose();
 
            var secondWeakReference = new ReferenceCountedDisposable<DisposableObject>.WeakReference(reference);
            Assert.Null(secondWeakReference.TryAddReference());
        }
 
        [Fact]
        public void TestWeakReferenceCannotTear()
        {
            // WeakReference contains a single field which is a reference type, so reads/writes cannot tear
            var field = Assert.Single(typeof(ReferenceCountedDisposable<>.WeakReference)
                .GetFields(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic));
 
            Assert.True(field.FieldType.IsClass);
        }
 
        private sealed class DisposableObject : IDisposable
        {
            public bool IsDisposed
            {
                get;
                private set;
            }
 
            public int DisposeCount
            {
                get;
                private set;
            }
 
            public void Dispose()
            {
                IsDisposed = true;
                DisposeCount++;
            }
        }
    }
}