File: Linux\LinuxUtilizationParserCgroupV1Tests.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.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.IO;
using System.Threading.Tasks;
using Microsoft.Shared.Pools;
using Microsoft.TestUtilities;
using Moq;
using Xunit;
 
namespace Microsoft.Extensions.Diagnostics.ResourceMonitoring.Linux.Test;
 
[OSSkipCondition(OperatingSystems.Windows | OperatingSystems.MacOSX, SkipReason = "Linux specific tests")]
public sealed class LinuxUtilizationParserCgroupV1Tests
{
    [ConditionalTheory]
    [InlineData("DFIJEUWGHFWGBWEFWOMDOWKSLA")]
    [InlineData("")]
    [InlineData("________________________Asdasdasdas          dd")]
    [InlineData(" ")]
    [InlineData("!@#!$%!@")]
    public void Parser_Throws_When_Data_Is_Invalid(string line)
    {
        var parser = new LinuxUtilizationParserCgroupV1(new HardcodedValueFileSystem(line), new FakeUserHz(100));
 
        Assert.Throws<InvalidOperationException>(() => parser.GetHostAvailableMemory());
        Assert.Throws<InvalidOperationException>(() => parser.GetAvailableMemoryInBytes());
        Assert.Throws<InvalidOperationException>(() => parser.GetMemoryUsageInBytes());
        Assert.Throws<InvalidOperationException>(() => parser.GetCgroupLimitedCpus());
        Assert.Throws<InvalidOperationException>(() => parser.GetHostCpuUsageInNanoseconds());
        Assert.Throws<InvalidOperationException>(() => parser.GetHostCpuCount());
        Assert.Throws<InvalidOperationException>(() => parser.GetCgroupCpuUsageInNanoseconds());
        Assert.Throws<InvalidOperationException>(() => parser.GetCgroupRequestCpu());
    }
 
    [ConditionalFact]
    public void Parser_Can_Read_Host_And_Cgroup_Available_Cpu_Count()
    {
        var parser = new LinuxUtilizationParserCgroupV1(new FileNamesOnlyFileSystem(TestResources.TestFilesLocation), new FakeUserHz(100));
        var hostCpuCount = parser.GetHostCpuCount();
        var cgroupCpuCount = parser.GetCgroupLimitedCpus();
 
        Assert.Equal(2.0, hostCpuCount);
        Assert.Equal(1.0, cgroupCpuCount);
    }
 
    [ConditionalFact]
    public void Parser_Provides_Total_Available_Memory_In_Bytes()
    {
        var fs = new FileNamesOnlyFileSystem(TestResources.TestFilesLocation);
        var parser = new LinuxUtilizationParserCgroupV1(fs, new FakeUserHz(100));
 
        var totalMem = parser.GetHostAvailableMemory();
 
        Assert.Equal(16_233_760UL * 1024, totalMem);
    }
 
    [ConditionalTheory]
    [InlineData("----------------------")]
    [InlineData("@ @#dddada")]
    [InlineData("1231234124124")]
    [InlineData("1024 KB")]
    [InlineData("1024 KB  d \n\r 1024")]
    [InlineData("\n\r")]
    [InlineData("")]
    [InlineData("Suspicious")]
    [InlineData("string@")]
    [InlineData("string12312")]
    [InlineData("total-inactive-file")]
    [InlineData("total_inactive-file")]
    [InlineData("total_active_file")]
    [InlineData("total_inactive_file:_ 213912")]
    [InlineData("Total_Inactive_File 2")]
    public void When_Calling_GetMemoryUsageInBytes_Parser_Throws_When_MemoryStat_Doesnt_Contain_Total_Inactive_File_Section(string content)
    {
        var f = new HardcodedValueFileSystem(new Dictionary<FileInfo, string>
        {
            { new FileInfo("/sys/fs/cgroup/memory/memory.stat"), content }
        });
 
        var p = new LinuxUtilizationParserCgroupV1(f, new FakeUserHz(100));
        var r = Record.Exception(() => p.GetMemoryUsageInBytes());
 
        Assert.IsAssignableFrom<InvalidOperationException>(r);
        Assert.Contains("/sys/fs/cgroup/memory/memory.stat", r.Message);
        Assert.Contains("total_inactive_file", r.Message);
    }
 
    [ConditionalTheory]
    [InlineData("----------------------")]
    [InlineData("@ @#dddada")]
    [InlineData("_1231234124124")]
    [InlineData("eee 1024 KB")]
    [InlineData("\n\r")]
    [InlineData("")]
    [InlineData("Suspicious")]
    [InlineData("Suspicious12312312")]
    [InlineData("string@")]
    [InlineData("string12312")]
    public void When_Calling_GetMemoryUsageInBytes_Parser_Throws_When_UsageInBytes_Doesnt_Contain_Just_A_Number(string content)
    {
        var f = new HardcodedValueFileSystem(new Dictionary<FileInfo, string>
        {
            { new FileInfo("/sys/fs/cgroup/memory/memory.stat"), "total_inactive_file 0" },
            { new FileInfo("/sys/fs/cgroup/memory/memory.usage_in_bytes"), content }
        });
 
        var p = new LinuxUtilizationParserCgroupV1(f, new FakeUserHz(100));
        var r = Record.Exception(() => p.GetMemoryUsageInBytes());
 
        Assert.IsAssignableFrom<InvalidOperationException>(r);
        Assert.Contains("/sys/fs/cgroup/memory/memory.usage_in_bytes", r.Message);
    }
 
    [ConditionalTheory]
    [InlineData(10, 1)]
    [InlineData(23, 22)]
    [InlineData(100000, 10000)]
    public void When_Calling_GetMemoryUsageInBytes_Parser_Throws_When_Inactive_Memory_Is_Bigger_Than_Total_Memory(int inactive, int total)
    {
        var f = new HardcodedValueFileSystem(new Dictionary<FileInfo, string>
        {
            { new FileInfo("/sys/fs/cgroup/memory/memory.stat"), $"total_inactive_file {inactive}" },
            { new FileInfo("/sys/fs/cgroup/memory/memory.usage_in_bytes"), total.ToString(CultureInfo.CurrentCulture) }
        });
 
        var p = new LinuxUtilizationParserCgroupV1(f, new FakeUserHz(100));
        var r = Record.Exception(() => p.GetMemoryUsageInBytes());
 
        Assert.IsAssignableFrom<InvalidOperationException>(r);
        Assert.Contains("lesser than", r.Message);
    }
 
    [ConditionalTheory]
    [InlineData("Mem")]
    [InlineData("MemTotal:")]
    [InlineData("MemTotal: 120")]
    [InlineData("MemTotal: kb")]
    [InlineData("MemTotal: KB")]
    [InlineData("MemTotal: PB")]
    [InlineData("MemTotal: 1024 PB")]
    [InlineData("MemTotal: 1024   ")]
    [InlineData("MemTotal: 1024 @@  ")]
    [InlineData("MemoryTotal: 1024 MB ")]
    [InlineData("MemoryTotal: 123123123123123123")]
    public void When_Calling_GetHostAvailableMemory_Parser_Throws_When_MemInfo_Does_Not_Contain_TotalMemory(string totalMemory)
    {
        var f = new HardcodedValueFileSystem(new Dictionary<FileInfo, string>
        {
            { new FileInfo("/proc/meminfo"), totalMemory },
        });
 
        var p = new LinuxUtilizationParserCgroupV1(f, new FakeUserHz(100));
        var r = Record.Exception(() => p.GetHostAvailableMemory());
 
        Assert.IsAssignableFrom<InvalidOperationException>(r);
        Assert.Contains("/proc/meminfo", r.Message);
    }
 
    [ConditionalTheory]
    [InlineData("kB", 231, 236544)]
    [InlineData("MB", 287, 300_941_312)]
    [InlineData("GB", 372, 399_431_958_528)]
    [InlineData("TB", 2, 219_902_325_555_2)]
    [SuppressMessage("Critical Code Smell", "S3937:Number patterns should be regular", Justification = "Its OK.")]
    public void When_Calling_GetHostAvailableMemory_Parser_Correctly_Transforms_Supported_Units_To_Bytes(string unit, int value, ulong bytes)
    {
        var f = new HardcodedValueFileSystem(new Dictionary<FileInfo, string>
        {
            { new FileInfo("/proc/meminfo"), $"MemTotal: {value} {unit}" },
        });
 
        var p = new LinuxUtilizationParserCgroupV1(f, new FakeUserHz(100));
        var memory = p.GetHostAvailableMemory();
 
        Assert.Equal(bytes, memory);
    }
 
    [ConditionalTheory]
    [InlineData("0-11", 12)]
    [InlineData("0", 1)]
    [InlineData("1000", 1)]
    [InlineData("0,1", 2)]
    [InlineData("0,1,2", 3)]
    [InlineData("0,1,2,4", 4)]
    [InlineData("0,1-2,3", 4)]
    [InlineData("0,1,2-3,4", 5)]
    [InlineData("0-1,2-3", 4)]
    [InlineData("0-1,2-3,4-5", 6)]
    [InlineData("0-2,3-5,6-8", 9)]
    public void When_No_Cgroup_Cpu_Limits_Are_Not_Set_We_Get_Available_Cpus_From_CpuSetCpus(string content, int result)
    {
        var f = new HardcodedValueFileSystem(new Dictionary<FileInfo, string>
        {
            { new FileInfo("/sys/fs/cgroup/cpuset/cpuset.cpus"), content },
            { new FileInfo("/sys/fs/cgroup/cpu/cpu.cfs_quota_us"), "-1" },
            { new FileInfo("/sys/fs/cgroup/cpu/cpu.cfs_period_us"), "-1" }
        });
 
        var p = new LinuxUtilizationParserCgroupV1(f, new FakeUserHz(100));
        var cpus = p.GetCgroupLimitedCpus();
 
        Assert.Equal(result, cpus);
    }
 
    [ConditionalTheory]
    [InlineData("-11")]
    [InlineData("0-")]
    [InlineData("d-22")]
    [InlineData("22-d")]
    [InlineData("22-18")]
    [InlineData("aaaa")]
    [InlineData("    d  182-1923")]
    [InlineData("")]
    [InlineData("1-18-22")]
    [InlineData("1-18                   \r\n")]
    [InlineData("\r\n")]
    public void Parser_Throws_When_CpuSet_Has_Invalid_Content(string content)
    {
        var f = new HardcodedValueFileSystem(new Dictionary<FileInfo, string>
        {
            { new FileInfo("/sys/fs/cgroup/cpuset/cpuset.cpus"), content },
                        { new FileInfo("/sys/fs/cgroup/cpu/cpu.cfs_quota_us"), "12" },
            { new FileInfo("/sys/fs/cgroup/cpu/cpu.cfs_period_us"), "-1" }
        });
 
        var p = new LinuxUtilizationParserCgroupV1(f, new FakeUserHz(100));
        var r = Record.Exception(() => p.GetCgroupLimitedCpus());
 
        Assert.IsAssignableFrom<InvalidOperationException>(r);
        Assert.Contains("/sys/fs/cgroup/cpuset/cpuset.cpus", r.Message);
    }
 
    [ConditionalTheory]
    [InlineData("-1", "18")]
    [InlineData("18", "-1")]
    [InlineData("18", "")]
    [InlineData("", "18")]
    public void When_Quota_And_Period_Are_Minus_One_It_Fallbacks_To_Cpuset(string quota, string period)
    {
        var f = new HardcodedValueFileSystem(new Dictionary<FileInfo, string>
        {
            { new FileInfo("/sys/fs/cgroup/cpuset/cpuset.cpus"), "@" },
                        { new FileInfo("/sys/fs/cgroup/cpu/cpu.cfs_quota_us"), quota },
            { new FileInfo("/sys/fs/cgroup/cpu/cpu.cfs_period_us"), period }
        });
 
        var p = new LinuxUtilizationParserCgroupV1(f, new FakeUserHz(100));
        var r = Record.Exception(() => p.GetCgroupLimitedCpus());
 
        Assert.IsAssignableFrom<InvalidOperationException>(r);
        Assert.Contains("/sys/fs/cgroup/cpuset/cpuset.cpus", r.Message);
    }
 
    [ConditionalTheory]
    [InlineData("dd1d", "18")]
    [InlineData("-18", "18")]
    [InlineData("\r\r\r\r\r", "18")]
    [InlineData("123", "\r\r\r\r\r")]
    [InlineData("-", "d'")]
    [InlineData("-", "d/:")]
    [InlineData("2", "d/:")]
    [InlineData("2d2d2d", "e3")]
    [InlineData("3d", "d3")]
    [InlineData("           12", "eeeee 12")]
    [InlineData("1 2", "eeeee 12")]
    [InlineData("12       ", "")]
    public void Parser_Throws_When_Cgroup_Cpu_Files_Contain_Invalid_Data(string quota, string period)
    {
        var f = new HardcodedValueFileSystem(new Dictionary<FileInfo, string>
        {
            { new FileInfo("/sys/fs/cgroup/cpu/cpu.cfs_quota_us"), quota },
            { new FileInfo("/sys/fs/cgroup/cpu/cpu.cfs_period_us"), period }
        });
 
        var p = new LinuxUtilizationParserCgroupV1(f, new FakeUserHz(100));
        var r = Record.Exception(() => p.GetCgroupLimitedCpus());
 
        Assert.IsAssignableFrom<InvalidOperationException>(r);
        Assert.Contains("/sys/fs/cgroup/cpu/cpu.cfs_", r.Message);
    }
 
    [ConditionalFact]
    public void ReadingCpuUsage_Does_Not_Throw_For_Valid_Input()
    {
        var f = new HardcodedValueFileSystem(new Dictionary<FileInfo, string>
        {
            { new FileInfo("/proc/stat"), "cpu  2569530 36700 245693 4860924 82283 0 4360 0 0 0" }
        });
 
        var p = new LinuxUtilizationParserCgroupV1(f, new FakeUserHz(100));
        var r = Record.Exception(() => p.GetHostCpuUsageInNanoseconds());
 
        Assert.Null(r);
    }
 
    [ConditionalFact]
    public void ReadingTotalMemory_Does_Not_Throw_For_Valid_Input()
    {
        var f = new HardcodedValueFileSystem(new Dictionary<FileInfo, string>
        {
            { new FileInfo("/sys/fs/cgroup/memory/memory.usage_in_bytes"), "32493514752\r\n" },
            { new FileInfo("/sys/fs/cgroup/memory/memory.stat"), "total_inactive_file 100" }
        });
 
        var p = new LinuxUtilizationParserCgroupV1(f, new FakeUserHz(100));
        var r = Record.Exception(() => p.GetMemoryUsageInBytes());
 
        Assert.Null(r);
    }
 
    [ConditionalTheory]
    [InlineData("2569530367000")]
    [InlineData("  2569530 36700 245693 4860924 82283 0 4360 0dsa 0 0 asdasd @@@@")]
    [InlineData("asdasd  2569530 36700 245693 4860924 82283 0 4360 0 0 0")]
    [InlineData("  2569530 36700 245693")]
    [InlineData("cpu  2569530 36700 245693")]
    [InlineData("  2")]
    public void ReadingCpuUsage_Does_Throws_For_Valid_Input(string content)
    {
        var f = new HardcodedValueFileSystem(new Dictionary<FileInfo, string>
        {
            { new FileInfo("/proc/stat"), content }
        });
 
        var p = new LinuxUtilizationParserCgroupV1(f, new FakeUserHz(100));
        var r = Record.Exception(() => p.GetHostCpuUsageInNanoseconds());
 
        Assert.IsAssignableFrom<InvalidOperationException>(r);
        Assert.Contains("proc/stat", r.Message);
    }
 
    [ConditionalTheory]
    [InlineData("-1")]
    [InlineData("")]
    public void Parser_Throws_When_Cgroup_Cpu_Shares_Files_Contain_Invalid_Data(string content)
    {
        var f = new HardcodedValueFileSystem(new Dictionary<FileInfo, string>
        {
            { new FileInfo("/sys/fs/cgroup/cpu/cpu.shares"), content },
        });
 
        var p = new LinuxUtilizationParserCgroupV1(f, new FakeUserHz(100));
        var r = Record.Exception(() => p.GetCgroupRequestCpu());
 
        Assert.IsAssignableFrom<InvalidOperationException>(r);
        Assert.Contains("/sys/fs/cgroup/cpu/cpu.shares", r.Message);
    }
 
    [ConditionalFact]
    public async Task ThreadSafetyAsync()
    {
        var f1 = new HardcodedValueFileSystem(new Dictionary<FileInfo, string>
        {
            { new FileInfo("/proc/stat"), "cpu  6163 0 3853 4222848 614 0 1155 0 0 0\r\ncpu0 240 0 279 210987 59 0 927 0 0 0" },
        });
        var f2 = new HardcodedValueFileSystem(new Dictionary<FileInfo, string>
        {
            { new FileInfo("/proc/stat"), "cpu  9137 0 9296 13972503 1148 0 2786 0 0 0\r\ncpu0 297 0 431 698663 59 0 2513 0 0 0" },
        });
 
        int callCount = 0;
        Mock<IFileSystem> fs = new();
        fs.Setup(x => x.ReadFirstLine(It.IsAny<FileInfo>(), It.IsAny<BufferWriter<char>>()))
             .Callback<FileInfo, BufferWriter<char>>((fileInfo, buffer) =>
             {
                 callCount++;
                 if (callCount % 2 == 0)
                 {
                     f1.ReadFirstLine(fileInfo, buffer);
                 }
                 else
                 {
                     f2.ReadFirstLine(fileInfo, buffer);
                 }
             })
             .Verifiable();
 
        var p = new LinuxUtilizationParserCgroupV1(fs.Object, new FakeUserHz(100));
 
        Task[] tasks = new Task[1_000];
        for (int i = 0; i < tasks.Length; i++)
        {
            tasks[i] = Task.Run(p.GetHostCpuUsageInNanoseconds);
        }
 
        await Task.WhenAll(tasks);
 
        Assert.True(true);
    }
}