From: kkeirstead <85592574+kkeirstead@users.noreply.github.com> Date: Wed, 11 Oct 2023 14:10:55 +0000 (-0700) Subject: Dotnet Counters + Dotnet Monitor Unification - Automated `Collect` Testing (#4253) X-Git-Tag: accepted/tizen/unified/riscv/20231226.055542~35^2~1^2~37 X-Git-Url: http://review.tizen.org/git/?a=commitdiff_plain;h=289a46a2c9fc87b977217d769b0aed45e2b37bcf;p=platform%2Fcore%2Fdotnet%2Fdiagnostics.git Dotnet Counters + Dotnet Monitor Unification - Automated `Collect` Testing (#4253) --- diff --git a/src/tests/CommonTestRunner/TestRunnerUtilities.cs b/src/tests/CommonTestRunner/TestRunnerUtilities.cs new file mode 100644 index 000000000..7cbc88b26 --- /dev/null +++ b/src/tests/CommonTestRunner/TestRunnerUtilities.cs @@ -0,0 +1,59 @@ +// 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.Threading; +using System.Threading.Tasks; +using Microsoft.Diagnostics.TestHelpers; +using Xunit.Abstractions; +using TestRunner = Microsoft.Diagnostics.CommonTestRunner.TestRunner; + +namespace CommonTestRunner +{ + public static class TestRunnerUtilities + { + public static async Task StartProcess(TestConfiguration config, string testArguments, ITestOutputHelper outputHelper, int testProcessTimeout = 60_000) + { + TestRunner runner = await TestRunner.Create(config, outputHelper, "EventPipeTracee", testArguments).ConfigureAwait(true); + await runner.Start(testProcessTimeout).ConfigureAwait(true); + return runner; + } + + public static async Task ExecuteCollection( + Func executeCollection, + TestRunner testRunner, + CancellationToken token) + { + Task collectionTask = executeCollection(token); + await ExecuteCollection(collectionTask, testRunner, token).ConfigureAwait(false); + } + + public static async Task ExecuteCollection( + Task collectionTask, + TestRunner testRunner, + CancellationToken token, + Func waitForPipeline = null) + { + // Begin event production + testRunner.WakeupTracee(); + + // Wait for event production to be done + testRunner.WaitForSignal(); + + try + { + if (waitForPipeline != null) + { + await waitForPipeline(token).ConfigureAwait(false); + } + + await collectionTask.ConfigureAwait(true); + } + finally + { + // Signal for debuggee that it's ok to end/move on. + testRunner.WakeupTracee(); + } + } + } +} diff --git a/src/tests/EventPipeTracee/CustomMetrics.cs b/src/tests/EventPipeTracee/CustomMetrics.cs new file mode 100644 index 000000000..d16b036f8 --- /dev/null +++ b/src/tests/EventPipeTracee/CustomMetrics.cs @@ -0,0 +1,38 @@ +// 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.Metrics; +using Constants = DotnetCounters.UnitTests.TestConstants; + +namespace EventPipeTracee +{ + internal sealed class CustomMetrics : IDisposable + { + private Meter _meter; + private Counter _counter; + private Histogram _histogram; + + public CustomMetrics() + { + _meter = new(Constants.TestMeterName); + _counter = _meter.CreateCounter(Constants.TestCounter, "dollars"); + _histogram = _meter.CreateHistogram(Constants.TestHistogram, "feet"); + // consider adding gauge/etc. here + } + + public void IncrementCounter(int v = 1) + { + _counter.Add(v); + } + + public void RecordHistogram(float v = 10.0f) + { + KeyValuePair tags = new(Constants.TagKey, Constants.TagValue); + _histogram.Record(v, tags); + } + + public void Dispose() => _meter?.Dispose(); + } +} diff --git a/src/tests/EventPipeTracee/EventPipeTracee.csproj b/src/tests/EventPipeTracee/EventPipeTracee.csproj index b94bb2b3c..f61b62ced 100644 --- a/src/tests/EventPipeTracee/EventPipeTracee.csproj +++ b/src/tests/EventPipeTracee/EventPipeTracee.csproj @@ -1,10 +1,14 @@ - + Exe $(BuildProjectFramework) net6.0;net7.0;net8.0 + + + + diff --git a/src/tests/EventPipeTracee/Program.cs b/src/tests/EventPipeTracee/Program.cs index 0ede793df..0d379b97d 100644 --- a/src/tests/EventPipeTracee/Program.cs +++ b/src/tests/EventPipeTracee/Program.cs @@ -6,6 +6,8 @@ using System.Collections.Generic; using System.Diagnostics; using System.IO.Pipes; using System.Linq; +using System.Threading; +using System.Threading.Tasks; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; @@ -29,6 +31,10 @@ namespace EventPipeTracee bool spinWait10 = args.Length > 2 && "SpinWait10".Equals(args[2], StringComparison.Ordinal); string loggerCategory = args[1]; + bool diagMetrics = args.Any("DiagMetrics".Equals); + + Console.WriteLine($"{pid} EventPipeTracee: DiagMetrics {diagMetrics}"); + Console.WriteLine($"{pid} EventPipeTracee: start process"); Console.Out.Flush(); @@ -54,12 +60,35 @@ namespace EventPipeTracee Console.WriteLine($"{pid} EventPipeTracee: {DateTime.UtcNow} Awaiting start"); Console.Out.Flush(); + using CustomMetrics metrics = diagMetrics ? new CustomMetrics() : null; + // Wait for server to send something int input = pipeStream.ReadByte(); Console.WriteLine($"{pid} {DateTime.UtcNow} Starting test body '{input}'"); Console.Out.Flush(); + CancellationTokenSource recordMetricsCancellationTokenSource = new(); + + if (diagMetrics) + { + _ = Task.Run(async () => { + + // Recording a single value appeared to cause test flakiness due to a race + // condition with the timing of when dotnet-counters starts collecting and + // when these values are published. Publishing values repeatedly bypasses this problem. + while (!recordMetricsCancellationTokenSource.Token.IsCancellationRequested) + { + recordMetricsCancellationTokenSource.Token.ThrowIfCancellationRequested(); + + metrics.IncrementCounter(); + metrics.RecordHistogram(10.0f); + await Task.Delay(1000).ConfigureAwait(true); + } + + }).ConfigureAwait(true); + } + TestBodyCore(customCategoryLogger, appCategoryLogger); Console.WriteLine($"{pid} EventPipeTracee: signal end of test data"); @@ -87,6 +116,8 @@ namespace EventPipeTracee // Wait for server to send something input = pipeStream.ReadByte(); + recordMetricsCancellationTokenSource.Cancel(); + Console.WriteLine($"{pid} EventPipeTracee {DateTime.UtcNow} Ending remote test process '{input}'"); return 0; } diff --git a/src/tests/Microsoft.Diagnostics.Monitoring.EventPipe/PipelineTestUtilities.cs b/src/tests/Microsoft.Diagnostics.Monitoring.EventPipe/PipelineTestUtilities.cs index 70f72ecb4..ffd52880e 100644 --- a/src/tests/Microsoft.Diagnostics.Monitoring.EventPipe/PipelineTestUtilities.cs +++ b/src/tests/Microsoft.Diagnostics.Monitoring.EventPipe/PipelineTestUtilities.cs @@ -4,6 +4,7 @@ using System; using System.Threading; using System.Threading.Tasks; +using CommonTestRunner; using Microsoft.Diagnostics.TestHelpers; using Xunit.Abstractions; using TestRunner = Microsoft.Diagnostics.CommonTestRunner.TestRunner; @@ -16,9 +17,7 @@ namespace Microsoft.Diagnostics.Monitoring.EventPipe.UnitTests public static async Task StartProcess(TestConfiguration config, string testArguments, ITestOutputHelper outputHelper, int testProcessTimeout = 60_000) { - TestRunner runner = await TestRunner.Create(config, outputHelper, "EventPipeTracee", testArguments); - await runner.Start(testProcessTimeout); - return runner; + return await TestRunnerUtilities.StartProcess(config, testArguments, outputHelper, testProcessTimeout); } public static async Task ExecutePipelineWithTracee( @@ -75,14 +74,7 @@ namespace Microsoft.Diagnostics.Monitoring.EventPipe.UnitTests { Task runTask = await startPipelineAsync(pipeline, token); - // Begin event production - testRunner.WakeupTracee(); - - // Wait for event production to be done - testRunner.WaitForSignal(); - - try - { + Func waitForPipeline = async (cancellationToken) => { // Optionally wait on caller before allowing the pipeline to stop. if (null != waitTaskSource) { @@ -96,15 +88,9 @@ namespace Microsoft.Diagnostics.Monitoring.EventPipe.UnitTests //Signal for the pipeline to stop await pipeline.StopAsync(token); + }; - //After a pipeline is stopped, we should expect the RunTask to eventually finish - await runTask; - } - finally - { - // Signal for debugee that's ok to end/move on. - testRunner.WakeupTracee(); - } + await TestRunnerUtilities.ExecuteCollection(runTask, testRunner, token, waitForPipeline); } } } diff --git a/src/tests/dotnet-counters/CSVExporterTests.cs b/src/tests/dotnet-counters/CSVExporterTests.cs index fca3d71c7..7c283ff4e 100644 --- a/src/tests/dotnet-counters/CSVExporterTests.cs +++ b/src/tests/dotnet-counters/CSVExporterTests.cs @@ -36,11 +36,7 @@ namespace DotnetCounters.UnitTests List lines = File.ReadLines(fileName).ToList(); Assert.Equal(101, lines.Count); // should be 101 including the headers - string[] headerTokens = lines[0].Split(','); - Assert.Equal("Provider", headerTokens[1]); - Assert.Equal("Counter Name", headerTokens[2]); - Assert.Equal("Counter Type", headerTokens[3]); - Assert.Equal("Mean/Increment", headerTokens[4]); + ValidateHeaderTokens(lines[0]); for (int i = 1; i < lines.Count; i++) { @@ -78,12 +74,7 @@ namespace DotnetCounters.UnitTests List lines = File.ReadLines(fileName).ToList(); Assert.Equal(11, lines.Count); // should be 11 including the headers - string[] headerTokens = lines[0].Split(','); - Assert.Equal("Provider", headerTokens[1]); - Assert.Equal("Counter Name", headerTokens[2]); - Assert.Equal("Counter Type", headerTokens[3]); - Assert.Equal("Mean/Increment", headerTokens[4]); - + ValidateHeaderTokens(lines[0]); for (int i = 1; i < lines.Count; i++) { @@ -121,11 +112,7 @@ namespace DotnetCounters.UnitTests List lines = File.ReadLines(fileName).ToList(); Assert.Equal(101, lines.Count); // should be 101 including the headers - string[] headerTokens = lines[0].Split(','); - Assert.Equal("Provider", headerTokens[1]); - Assert.Equal("Counter Name", headerTokens[2]); - Assert.Equal("Counter Type", headerTokens[3]); - Assert.Equal("Mean/Increment", headerTokens[4]); + ValidateHeaderTokens(lines[0]); for (int i = 1; i < lines.Count; i++) { @@ -163,11 +150,7 @@ namespace DotnetCounters.UnitTests List lines = File.ReadLines(fileName).ToList(); Assert.Equal(101, lines.Count); // should be 101 including the headers - string[] headerTokens = lines[0].Split(','); - Assert.Equal("Provider", headerTokens[1]); - Assert.Equal("Counter Name", headerTokens[2]); - Assert.Equal("Counter Type", headerTokens[3]); - Assert.Equal("Mean/Increment", headerTokens[4]); + ValidateHeaderTokens(lines[0]); for (int i = 1; i < lines.Count; i++) { @@ -205,11 +188,7 @@ namespace DotnetCounters.UnitTests List lines = File.ReadLines(fileName).ToList(); Assert.Equal(101, lines.Count); // should be 101 including the headers - string[] headerTokens = lines[0].Split(','); - Assert.Equal("Provider", headerTokens[1]); - Assert.Equal("Counter Name", headerTokens[2]); - Assert.Equal("Counter Type", headerTokens[3]); - Assert.Equal("Mean/Increment", headerTokens[4]); + ValidateHeaderTokens(lines[0]); for (int i = 1; i < lines.Count; i++) { @@ -247,11 +226,7 @@ namespace DotnetCounters.UnitTests List lines = File.ReadLines(fileName).ToList(); Assert.Equal(101, lines.Count); // should be 101 including the headers - string[] headerTokens = lines[0].Split(','); - Assert.Equal("Provider", headerTokens[1]); - Assert.Equal("Counter Name", headerTokens[2]); - Assert.Equal("Counter Type", headerTokens[3]); - Assert.Equal("Mean/Increment", headerTokens[4]); + ValidateHeaderTokens(lines[0]); for (int i = 1; i < lines.Count; i++) { @@ -268,5 +243,14 @@ namespace DotnetCounters.UnitTests File.Delete(fileName); } } + + internal static void ValidateHeaderTokens(string headerLine) + { + string[] headerTokens = headerLine.Split(','); + Assert.Equal("Provider", headerTokens[TestConstants.ProviderIndex]); + Assert.Equal("Counter Name", headerTokens[TestConstants.CounterNameIndex]); + Assert.Equal("Counter Type", headerTokens[TestConstants.CounterTypeIndex]); + Assert.Equal("Mean/Increment", headerTokens[TestConstants.ValueIndex]); + } } } diff --git a/src/tests/dotnet-counters/CounterMonitorPayloadTests.cs b/src/tests/dotnet-counters/CounterMonitorPayloadTests.cs new file mode 100644 index 000000000..b2c38cbd4 --- /dev/null +++ b/src/tests/dotnet-counters/CounterMonitorPayloadTests.cs @@ -0,0 +1,336 @@ +// 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.CommandLine; +using System.CommandLine.IO; +using System.IO; +using System.Linq; +using System.Runtime.InteropServices; +using System.Text.Json; +using System.Threading; +using System.Threading.Tasks; +using CommonTestRunner; +using Microsoft.Diagnostics.TestHelpers; +using Microsoft.Diagnostics.Tools.Counters; +using Xunit; +using Xunit.Abstractions; +using Xunit.Extensions; +using TestRunner = Microsoft.Diagnostics.CommonTestRunner.TestRunner; +using Constants = DotnetCounters.UnitTests.TestConstants; + +namespace DotnetCounters.UnitTests +{ + /// + /// Tests the behavior of CounterMonitor's Collect command. + /// + public class CounterMonitorPayloadTests + { + private enum CounterTypes + { + Metric, Rate + } + + private ITestOutputHelper _outputHelper; + private static readonly TimeSpan DefaultTimeout = TimeSpan.FromMinutes(2); + private static readonly string SystemRuntimeName = "System.Runtime"; + private static readonly string TagStart = "["; + + private static HashSet ExpectedCounterTypes = new() { CounterTypes.Metric, CounterTypes.Rate }; + + public CounterMonitorPayloadTests(ITestOutputHelper outputHelper) + { + _outputHelper = outputHelper; + } + + [SkippableTheory, MemberData(nameof(Configurations))] + public async Task TestCounterMonitorCustomMetricsJSON(TestConfiguration configuration) + { + CheckRuntimeOS(); + CheckFramework(configuration); + + List metricComponents = await GetCounterTraceJSON(configuration, new List { Constants.TestMeterName }); + + ValidateCustomMetrics(metricComponents, CountersExportFormat.json); + } + + [SkippableTheory, MemberData(nameof(Configurations))] + public async Task TestCounterMonitorCustomMetricsCSV(TestConfiguration configuration) + { + CheckRuntimeOS(); + CheckFramework(configuration); + + List metricComponents = await GetCounterTraceCSV(configuration, new List { Constants.TestMeterName }); + + ValidateCustomMetrics(metricComponents, CountersExportFormat.csv); + } + + [SkippableTheory, MemberData(nameof(Configurations))] + public async Task TestCounterMonitorSystemRuntimeMetricsJSON(TestConfiguration configuration) + { + CheckRuntimeOS(); + + List metricComponents = await GetCounterTraceJSON(configuration, new List { SystemRuntimeName }); + + ValidateSystemRuntimeMetrics(metricComponents); + } + + [SkippableTheory, MemberData(nameof(Configurations))] + public async Task TestCounterMonitorSystemRuntimeMetricsCSV(TestConfiguration configuration) + { + CheckRuntimeOS(); + + List metricComponents = await GetCounterTraceCSV(configuration, new List { SystemRuntimeName }); + + ValidateSystemRuntimeMetrics(metricComponents); + } + + private void ValidateSystemRuntimeMetrics(List metricComponents) + { + string[] ExpectedProviders = { "System.Runtime" }; + Assert.Equal(ExpectedProviders, metricComponents.Select(c => c.ProviderName).ToHashSet()); + + // Could also just check the number of counter names + HashSet expectedCounterNames = new() + { + "CPU Usage (%)", + "Working Set (MB)", + "GC Heap Size (MB)", + "Gen 0 GC Count (Count / 1 sec)", + "Gen 1 GC Count (Count / 1 sec)", + "Gen 2 GC Count (Count / 1 sec)", + "ThreadPool Thread Count", + "Monitor Lock Contention Count (Count / 1 sec)", + "ThreadPool Queue Length", + "ThreadPool Completed Work Item Count (Count / 1 sec)", + "Allocation Rate (B / 1 sec)", + "Number of Active Timers", + "GC Fragmentation (%)", + "GC Committed Bytes (MB)", + "Exception Count (Count / 1 sec)", + "% Time in GC since last GC (%)", + "Gen 0 Size (B)", + "Gen 1 Size (B)", + "Gen 2 Size (B)", + "LOH Size (B)", + "POH (Pinned Object Heap) Size (B)", + "Number of Assemblies Loaded", + "IL Bytes Jitted (B)", + "Number of Methods Jitted", + "Time spent in JIT (ms / 1 sec)" + }; + + Assert.Subset(metricComponents.Select(c => c.CounterName).ToHashSet(), expectedCounterNames); + + Assert.Equal(ExpectedCounterTypes, metricComponents.Select(c => c.CounterType).ToHashSet()); + } + + private async Task> GetCounterTraceJSON(TestConfiguration configuration, List counterList) + { + string path = Path.ChangeExtension(Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString()), "json"); + + Func> createMetricComponents = () => + { + using FileStream metricsFile = File.OpenRead(path); + JSONCounterTrace trace = JsonSerializer.Deserialize(metricsFile, new JsonSerializerOptions { PropertyNameCaseInsensitive = true }); + + var providers = trace.events.Select(e => e.provider).ToList(); + var counterNames = trace.events.Select(e => e.name).ToList(); + var counterTypes = trace.events.Select(e => e.counterType).ToList(); + var tags = trace.events.Select(e => e.tags).ToList(); + var values = trace.events.Select(e => e.value).ToList(); + + return CreateMetricComponents(providers, counterNames, counterTypes, tags, values); + }; + + return await GetCounterTrace(configuration, counterList, path, CountersExportFormat.json, createMetricComponents); + } + + private async Task> GetCounterTraceCSV(TestConfiguration configuration, List counterList) + { + string path = Path.ChangeExtension(Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString()), "csv"); + + Func> createMetricComponents = () => + { + List lines = File.ReadLines(path).ToList(); + CSVExporterTests.ValidateHeaderTokens(lines[0]); + lines.RemoveAt(0); // Trim the header + + IEnumerable splitLines = lines.Select(l => l.Split(",")); + + var providers = splitLines.Select(line => line[Constants.ProviderIndex]).ToList(); + var countersList = splitLines.Select(line => line[Constants.CounterNameIndex]).ToList(); + var counterNames = countersList.Select(counter => counter.Split(TagStart)[0]).ToList(); + var counterTypes = splitLines.Select(line => line[Constants.CounterTypeIndex]).ToList(); + var tags = GetCSVTags(countersList); + var values = GetCSVValues(splitLines); + + return CreateMetricComponents(providers, counterNames, counterTypes, tags, values); + }; + + return await GetCounterTrace(configuration, counterList, path, CountersExportFormat.csv, createMetricComponents); + } + + private List CreateMetricComponents(List providers, List counterNames, List counterTypes, List tags, List values) + { + List metricComponents = new(providers.Count()); + + for (int index = 0; index < providers.Count(); ++index) + { + CounterTypes type; + Enum.TryParse(counterTypes[index], out type); + + metricComponents.Add(new MetricComponents() + { + ProviderName = providers[index], + CounterName = counterNames[index], + CounterType = type, + Tags = tags[index], + Value = values[index] + }); + } + + return metricComponents; + } + + private async Task> GetCounterTrace(TestConfiguration configuration, List counterList, string path, CountersExportFormat exportFormat, Func> CreateMetricComponents) + { + try + { + CounterMonitor monitor = new CounterMonitor(); + + using CancellationTokenSource source = new CancellationTokenSource(DefaultTimeout); + + await using var testRunner = await TestRunnerUtilities.StartProcess(configuration, "TestCounterMonitor DiagMetrics", _outputHelper); + + await TestRunnerUtilities.ExecuteCollection((ct) => { + return Task.Run(async () => + await monitor.Collect( + ct: ct, + counter_list: counterList, + counters: null, + console: new TestConsole(), + processId: testRunner.Pid, + refreshInterval: 1, + format: exportFormat, + output: path, + name: null, + diagnosticPort: null, + resumeRuntime: false, + maxHistograms: 10, + maxTimeSeries: 1000, + duration: TimeSpan.FromSeconds(10))); + }, testRunner, source.Token); + + return CreateMetricComponents(); + } + finally + { + try + { + File.Delete(path); + } + catch { } + } + } + + private void ValidateCustomMetrics(List metricComponents, CountersExportFormat format) + { + // Currently not validating timestamp due to https://github.com/dotnet/diagnostics/issues/3905 + + HashSet expectedProviders = new() { Constants.TestMeterName }; + Assert.Equal(expectedProviders, metricComponents.Select(c => c.ProviderName).ToHashSet()); + + HashSet expectedCounterNames = new() { Constants.TestHistogramName, Constants.TestCounterName }; + Assert.Equal(expectedCounterNames, metricComponents.Select(c => c.CounterName).ToHashSet()); + + Assert.Equal(ExpectedCounterTypes, metricComponents.Select(c => c.CounterType).ToHashSet()); + + string tagSeparator = format == CountersExportFormat.csv ? ";" : ","; + string tag = Constants.TagKey + "=" + Constants.TagValue + tagSeparator + Constants.PercentileKey + "="; + HashSet expectedTags = new() { $"{tag}{Constants.Quantile50}", $"{tag}{Constants.Quantile95}", $"{tag}{Constants.Quantile99}" }; + Assert.Equal(expectedTags, metricComponents.Where(c => c.CounterName == Constants.TestHistogramName).Select(c => c.Tags).Distinct()); + Assert.Empty(metricComponents.Where(c => c.CounterName == Constants.TestCounterName).Where(c => c.Tags != string.Empty)); + + var actualCounterValues = metricComponents.Where(c => c.CounterName == Constants.TestCounterName).Select(c => c.Value); + + Assert.NotEmpty(actualCounterValues); + double histogramValue = Assert.Single(metricComponents.Where(c => c.CounterName == Constants.TestHistogramName).Select(c => c.Value).Distinct()); + Assert.Equal(10, histogramValue); + } + + private List GetCSVTags(List countersList) + { + var tags = countersList.Select(counter => { + var split = counter.Split(TagStart); + return split.Length > 1 ? split[1].Remove(split[1].Length - 1) : string.Empty; + }).ToList(); + + return tags; + } + + private List GetCSVValues(IEnumerable splitLines) + { + return splitLines.Select(line => { + return double.TryParse(line[Constants.ValueIndex], out double val) ? val : -1; + }).ToList(); + } + + private void CheckFramework(TestConfiguration configuration) + { + if (configuration.RuntimeFrameworkVersionMajor < 8) + { + throw new SkipTestException("Not supported on < .NET 8.0"); + } + } + + private void CheckRuntimeOS() + { + if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) + { + throw new SkipTestException("Test instability on OSX"); + } + } + + public static IEnumerable Configurations => TestRunner.Configurations; + + private sealed class MetricComponents + { + public string ProviderName { get; set; } + public string CounterName { get; set; } + public double Value { get; set; } + public string Tags { get; set; } + public CounterTypes CounterType { get; set; } + } + + private sealed class TestConsole : IConsole + { + private readonly TestStandardStreamWriter _outWriter; + private readonly TestStandardStreamWriter _errorWriter; + + private sealed class TestStandardStreamWriter : IStandardStreamWriter + { + private StringWriter _writer = new(); + public void Write(string value) => _writer.Write(value); + public void WriteLine(string value) => _writer.WriteLine(value); + } + + public TestConsole() + { + _outWriter = new TestStandardStreamWriter(); + _errorWriter = new TestStandardStreamWriter(); + } + + public IStandardStreamWriter Out => _outWriter; + + public bool IsOutputRedirected => true; + + public IStandardStreamWriter Error => _errorWriter; + + public bool IsErrorRedirected => true; + + public bool IsInputRedirected => false; + } + } +} diff --git a/src/tests/dotnet-counters/DotnetCounters.UnitTests.csproj b/src/tests/dotnet-counters/DotnetCounters.UnitTests.csproj index 06c187814..00f6e91c0 100644 --- a/src/tests/dotnet-counters/DotnetCounters.UnitTests.csproj +++ b/src/tests/dotnet-counters/DotnetCounters.UnitTests.csproj @@ -6,6 +6,8 @@ + + diff --git a/src/tests/dotnet-counters/TestConstants.cs b/src/tests/dotnet-counters/TestConstants.cs new file mode 100644 index 000000000..4e9912d2e --- /dev/null +++ b/src/tests/dotnet-counters/TestConstants.cs @@ -0,0 +1,25 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace DotnetCounters.UnitTests +{ + public static class TestConstants + { + public const string TestCounter = "TestCounter"; + public const string TestCounterName = TestCounter + " (dollars / 1 sec)"; + public const string TestHistogram = "TestHistogram"; + public const string TestHistogramName = TestHistogram + " (feet)"; + public const string PercentileKey = "Percentile"; + public const string TagKey = "TestTag"; + public const string TagValue = "5"; + public const string TestMeterName = "TestMeter"; + public const string Quantile50 = "50"; + public const string Quantile95 = "95"; + public const string Quantile99 = "99"; + + public const int ProviderIndex = 1; + public const int CounterNameIndex = 2; + public const int CounterTypeIndex = 3; + public const int ValueIndex = 4; + } +}