File: ResourceMonitoringServiceTests.cs
Web Access
Project: src\test\Libraries\Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests\Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests.csproj (Microsoft.Extensions.Diagnostics.ResourceMonitoring.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;
using System.Collections.Generic;
using System.Reflection;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Diagnostics.ResourceMonitoring.Test.Providers;
using Microsoft.Extensions.Diagnostics.ResourceMonitoring.Test.Publishers;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Logging.Testing;
using Microsoft.Extensions.Time.Testing;
using Moq;
using VerifyXunit;
using Xunit;
using static Microsoft.Extensions.Options.Options;
 
namespace Microsoft.Extensions.Diagnostics.ResourceMonitoring.Test;
 
/// <summary>
/// Tests for the DataTracker class.
/// </summary>
[UsesVerify]
public sealed class ResourceMonitoringServiceTests
{
    private const string ProviderUnableToGatherData = "Unable to gather utilization statistics.";
 
    // We use this static fake clock in tests that doesn't advance the time.
    private static readonly FakeTimeProvider _clock = new();
    private static readonly string _publisherUnableToPublishErrorMessage = $"Publisher `{typeof(FaultPublisher).FullName}` was unable to publish utilization statistics.";
 
    /// <summary>
    /// Simply construct the object.
    /// </summary>
    /// <remarks>Tests that look into internals like this are evil. Consider removing long term.</remarks>
    [Fact]
    public void BasicConstructor()
    {
        var mockProvider = new Mock<ISnapshotProvider>(MockBehavior.Loose);
        var mockLogger = new Mock<ILogger<ResourceMonitorService>>(MockBehavior.Loose);
        var publishersList = new List<IResourceUtilizationPublisher>
        {
            new EmptyPublisher(),
            new AnotherPublisher()
        };
 
        using var tracker = new ResourceMonitorService(
            mockProvider.Object,
            mockLogger.Object,
            Create(new ResourceMonitoringOptions()),
            publishersList,
            _clock);
        var provider = GetDataTrackerField<ISnapshotProvider>(tracker, "_provider");
        var logger = GetDataTrackerField<ILogger<ResourceMonitorService>>(tracker, "_logger");
        var publishers =
            GetDataTrackerField<IResourceUtilizationPublisher[]>(tracker, "_publishers");
 
        Assert.NotNull(provider);
        Assert.NotNull(logger);
        Assert.NotNull(publishers);
        Assert.Equal(2, publishers.Length);
    }
 
    [Fact]
    public void BasicConstructor_NullOptions_Throws()
    {
        var mockProvider = new Mock<ISnapshotProvider>(MockBehavior.Loose);
        var mockLogger = new Mock<ILogger<ResourceMonitorService>>(MockBehavior.Loose);
        var mockPublishers = new Mock<IEnumerable<IResourceUtilizationPublisher>>(MockBehavior.Loose);
 
        Assert.Throws<ArgumentException>(() =>
            new ResourceMonitorService(mockProvider.Object, mockLogger.Object, Create((ResourceMonitoringOptions)null!), mockPublishers.Object, _clock));
    }
 
    /// <summary>
    /// Simply construct the object (publisher constructor).
    /// </summary>
    /// <remarks>Tests that look into internals like this are evil. Consider removing long term.</remarks>
    [Fact]
    public void BasicConstructor_NullPublishers_Throws()
    {
        var mockProvider = new Mock<ISnapshotProvider>(MockBehavior.Loose);
        var mockLogger = new Mock<ILogger<ResourceMonitorService>>(MockBehavior.Loose);
 
        Assert.Throws<ArgumentNullException>(() =>
            new ResourceMonitorService(mockProvider.Object, mockLogger.Object, Create(new ResourceMonitoringOptions()), null!, TimeProvider.System));
    }
 
    /// <summary>
    /// Construct the object configured with maximum allowed values of <see cref="ResourceMonitoringOptions.AveragingWindow"/> and
    /// <see cref="ResourceMonitoringOptions.NumberOfSamples"/>.
    /// </summary>
    [Fact]
    public void BasicConstructor_ConfiguredWithMaxValuesOfSamplingWindowAndSamplingPeriod_DoesNotThrow()
    {
        var mockProvider = new Mock<ISnapshotProvider>(MockBehavior.Loose);
        var mockLogger = new Mock<ILogger<ResourceMonitorService>>(MockBehavior.Loose);
        var publishersList = new List<IResourceUtilizationPublisher>
        {
            new EmptyPublisher(),
            new AnotherPublisher()
        };
 
        var exception = Record.Exception(() =>
        {
            using var tracker = new ResourceMonitorService(
                mockProvider.Object,
                mockLogger.Object,
                Create(new ResourceMonitoringOptions
                {
                    CollectionWindow = TimeSpan.FromMilliseconds(int.MaxValue),
                    SamplingInterval = TimeSpan.FromMilliseconds(int.MaxValue)
                }),
                publishersList,
                _clock);
        });
 
        Assert.Null(exception);
    }
 
    /// <summary>
    /// Simply construct the object (publisher constructor).
    /// </summary>
    /// <remarks>Tests that look into internals like this are evil.  Consider removing long term.</remarks>
    [Fact]
    public void BasicConstructor_WithTwoPublishers()
    {
        var mockProvider = new Mock<ISnapshotProvider>(MockBehavior.Loose);
        var mockLogger = new Mock<ILogger<ResourceMonitorService>>(MockBehavior.Loose);
        var publishersList = new List<IResourceUtilizationPublisher>
        {
            new EmptyPublisher(),
            new EmptyPublisher()
        };
 
        using var tracker = new ResourceMonitorService(
            mockProvider.Object,
            mockLogger.Object,
            Create(new ResourceMonitoringOptions()),
            publishersList,
            _clock);
        Assert.NotNull(tracker);
 
        var provider = GetDataTrackerField<ISnapshotProvider>(tracker, "_provider");
        var publishers
            = GetDataTrackerField<IResourceUtilizationPublisher[]>(tracker, "_publishers");
        var logger = GetDataTrackerField<ILogger>(tracker, "_logger");
 
        Assert.NotNull(logger);
        Assert.NotNull(provider);
        Assert.NotNull(logger);
        Assert.NotNull(publishers);
        Assert.IsType<IResourceUtilizationPublisher[]>(publishers);
        Assert.Equal(2, publishers.Length);
    }
 
    /// <summary>
    /// Simply construct the object (complex constructor, with counter publishing).
    /// </summary>
    /// <remarks>Tests that look into internals like this are evil.  Consider removing long term.</remarks>
    [Fact]
    public void BasicConstructor_Complex_WithEmptyPublisher()
    {
        var mockProvider = new Mock<ISnapshotProvider>(MockBehavior.Loose);
        var mockLogger = new Mock<ILogger<ResourceMonitorService>>(MockBehavior.Loose);
        using var tracker = new ResourceMonitorService(mockProvider.Object, mockLogger.Object,
            Create(new ResourceMonitoringOptions
            {
                CollectionWindow = TimeSpan.FromSeconds(1),
                PublishingWindow = TimeSpan.FromSeconds(1)
            }),
            new List<IResourceUtilizationPublisher>
            {
                new EmptyPublisher()
            },
            _clock);
 
        var publishers = GetDataTrackerField<IResourceUtilizationPublisher[]>(tracker, "_publishers");
 
        Assert.NotNull(publishers);
        Assert.IsType<IResourceUtilizationPublisher[]>(publishers);
        Assert.Single(publishers);
    }
 
    [Fact]
    public async Task StartAsync_WithSimulatingThatTimeDidNotPass_NoUtilizationDataWillBeGathered()
    {
        const int TimerPeriod = 100;
        var numberOfSnapshots = 0;
        var logger = new FakeLogger<ResourceMonitorService>();
        var provider = new FakeProvider();
 
        using var tracker = new ResourceMonitorService(
            provider,
            logger,
 
            // Here we set the number of options to 1 so the internal timer
            // period will equal the AverageWindow.
            Create(new ResourceMonitoringOptions
            {
                CollectionWindow = TimeSpan.FromMilliseconds(TimerPeriod),
                SamplingInterval = TimeSpan.FromMilliseconds(TimerPeriod),
                PublishingWindow = TimeSpan.FromMilliseconds(TimerPeriod)
            }),
            new List<IResourceUtilizationPublisher>
            {
                new GenericPublisher(_ => numberOfSnapshots++)
            },
            _clock);
 
        // Start running the tracker.
        _ = tracker.StartAsync(CancellationToken.None);
 
        // waiting for 3 cycles of execution, however the tracker's timer won't
        // be triggered as we didn't advance time via the fake clock.
        await Task.Delay(TimeSpan.FromMilliseconds(TimerPeriod * 3));
 
        await tracker.StopAsync(CancellationToken.None);
 
        // Since we did not advance the clock, then time did not pass. So, the timer should remain idle.
        Assert.Equal(0, numberOfSnapshots);
    }
 
    [Fact]
    public async Task RunTrackerAsync_IfProviderThrows_LogsError()
    {
        var clock = new FakeTimeProvider();
        var logger = new FakeLogger<ResourceMonitorService>();
        var provider = new FaultProvider
        {
            // prevent the provider from throwing exception just to not fail the test
            // while initializing the tracker.
            ShouldThrow = false
        };
        using var e = new ManualResetEventSlim();
 
        using var tracker = new ResourceMonitorService(
            provider,
            logger,
            Create(new ResourceMonitoringOptions
            {
                CollectionWindow = TimeSpan.FromMilliseconds(100),
                PublishingWindow = TimeSpan.FromMilliseconds(100),
                SamplingInterval = TimeSpan.FromMilliseconds(1)
            }),
            new List<IResourceUtilizationPublisher>
            {
                new GenericPublisher((_) => ResilientSetEvent(e))
            },
            clock);
 
        await tracker.StartAsync(CancellationToken.None);
 
        // Now, allow the faulted provider to throw.
        provider.ShouldThrow = true;
 
        clock.Advance(TimeSpan.FromMilliseconds(1));
        clock.Advance(TimeSpan.FromMilliseconds(1));
 
        e.Wait();
 
        Assert.Contains(ProviderUnableToGatherData, logger.Collector.LatestRecord.Message, StringComparison.OrdinalIgnoreCase);
    }
 
    [Fact]
    public async Task RunTrackerAsync_IfPublisherThrows_LogsError()
    {
        var logger = new FakeLogger<ResourceMonitorService>();
 
        using var tracker = new ResourceMonitorService(
            new FakeProvider(),
            logger,
            Create(new ResourceMonitoringOptions
            {
                CollectionWindow = TimeSpan.FromMilliseconds(100),
                PublishingWindow = TimeSpan.FromMilliseconds(100)
            }),
            new List<IResourceUtilizationPublisher>
            {
                // initialize with a publisher that throws an exception when trying to publish.
                new FaultPublisher()
            },
            _clock);
 
        // running the tracker logic for a single time without starting the tracker
        // service itself. By this we avoid starting the timer and the async behavior
        // and thus eliminate the flakiness introduced because of it.
        await tracker.PublishUtilizationAsync(CancellationToken.None);
 
        Assert.Contains(_publisherUnableToPublishErrorMessage, logger.Collector.LatestRecord.Message, StringComparison.OrdinalIgnoreCase);
    }
 
    /// <summary>
    /// Validate that the tracker invokes the publisher's Publish method.
    /// </summary>
    /// <remarks>Tests that look into internals like this are evil. Consider removing long term.</remarks>
    [Fact]
    public async Task ResourceUtilizationTracker_InitializedProperly_InvokesPublishers()
    {
        const int TimerPeriod = 100;
        bool publisherCalled = false;
 
        // Here we use local clock to keep its state local to the test.
        var clock = new FakeTimeProvider();
 
        using var autoResetEvent = new AutoResetEvent(false);
 
        using var tracker = new ResourceMonitorService(
            new FakeProvider(),
            new NullLogger<ResourceMonitorService>(),
 
            // Here we set the number of options to 1 so the internal timer
            // period will equal the AverageWindow.
            Create(new ResourceMonitoringOptions
            {
                CollectionWindow = TimeSpan.FromMilliseconds(TimerPeriod),
                PublishingWindow = TimeSpan.FromMilliseconds(TimerPeriod),
                SamplingInterval = TimeSpan.FromMilliseconds(TimerPeriod)
            }),
            new List<IResourceUtilizationPublisher>
            {
                new GenericPublisher(_ =>
                {
                    publisherCalled = true;
                    ResilientSetEvent(autoResetEvent);
                })
            },
            clock);
 
        // Start running the tracker.
        await tracker.StartAsync(CancellationToken.None);
 
        do
        {
            // Advance the clock 100 milliseconds to simulate passing of time
            // and allow the tracker to execute one cycle of gathering utilization data.
            clock.Advance(TimeSpan.FromMilliseconds(TimerPeriod));
        }
        while (!autoResetEvent.WaitOne(1));
 
        await tracker.StopAsync(CancellationToken.None);
 
        // Asserts that the publisher was called.
        Assert.True(publisherCalled);
    }
 
    [Fact]
    public async Task ResourceUtilizationTracker_LogsSnapshotInformation()
    {
        const int TimerPeriod = 100;
        var logger = new FakeLogger<ResourceMonitorService>();
        var clock = new FakeTimeProvider();
 
        using var tracker = new ResourceMonitorService(
            new FakeProvider(),
            logger,
            Create(new ResourceMonitoringOptions
            {
                CollectionWindow = TimeSpan.FromMilliseconds(TimerPeriod),
                PublishingWindow = TimeSpan.FromMilliseconds(TimerPeriod),
                SamplingInterval = TimeSpan.FromMilliseconds(TimerPeriod)
            }),
            Array.Empty<IResourceUtilizationPublisher>(),
            clock);
 
        // Start running the tracker.
        await tracker.StartAsync(CancellationToken.None);
 
        clock.Advance(TimeSpan.FromMilliseconds(TimerPeriod));
 
        await tracker.StopAsync(CancellationToken.None);
 
        await Verifier.Verify(logger.Collector.LatestRecord).UseDirectory("Verified");
    }
 
    [Fact(Skip = "Broken test, see https://github.com/dotnet/extensions/issues/4529")]
    public async Task ResourceUtilizationTracker_WhenInitializedWithZeroSnapshots_ReportsHighCpuSpikesThenConvergeInFewCycles()
    {
        // This test shows that initializing the internal buffer of the tracker with snapshots
        // with zeros for the kernel time and user time will cause the first values of CPU
        // utilization to be very high in a way that don't reflect the change in CPU time.
 
        const int TimerPeriod = 100;
 
        var clock = new FakeTimeProvider();
        var zerosSnapshot = new Snapshot(
            TimeSpan.FromTicks(clock.GetUtcNow().Ticks),
            TimeSpan.Zero,
            TimeSpan.Zero,
            0);
 
        // This array simulates a series of snapshot values where the CPU kernel time
        // and CPU user time are constant and don't change, with this series of snapshots
        // the tracker should emit 0% CPU all the time, however, because the tracker
        // internal buffer was initialized with zero values snapshot, we will find that
        // the produced CPU% will start with high values then with time it will converge
        // to 0%.
        var snapshotsSequence = new[]
        {
            new Snapshot(
                TimeSpan.FromTicks(clock.GetUtcNow().AddMilliseconds(TimerPeriod).Ticks),
                TimeSpan.FromMilliseconds(250),
                TimeSpan.FromMilliseconds(250),
                200),
            new Snapshot(
                TimeSpan.FromTicks(clock.GetUtcNow().AddMilliseconds(TimerPeriod * 2).Ticks),
                TimeSpan.FromMilliseconds(250),
                TimeSpan.FromMilliseconds(250),
                200),
            new Snapshot(
                TimeSpan.FromTicks(clock.GetUtcNow().AddMilliseconds(TimerPeriod * 3).Ticks),
                TimeSpan.FromMilliseconds(250),
                TimeSpan.FromMilliseconds(250),
                200),
            new Snapshot(
                TimeSpan.FromTicks(clock.GetUtcNow().AddMilliseconds(TimerPeriod * 4).Ticks),
                TimeSpan.FromMilliseconds(250),
                TimeSpan.FromMilliseconds(250),
                200),
            new Snapshot(
                TimeSpan.FromTicks(clock.GetUtcNow().AddMilliseconds(TimerPeriod * 5).Ticks),
                TimeSpan.FromMilliseconds(250),
                TimeSpan.FromMilliseconds(250),
                200)
        };
 
        using var autoResetEvent = new AutoResetEvent(false);
        var provider = new FakeProvider();
 
        // Initially set the provider to return zero snapshot, and use this snapshot
        // to initialize the tracker's internal buffer with zero snapshots.
        provider.SetNextSnapshot(zerosSnapshot);
 
        var options = new ResourceMonitoringOptions
        {
            CollectionWindow = TimeSpan.FromMilliseconds(TimerPeriod * 2),
            PublishingWindow = TimeSpan.FromMilliseconds(TimerPeriod * 2),
            SamplingInterval = TimeSpan.FromMilliseconds(TimerPeriod)
        };
 
        using var tracker = new ResourceMonitorService(
            provider,
            new NullLogger<ResourceMonitorService>(),
            Create(options),
            new List<IResourceUtilizationPublisher>
            {
                new GenericPublisher(_ => ResilientSetEvent(autoResetEvent))
            },
            clock);
 
        // Start running the tracker.
        _ = tracker.StartAsync(CancellationToken.None);
 
        var cpuValuesWithHighSpikes = new int[5];
 
        // In the following cycles the CPU% will be reported with high
        // values (100%) then should reduce with each cycle until reach
        // the value of 0%
        for (int i = 0; i < snapshotsSequence.Length; i++)
        {
            provider.SetNextSnapshot(snapshotsSequence[i]);
 
            // Advance time.
            clock.Advance(TimeSpan.FromMilliseconds(TimerPeriod));
 
            // This is to allow the service code to execute until it gets blocked in Task.Delay, and then
            // make sure it gets unblocked from the Delay.
            while (!autoResetEvent.WaitOne(10))
            {
                clock.Advance(TimeSpan.FromTicks(1));
            }
 
            var utilization = tracker.GetUtilization(options.CollectionWindow);
            cpuValuesWithHighSpikes[i] = (int)utilization.CpuUsedPercentage;
        }
 
        // Stop the tracker.
        await tracker.StopAsync(CancellationToken.None);
 
        Assert.Equal(0, cpuValuesWithHighSpikes[cpuValuesWithHighSpikes.Length - 1]);
    }
 
    [Fact]
    public async Task ResourceUtilizationTracker_WhenInitializedWithProperSnapshots_ReportsNoHighCpuSpikes()
    {
        // This test shows that initializing the internal buffer of the tracker with snapshots
        // with normal values for the kernel time and user time will eliminate any high CPU
        // utilization spikes that may appear in the first readings from the tracker.
 
        const int TimerPeriod = 100;
 
        var clock = new FakeTimeProvider();
        var properInitSnapshot = new Snapshot(
            TimeSpan.FromTicks(clock.GetUtcNow().Ticks),
            TimeSpan.FromMilliseconds(250),
            TimeSpan.FromMilliseconds(250),
            200);
 
        // This array simulates a series of snapshot values where the CPU kernel time
        // and CPU user time are constant and don't change, with this series of snapshots
        // the tracker should emit 0% CPU all the time. Since in this test the tracker
        // is initialized with proper values, the tracker will produce 0% CPU all the
        // time.
        var snapshotsSequence = new[]
        {
            new Snapshot(
                TimeSpan.FromTicks(clock.GetUtcNow().AddMilliseconds(TimerPeriod).Ticks),
                TimeSpan.FromMilliseconds(250),
                TimeSpan.FromMilliseconds(250),
                200),
            new Snapshot(
                TimeSpan.FromTicks(clock.GetUtcNow().AddMilliseconds(TimerPeriod * 2).Ticks),
                TimeSpan.FromMilliseconds(250),
                TimeSpan.FromMilliseconds(250),
                200),
            new Snapshot(
                TimeSpan.FromTicks(clock.GetUtcNow().AddMilliseconds(TimerPeriod * 3).Ticks),
                TimeSpan.FromMilliseconds(250),
                TimeSpan.FromMilliseconds(250),
                200),
            new Snapshot(
                TimeSpan.FromTicks(clock.GetUtcNow().AddMilliseconds(TimerPeriod * 4).Ticks),
                TimeSpan.FromMilliseconds(250),
                TimeSpan.FromMilliseconds(250),
                200),
            new Snapshot(
                TimeSpan.FromTicks(clock.GetUtcNow().AddMilliseconds(TimerPeriod * 5).Ticks),
                TimeSpan.FromMilliseconds(250),
                TimeSpan.FromMilliseconds(250),
                200)
        };
 
        // These are the expected CPU% values.
        var expectedCpuValues = new[] { 0, 0, 0, 0, 0 };
 
        using var autoResetEvent = new AutoResetEvent(false);
        var provider = new FakeProvider();
 
        // Initially set the provider to return zero snapshot, and use this snapshot
        // to initialize the tracker's internal buffer with zero snapshots.
        provider.SetNextSnapshot(properInitSnapshot);
 
        var options = new ResourceMonitoringOptions
        {
            CollectionWindow = TimeSpan.FromMilliseconds(TimerPeriod * 2),
            PublishingWindow = TimeSpan.FromMilliseconds(TimerPeriod * 2),
            SamplingInterval = TimeSpan.FromMilliseconds(TimerPeriod)
        };
 
        using var tracker = new ResourceMonitorService(
            provider,
            new NullLogger<ResourceMonitorService>(),
            Create(options),
            new List<IResourceUtilizationPublisher>
            {
                new GenericPublisher(_ => ResilientSetEvent(autoResetEvent))
            },
            clock);
 
        // Start running the tracker.
        _ = tracker.StartAsync(CancellationToken.None);
 
        var cpuValuesWithNoHighSpikes = new int[5];
 
        // In the following cycles the CPU% will be 0% all the time.
        for (int i = 0; i < snapshotsSequence.Length; i++)
        {
            provider.SetNextSnapshot(snapshotsSequence[i]);
 
            do
            {
                // Advance time.
                clock.Advance(TimeSpan.FromMilliseconds(TimerPeriod));
            }
            while (!autoResetEvent.WaitOne(1));
 
            var utilization = tracker.GetUtilization(options.CollectionWindow);
            cpuValuesWithNoHighSpikes[i] = (int)utilization.CpuUsedPercentage;
        }
 
        // Stop the tracker.
        await tracker.StopAsync(CancellationToken.None);
 
        Assert.Equal(expectedCpuValues, cpuValuesWithNoHighSpikes);
    }
 
    [Fact]
    public void Dispose_CalledMultipleTimes_DoNotThrow()
    {
        using var tracker = new ResourceMonitorService(
            new FakeProvider(),
            new NullLogger<ResourceMonitorService>(),
            Create(new ResourceMonitoringOptions()),
            new List<IResourceUtilizationPublisher>
            {
                new EmptyPublisher()
            },
            _clock);
 
        var exception = Record.Exception(() =>
        {
            tracker.Dispose();
            tracker.Dispose();
        });
 
        Assert.Null(exception);
    }
 
    [Fact]
    public async Task StopAsync_CalledTwice_DoesNotThrow()
    {
        using var tracker = new ResourceMonitorService(
            new FakeProvider(),
            new NullLogger<ResourceMonitorService>(),
            Create(new ResourceMonitoringOptions()),
            new List<IResourceUtilizationPublisher>
            {
                new EmptyPublisher()
            },
            _clock);
 
        _ = tracker.StartAsync(CancellationToken.None);
 
        var exception = await Record.ExceptionAsync(async () =>
        {
            await tracker.StopAsync(CancellationToken.None);
            await tracker.StopAsync(CancellationToken.None);
        });
 
        Assert.Null(exception);
    }
 
    [Fact]
    public void GetUtilization_BasicTest()
    {
        var providerMock = new Mock<ISnapshotProvider>(MockBehavior.Loose);
 
        providerMock.Setup(x => x.Resources)
            .Returns(new SystemResources(1.0, 1.0, 100, 100));
        providerMock.Setup(x => x.GetSnapshot())
            .Returns(new Snapshot(
                TimeSpan.FromTicks(_clock.GetUtcNow().Ticks),
                TimeSpan.FromSeconds(1),
                TimeSpan.FromSeconds(1),
                50));
 
        using var tracker = new ResourceMonitorService(
            providerMock.Object,
            new NullLogger<ResourceMonitorService>(),
            Create(new ResourceMonitoringOptions
            {
                CollectionWindow = TimeSpan.FromSeconds(1),
                SamplingInterval = TimeSpan.FromMilliseconds(100),
                PublishingWindow = TimeSpan.FromSeconds(1)
            }),
            new List<IResourceUtilizationPublisher>
            {
                new EmptyPublisher()
            },
            _clock);
 
        var exception = Record.Exception(() => _ = tracker.GetUtilization(TimeSpan.FromSeconds(1)));
 
        Assert.Null(exception);
    }
 
    [Fact]
    public void GetUtilization_ProvidedByWindowGreaterThanSamplingWindowButLesserThanCollectionWindow_Succeeds()
    {
        var providerMock = new Mock<ISnapshotProvider>(MockBehavior.Loose);
 
        using var tracker = new ResourceMonitorService(
            providerMock.Object,
            new NullLogger<ResourceMonitorService>(),
            Create(new ResourceMonitoringOptions
            {
                CollectionWindow = TimeSpan.FromSeconds(6),
                PublishingWindow = TimeSpan.FromSeconds(5)
            }),
            new List<IResourceUtilizationPublisher>
            {
                new EmptyPublisher(),
            },
            _clock);
 
        var exception = Record.Exception(() => tracker.GetUtilization(TimeSpan.FromSeconds(6)));
 
        Assert.Null(exception);
    }
 
    [Fact]
    public void GetUtilization_ProvidedByWindowGreaterThanBuffer_ThrowsArgumentOutOfRangeException()
    {
        var providerMock = new Mock<ISnapshotProvider>(MockBehavior.Loose);
 
        using var tracker = new ResourceMonitorService(
            providerMock.Object,
            new NullLogger<ResourceMonitorService>(),
            Create(new ResourceMonitoringOptions
            {
                CollectionWindow = TimeSpan.FromSeconds(5)
            }),
            new List<IResourceUtilizationPublisher>
            {
                new EmptyPublisher(),
            },
            _clock);
 
        var exception = Record.Exception(() => tracker.GetUtilization(TimeSpan.FromSeconds(6)));
 
        Assert.NotNull(exception);
        Assert.IsType<ArgumentOutOfRangeException>(exception);
    }
 
    [Fact]
    public async Task Disposing_Service_Twice_Does_Not_Throw()
    {
        using var s = new ResourceMonitorService(new FakeProvider(), NullLogger<ResourceMonitorService>.Instance,
            Microsoft.Extensions.Options.Options.Create(new ResourceMonitoringOptions()), Array.Empty<IResourceUtilizationPublisher>(), TimeProvider.System);
 
        var r = await Record.ExceptionAsync(async () =>
        {
            await s.StopAsync(CancellationToken.None);
            await s.StopAsync(CancellationToken.None);
            await s.StopAsync(CancellationToken.None);
        });
 
        Assert.Null(r);
    }
 
    private static T? GetDataTrackerField<T>(ResourceMonitorService tracker, string name)
    {
        var typ = typeof(ResourceMonitorService);
        var type = typ.GetField(name, BindingFlags.NonPublic | BindingFlags.Instance);
        return (T?)type?.GetValue(tracker);
    }
 
    private static void ResilientSetEvent(AutoResetEvent e)
    {
        try
        {
            e.Set();
        }
#pragma warning disable CA1031
        catch
#pragma warning restore CA1031
        {
            // can happen since test termination is racy and the event might have already been disposed
        }
    }
 
    private static void ResilientSetEvent(ManualResetEventSlim e)
    {
        try
        {
            e.Set();
        }
#pragma warning disable CA1031
        catch
#pragma warning restore CA1031
        {
            // can happen since test termination is racy and the event might have already been disposed
        }
    }
}