using System.Threading.Tasks; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.Testing; using Xunit; using Verifier = Microsoft.CodeAnalysis.CSharp.Testing.XUnit.AnalyzerVerifier; namespace StellaOps.Telemetry.Analyzers.Tests; public sealed class MetricLabelAnalyzerTests { [Fact] public async Task ValidLabelKey_NoDiagnostic() { var test = """ using System; using System.Collections.Generic; namespace TestNamespace { public class GoldenSignalMetrics { public static KeyValuePair Tag(string key, object? value) => new(key, value); public void RecordLatency(double value, params KeyValuePair[] tags) { } } public class TestClass { public void TestMethod() { var metrics = new GoldenSignalMetrics(); metrics.RecordLatency(100.0, GoldenSignalMetrics.Tag("status_code", "200")); } } } """; await Verifier.VerifyAnalyzerAsync(test); } [Fact] public async Task InvalidLabelKey_UpperCase_ReportsDiagnostic() { var test = """ using System; using System.Collections.Generic; namespace TestNamespace { public class GoldenSignalMetrics { public static KeyValuePair Tag(string key, object? value) => new(key, value); public void RecordLatency(double value, params KeyValuePair[] tags) { } } public class TestClass { public void TestMethod() { var metrics = new GoldenSignalMetrics(); metrics.RecordLatency(100.0, GoldenSignalMetrics.Tag({|#0:"StatusCode"|}, "200")); } } } """; var expected = Verifier.Diagnostic(MetricLabelAnalyzer.InvalidLabelKeyDiagnosticId) .WithLocation(0) .WithArguments("StatusCode"); await Verifier.VerifyAnalyzerAsync(test, expected); } [Fact] public async Task HighCardinalityLabelKey_UserId_ReportsDiagnostic() { var test = """ using System; using System.Collections.Generic; namespace TestNamespace { public class GoldenSignalMetrics { public static KeyValuePair Tag(string key, object? value) => new(key, value); public void RecordLatency(double value, params KeyValuePair[] tags) { } } public class TestClass { public void TestMethod() { var metrics = new GoldenSignalMetrics(); metrics.RecordLatency(100.0, GoldenSignalMetrics.Tag({|#0:"user_id"|}, "123")); } } } """; var expected = Verifier.Diagnostic(MetricLabelAnalyzer.HighCardinalityDiagnosticId) .WithLocation(0) .WithArguments("user_id"); await Verifier.VerifyAnalyzerAsync(test, expected); } [Fact] public async Task HighCardinalityLabelKey_RequestId_ReportsDiagnostic() { var test = """ using System; using System.Collections.Generic; namespace TestNamespace { public class GoldenSignalMetrics { public static KeyValuePair Tag(string key, object? value) => new(key, value); public void IncrementRequests(params KeyValuePair[] tags) { } } public class TestClass { public void TestMethod() { var metrics = new GoldenSignalMetrics(); metrics.IncrementRequests(GoldenSignalMetrics.Tag({|#0:"request_id"|}, "abc-123")); } } } """; var expected = Verifier.Diagnostic(MetricLabelAnalyzer.HighCardinalityDiagnosticId) .WithLocation(0) .WithArguments("request_id"); await Verifier.VerifyAnalyzerAsync(test, expected); } [Fact] public async Task HighCardinalityLabelKey_Email_ReportsDiagnostic() { var test = """ using System; using System.Collections.Generic; namespace TestNamespace { public class GoldenSignalMetrics { public static KeyValuePair Tag(string key, object? value) => new(key, value); public void IncrementErrors(params KeyValuePair[] tags) { } } public class TestClass { public void TestMethod() { var metrics = new GoldenSignalMetrics(); metrics.IncrementErrors(GoldenSignalMetrics.Tag({|#0:"user_email"|}, "test@example.com")); } } } """; var expected = Verifier.Diagnostic(MetricLabelAnalyzer.HighCardinalityDiagnosticId) .WithLocation(0) .WithArguments("user_email"); await Verifier.VerifyAnalyzerAsync(test, expected); } [Fact] public async Task DynamicLabelValue_Variable_ReportsDiagnostic() { var test = """ using System; using System.Collections.Generic; namespace TestNamespace { public class GoldenSignalMetrics { public static KeyValuePair Tag(string key, object? value) => new(key, value); public void RecordLatency(double value, params KeyValuePair[] tags) { } } public class TestClass { public void TestMethod(string dynamicValue) { var metrics = new GoldenSignalMetrics(); metrics.RecordLatency(100.0, GoldenSignalMetrics.Tag("operation", {|#0:dynamicValue|})); } } } """; var expected = Verifier.Diagnostic(MetricLabelAnalyzer.DynamicLabelDiagnosticId) .WithLocation(0); await Verifier.VerifyAnalyzerAsync(test, expected); } [Fact] public async Task DynamicLabelValue_InterpolatedString_ReportsDiagnostic() { var test = """ using System; using System.Collections.Generic; namespace TestNamespace { public class GoldenSignalMetrics { public static KeyValuePair Tag(string key, object? value) => new(key, value); public void RecordLatency(double value, params KeyValuePair[] tags) { } } public class TestClass { public void TestMethod(int code) { var metrics = new GoldenSignalMetrics(); metrics.RecordLatency(100.0, GoldenSignalMetrics.Tag("status", {|#0:$"code_{code}"|})); } } } """; var expected = Verifier.Diagnostic(MetricLabelAnalyzer.DynamicLabelDiagnosticId) .WithLocation(0); await Verifier.VerifyAnalyzerAsync(test, expected); } [Fact] public async Task StaticLabelValue_Constant_NoDiagnostic() { var test = """ using System; using System.Collections.Generic; namespace TestNamespace { public class GoldenSignalMetrics { public static KeyValuePair Tag(string key, object? value) => new(key, value); public void RecordLatency(double value, params KeyValuePair[] tags) { } } public class TestClass { private const string StatusOk = "ok"; public void TestMethod() { var metrics = new GoldenSignalMetrics(); metrics.RecordLatency(100.0, GoldenSignalMetrics.Tag("status", StatusOk)); } } } """; await Verifier.VerifyAnalyzerAsync(test); } [Fact] public async Task EnumLabelValue_NoDiagnostic() { var test = """ using System; using System.Collections.Generic; namespace TestNamespace { public enum Status { Ok, Error } public class GoldenSignalMetrics { public static KeyValuePair Tag(string key, object? value) => new(key, value); public void RecordLatency(double value, params KeyValuePair[] tags) { } } public class TestClass { public void TestMethod() { var metrics = new GoldenSignalMetrics(); metrics.RecordLatency(100.0, GoldenSignalMetrics.Tag("status", Status.Ok)); } } } """; await Verifier.VerifyAnalyzerAsync(test); } [Fact] public async Task EnumToStringLabelValue_NoDiagnostic() { var test = """ using System; using System.Collections.Generic; namespace TestNamespace { public enum Status { Ok, Error } public class GoldenSignalMetrics { public static KeyValuePair Tag(string key, object? value) => new(key, value); public void RecordLatency(double value, params KeyValuePair[] tags) { } } public class TestClass { public void TestMethod() { var metrics = new GoldenSignalMetrics(); metrics.RecordLatency(100.0, GoldenSignalMetrics.Tag("status", Status.Ok.ToString())); } } } """; await Verifier.VerifyAnalyzerAsync(test); } [Fact] public async Task TupleSyntax_ValidLabel_NoDiagnostic() { var test = """ using System; using System.Diagnostics.Metrics; namespace TestNamespace { public class TestClass { public void TestMethod(Counter counter) { counter.Add(1, ("status_code", "200")); } } } """; await Verifier.VerifyAnalyzerAsync(test); } [Fact] public async Task KeyValuePairCreation_HighCardinalityKey_ReportsDiagnostic() { var test = """ using System; using System.Collections.Generic; using System.Diagnostics.Metrics; namespace TestNamespace { public class TestClass { public void TestMethod(Counter counter) { counter.Add(1, new KeyValuePair({|#0:"session_id"|}, "abc")); } } } """; var expected = Verifier.Diagnostic(MetricLabelAnalyzer.HighCardinalityDiagnosticId) .WithLocation(0) .WithArguments("session_id"); await Verifier.VerifyAnalyzerAsync(test, expected); } [Fact] public async Task NonMetricMethod_NoDiagnostic() { var test = """ using System; using System.Collections.Generic; namespace TestNamespace { public class RegularClass { public static KeyValuePair Tag(string key, object? value) => new(key, value); public void SomeMethod(params KeyValuePair[] tags) { } } public class TestClass { public void TestMethod() { var obj = new RegularClass(); obj.SomeMethod(RegularClass.Tag("user_id", "123")); } } } """; await Verifier.VerifyAnalyzerAsync(test); } [Fact] public async Task MultipleIssues_ReportsAllDiagnostics() { var test = """ using System; using System.Collections.Generic; namespace TestNamespace { public class GoldenSignalMetrics { public static KeyValuePair Tag(string key, object? value) => new(key, value); public void RecordLatency(double value, params KeyValuePair[] tags) { } } public class TestClass { public void TestMethod(string dynamicValue) { var metrics = new GoldenSignalMetrics(); metrics.RecordLatency(100.0, GoldenSignalMetrics.Tag({|#0:"UserId"|}, "static"), GoldenSignalMetrics.Tag("operation", {|#1:dynamicValue|})); } } } """; var expected1 = Verifier.Diagnostic(MetricLabelAnalyzer.InvalidLabelKeyDiagnosticId) .WithLocation(0) .WithArguments("UserId"); var expected2 = Verifier.Diagnostic(MetricLabelAnalyzer.DynamicLabelDiagnosticId) .WithLocation(1); await Verifier.VerifyAnalyzerAsync(test, expected1, expected2); } [Fact] public async Task StaticReadonlyField_LabelValue_NoDiagnostic() { var test = """ using System; using System.Collections.Generic; namespace TestNamespace { public class GoldenSignalMetrics { public static KeyValuePair Tag(string key, object? value) => new(key, value); public void RecordLatency(double value, params KeyValuePair[] tags) { } } public static class Labels { public static readonly string StatusOk = "ok"; } public class TestClass { public void TestMethod() { var metrics = new GoldenSignalMetrics(); metrics.RecordLatency(100.0, GoldenSignalMetrics.Tag("status", Labels.StatusOk)); } } } """; await Verifier.VerifyAnalyzerAsync(test); } }