From 6d56a4a01bbce172326f29c77ad341f49b91d1e2 Mon Sep 17 00:00:00 2001 From: Scottxjw <13374147+scottxjw@user.noreply.gitee.com> Date: Mon, 17 Aug 2026 10:47:33 +0800 Subject: [PATCH] =?UTF-8?q?feat(parser):=20=E6=94=AF=E6=8C=81=20Ploss=20Fi?= =?UTF-8?q?eld1=20=E5=8D=81=E5=85=AD=E8=BF=9B=E5=88=B6=E8=A7=A3=E6=9E=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.8 (1M context) --- .../Application/Parsing/PlossParser.cs | 27 ++++++++++++++----- .../Commands/AnalyzeCommandTests.cs | 20 ++++++++++++++ .../Commands/ParseCommandTests.cs | 24 +++++++++++++++++ .../TestFixtures.cs | 19 ++++++++++--- 4 files changed, 80 insertions(+), 10 deletions(-) diff --git a/src/Gpulse.WCT.DataAnalyzer.Core/Application/Parsing/PlossParser.cs b/src/Gpulse.WCT.DataAnalyzer.Core/Application/Parsing/PlossParser.cs index 858aecc..1b8fccf 100644 --- a/src/Gpulse.WCT.DataAnalyzer.Core/Application/Parsing/PlossParser.cs +++ b/src/Gpulse.WCT.DataAnalyzer.Core/Application/Parsing/PlossParser.cs @@ -1,3 +1,4 @@ +using System.Globalization; using System.Text.RegularExpressions; using Gpulse.WCT.DataAnalyzer.Core.Domain.Local; @@ -17,15 +18,18 @@ public class PlossParser : IParser RegexOptions.Compiled ); - // 第二行正则:FOD-> 后跟16个数字(均支持负数) + // 第二行正则:FOD-> 后跟16个字段 + // Field1 支持十六进制格式(0X/0x 前缀,如 0C、0xFF)或普通十进制; + // 其余字段为十进制整数(均支持负数)。 + // Field1 用捕获组是为了方便从 Match 取值(解析逻辑见 ParseFieldValue)。 private static readonly Regex FodPattern = new( - @"^FOD->\s+(-?\d+)\s+(-?\d+)\s+(-?\d+)\s+(-?\d+)\s+(-?\d+)\s+(-?\d+)\s+(-?\d+)\s+(-?\d+)\s+(-?\d+)\s+(-?\d+)\s+(-?\d+)\s+(-?\d+)\s+(-?\d+)\s+(-?\d+)\s+(-?\d+)\s+(-?\d+)$", + @"^FOD->\s+(0[0-9A-Fa-f]+|-?\d+)\s+(-?\d+)\s+(-?\d+)\s+(-?\d+)\s+(-?\d+)\s+(-?\d+)\s+(-?\d+)\s+(-?\d+)\s+(-?\d+)\s+(-?\d+)\s+(-?\d+)\s+(-?\d+)\s+(-?\d+)\s+(-?\d+)\s+(-?\d+)\s+(-?\d+)\s+(-?\d+)$", RegexOptions.Compiled ); - // 旧格式正则(兼容):前3个字段被忽略,所有捕获组支持负数 + // 旧格式正则(兼容):前3个字段被忽略,所有捕获组支持负数;Field1 兼容十六进制 private static readonly Regex LegacyPattern = new( - @"^FOD->\s+(-?\d+)\s+(-?\d+)\s+(-?\d+)\s+(-?\d+)\s+(-?\d+)\s+(-?\d+)\s+(-?\d+)\s+(-?\d+)\s+(-?\d+)\s+(-?\d+)\s+(-?\d+)\s+(-?\d+)\s+(-?\d+)\s+(-?\d+)$", + @"^FOD->\s+(0[0-9A-Fa-f]+|-?\d+)\s+(-?\d+)\s+(-?\d+)\s+(-?\d+)\s+(-?\d+)\s+(-?\d+)\s+(-?\d+)\s+(-?\d+)\s+(-?\d+)\s+(-?\d+)\s+(-?\d+)\s+(-?\d+)\s+(-?\d+)\s+(-?\d+)$", RegexOptions.Compiled ); @@ -103,7 +107,7 @@ public class PlossParser : IParser PowLoss = int.Parse(headerMatch.Groups[1].Value), DeltaP = int.Parse(headerMatch.Groups[2].Value), // 第二行字段 - Field1 = int.Parse(fodMatch.Groups[1].Value), + Field1 = ParseFieldValue(fodMatch.Groups[1].Value), Field2 = int.Parse(fodMatch.Groups[2].Value), Field3 = int.Parse(fodMatch.Groups[3].Value), Field4 = int.Parse(fodMatch.Groups[4].Value), @@ -144,7 +148,7 @@ public class PlossParser : IParser PowLoss = null, DeltaP = null, // 映射到新字段名 - Field1 = int.Parse(match.Groups[1].Value), + Field1 = ParseFieldValue(match.Groups[1].Value), Field2 = int.Parse(match.Groups[2].Value), Field3 = int.Parse(match.Groups[3].Value), Field4 = int.Parse(match.Groups[4].Value), // RX power @@ -206,7 +210,7 @@ public class PlossParser : IParser ScenarioId = scenarioId, PowLoss = null, DeltaP = null, - Field1 = byte.Parse(match.Groups[1].Value), + Field1 = ParseFieldValue(match.Groups[1].Value), Field2 = int.Parse(match.Groups[2].Value), Field3 = int.Parse(match.Groups[3].Value), Field4 = int.Parse(match.Groups[4].Value), @@ -233,4 +237,13 @@ public class PlossParser : IParser ); } } + + /// + /// 解析 Field1 的值,支持十六进制格式(0C、0xFF)和普通十进制。 + /// 含 a-f/A-F 字符时按十六进制解析,否则按十进制解析。 + /// + private static int ParseFieldValue(string value) => + value.AsSpan().IndexOfAny("abcdefABCDEF".AsSpan()) >= 0 + ? int.Parse(value, NumberStyles.HexNumber) + : int.Parse(value); } \ No newline at end of file diff --git a/tests/Gpulse.WCT.DataAnalyzer.Tests/Commands/AnalyzeCommandTests.cs b/tests/Gpulse.WCT.DataAnalyzer.Tests/Commands/AnalyzeCommandTests.cs index 522e720..8f5dbfc 100644 --- a/tests/Gpulse.WCT.DataAnalyzer.Tests/Commands/AnalyzeCommandTests.cs +++ b/tests/Gpulse.WCT.DataAnalyzer.Tests/Commands/AnalyzeCommandTests.cs @@ -87,6 +87,26 @@ public class AnalyzeCommandTests : CommandTestBase Assert.That(stdout, Does.Contain("Please specify --pure and --foreign for Qfod calibration")); } + [Test] + public async Task Analyze_Ploss_LegacyFile_HexField1_DecodedCorrectly() + { + var dir = NewDir(); + // 旧单行格式 Field1=0C (十六进制) 应被解析为十进制 12 + var file = TestFixtures.WriteLegacyPlossLog( + dir, + "singleMold-v1.0-hex2_1-iPhone15-ploss_legacy-20260710-1.log", + new (int, int, int)[] { (500, 3000, 0) }, + field1Hex: "0C"); + + var (code, stdout, _) = await Analyze().RunCaptureAsync("--type", "ploss", "--file", file); + + Assert.Multiple(() => + { + Assert.That(code, Is.EqualTo(0)); + Assert.That(stdout, Does.Contain("Total: 1 (TwoLine: 0, Legacy: 1), FOD: 0, Errors: 0")); + }); + } + [Test] public async Task Analyze_Qfod_ValidFiles_ReportsThreshold() { diff --git a/tests/Gpulse.WCT.DataAnalyzer.Tests/Commands/ParseCommandTests.cs b/tests/Gpulse.WCT.DataAnalyzer.Tests/Commands/ParseCommandTests.cs index 4a6f1a2..8cc14d6 100644 --- a/tests/Gpulse.WCT.DataAnalyzer.Tests/Commands/ParseCommandTests.cs +++ b/tests/Gpulse.WCT.DataAnalyzer.Tests/Commands/ParseCommandTests.cs @@ -76,6 +76,30 @@ public class ParseCommandTests : CommandTestBase Assert.That(await Host.LocalDb.PlossRecords.CountAsync(), Is.EqualTo(2)); } + [TestCase("0C", 12)] // 上位 nibble = 0,下位 nibble = C + [TestCase("0xFF", 255)] // 标准 0x 前缀 + [TestCase("0xff", 255)] // 小写 hex + [TestCase("0A", 10)] + [TestCase("0", 0)] // 纯 0:应识别为十进制 0 + [TestCase("10", 10)] // 纯十进制:应识别为 10(不是 0x10 = 16) + public async Task Parse_File_PlossLog_HexField1_DecodedCorrectly(string field1Raw, int expected) + { + var dir = NewDir(); + var file = TestFixtures.WritePlossLog(dir, TestFixtures.PlossFileName, + new TestFixtures.PlossTwoLineRow(Field1AsHex: field1Raw)); + + var (code, stdout, _) = await Parse().RunCaptureAsync("--file", file); + + Assert.Multiple(() => + { + Assert.That(code, Is.EqualTo(0)); + Assert.That(stdout, Does.Contain("Ploss: 1")); + }); + + var record = await Host.LocalDb.PlossRecords.SingleAsync(); + Assert.That(record.Field1, Is.EqualTo(expected)); + } + [Test] public async Task Parse_File_WithForceOption_Accepted() { diff --git a/tests/Gpulse.WCT.DataAnalyzer.Tests/TestFixtures.cs b/tests/Gpulse.WCT.DataAnalyzer.Tests/TestFixtures.cs index fe9e271..de486fb 100644 --- a/tests/Gpulse.WCT.DataAnalyzer.Tests/TestFixtures.cs +++ b/tests/Gpulse.WCT.DataAnalyzer.Tests/TestFixtures.cs @@ -13,10 +13,15 @@ public static class TestFixtures /// 两行 Ploss 记录(header + FOD 16 字段)。 /// 默认值让恒等式成立(Field5 - Field4 - PowLoss - ploss == DeltaP)且余量 > 2000。 /// + /// + /// 当不为 null 时,覆写 Field1,按十六进制字面量写入日志(如 "0C" = 十进制 12), + /// 用于验证解析器对 Field1 十六进制格式的支持。 + /// public sealed record PlossTwoLineRow( int PowLoss = 2200, int DeltaP = 2500, int Field1 = 1, + string? Field1AsHex = null, int Field2 = 0, int Field3 = 0, int Field4 = 800, // RX power @@ -41,8 +46,9 @@ public static class TestFixtures foreach (var r in rows) { lines.Add($"pow_loss = {r.PowLoss}, delta_p = {r.DeltaP}"); + var field1 = r.Field1AsHex ?? r.Field1.ToString(); lines.Add( - $"FOD-> {r.Field1} {r.Field2} {r.Field3} {r.Field4} {r.Field5} {r.Field6} " + + $"FOD-> {field1} {r.Field2} {r.Field3} {r.Field4} {r.Field5} {r.Field6} " + $"{r.Field7} {r.Field8} {r.Field9} {r.Field10} {r.Field11} {r.Field12} " + $"{r.Field13} {r.Field14} {r.Field15} {r.Field16}"); } @@ -51,11 +57,18 @@ public static class TestFixtures } /// 写旧单行格式 Ploss 日志(14 字段),仅指定 ploss/threshold/FOD 三个关键字段。 - public static string WriteLegacyPlossLog(string dir, string fileName, params (int Ploss, int Threshold, int Fod)[] rows) + /// 每行 FOD 记录关键字段(ploss/threshold/FOD) + /// 若非 null 则覆盖首字段为十六进制字面量(否则固定为 "1")。 + public static string WriteLegacyPlossLog( + string dir, + string fileName, + (int Ploss, int Threshold, int Fod)[] rows, + string? field1Hex = null) { var path = Path.Combine(dir, fileName); + var head = field1Hex ?? "1"; var lines = rows - .Select(r => $"FOD-> 1 0 0 0 0 0 0 0 {r.Ploss} {r.Threshold} 0 {r.Fod} 0 0") + .Select(r => $"FOD-> {head} 0 0 0 0 0 0 0 {r.Ploss} {r.Threshold} 0 {r.Fod} 0 0") .ToArray(); File.WriteAllLines(path, lines); return path;