using System.Text.RegularExpressions; using AutopilotMonitor.Functions.Functions.Rules; namespace AutopilotMonitor.Functions.Tests; /// /// Pins the agent-parity semantics of the logparser pattern test endpoint: case-sensitive /// matching (the field incident that motivated the endpoint: a pattern verified with a PHP /// regex tester behaved differently under .NET), first-match-per-line, group extraction /// identical to what LogParserCollector puts into the emitted event data, CMTrace parsing /// via the SHARED CmTraceLogParser, and the format=text hint. /// public class TestLogPatternFunctionTests { private static Regex AgentRegex(string pattern) => new(pattern, RegexOptions.Compiled, TimeSpan.FromSeconds(1)); private const string CmTraceLine = "" + ""; // ===== text mode ===== [Fact] public void TextMode_NamedGroups_LandInGroups() { var result = TestLogPatternFunction.EvaluatePattern( AgentRegex(@"(?ERROR|FATAL)\s+(?\w+):\s+(?.+)"), isTextMode: true, new[] { "ERROR Installer: disk full", "INFO Installer: all good" }); Assert.Equal(1, result.MatchCount); Assert.Equal("matched", result.Lines[0].Outcome); Assert.Equal("ERROR", result.Lines[0].Groups!["level"]); Assert.Equal("Installer", result.Lines[0].Groups!["component"]); Assert.Equal("disk full", result.Lines[0].Groups!["message"]); Assert.Equal("no_match", result.Lines[1].Outcome); } /// The motivating field case: logparser matching has NO IgnoreCase — a pattern /// that matched in a case-insensitive tester silently never fires on the device. [Fact] public void TextMode_IsCaseSensitive_AndSaysSoInTheNote() { var result = TestLogPatternFunction.EvaluatePattern( AgentRegex("ERROR"), isTextMode: true, new[] { "error: something broke" }); Assert.Equal(0, result.MatchCount); Assert.Contains(result.Notes, n => n.Contains("case-SENSITIVE")); } [Fact] public void TextMode_UnsuccessfulOptionalGroup_IsOmitted() { var result = TestLogPatternFunction.EvaluatePattern( AgentRegex(@"ERROR(?\s0x[0-9A-F]+)?"), isTextMode: true, new[] { "ERROR without code" }); Assert.Equal(1, result.MatchCount); Assert.False(result.Lines[0].Groups!.ContainsKey("code")); } // ===== cmtrace mode ===== [Fact] public void CmTraceMode_MatchesAgainstParsedMessage_AndReportsComponentAndType() { var result = TestLogPatternFunction.EvaluatePattern( AgentRegex(@"error (?0x[0-9A-Fa-f]+)"), isTextMode: false, new[] { CmTraceLine }); Assert.Equal(1, result.MatchCount); var row = result.Lines[0]; Assert.Equal("matched", row.Outcome); Assert.Equal("0x87D1041C", row.Groups!["errorCode"]); Assert.Equal("AppWorkload", row.Component); Assert.Equal(3, row.CmTraceType); Assert.Contains("Installation failed", row.Message); } [Fact] public void CmTraceMode_RegexDoesNotSeeTheEnvelope() { // "component=" only exists in the CMTrace envelope, never in the message the // agent matches against — a pattern targeting it must NOT match. var result = TestLogPatternFunction.EvaluatePattern( AgentRegex("component="), isTextMode: false, new[] { CmTraceLine }); Assert.Equal(0, result.MatchCount); Assert.Equal("no_match", result.Lines[0].Outcome); } [Fact] public void CmTraceMode_PlainTextLines_ParseFail_WithFormatHint() { var result = TestLogPatternFunction.EvaluatePattern( AgentRegex("ERROR"), isTextMode: false, new[] { "ERROR plain line", "another plain line" }); Assert.Equal(2, result.ParseFailureCount); Assert.All(result.Lines, l => Assert.Equal("parse_failed", l.Outcome)); Assert.Contains(result.Notes, n => n.Contains("format=text")); } // ===== request validation ===== [Fact] public void Validate_RejectsMissingPatternAndLines() { Assert.Contains("pattern", TestLogPatternFunction.ValidateRequest( new TestLogPatternFunction.TestLogPatternRequest { SampleLines = new List { "x" } })!); Assert.Contains("sampleLines", TestLogPatternFunction.ValidateRequest( new TestLogPatternFunction.TestLogPatternRequest { Pattern = "a" })!); } [Fact] public void Validate_RejectsTooManyLines_AndBadFormat() { var tooMany = new TestLogPatternFunction.TestLogPatternRequest { Pattern = "a", SampleLines = Enumerable.Repeat("line", TestLogPatternFunction.MaxSampleLines + 1).ToList(), }; Assert.Contains("at most", TestLogPatternFunction.ValidateRequest(tooMany)!); var badFormat = new TestLogPatternFunction.TestLogPatternRequest { Pattern = "a", Format = "json", SampleLines = new List { "x" }, }; Assert.Contains("format", TestLogPatternFunction.ValidateRequest(badFormat)!); } [Fact] public void Validate_AcceptsBothFormatsAndDefault() { foreach (var format in new[] { null, "cmtrace", "text", "TEXT" }) { Assert.Null(TestLogPatternFunction.ValidateRequest(new TestLogPatternFunction.TestLogPatternRequest { Pattern = "a", Format = format, SampleLines = new List { "x" }, })); } } }