namespace Gpulse.WCT.DataAnalyzer.Tests.Commands; [TestFixture] public class AnalyzeCommandTests : CommandTestBase { /// /// 真实产线 .DAT:含 35 条两行 Ploss(1 条 FOD),全部恒等式成立, /// 余量与 delta_p' 反解在真实数据上应当与手算一致。 /// 直接指向仓库根的 Resources 目录(gitignored,但本地存在);CI 无此文件则跳过。 /// [Test] public async Task Analyze_Ploss_RealVivoFile_ReportsSummary() { var file = ResolveResourcesFile( "C车-单充快速成型件-A03-V55-vivo iQOD12 Pro-20250519-01.DAT"); var (code, stdout, _) = await Analyze().RunCaptureAsync("--type", "ploss", "--file", file); Assert.Multiple(() => { Assert.That(code, Is.EqualTo(0)); Assert.That(stdout, Does.Contain("=== Ploss Analysis:")); Assert.That(stdout, Does.Contain("Total: 35 (TwoLine: 35, Legacy: 0), FOD: 1, Errors: 0")); Assert.That(stdout, Does.Contain("Normal margin>2000: 34/34 (100.0%)")); Assert.That(stdout, Does.Contain("FOD margin>2000: 1/1 (100.0%)")); Assert.That(stdout, Does.Contain("Identity (delta_p = Tx - Rx - pow_loss - ploss): 35/35")); // FOD 行(第 92 行)反解:delta_p'=18801+2001+(-18447)-1750=605 Assert.That(stdout, Does.Contain("delta_p'=605")); Assert.That(stdout, Does.Contain("rewrite: pow_loss = 3394, delta_p = 605")); }); } /// /// 真实产线 Qfod 标定:从「小米17功率上不去」文件中分离出纯手机与异物两段, /// 新建两个符合 FileNameParser 约定的 .log 文件: /// /// pure:前 21 条稳定读数,ΔQ=3~5 /// foreign:后 7 条衰减读数,ΔQ 为负值(-5 ~ -49) /// /// 阈值 = (pureMax + foreignMin) / 2 = (5 + -49) / 2 = -22 /// [Test] public async Task Analyze_Qfod_RealXiaomiFiles_ReportsCalibration() { var pure = ResolveResourcesFile("rapidMold-v1.5-hex2_5-Xiaomi17-qfod_pure-20250817-1.log"); var foreign = ResolveResourcesFile("rapidMold-v1.5-hex2_5-Xiaomi17-qfod_foreign-20250817-1.log"); var (code, stdout, _) = await Analyze().RunCaptureAsync( "--type", "qfod", "--pure", pure, "--foreign", foreign); Assert.Multiple(() => { Assert.That(code, Is.EqualTo(0)); Assert.That(stdout, Does.Contain("=== Qfod Calibration ===")); // Pure: 21 条,min=3 max=5 avg=4.0 Assert.That(stdout, Does.Contain("Pure phone")); Assert.That(stdout, Does.Contain("count=21 min=3 max=5 avg=4.0")); // Foreign: 7 条,min=-49 max=-5 avg=-27.7 Assert.That(stdout, Does.Contain("Foreign object")); Assert.That(stdout, Does.Contain("count=7 min=-49 max=-5 avg=-27.7")); // Threshold = (5 + -49) / 2 = -22 Assert.That(stdout, Does.Contain("Threshold = (pureMax + foreignMin) / 2 = (5 + -49) / 2 = -22")); Assert.That(stdout, Does.Contain("pureMax - threshold = 27")); Assert.That(stdout, Does.Contain("foreignMin - threshold = -27")); }); } [Test] public async Task Analyze_NoType_ReturnsValidationError() { var (code, stdout, stderr) = await Analyze().RunCaptureAsync(); Assert.Multiple(() => { Assert.That(code, Is.EqualTo(1)); Assert.That(stdout + stderr, Does.Contain("--type")); }); } [Test] public async Task Analyze_UnknownType_PrintsMessage() { var (code, stdout, _) = await Analyze().RunCaptureAsync("--type", "foo"); Assert.Multiple(() => { Assert.That(code, Is.EqualTo(0)); Assert.That(stdout, Does.Contain("Unknown type: foo. Use 'ploss' or 'qfod'.")); }); } [Test] public async Task Analyze_Ploss_MissingFile_PrintsPrompt() { var (_, stdout, _) = await Analyze().RunCaptureAsync("--type", "ploss"); Assert.That(stdout, Does.Contain("Please specify --file for Ploss analysis")); } [Test] public async Task Analyze_Ploss_ValidTwoLineFile_ReportsSummary() { var dir = NewDir(); var file = TestFixtures.WritePlossLog(dir, TestFixtures.PlossFileName, // 正常两行:margin = 3000-500 = 2500 > 2000,恒等式成立 new TestFixtures.PlossTwoLineRow(), // FOD 两行:ploss=2500 threshold=3000 margin=500,恒等式成立,delta_p'=2500+2001+2500-3000=4001 new TestFixtures.PlossTwoLineRow(Field5: 8000, Field9: 2500, Field10: 3000, Field12: 1)); var (code, stdout, _) = await Analyze().RunCaptureAsync("--type", "ploss", "--file", file); Assert.Multiple(() => { Assert.That(code, Is.EqualTo(0)); Assert.That(stdout, Does.Contain("=== Ploss Analysis:")); Assert.That(stdout, Does.Contain("Total: 2 (TwoLine: 2, Legacy: 0), FOD: 1, Errors: 0")); Assert.That(stdout, Does.Contain("Normal margin>2000: 1/1 (100.0%)")); Assert.That(stdout, Does.Contain("FOD margin>2000: 0/1 (0.0%)")); Assert.That(stdout, Does.Contain("Identity (delta_p = Tx - Rx - pow_loss - ploss): 2/2")); Assert.That(stdout, Does.Contain("delta_p'=4001")); Assert.That(stdout, Does.Contain("rewrite: pow_loss = 2200, delta_p = 4001")); }); } [Test] public async Task Analyze_Ploss_LegacyFile_CountsLegacyRows() { var dir = NewDir(); var file = TestFixtures.WriteLegacyPlossLog( dir, "singleMold-v1.0-hex2_1-iPhone15-ploss_legacy-20260710-1.log", new (int Ploss, int Threshold, int Fod)[] { (500, 3000, 0), (2500, 3000, 1) }); var (code, stdout, _) = await Analyze().RunCaptureAsync("--type", "ploss", "--file", file); Assert.Multiple(() => { Assert.That(code, Is.EqualTo(0)); Assert.That(stdout, Does.Contain("Total: 2 (TwoLine: 0, Legacy: 2), FOD: 1, Errors: 0")); Assert.That(stdout, Does.Contain("Normal margin>2000: 1/1 (100.0%)")); Assert.That(stdout, Does.Contain("FOD margin>2000: 0/1 (0.0%)")); }); } [Test] public async Task Analyze_Qfod_MissingArgs_PrintsPrompt() { var (_, stdout, _) = await Analyze().RunCaptureAsync("--type", "qfod"); 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() { var dir = NewDir(); var pure = TestFixtures.WriteQfodLog( dir, "singleMold-v1.0-hex2_1-iPhone15-qfod_pure-20260710-1.log", 20, 25, 21); var foreign = TestFixtures.WriteQfodLog( dir, "singleMold-v1.0-hex2_1-iPhone15-qfod_foreign-20260710-1.log", 42, 40, 45); var (code, stdout, _) = await Analyze().RunCaptureAsync("--type", "qfod", "--pure", pure, "--foreign", foreign); Assert.Multiple(() => { Assert.That(code, Is.EqualTo(0)); Assert.That(stdout, Does.Contain("=== Qfod Calibration ===")); Assert.That(stdout, Does.Contain("Pure phone")); Assert.That(stdout, Does.Contain("Foreign object")); Assert.That(stdout, Does.Contain("count=3 min=20 max=25 avg=22.0")); Assert.That(stdout, Does.Contain("count=3 min=40 max=45 avg=42.3")); Assert.That(stdout, Does.Contain("Threshold = (pureMax + foreignMin) / 2 = (25 + 40) / 2 = 32")); Assert.That(stdout, Does.Contain("pureMax - threshold = -7")); Assert.That(stdout, Does.Contain("foreignMin - threshold = 8")); }); } [Test] public async Task Analyze_Qfod_MissingFile_ReportsFailure() { var dir = NewDir(); var foreign = TestFixtures.WriteQfodLog( dir, "singleMold-v1.0-hex2_1-iPhone15-qfod_foreign-20260710-1.log", 42, 40); var (_, stdout, _) = await Analyze().RunCaptureAsync( "--type", "qfod", "--pure", Path.Combine(dir, "nope.log"), "--foreign", foreign); Assert.That(stdout, Does.Contain("FAILED")); } /// /// 在测试输出目录、以及从该目录向上一路找的仓库根 Resources/ 中定位真实数据文件。 /// 找不到时调用 跳过,CI 无产线 .DAT 时不会让用例变红。 /// 与其它 fixture 模式不同:真实数据不复制到 bin/ —— 直接以相对仓库根的路径读取。 /// private static string ResolveResourcesFile(string fileName) { // 1) 测试输出目录下的 Resources 副本(csproj 配置 CopyToOutputDirectory 时) var candidates = new List { Path.Combine(AppContext.BaseDirectory, "Resources", fileName), }; // 2) 从测试输出目录向上回溯寻找仓库根的 Resources 目录 // 典型路径:…/tests/Gpulse.WCT.DataAnalyzer.Tests/bin/Debug/net8.0 // → 仓库根在这之上 5 级 var dir = new DirectoryInfo(AppContext.BaseDirectory); for (int up = 0; up < 8 && dir != null; up++, dir = dir.Parent) { var candidate = Path.Combine(dir.FullName, "Resources", fileName); candidates.Add(candidate); if (File.Exists(candidate)) return candidate; } Assert.Ignore($"真实数据文件不存在(已检索 {candidates.Count} 个候选路径):{fileName}"); return null!; // 不会执行 } }