File: HotReloadAgentTest.cs
Web Access
Project: ..\..\..\test\Microsoft.Extensions.DotNetDeltaApplier.Tests\Microsoft.Extensions.DotNetDeltaApplier.Tests.csproj (Microsoft.Extensions.DotNetDeltaApplier.Tests)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System.Reflection;
using Microsoft.DotNet.HotReload;
using Moq;
 
namespace Microsoft.DotNet.Watch.UnitTests
{
    public class HotReloadAgentTest
    {
        [Fact]
        public void ClearHotReloadEnvironmentVariables_ClearsStartupHook()
        {
            Assert.Equal("",
                HotReloadAgent.RemoveCurrentAssembly(typeof(StartupHook), typeof(StartupHook).Assembly.Location));
        }
 
        [Fact]
        public void ClearHotReloadEnvironmentVariables_PreservedOtherStartupHooks()
        {
            var customStartupHook = "/path/mycoolstartup.dll";
            Assert.Equal(customStartupHook,
                HotReloadAgent.RemoveCurrentAssembly(typeof(StartupHook), typeof(StartupHook).Assembly.Location + Path.PathSeparator + customStartupHook));
        }
 
        [PlatformSpecificFact(TestPlatforms.Windows)]
        public void ClearHotReloadEnvironmentVariables_RemovesHotReloadStartup_InCaseInvariantManner()
        {
            var customStartupHook = "/path/mycoolstartup.dll";
            Assert.Equal(customStartupHook,
                HotReloadAgent.RemoveCurrentAssembly(typeof(StartupHook), customStartupHook + Path.PathSeparator + typeof(StartupHook).Assembly.Location.ToUpperInvariant()));
        }
 
        [Fact]
        public void TopologicalSort_Works()
        {
            // Arrange
            var assembly1 = GetAssembly("System.Private.CoreLib", Array.Empty<AssemblyName>());
            var assembly2 = GetAssembly("System.Text.Json", new[] { new AssemblyName("System.Private.CoreLib"), });
            var assembly3 = GetAssembly("Microsoft.AspNetCore.Components", new[] { new AssemblyName("System.Private.CoreLib"), });
            var assembly4 = GetAssembly("Microsoft.AspNetCore.Components.Web", new[] { new AssemblyName("Microsoft.AspNetCore.Components"), new AssemblyName("System.Text.Json"), });
 
            var sortedList = MetadataUpdateHandlerInvoker.TopologicalSort(new[] { assembly2, assembly4, assembly1, assembly3 });
 
            // Assert
            Assert.Equal(new[] { assembly1, assembly2, assembly3, assembly4 }, sortedList);
        }
 
        [Fact]
        public void TopologicalSort_IgnoresUnknownReferencedAssemblies()
        {
            // Arrange
            var assembly1 = GetAssembly("System.Private.CoreLib", Array.Empty<AssemblyName>());
            var assembly2 = GetAssembly("System.Text.Json", new[] { new AssemblyName("netstandard"), new AssemblyName("System.Private.CoreLib"), });
            var assembly3 = GetAssembly("Microsoft.AspNetCore.Components", new[] { new AssemblyName("System.Private.CoreLib"), new AssemblyName("Microsoft.Extensions.DependencyInjection"), });
            var assembly4 = GetAssembly("Microsoft.AspNetCore.Components.Web", new[] { new AssemblyName("Microsoft.AspNetCore.Components"), new AssemblyName("System.Text.Json"), });
 
            var sortedList = MetadataUpdateHandlerInvoker.TopologicalSort(new[] { assembly2, assembly4, assembly1, assembly3 });
 
            // Assert
            Assert.Equal(new[] { assembly1, assembly2, assembly3, assembly4 }, sortedList);
        }
 
        [Fact]
        public void TopologicalSort_WithCycles()
        {
            // Arrange
            var assembly1 = GetAssembly("System.Private.CoreLib", Array.Empty<AssemblyName>());
            var assembly2 = GetAssembly("System.Text.Json", new[] { new AssemblyName("System.Collections.Immutable"), new AssemblyName("System.Private.CoreLib"), });
            var assembly3 = GetAssembly("System.Collections.Immutable", new[] { new AssemblyName("System.Text.Json"), new AssemblyName("System.Private.CoreLib"), });
            var assembly4 = GetAssembly("Microsoft.AspNetCore.Components", new[] { new AssemblyName("System.Private.CoreLib"), new AssemblyName("Microsoft.Extensions.DependencyInjection"), });
            var assembly5 = GetAssembly("Microsoft.AspNetCore.Components.Web", new[] { new AssemblyName("Microsoft.AspNetCore.Components"), new AssemblyName("System.Text.Json"), });
 
            var sortedList = MetadataUpdateHandlerInvoker.TopologicalSort(new[] { assembly2, assembly4, assembly1, assembly3, assembly5 });
 
            // Assert
            Assert.Equal(new[] { assembly1, assembly3, assembly2, assembly4, assembly5 }, sortedList);
        }
 
        [Theory]
        [InlineData(typeof(HandlerWithClearCache))]
        [InlineData(typeof(HandlerWithUpdateApplication))]
        [InlineData(typeof(HandlerWithUpdateContent))]
        public void GetHandlerActions_SingleAction(Type handlerType)
        {
            var reporter = new AgentReporter();
            var invoker = new MetadataUpdateHandlerInvoker(reporter);
            var actions = invoker.GetUpdateHandlerActions([handlerType]);
 
            Assert.Empty(reporter.GetAndClearLogEntries(ResponseLoggingLevel.Verbose));
 
            if (handlerType == typeof(HandlerWithUpdateContent))
            {
                Assert.Single(actions.UpdateContentHandlers);
                Assert.Empty(actions.ClearCacheHandlers);
                Assert.Empty(actions.UpdateApplicationHandlers);
            }
            else if (handlerType == typeof(HandlerWithUpdateApplication))
            {
                Assert.Single(actions.UpdateApplicationHandlers);
                Assert.Empty(actions.ClearCacheHandlers);
                Assert.Empty(actions.UpdateContentHandlers);
            }
            else if (handlerType == typeof(HandlerWithClearCache))
            {
                Assert.Single(actions.ClearCacheHandlers);
                Assert.Empty(actions.UpdateContentHandlers);
                Assert.Empty(actions.UpdateApplicationHandlers);
            }
        }
 
        [Fact]
        public void GetHandlerActions_DiscoversActionsOnTypeWithAllActions()
        {
            var reporter = new AgentReporter();
            var invoker = new MetadataUpdateHandlerInvoker(reporter);
            var actions = invoker.GetUpdateHandlerActions([typeof(HandlerWithAllActions)]);
 
            AssertEx.Empty(reporter.GetAndClearLogEntries(ResponseLoggingLevel.Verbose));
            Assert.Equal(typeof(HandlerWithAllActions).GetMethod("ClearCache", BindingFlags.Static | BindingFlags.NonPublic), actions.ClearCacheHandlers.Single().Method);
            Assert.Equal(typeof(HandlerWithAllActions).GetMethod("UpdateApplication", BindingFlags.Static | BindingFlags.NonPublic), actions.UpdateApplicationHandlers.Single().Method);
            Assert.Equal(typeof(HandlerWithAllActions).GetMethod("UpdateContent", BindingFlags.Static | BindingFlags.NonPublic), actions.UpdateContentHandlers.Single().Method);
        }
 
        [Fact]
        public void GetHandlerActions_LogsMessageIfMethodHasIncorrectSignature()
        {
            var reporter = new AgentReporter();
            var invoker = new MetadataUpdateHandlerInvoker(reporter);
 
            var handlerType = typeof(HandlerWithIncorrectSignature);
            var actions = invoker.GetUpdateHandlerActions([handlerType]);
 
            var log = reporter.GetAndClearLogEntries(ResponseLoggingLevel.WarningsAndErrors);
            AssertEx.SequenceEqual(
            [
                $"Warning: Type '{handlerType}' has method 'Void ClearCache()' that does not match the required signature.",
                $"Warning: Type '{handlerType}' has method 'Void UpdateContent()' that does not match the required signature."
            ],
            log.Select(e => $"{e.severity}: {e.message}"));
 
            Assert.Empty(actions.ClearCacheHandlers);
            Assert.Empty(actions.UpdateContentHandlers);
            Assert.Single(actions.UpdateApplicationHandlers);
        }
 
        [Fact]
        public void GetHandlerActions_LogsMessageIfNoActionsAreDiscovered()
        {
            var reporter = new AgentReporter();
            var invoker = new MetadataUpdateHandlerInvoker(reporter);
 
            var handlerType = typeof(HandlerWithNoActions);
            var actions = invoker.GetUpdateHandlerActions([handlerType]);
 
            var log = reporter.GetAndClearLogEntries(ResponseLoggingLevel.WarningsAndErrors);
            var logEntry = Assert.Single(log);
            Assert.Equal(
                $"Expected to find a static method 'ClearCache', 'UpdateApplication' or 'UpdateContent' on type '{handlerType.AssemblyQualifiedName}' but neither exists.", logEntry.message);
 
            Assert.Equal(AgentMessageSeverity.Warning, logEntry.severity);
            Assert.Empty(actions.ClearCacheHandlers);
            Assert.Empty(actions.UpdateApplicationHandlers);
        }
 
        private static Assembly GetAssembly(string fullName, AssemblyName[] dependencies)
        {
            var assembly = new Mock<Assembly>();
            assembly.Setup(a => a.GetName()).Returns(new AssemblyName(fullName));
            assembly.SetupGet(a => a.FullName).Returns(fullName);
            assembly.Setup(a => a.GetReferencedAssemblies()).Returns(dependencies);
            assembly.Setup(a => a.ToString()).Returns(fullName);
            return assembly.Object;
        }
 
        private class HandlerWithClearCache
        {
            internal static void ClearCache(Type[]? _) { }
        }
 
        private class HandlerWithUpdateApplication
        {
            internal static void UpdateApplication(Type[]? _) { }
        }
 
        private class HandlerWithUpdateContent
        {
            public static void UpdateContent(string assemblyName, bool isApplicationProject, string relativePath, byte[] contents) { }
        }
 
        private class HandlerWithAllActions
        {
            internal static void ClearCache(Type[]? _) { }
            internal static void UpdateApplication(Type[]? _) { }
            internal static void UpdateContent(string assemblyName, bool isApplicationProject, string relativePath, byte[] contents) { }
        }
 
        private class HandlerWithIncorrectSignature
        {
            internal static void ClearCache() { }   
            internal static void UpdateContent() { }
            internal static void UpdateApplication(Type[]? _) { }
        }
 
        private class HandlerWithNoActions
        {
            internal static void SomeMethod() { }
        }
    }
}