File: HotReloadClientTests.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 Microsoft.DotNet.HotReload;
 
namespace Microsoft.DotNet.Watch.UnitTests;
 
public class HotReloadClientTests(ITestOutputHelper output)
{
    private sealed class Test : IAsyncDisposable
    {
        public readonly TestLogger Logger;
        public readonly TestLogger AgentLogger;
        public readonly DefaultHotReloadClient Client;
        private readonly CancellationTokenSource _cancellationSource;
        private readonly Task<Task> _listenerTaskFactory;
 
        public Test(ITestOutputHelper output, TestHotReloadAgent agent)
        {
            Logger = new TestLogger(output);
            AgentLogger = new TestLogger(output);
            Client = new DefaultHotReloadClient(Logger, AgentLogger, startupHookPath: "", enableStaticAssetUpdates: true);
 
            _cancellationSource = new CancellationTokenSource();
 
            Client.InitiateConnection(CancellationToken.None);
            var listener = new PipeListener(Client.NamedPipeName, agent, log: _ => { }, connectionTimeoutMS: Timeout.Infinite);
            _listenerTaskFactory = Task.Run<Task>(() => listener.Listen(_cancellationSource.Token));
        }
 
        public async ValueTask DisposeAsync()
        {
            _cancellationSource.Cancel();
            await await _listenerTaskFactory;
 
            Client.Dispose();
        }
    }
 
    [Fact]
    public async Task ApplyManagedCodeUpdates_ProcessNotSuspended()
    {
        var moduleId = Guid.NewGuid();
 
        var agent = new TestHotReloadAgent()
        {
            Capabilities = "Baseline AddMethodToExistingType AddStaticFieldToExistingType",
            ApplyManagedCodeUpdatesImpl = updates =>
            {
                Assert.Single(updates);
                var update = updates.First();
                Assert.Equal(moduleId, update.ModuleId);
                AssertEx.SequenceEqual<byte>([1, 2, 3], update.MetadataDelta);
            }
        };
 
        await using var test = new Test(output, agent);
 
        var actualCapabilities = await test.Client.GetUpdateCapabilitiesAsync(CancellationToken.None);
        AssertEx.SequenceEqual(["Baseline", "AddMethodToExistingType", "AddStaticFieldToExistingType"], actualCapabilities);
 
        var update = new HotReloadManagedCodeUpdate(
            moduleId: moduleId,
            metadataDelta: [1, 2, 3],
            ilDelta: [],
            pdbDelta: [],
            updatedTypes: [],
            requiredCapabilities: ["Baseline"]);
 
        Assert.Equal(ApplyStatus.AllChangesApplied, await test.Client.ApplyManagedCodeUpdatesAsync([update], isProcessSuspended: false, CancellationToken.None));
 
        Assert.Contains("[Debug] Writing capabilities: Baseline AddMethodToExistingType AddStaticFieldToExistingType", test.AgentLogger.GetAndClearMessages());
        Assert.Contains("[Debug] Updates applied: 1 out of 1.", test.Logger.GetAndClearMessages());
    }
 
    [Fact]
    public async Task ApplyManagedCodeUpdates_ProcessSuspended()
    {
        var moduleId = Guid.NewGuid();
 
        var agent = new TestHotReloadAgent()
        {
            Capabilities = "Baseline AddMethodToExistingType AddStaticFieldToExistingType",
        };
 
        await using var test = new Test(output, agent);
 
        var actualCapabilities = await test.Client.GetUpdateCapabilitiesAsync(CancellationToken.None);
        AssertEx.SequenceEqual(["Baseline", "AddMethodToExistingType", "AddStaticFieldToExistingType"], actualCapabilities);
 
        var update = new HotReloadManagedCodeUpdate(
            moduleId: moduleId,
            metadataDelta: [1, 2, 3],
            ilDelta: [],
            pdbDelta: [],
            updatedTypes: [],
            requiredCapabilities: ["Baseline"]);
 
        var agentMessage = "[Debug] Writing capabilities: Baseline AddMethodToExistingType AddStaticFieldToExistingType";
 
        Assert.Equal(ApplyStatus.AllChangesApplied, await test.Client.ApplyManagedCodeUpdatesAsync([update], isProcessSuspended: true, CancellationToken.None));
 
        // emulate process being resumed:
        await test.Client.PendingUpdates;
 
        var clientMessages = test.Logger.GetAndClearMessages();
        var agentMessages = test.AgentLogger.GetAndClearMessages();
 
        // agent log messages not reported to the client logger while the process is suspended:
        Assert.Contains("[Debug] Sending update batch #0", clientMessages);
        Assert.Contains("[Debug] Updates applied: 1 out of 1.", clientMessages);
        Assert.Contains("[Debug] Update batch #0 completed.", clientMessages);
        Assert.Contains(agentMessage, agentMessages);
    }
 
    [Fact]
    public async Task ApplyManagedCodeUpdates_Failure()
    {
        var agent = new TestHotReloadAgent()
        {
            Capabilities = "Baseline AddMethodToExistingType AddStaticFieldToExistingType",
            ApplyManagedCodeUpdatesImpl = updates => throw new Exception("Bug!")
        };
 
        await using var test = new Test(output, agent);
 
        var actualCapabilities = await test.Client.GetUpdateCapabilitiesAsync(CancellationToken.None);
        AssertEx.SequenceEqual(["Baseline", "AddMethodToExistingType", "AddStaticFieldToExistingType"], actualCapabilities);
 
        var update = new HotReloadManagedCodeUpdate(
            moduleId: Guid.NewGuid(),
            metadataDelta: [],
            ilDelta: [],
            pdbDelta: [],
            updatedTypes: [],
            requiredCapabilities: ["Baseline"]);
 
        Assert.Equal(ApplyStatus.Failed, await test.Client.ApplyManagedCodeUpdatesAsync([update], isProcessSuspended: false, CancellationToken.None));
 
        // agent log messages were reported to the agent logger:
        var agentMessages = test.AgentLogger.GetAndClearMessages();
        Assert.Contains("[Error] The runtime failed to applying the change: Bug!", agentMessages);
        Assert.Contains("[Warning] Further changes won't be applied to this process.", agentMessages);
    }
}