diff --git a/tests/Gpulse.WCT.DataAnalyzer.Tests/Commands/AnalyzeCommandTests.cs b/tests/Gpulse.WCT.DataAnalyzer.Tests/Commands/AnalyzeCommandTests.cs
index 8f5dbfc..1960c7d 100644
--- a/tests/Gpulse.WCT.DataAnalyzer.Tests/Commands/AnalyzeCommandTests.cs
+++ b/tests/Gpulse.WCT.DataAnalyzer.Tests/Commands/AnalyzeCommandTests.cs
@@ -3,6 +3,68 @@ 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()
{
@@ -66,7 +128,7 @@ public class AnalyzeCommandTests : CommandTestBase
var dir = NewDir();
var file = TestFixtures.WriteLegacyPlossLog(
dir, "singleMold-v1.0-hex2_1-iPhone15-ploss_legacy-20260710-1.log",
- (500, 3000, 0), (2500, 3000, 1));
+ new (int Ploss, int Threshold, int Fod)[] { (500, 3000, 0), (2500, 3000, 1) });
var (code, stdout, _) = await Analyze().RunCaptureAsync("--type", "ploss", "--file", file);
@@ -146,4 +208,33 @@ public class AnalyzeCommandTests : CommandTestBase
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!; // 不会执行
+ }
}