Compare commits
3 Commits
348d11c480
...
ccf3885c38
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ccf3885c38 | ||
|
|
81c5316eac | ||
|
|
eba1d2080c |
4
.gitignore
vendored
4
.gitignore
vendored
@@ -30,4 +30,6 @@ test_logs/
|
|||||||
packages/
|
packages/
|
||||||
|
|
||||||
# 发布产物
|
# 发布产物
|
||||||
publish/
|
publish/
|
||||||
|
|
||||||
|
Resources/
|
||||||
@@ -21,15 +21,16 @@ public class PlossParser : IParser<PlossRecord>
|
|||||||
// 第二行正则:FOD-> 后跟16个字段
|
// 第二行正则:FOD-> 后跟16个字段
|
||||||
// Field1 支持十六进制格式(0X/0x 前缀,如 0C、0xFF)或普通十进制;
|
// Field1 支持十六进制格式(0X/0x 前缀,如 0C、0xFF)或普通十进制;
|
||||||
// 其余字段为十进制整数(均支持负数)。
|
// 其余字段为十进制整数(均支持负数)。
|
||||||
// Field1 用捕获组是为了方便从 Match 取值(解析逻辑见 ParseFieldValue)。
|
// 共 16 个捕获组(Field1 文本 + Field2..Field16),对应 PlossRecord 的 Field1..Field16。
|
||||||
private static readonly Regex FodPattern = new(
|
private static readonly Regex FodPattern = new(
|
||||||
@"^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+)$",
|
@"^FOD->\s+(0[xX][0-9A-Fa-f]+|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+)$",
|
||||||
RegexOptions.Compiled
|
RegexOptions.Compiled
|
||||||
);
|
);
|
||||||
|
|
||||||
// 旧格式正则(兼容):前3个字段被忽略,所有捕获组支持负数;Field1 兼容十六进制
|
// 旧格式正则(兼容):前3个字段被忽略,所有捕获组支持负数;Field1 兼容十六进制
|
||||||
|
// 共 14 个捕获组(Field1 文本 + 13 个 (-?\d+)),对应旧格式 14 个原始字段(含前3个占位字段)
|
||||||
private static readonly Regex LegacyPattern = new(
|
private static readonly Regex LegacyPattern = new(
|
||||||
@"^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+)$",
|
@"^FOD->\s+(0[xX][0-9A-Fa-f]+|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
|
RegexOptions.Compiled
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -239,11 +240,20 @@ public class PlossParser : IParser<PlossRecord>
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 解析 Field1 的值,支持十六进制格式(0C、0xFF)和普通十进制。
|
/// 解析 Field1 的值,支持十六进制格式(0C、0xFF、0xFF)和普通十进制。
|
||||||
/// 含 a-f/A-F 字符时按十六进制解析,否则按十进制解析。
|
/// 含 a-f/A-F/X 字符时按十六进制解析,否则按十进制解析;十六进制时先去掉 "0x"/"0X" 前缀。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private static int ParseFieldValue(string value) =>
|
private static int ParseFieldValue(string value)
|
||||||
value.AsSpan().IndexOfAny("abcdefABCDEF".AsSpan()) >= 0
|
{
|
||||||
? int.Parse(value, NumberStyles.HexNumber)
|
// 含十六进制字符(A-F/a-f)或显式 0x 前缀时按十六进制解析
|
||||||
: int.Parse(value);
|
bool isHex = value.AsSpan().IndexOfAny("abcdefABCDEFxX".AsSpan()) >= 0;
|
||||||
|
if (!isHex)
|
||||||
|
return int.Parse(value);
|
||||||
|
|
||||||
|
// 去掉可选的 0x/0X 前缀(NumberStyles.HexNumber 不接受前缀)
|
||||||
|
var span = value.AsSpan();
|
||||||
|
if (span.Length > 2 && (span[1] == 'x' || span[1] == 'X') && span[0] == '0')
|
||||||
|
span = span[2..];
|
||||||
|
return int.Parse(span, NumberStyles.HexNumber);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -3,6 +3,68 @@ namespace Gpulse.WCT.DataAnalyzer.Tests.Commands;
|
|||||||
[TestFixture]
|
[TestFixture]
|
||||||
public class AnalyzeCommandTests : CommandTestBase
|
public class AnalyzeCommandTests : CommandTestBase
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 真实产线 .DAT:含 35 条两行 Ploss(1 条 FOD),全部恒等式成立,
|
||||||
|
/// 余量与 delta_p' 反解在真实数据上应当与手算一致。
|
||||||
|
/// 直接指向仓库根的 Resources 目录(gitignored,但本地存在);CI 无此文件则跳过。
|
||||||
|
/// </summary>
|
||||||
|
[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"));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 真实产线 Qfod 标定:从「小米17功率上不去」文件中分离出纯手机与异物两段,
|
||||||
|
/// 新建两个符合 FileNameParser 约定的 .log 文件:
|
||||||
|
/// <list type="bullet">
|
||||||
|
/// <item>pure:前 21 条稳定读数,ΔQ=3~5</item>
|
||||||
|
/// <item>foreign:后 7 条衰减读数,ΔQ 为负值(-5 ~ -49)</item>
|
||||||
|
/// </list>
|
||||||
|
/// 阈值 = (pureMax + foreignMin) / 2 = (5 + -49) / 2 = -22
|
||||||
|
/// </summary>
|
||||||
|
[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]
|
[Test]
|
||||||
public async Task Analyze_NoType_ReturnsValidationError()
|
public async Task Analyze_NoType_ReturnsValidationError()
|
||||||
{
|
{
|
||||||
@@ -66,7 +128,7 @@ public class AnalyzeCommandTests : CommandTestBase
|
|||||||
var dir = NewDir();
|
var dir = NewDir();
|
||||||
var file = TestFixtures.WriteLegacyPlossLog(
|
var file = TestFixtures.WriteLegacyPlossLog(
|
||||||
dir, "singleMold-v1.0-hex2_1-iPhone15-ploss_legacy-20260710-1.log",
|
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);
|
var (code, stdout, _) = await Analyze().RunCaptureAsync("--type", "ploss", "--file", file);
|
||||||
|
|
||||||
@@ -146,4 +208,33 @@ public class AnalyzeCommandTests : CommandTestBase
|
|||||||
|
|
||||||
Assert.That(stdout, Does.Contain("FAILED"));
|
Assert.That(stdout, Does.Contain("FAILED"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 在测试输出目录、以及从该目录向上一路找的仓库根 <c>Resources/</c> 中定位真实数据文件。
|
||||||
|
/// 找不到时调用 <see cref="Assert.Ignore(string)"/> 跳过,CI 无产线 .DAT 时不会让用例变红。
|
||||||
|
/// 与其它 fixture 模式不同:真实数据不复制到 bin/ —— 直接以相对仓库根的路径读取。
|
||||||
|
/// </summary>
|
||||||
|
private static string ResolveResourcesFile(string fileName)
|
||||||
|
{
|
||||||
|
// 1) 测试输出目录下的 Resources 副本(csproj 配置 CopyToOutputDirectory 时)
|
||||||
|
var candidates = new List<string>
|
||||||
|
{
|
||||||
|
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!; // 不会执行
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user