Compare commits
3 Commits
759da546ee
...
348d11c480
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
348d11c480 | ||
|
|
6d56a4a01b | ||
|
|
36c21f03dd |
5
.gitignore
vendored
5
.gitignore
vendored
@@ -27,4 +27,7 @@ test_logs/
|
|||||||
.idea/
|
.idea/
|
||||||
|
|
||||||
# 包目录(由 nuget restore 生成)
|
# 包目录(由 nuget restore 生成)
|
||||||
packages/
|
packages/
|
||||||
|
|
||||||
|
# 发布产物
|
||||||
|
publish/
|
||||||
126
scripts/publish.ps1
Normal file
126
scripts/publish.ps1
Normal file
@@ -0,0 +1,126 @@
|
|||||||
|
<#
|
||||||
|
.SYNOPSIS
|
||||||
|
发布 Gpulse.WCT.DataAnalyzer,生成两种包:
|
||||||
|
1) Framework-Dependent(需要目标机器装 .NET 8 运行时)
|
||||||
|
2) Self-Contained(自带 .NET 8 运行时,无需预装)
|
||||||
|
两者均为单文件 exe,附带 appsettings.json。
|
||||||
|
|
||||||
|
.PARAMETER Version
|
||||||
|
版本号,用于产物目录命名。默认从 csproj 读取 <Version>,若 csproj 未定义则 fallback 1.0.0。
|
||||||
|
|
||||||
|
.PARAMETER OutputBase
|
||||||
|
产物输出根目录,默认 .\publish
|
||||||
|
|
||||||
|
.EXAMPLE
|
||||||
|
.\scripts\publish.ps1
|
||||||
|
.\scripts\publish.ps1 -Version 1.2.0
|
||||||
|
.\scripts\publish.ps1 -Version 1.2.0 -OutputBase D:\release
|
||||||
|
#>
|
||||||
|
param(
|
||||||
|
[string]$Version,
|
||||||
|
[string]$OutputBase = ".\publish"
|
||||||
|
)
|
||||||
|
|
||||||
|
Set-StrictMode -Version Latest
|
||||||
|
$ErrorActionPreference = "Stop"
|
||||||
|
|
||||||
|
# ── 定位项目 ──────────────────────────────────────────────────────────────────
|
||||||
|
$RepoRoot = Resolve-Path (Join-Path $PSScriptRoot "..")
|
||||||
|
$ProjectDir = Join-Path $RepoRoot "src\Gpulse.WCT.DataAnalyzer"
|
||||||
|
$ProjectCs = Join-Path $ProjectDir "Gpulse.WCT.DataAnalyzer.csproj"
|
||||||
|
$AppName = "Gpulse.WCT.DataAnalyzer"
|
||||||
|
|
||||||
|
if (-not (Test-Path $ProjectCs)) {
|
||||||
|
Write-Error "找不到项目文件: $ProjectCs"
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── 解析版本号 ─────────────────────────────────────────────────────────────────
|
||||||
|
if (-not $Version) {
|
||||||
|
# 尝试从 csproj 的 <Version> 节点读取
|
||||||
|
[xml]$csprojXml = Get-Content $ProjectCs -Raw
|
||||||
|
$xmlNs = New-Object System.Xml.XmlNamespaceManager($csprojXml.NameTable)
|
||||||
|
$xmlNs.AddNamespace("ms", "http://schemas.microsoft.com/developer/msbuild/2003")
|
||||||
|
$versionNode = $csprojXml.SelectSingleNode("//ms:Version", $xmlNs)
|
||||||
|
if ($versionNode) {
|
||||||
|
$Version = $versionNode.InnerText
|
||||||
|
} else {
|
||||||
|
$Version = "1.0.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Write-Host "版本号: $Version" -ForegroundColor Cyan
|
||||||
|
|
||||||
|
# ── 目录准备 ───────────────────────────────────────────────────────────────────
|
||||||
|
$Timestamp = Get-Date -Format "yyyyMMdd-HHmmss"
|
||||||
|
|
||||||
|
$FdDir = Join-Path $OutputBase "framework-dependent\$AppName-$Version-fd"
|
||||||
|
$ScDir = Join-Path $OutputBase "self-contained\$AppName-$Version-sc"
|
||||||
|
|
||||||
|
foreach ($dir in @($FdDir, $ScDir)) {
|
||||||
|
if (Test-Path $dir) { Remove-Item $dir -Recurse -Force }
|
||||||
|
New-Item -ItemType Directory -Path $dir -Force | Out-Null
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── 公共发布参数 ────────────────────────────────────────────────────────────────
|
||||||
|
$CommonPublishArgs = @(
|
||||||
|
"publish"
|
||||||
|
$ProjectCs
|
||||||
|
"-c", "Release"
|
||||||
|
"-r", "win-x64"
|
||||||
|
"--nologo"
|
||||||
|
"-p:PublishSingleFile=true"
|
||||||
|
"-p:IncludeNativeLibrariesForSelfExtract=true"
|
||||||
|
)
|
||||||
|
|
||||||
|
# ── 1) Framework-Dependent(不含运行时)─────────────────────────────────────────
|
||||||
|
Write-Host ""
|
||||||
|
Write-Host ">>> 构建 Framework-Dependent(不含运行时)..." -ForegroundColor Green
|
||||||
|
|
||||||
|
$fdArgs = $CommonPublishArgs + @(
|
||||||
|
"--no-self-contained"
|
||||||
|
"-o", $FdDir
|
||||||
|
)
|
||||||
|
|
||||||
|
& dotnet @fdArgs
|
||||||
|
if ($LASTEXITCODE -ne 0) {
|
||||||
|
Write-Error "Framework-Dependent 构建失败 (exit code $LASTEXITCODE)"
|
||||||
|
exit $LASTEXITCODE
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Host " 产物: $FdDir\$AppName.exe" -ForegroundColor Gray
|
||||||
|
|
||||||
|
# ── 2) Self-Contained(含运行时)────────────────────────────────────────────────
|
||||||
|
Write-Host ""
|
||||||
|
Write-Host ">>> 构建 Self-Contained(含 .NET 8 运行时)..." -ForegroundColor Green
|
||||||
|
|
||||||
|
$scArgs = $CommonPublishArgs + @(
|
||||||
|
"--self-contained"
|
||||||
|
"-o", $ScDir
|
||||||
|
"-p:EnableCompressionInSingleFile=true"
|
||||||
|
)
|
||||||
|
|
||||||
|
& dotnet @scArgs
|
||||||
|
if ($LASTEXITCODE -ne 0) {
|
||||||
|
Write-Error "Self-Contained 构建失败 (exit code $LASTEXITCODE)"
|
||||||
|
exit $LASTEXITCODE
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Host " 产物: $ScDir\$AppName.exe" -ForegroundColor Gray
|
||||||
|
|
||||||
|
# ── 汇总 ───────────────────────────────────────────────────────────────────────
|
||||||
|
$FdSize = [math]::Round((Get-Item "$FdDir\$AppName.exe").Length / 1MB, 1)
|
||||||
|
$ScSize = [math]::Round((Get-Item "$ScDir\$AppName.exe").Length / 1MB, 1)
|
||||||
|
|
||||||
|
Write-Host ""
|
||||||
|
Write-Host "============================================================" -ForegroundColor Cyan
|
||||||
|
Write-Host " 发布完成 v$Version $Timestamp"
|
||||||
|
Write-Host "============================================================" -ForegroundColor Cyan
|
||||||
|
Write-Host ""
|
||||||
|
Write-Host " Framework-Dependent(需预装 .NET 8 运行时)"
|
||||||
|
Write-Host " 目录: $FdDir"
|
||||||
|
Write-Host " 大小: ${FdSize} MB"
|
||||||
|
Write-Host ""
|
||||||
|
Write-Host " Self-Contained(自带 .NET 8 运行时)"
|
||||||
|
Write-Host " 目录: $ScDir"
|
||||||
|
Write-Host " 大小: ${ScSize} MB"
|
||||||
|
Write-Host ""
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
using System.Globalization;
|
||||||
using System.Text.RegularExpressions;
|
using System.Text.RegularExpressions;
|
||||||
using Gpulse.WCT.DataAnalyzer.Core.Domain.Local;
|
using Gpulse.WCT.DataAnalyzer.Core.Domain.Local;
|
||||||
|
|
||||||
@@ -17,15 +18,18 @@ public class PlossParser : IParser<PlossRecord>
|
|||||||
RegexOptions.Compiled
|
RegexOptions.Compiled
|
||||||
);
|
);
|
||||||
|
|
||||||
// 第二行正则:FOD-> 后跟16个数字(均支持负数)
|
// 第二行正则:FOD-> 后跟16个字段
|
||||||
|
// Field1 支持十六进制格式(0X/0x 前缀,如 0C、0xFF)或普通十进制;
|
||||||
|
// 其余字段为十进制整数(均支持负数)。
|
||||||
|
// Field1 用捕获组是为了方便从 Match 取值(解析逻辑见 ParseFieldValue)。
|
||||||
private static readonly Regex FodPattern = new(
|
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
|
RegexOptions.Compiled
|
||||||
);
|
);
|
||||||
|
|
||||||
// 旧格式正则(兼容):前3个字段被忽略,所有捕获组支持负数
|
// 旧格式正则(兼容):前3个字段被忽略,所有捕获组支持负数;Field1 兼容十六进制
|
||||||
private static readonly Regex LegacyPattern = new(
|
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
|
RegexOptions.Compiled
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -103,7 +107,7 @@ public class PlossParser : IParser<PlossRecord>
|
|||||||
PowLoss = int.Parse(headerMatch.Groups[1].Value),
|
PowLoss = int.Parse(headerMatch.Groups[1].Value),
|
||||||
DeltaP = int.Parse(headerMatch.Groups[2].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),
|
Field2 = int.Parse(fodMatch.Groups[2].Value),
|
||||||
Field3 = int.Parse(fodMatch.Groups[3].Value),
|
Field3 = int.Parse(fodMatch.Groups[3].Value),
|
||||||
Field4 = int.Parse(fodMatch.Groups[4].Value),
|
Field4 = int.Parse(fodMatch.Groups[4].Value),
|
||||||
@@ -144,7 +148,7 @@ public class PlossParser : IParser<PlossRecord>
|
|||||||
PowLoss = null,
|
PowLoss = null,
|
||||||
DeltaP = null,
|
DeltaP = null,
|
||||||
// 映射到新字段名
|
// 映射到新字段名
|
||||||
Field1 = int.Parse(match.Groups[1].Value),
|
Field1 = ParseFieldValue(match.Groups[1].Value),
|
||||||
Field2 = int.Parse(match.Groups[2].Value),
|
Field2 = int.Parse(match.Groups[2].Value),
|
||||||
Field3 = int.Parse(match.Groups[3].Value),
|
Field3 = int.Parse(match.Groups[3].Value),
|
||||||
Field4 = int.Parse(match.Groups[4].Value), // RX power
|
Field4 = int.Parse(match.Groups[4].Value), // RX power
|
||||||
@@ -206,7 +210,7 @@ public class PlossParser : IParser<PlossRecord>
|
|||||||
ScenarioId = scenarioId,
|
ScenarioId = scenarioId,
|
||||||
PowLoss = null,
|
PowLoss = null,
|
||||||
DeltaP = null,
|
DeltaP = null,
|
||||||
Field1 = byte.Parse(match.Groups[1].Value),
|
Field1 = ParseFieldValue(match.Groups[1].Value),
|
||||||
Field2 = int.Parse(match.Groups[2].Value),
|
Field2 = int.Parse(match.Groups[2].Value),
|
||||||
Field3 = int.Parse(match.Groups[3].Value),
|
Field3 = int.Parse(match.Groups[3].Value),
|
||||||
Field4 = int.Parse(match.Groups[4].Value),
|
Field4 = int.Parse(match.Groups[4].Value),
|
||||||
@@ -233,4 +237,13 @@ public class PlossParser : IParser<PlossRecord>
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 解析 Field1 的值,支持十六进制格式(0C、0xFF)和普通十进制。
|
||||||
|
/// 含 a-f/A-F 字符时按十六进制解析,否则按十进制解析。
|
||||||
|
/// </summary>
|
||||||
|
private static int ParseFieldValue(string value) =>
|
||||||
|
value.AsSpan().IndexOfAny("abcdefABCDEF".AsSpan()) >= 0
|
||||||
|
? int.Parse(value, NumberStyles.HexNumber)
|
||||||
|
: int.Parse(value);
|
||||||
}
|
}
|
||||||
@@ -10,6 +10,10 @@
|
|||||||
<LangVersion>12.0</LangVersion>
|
<LangVersion>12.0</LangVersion>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<InternalsVisibleTo Include="Gpulse.WCT.DataAnalyzer.Tests" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<!-- CLI -->
|
<!-- CLI -->
|
||||||
<PackageReference Include="System.CommandLine" Version="2.0.0-beta4.22272.1" />
|
<PackageReference Include="System.CommandLine" Version="2.0.0-beta4.22272.1" />
|
||||||
@@ -36,9 +40,7 @@
|
|||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<None Update="appsettings.json">
|
<EmbeddedResource Include="appsettings.json" />
|
||||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
|
||||||
</None>
|
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
using System.CommandLine;
|
using System.CommandLine;
|
||||||
|
using System.Reflection;
|
||||||
|
using System.Text.Json;
|
||||||
using Microsoft.Extensions.Configuration;
|
using Microsoft.Extensions.Configuration;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using Microsoft.Extensions.Hosting;
|
using Microsoft.Extensions.Hosting;
|
||||||
@@ -97,8 +99,16 @@ public class Program
|
|||||||
{
|
{
|
||||||
var builder = new ConfigurationBuilder()
|
var builder = new ConfigurationBuilder()
|
||||||
.SetBasePath(AppContext.BaseDirectory)
|
.SetBasePath(AppContext.BaseDirectory)
|
||||||
.AddJsonFile("appsettings.json", optional: false)
|
.AddEmbeddedResourceJson(
|
||||||
.AddJsonFile($"appsettings.{Environment.GetEnvironmentVariable("DOTNET_ENVIRONMENT") ?? "Production"}.json", optional: true)
|
Assembly.GetExecutingAssembly(),
|
||||||
|
"Gpulse.WCT.DataAnalyzer.appsettings.json")
|
||||||
|
.AddJsonFile(
|
||||||
|
Path.Combine(AppContext.BaseDirectory, "appsettings.json"),
|
||||||
|
optional: true, reloadOnChange: false)
|
||||||
|
.AddJsonFile(
|
||||||
|
Path.Combine(AppContext.BaseDirectory,
|
||||||
|
$"appsettings.{Environment.GetEnvironmentVariable("DOTNET_ENVIRONMENT") ?? "Production"}.json"),
|
||||||
|
optional: true, reloadOnChange: false)
|
||||||
.AddEnvironmentVariables()
|
.AddEnvironmentVariables()
|
||||||
.AddCommandLine(args);
|
.AddCommandLine(args);
|
||||||
|
|
||||||
@@ -163,3 +173,84 @@ public class Program
|
|||||||
: Path.GetFullPath(Path.Combine(baseDirectory, value));
|
: Path.GetFullPath(Path.Combine(baseDirectory, value));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── 嵌入资源配置加载 ──────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
internal sealed class EmbeddedResourceJsonConfigurationProvider(
|
||||||
|
EmbeddedResourceJsonConfigurationSource source) : FileConfigurationProvider(source)
|
||||||
|
{
|
||||||
|
public override void Load(Stream stream)
|
||||||
|
{
|
||||||
|
using var doc = JsonDocument.Parse(stream);
|
||||||
|
Data = FlattenJson(doc.RootElement);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void Load()
|
||||||
|
{
|
||||||
|
var assembly = source.Assembly;
|
||||||
|
var resourceName = source.ResourceName;
|
||||||
|
|
||||||
|
using var stream = assembly.GetManifestResourceStream(resourceName)
|
||||||
|
?? throw new InvalidOperationException(
|
||||||
|
$"嵌入资源 '{resourceName}' 不存在。"
|
||||||
|
+ $" 可用资源: [{string.Join(", ", assembly.GetManifestResourceNames())}]");
|
||||||
|
|
||||||
|
Load(stream);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Dictionary<string, string?> FlattenJson(JsonElement element)
|
||||||
|
{
|
||||||
|
var result = new Dictionary<string, string?>(StringComparer.OrdinalIgnoreCase);
|
||||||
|
Walk(element, "", result);
|
||||||
|
return result;
|
||||||
|
|
||||||
|
static void Walk(JsonElement el, string prefix, Dictionary<string, string?> bag)
|
||||||
|
{
|
||||||
|
switch (el.ValueKind)
|
||||||
|
{
|
||||||
|
case JsonValueKind.Object:
|
||||||
|
foreach (var prop in el.EnumerateObject())
|
||||||
|
Walk(prop.Value, string.IsNullOrEmpty(prefix) ? prop.Name : $"{prefix}:{prop.Name}", bag);
|
||||||
|
break;
|
||||||
|
case JsonValueKind.Array:
|
||||||
|
var i = 0;
|
||||||
|
foreach (var item in el.EnumerateArray())
|
||||||
|
{
|
||||||
|
Walk(item, $"{prefix}:{i}", bag);
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
bag[prefix] = el.ToString();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal sealed class EmbeddedResourceJsonConfigurationSource : FileConfigurationSource
|
||||||
|
{
|
||||||
|
public Assembly Assembly { get; } = null!;
|
||||||
|
public string ResourceName { get; } = null!;
|
||||||
|
|
||||||
|
public EmbeddedResourceJsonConfigurationSource(Assembly assembly, string resourceName)
|
||||||
|
{
|
||||||
|
Assembly = assembly;
|
||||||
|
ResourceName = resourceName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override IConfigurationProvider Build(IConfigurationBuilder builder) =>
|
||||||
|
new EmbeddedResourceJsonConfigurationProvider(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class EmbeddedResourceConfigurationExtensions
|
||||||
|
{
|
||||||
|
public static IConfigurationBuilder AddEmbeddedResourceJson(
|
||||||
|
this IConfigurationBuilder builder,
|
||||||
|
Assembly assembly,
|
||||||
|
string resourceName)
|
||||||
|
{
|
||||||
|
builder.Add(new EmbeddedResourceJsonConfigurationSource(assembly, resourceName));
|
||||||
|
return builder;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
using System.Reflection;
|
||||||
using Gpulse.WCT.DataAnalyzer.Core.Infrastructure.Extensions;
|
using Gpulse.WCT.DataAnalyzer.Core.Infrastructure.Extensions;
|
||||||
using Gpulse.WCT.DataAnalyzer.Core.Infrastructure.LocalData;
|
using Gpulse.WCT.DataAnalyzer.Core.Infrastructure.LocalData;
|
||||||
using Microsoft.Data.Sqlite;
|
using Microsoft.Data.Sqlite;
|
||||||
@@ -56,7 +57,11 @@ public sealed class CliTestHost : IDisposable
|
|||||||
var releaseConnection = new SqliteConnection("Data Source=:memory:");
|
var releaseConnection = new SqliteConnection("Data Source=:memory:");
|
||||||
await releaseConnection.OpenAsync();
|
await releaseConnection.OpenAsync();
|
||||||
|
|
||||||
|
// 从嵌入资源加载 appsettings.json,与 Program.Main 生产路径一致
|
||||||
var configuration = new ConfigurationBuilder()
|
var configuration = new ConfigurationBuilder()
|
||||||
|
.AddEmbeddedResourceJson(
|
||||||
|
typeof(Program).Assembly,
|
||||||
|
"Gpulse.WCT.DataAnalyzer.appsettings.json")
|
||||||
.AddInMemoryCollection(new Dictionary<string, string?>
|
.AddInMemoryCollection(new Dictionary<string, string?>
|
||||||
{
|
{
|
||||||
["Database:Type"] = "sqlite",
|
["Database:Type"] = "sqlite",
|
||||||
|
|||||||
@@ -87,6 +87,26 @@ public class AnalyzeCommandTests : CommandTestBase
|
|||||||
Assert.That(stdout, Does.Contain("Please specify --pure and --foreign for Qfod calibration"));
|
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]
|
[Test]
|
||||||
public async Task Analyze_Qfod_ValidFiles_ReportsThreshold()
|
public async Task Analyze_Qfod_ValidFiles_ReportsThreshold()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -76,6 +76,30 @@ public class ParseCommandTests : CommandTestBase
|
|||||||
Assert.That(await Host.LocalDb.PlossRecords.CountAsync(), Is.EqualTo(2));
|
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]
|
[Test]
|
||||||
public async Task Parse_File_WithForceOption_Accepted()
|
public async Task Parse_File_WithForceOption_Accepted()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -13,10 +13,15 @@ public static class TestFixtures
|
|||||||
/// 两行 Ploss 记录(header + FOD 16 字段)。
|
/// 两行 Ploss 记录(header + FOD 16 字段)。
|
||||||
/// 默认值让恒等式成立(Field5 - Field4 - PowLoss - ploss == DeltaP)且余量 > 2000。
|
/// 默认值让恒等式成立(Field5 - Field4 - PowLoss - ploss == DeltaP)且余量 > 2000。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <param name="Field1AsHex">
|
||||||
|
/// 当不为 null 时,覆写 Field1,按十六进制字面量写入日志(如 "0C" = 十进制 12),
|
||||||
|
/// 用于验证解析器对 Field1 十六进制格式的支持。
|
||||||
|
/// </param>
|
||||||
public sealed record PlossTwoLineRow(
|
public sealed record PlossTwoLineRow(
|
||||||
int PowLoss = 2200,
|
int PowLoss = 2200,
|
||||||
int DeltaP = 2500,
|
int DeltaP = 2500,
|
||||||
int Field1 = 1,
|
int Field1 = 1,
|
||||||
|
string? Field1AsHex = null,
|
||||||
int Field2 = 0,
|
int Field2 = 0,
|
||||||
int Field3 = 0,
|
int Field3 = 0,
|
||||||
int Field4 = 800, // RX power
|
int Field4 = 800, // RX power
|
||||||
@@ -41,8 +46,9 @@ public static class TestFixtures
|
|||||||
foreach (var r in rows)
|
foreach (var r in rows)
|
||||||
{
|
{
|
||||||
lines.Add($"pow_loss = {r.PowLoss}, delta_p = {r.DeltaP}");
|
lines.Add($"pow_loss = {r.PowLoss}, delta_p = {r.DeltaP}");
|
||||||
|
var field1 = r.Field1AsHex ?? r.Field1.ToString();
|
||||||
lines.Add(
|
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.Field7} {r.Field8} {r.Field9} {r.Field10} {r.Field11} {r.Field12} " +
|
||||||
$"{r.Field13} {r.Field14} {r.Field15} {r.Field16}");
|
$"{r.Field13} {r.Field14} {r.Field15} {r.Field16}");
|
||||||
}
|
}
|
||||||
@@ -51,11 +57,18 @@ public static class TestFixtures
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>写旧单行格式 Ploss 日志(14 字段),仅指定 ploss/threshold/FOD 三个关键字段。</summary>
|
/// <summary>写旧单行格式 Ploss 日志(14 字段),仅指定 ploss/threshold/FOD 三个关键字段。</summary>
|
||||||
public static string WriteLegacyPlossLog(string dir, string fileName, params (int Ploss, int Threshold, int Fod)[] rows)
|
/// <param name="rows">每行 FOD 记录关键字段(ploss/threshold/FOD)</param>
|
||||||
|
/// <param name="field1Hex">若非 null 则覆盖首字段为十六进制字面量(否则固定为 "1")。</param>
|
||||||
|
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 path = Path.Combine(dir, fileName);
|
||||||
|
var head = field1Hex ?? "1";
|
||||||
var lines = rows
|
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();
|
.ToArray();
|
||||||
File.WriteAllLines(path, lines);
|
File.WriteAllLines(path, lines);
|
||||||
return path;
|
return path;
|
||||||
|
|||||||
Reference in New Issue
Block a user