File: Processes\LauncherLivenessMonitorTests.cs
Web Access
Project: src\tests\Aspire.Cli.Tests\Aspire.Cli.Tests.csproj (Aspire.Cli.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.Diagnostics;
using System.Globalization;
using Aspire.Cli.Processes;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging.Abstractions;
 
namespace Aspire.Cli.Tests.Processes;
 
public class LauncherLivenessMonitorTests
{
    private static readonly TimeSpan s_observationTimeout = TimeSpan.FromSeconds(15);
 
    [Fact]
    public void StartIfConfigured_WithoutLauncherConfig_ReturnsNull()
    {
        var configuration = new ConfigurationBuilder().Build();
        using var runCts = new CancellationTokenSource();
 
        var monitor = LauncherLivenessMonitor.StartIfConfigured(configuration, runCts, TimeProvider.System, NullLogger.Instance);
 
        Assert.Null(monitor);
        Assert.False(runCts.IsCancellationRequested);
    }
 
    [Fact]
    public async Task LauncherExit_CancelsRun()
    {
        using var launcher = StartLongRunningProcess();
        var configuration = CreateLauncherConfiguration(launcher);
        using var runCts = new CancellationTokenSource();
 
        var cancelTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
        await using var registration = runCts.Token.Register(() => cancelTcs.TrySetResult());
 
        await using var monitor = LauncherLivenessMonitor.StartIfConfigured(configuration, runCts, TimeProvider.System, NullLogger.Instance);
        Assert.NotNull(monitor);
 
        // The launcher dies while the monitor is armed: the run must be cancelled so the AppHost is torn down.
        launcher.Kill(entireProcessTree: true);
        launcher.WaitForExit();
 
        await cancelTcs.Task.WaitAsync(s_observationTimeout);
        Assert.True(runCts.IsCancellationRequested);
    }
 
    [Fact]
    public async Task LauncherAlive_DoesNotCancelRun()
    {
        // The current process stands in for a launcher that stays alive through startup.
        var configuration = new ConfigurationBuilder()
            .AddInMemoryCollection(new Dictionary<string, string?>
            {
                ["ASPIRE_LAUNCHER_PID"] = Environment.ProcessId.ToString(CultureInfo.InvariantCulture),
                ["ASPIRE_LAUNCHER_STARTED"] = ProcessStartTimeHelper.GetCurrentProcessStartTimeUnixMilliseconds().ToString(CultureInfo.InvariantCulture),
            })
            .Build();
        using var runCts = new CancellationTokenSource();
 
        await using (var monitor = LauncherLivenessMonitor.StartIfConfigured(configuration, runCts, TimeProvider.System, NullLogger.Instance))
        {
            Assert.NotNull(monitor);
 
            // Give the monitor several poll intervals to (incorrectly) fire.
            await Task.Delay(TimeSpan.FromSeconds(3));
            Assert.False(runCts.IsCancellationRequested);
        }
 
        Assert.False(runCts.IsCancellationRequested);
    }
 
    [Fact]
    public async Task Dispose_DisarmsMonitor()
    {
        using var launcher = StartLongRunningProcess();
        var configuration = CreateLauncherConfiguration(launcher);
        using var runCts = new CancellationTokenSource();
 
        var monitor = LauncherLivenessMonitor.StartIfConfigured(configuration, runCts, TimeProvider.System, NullLogger.Instance);
        Assert.NotNull(monitor);
 
        // Disarm first (mimicking startup reaching readiness), then kill the launcher. A disarmed monitor
        // must not cancel the run when the launcher later exits normally.
        await monitor.DisposeAsync();
 
        launcher.Kill(entireProcessTree: true);
        launcher.WaitForExit();
 
        await Task.Delay(TimeSpan.FromSeconds(3));
        Assert.False(runCts.IsCancellationRequested);
    }
 
    private static IConfiguration CreateLauncherConfiguration(Process launcher)
    {
        return new ConfigurationBuilder()
            .AddInMemoryCollection(new Dictionary<string, string?>
            {
                ["ASPIRE_LAUNCHER_PID"] = launcher.Id.ToString(CultureInfo.InvariantCulture),
                ["ASPIRE_LAUNCHER_STARTED"] = GetProcessStartTimeUnixMilliseconds(launcher).ToString(CultureInfo.InvariantCulture),
            })
            .Build();
    }
 
    private static Process StartLongRunningProcess() => TestProcesses.StartLongRunning();
 
    private static long GetProcessStartTimeUnixMilliseconds(Process process)
    {
        var startTime = ProcessStartTimeHelper.TryGetProcessStartTimeUnixMilliseconds(process.Id);
        Assert.NotNull(startTime);
        return startTime.Value;
    }
}